1 /*
<lambda>null2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.egg.landroid
18
19 import androidx.compose.runtime.mutableStateOf
20 import androidx.compose.ui.graphics.Color
21 import androidx.compose.ui.graphics.Path
22 import androidx.compose.ui.graphics.PathEffect
23 import androidx.compose.ui.graphics.PointMode
24 import androidx.compose.ui.graphics.drawscope.DrawScope
25 import androidx.compose.ui.graphics.drawscope.Stroke
26 import androidx.compose.ui.graphics.drawscope.rotateRad
27 import androidx.compose.ui.graphics.drawscope.scale
28 import androidx.compose.ui.graphics.drawscope.translate
29 import androidx.compose.ui.util.lerp
30 import androidx.core.math.MathUtils.clamp
31 import java.lang.Float.max
32 import kotlin.math.sqrt
33
34 const val DRAW_ORBITS = true
35 const val DRAW_GRAVITATIONAL_FIELDS = true
36 const val DRAW_STAR_GRAVITATIONAL_FIELDS = true
37
38 val STAR_POINTS = android.os.Build.VERSION.SDK_INT.takeIf { it in 1..99 } ?: 31
39
40 /**
41 * A zoomedDrawScope is one that is scaled, but remembers its zoom level, so you can correct for it
42 * if you want to draw single-pixel lines. Which we do.
43 */
44 interface ZoomedDrawScope : DrawScope {
45 val zoom: Float
46 }
47
DrawScopenull48 fun DrawScope.zoom(zoom: Float, block: ZoomedDrawScope.() -> Unit) {
49 val ds =
50 object : ZoomedDrawScope, DrawScope by this {
51 override var zoom = zoom
52 }
53 ds.scale(zoom) { block(ds) }
54 }
55
56 class VisibleUniverse(namer: Namer, randomSeed: Long) : Universe(namer, randomSeed) {
57 // Magic variable. Every time we update it, Compose will notice and redraw the universe.
58 val triggerDraw = mutableStateOf(0L)
59
simulateAndDrawFramenull60 fun simulateAndDrawFrame(nanos: Long) {
61 // By writing this value, Compose will look for functions that read it (like drawZoomed).
62 triggerDraw.value = nanos
63
64 step(nanos)
65 }
66 }
67
ZoomedDrawScopenull68 fun ZoomedDrawScope.drawUniverse(universe: VisibleUniverse) {
69 with(universe) {
70 triggerDraw.value // Please recompose when this value changes.
71
72 // star.drawZoomed(ds, zoom)
73 // planets.forEach { p ->
74 // p.drawZoomed(ds, zoom)
75 // if (p == follow) {
76 // drawCircle(Color.Red, 20f / zoom, p.pos)
77 // }
78 // }
79 //
80 // ship.drawZoomed(ds, zoom)
81
82 constraints.forEach {
83 when (it) {
84 is Landing -> drawLanding(it)
85 is Container -> drawContainer(it)
86 }
87 }
88 drawStar(star)
89 entities.forEach {
90 if (it === ship || it === star) return@forEach // draw the ship last
91 when (it) {
92 is Spacecraft -> drawSpacecraft(it)
93 is Spark -> drawSpark(it)
94 is Planet -> drawPlanet(it)
95 }
96 }
97 drawSpacecraft(ship)
98 }
99 }
100
ZoomedDrawScopenull101 fun ZoomedDrawScope.drawContainer(container: Container) {
102 drawCircle(
103 color = Color(0xFF800000),
104 radius = container.radius,
105 center = Vec2.Zero,
106 style =
107 Stroke(
108 width = 1f / zoom,
109 pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f / zoom, 8f / zoom), 0f)
110 )
111 )
112 // val path = Path().apply {
113 // fillType = PathFillType.EvenOdd
114 // addOval(Rect(center = Vec2.Zero, radius = container.radius))
115 // addOval(Rect(center = Vec2.Zero, radius = container.radius + 10_000))
116 // }
117 // drawPath(
118 // path = path,
119 //
120 // )
121 }
122
ZoomedDrawScopenull123 fun ZoomedDrawScope.drawGravitationalField(planet: Planet) {
124 val rings = 8
125 for (i in 0 until rings) {
126 val force =
127 lerp(
128 200f,
129 0.01f,
130 i.toFloat() / rings
131 ) // first rings at force = 1N, dropping off after that
132 val r = sqrt(GRAVITATION * planet.mass * SPACECRAFT_MASS / force)
133 drawCircle(
134 color = Color(1f, 0f, 0f, lerp(0.5f, 0.1f, i.toFloat() / rings)),
135 center = planet.pos,
136 style = Stroke(2f / zoom),
137 radius = r
138 )
139 }
140 }
141
ZoomedDrawScopenull142 fun ZoomedDrawScope.drawPlanet(planet: Planet) {
143 with(planet) {
144 if (DRAW_ORBITS)
145 drawCircle(
146 color = Color(0x8000FFFF),
147 radius = pos.distance(orbitCenter),
148 center = orbitCenter,
149 style =
150 Stroke(
151 width = 1f / zoom,
152 )
153 )
154
155 if (DRAW_GRAVITATIONAL_FIELDS) {
156 drawGravitationalField(this)
157 }
158
159 drawCircle(color = Colors.Eigengrau, radius = radius, center = pos)
160 drawCircle(color = color, radius = radius, center = pos, style = Stroke(2f / zoom))
161 }
162 }
163
drawStarnull164 fun ZoomedDrawScope.drawStar(star: Star) {
165 translate(star.pos.x, star.pos.y) {
166 drawCircle(color = star.color, radius = star.radius, center = Vec2.Zero)
167
168 if (DRAW_STAR_GRAVITATIONAL_FIELDS) this@drawStar.drawGravitationalField(star)
169
170 rotateRad(radians = star.anim / 23f * PI2f, pivot = Vec2.Zero) {
171 drawPath(
172 path =
173 createStar(
174 radius1 = star.radius + 80,
175 radius2 = star.radius + 250,
176 points = STAR_POINTS
177 ),
178 color = star.color,
179 style =
180 Stroke(
181 width = 3f / this@drawStar.zoom,
182 pathEffect = PathEffect.cornerPathEffect(radius = 200f)
183 )
184 )
185 }
186 rotateRad(radians = star.anim / -19f * PI2f, pivot = Vec2.Zero) {
187 drawPath(
188 path =
189 createStar(
190 radius1 = star.radius + 20,
191 radius2 = star.radius + 200,
192 points = STAR_POINTS + 1
193 ),
194 color = star.color,
195 style =
196 Stroke(
197 width = 3f / this@drawStar.zoom,
198 pathEffect = PathEffect.cornerPathEffect(radius = 200f)
199 )
200 )
201 }
202 }
203 }
204
205 val spaceshipPath =
<lambda>null206 Path().apply {
207 parseSvgPathData(
208 """
209 M11.853 0
210 C11.853 -4.418 8.374 -8 4.083 -8
211 L-5.5 -8
212 C-6.328 -8 -7 -7.328 -7 -6.5
213 C-7 -5.672 -6.328 -5 -5.5 -5
214 L-2.917 -5
215 C-1.26 -5 0.083 -3.657 0.083 -2
216 L0.083 2
217 C0.083 3.657 -1.26 5 -2.917 5
218 L-5.5 5
219 C-6.328 5 -7 5.672 -7 6.5
220 C-7 7.328 -6.328 8 -5.5 8
221 L4.083 8
222 C8.374 8 11.853 4.418 11.853 0
223 Z
224 """
225 )
226 }
<lambda>null227 val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-4f, 0f)) }
228
ZoomedDrawScopenull229 fun ZoomedDrawScope.drawSpacecraft(ship: Spacecraft) {
230 with(ship) {
231 rotateRad(angle, pivot = pos) {
232 translate(pos.x, pos.y) {
233 // drawPath(
234 // path = createStar(200f, 100f, 3),
235 // color = Color.White,
236 // style = Stroke(width = 2f / zoom)
237 // )
238 drawPath(path = spaceshipPath, color = Colors.Eigengrau) // fauxpaque
239 drawPath(
240 path = spaceshipPath,
241 color = if (transit) Color.Black else Color.White,
242 style = Stroke(width = 2f / this@drawSpacecraft.zoom)
243 )
244 if (thrust != Vec2.Zero) {
245 drawPath(
246 path = thrustPath,
247 color = Color(0xFFFF8800),
248 style =
249 Stroke(
250 width = 2f / this@drawSpacecraft.zoom,
251 pathEffect = PathEffect.cornerPathEffect(radius = 1f)
252 )
253 )
254 }
255 // drawRect(
256 // topLeft = Offset(-1f, -1f),
257 // size = Size(2f, 2f),
258 // color = Color.Cyan,
259 // style = Stroke(width = 2f / zoom)
260 // )
261 // drawLine(
262 // start = Vec2.Zero,
263 // end = Vec2(20f, 0f),
264 // color = Color.Cyan,
265 // strokeWidth = 2f / zoom
266 // )
267 }
268 }
269 // // DEBUG: draw velocity vector
270 // drawLine(
271 // start = pos,
272 // end = pos + velocity,
273 // color = Color.Red,
274 // strokeWidth = 3f / zoom
275 // )
276 drawTrack(track)
277 }
278 }
279
ZoomedDrawScopenull280 fun ZoomedDrawScope.drawLanding(landing: Landing) {
281 val v = landing.planet.pos + Vec2.makeWithAngleMag(landing.angle, landing.planet.radius)
282 drawLine(Color.Red, v + Vec2(-5f, -5f), v + Vec2(5f, 5f), strokeWidth = 1f / zoom)
283 drawLine(Color.Red, v + Vec2(5f, -5f), v + Vec2(-5f, 5f), strokeWidth = 1f / zoom)
284 }
285
ZoomedDrawScopenull286 fun ZoomedDrawScope.drawSpark(spark: Spark) {
287 with(spark) {
288 if (lifetime < 0) return
289 when (style) {
290 Spark.Style.LINE ->
291 if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size)
292 Spark.Style.LINE_ABSOLUTE ->
293 if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size / zoom)
294 Spark.Style.DOT -> drawCircle(color, size, pos)
295 Spark.Style.DOT_ABSOLUTE -> drawCircle(color, size, pos / zoom)
296 Spark.Style.RING -> drawCircle(color, size, pos, style = Stroke(width = 1f / zoom))
297 // drawPoints(listOf(pos), PointMode.Points, color, strokeWidth = 2f/zoom)
298 // drawCircle(color, 2f/zoom, pos)
299 }
300 // drawCircle(Color.Gray, center = pos, radius = 1.5f / zoom)
301 }
302 }
303
ZoomedDrawScopenull304 fun ZoomedDrawScope.drawTrack(track: Track) {
305 with(track) {
306 if (SIMPLE_TRACK_DRAWING) {
307 drawPoints(
308 positions,
309 pointMode = PointMode.Lines,
310 color = Color.Green,
311 strokeWidth = 1f / zoom
312 )
313 // if (positions.size < 2) return
314 // drawPath(Path()
315 // .apply {
316 // val p = positions[positions.size - 1]
317 // moveTo(p.x, p.y)
318 // positions.reversed().subList(1, positions.size).forEach { p ->
319 // lineTo(p.x, p.y)
320 // }
321 // },
322 // color = Color.Green, style = Stroke(1f/zoom))
323 } else {
324 if (positions.size < 2) return
325 var prev: Vec2 = positions[positions.size - 1]
326 var a = 0.5f
327 positions.reversed().subList(1, positions.size).forEach { pos ->
328 drawLine(Color(0f, 1f, 0f, a), prev, pos, strokeWidth = max(1f, 1f / zoom))
329 prev = pos
330 a = clamp((a - 1f / TRACK_LENGTH), 0f, 1f)
331 }
332 }
333 }
334 }
335