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 com.android.egg.flags.Flags.flagFlag
32 import java.lang.Float.max
33 import kotlin.math.exp
34 import kotlin.math.sqrt
35 
36 const val DRAW_ORBITS = true
37 const val DRAW_GRAVITATIONAL_FIELDS = true
38 const val DRAW_STAR_GRAVITATIONAL_FIELDS = true
39 
40 val STAR_POINTS = android.os.Build.VERSION.SDK_INT.takeIf { it in 1..99 } ?: 31
41 
42 /**
43  * A zoomedDrawScope is one that is scaled, but remembers its zoom level, so you can correct for it
44  * if you want to draw single-pixel lines. Which we do.
45  */
46 interface ZoomedDrawScope : DrawScope {
47     val zoom: Float
48 }
49 
DrawScopenull50 fun DrawScope.zoom(zoom: Float, block: ZoomedDrawScope.() -> Unit) {
51     val ds =
52         object : ZoomedDrawScope, DrawScope by this {
53             override var zoom = zoom
54         }
55     ds.scale(zoom) { block(ds) }
56 }
57 
58 class VisibleUniverse(namer: Namer, randomSeed: Long) : Universe(namer, randomSeed) {
59     // Magic variable. Every time we update it, Compose will notice and redraw the universe.
60     val triggerDraw = mutableStateOf(0L)
61 
simulateAndDrawFramenull62     fun simulateAndDrawFrame(nanos: Long) {
63         // By writing this value, Compose will look for functions that read it (like drawZoomed).
64         triggerDraw.value = nanos
65 
66         step(nanos)
67     }
68 }
69 
ZoomedDrawScopenull70 fun ZoomedDrawScope.drawUniverse(universe: VisibleUniverse) {
71     with(universe) {
72         triggerDraw.value // Please recompose when this value changes.
73 
74         constraints.forEach {
75             when (it) {
76                 is Landing -> drawLanding(it)
77                 is Container -> drawContainer(it)
78             }
79         }
80         drawStar(star)
81         entities.forEach {
82             if (it === star) return@forEach // don't draw the star as a planet
83             when (it) {
84                 is Spark -> drawSpark(it)
85                 is Planet -> drawPlanet(it)
86                 else -> Unit // draw these at a different time, or not at all
87             }
88         }
89         ship.autopilot?.let { drawAutopilot(it) }
90         drawSpacecraft(ship)
91     }
92 }
93 
ZoomedDrawScopenull94 fun ZoomedDrawScope.drawContainer(container: Container) {
95     drawCircle(
96         color = Color(0xFF800000),
97         radius = container.radius,
98         center = Vec2.Zero,
99         style =
100             Stroke(
101                 width = 1f / zoom,
102                 pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f / zoom, 8f / zoom), 0f)
103             )
104     )
105 }
106 
ZoomedDrawScopenull107 fun ZoomedDrawScope.drawGravitationalField(planet: Planet) {
108     val rings = 8
109     for (i in 0 until rings) {
110         val force =
111             lerp(
112                 200f,
113                 0.01f,
114                 i.toFloat() / rings
115             ) // first rings at force = 1N, dropping off after that
116         val r = sqrt(GRAVITATION * planet.mass * SPACECRAFT_MASS / force)
117         drawCircle(
118             color = Color(1f, 0f, 0f, lerp(0.5f, 0.1f, i.toFloat() / rings)),
119             center = planet.pos,
120             style = Stroke(2f / zoom),
121             radius = r
122         )
123     }
124 }
125 
ZoomedDrawScopenull126 fun ZoomedDrawScope.drawPlanet(planet: Planet) {
127     with(planet) {
128         if (DRAW_ORBITS)
129             drawCircle(
130                 color = Color(0x8000FFFF),
131                 radius = pos.distance(orbitCenter),
132                 center = orbitCenter,
133                 style =
134                     Stroke(
135                         width = 1f / zoom,
136                     )
137             )
138 
139         if (DRAW_GRAVITATIONAL_FIELDS) {
140             drawGravitationalField(this)
141         }
142 
143         drawCircle(color = Colors.Eigengrau, radius = radius, center = pos)
144         drawCircle(color = color, radius = radius, center = pos, style = Stroke(2f / zoom))
145     }
146 }
147 
drawStarnull148 fun ZoomedDrawScope.drawStar(star: Star) {
149     translate(star.pos.x, star.pos.y) {
150         drawCircle(color = star.color, radius = star.radius, center = Vec2.Zero)
151 
152         if (DRAW_STAR_GRAVITATIONAL_FIELDS) this@drawStar.drawGravitationalField(star)
153 
154         rotateRad(radians = star.anim / 23f * PI2f, pivot = Vec2.Zero) {
155             drawPath(
156                 path =
157                     createStar(
158                         radius1 = star.radius + 80,
159                         radius2 = star.radius + 250,
160                         points = STAR_POINTS
161                     ),
162                 color = star.color,
163                 style =
164                     Stroke(
165                         width = 3f / this@drawStar.zoom,
166                         pathEffect = PathEffect.cornerPathEffect(radius = 200f)
167                     )
168             )
169         }
170         rotateRad(radians = star.anim / -19f * PI2f, pivot = Vec2.Zero) {
171             drawPath(
172                 path =
173                     createStar(
174                         radius1 = star.radius + 20,
175                         radius2 = star.radius + 200,
176                         points = STAR_POINTS + 1
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     }
187 }
188 
189 val spaceshipPath =
<lambda>null190     Path().apply {
191         parseSvgPathData(
192             """
193 M11.853 0
194 C11.853 -4.418 8.374 -8 4.083 -8
195 L-5.5 -8
196 C-6.328 -8 -7 -7.328 -7 -6.5
197 C-7 -5.672 -6.328 -5 -5.5 -5
198 L-2.917 -5
199 C-1.26 -5 0.083 -3.657 0.083 -2
200 L0.083 2
201 C0.083 3.657 -1.26 5 -2.917 5
202 L-5.5 5
203 C-6.328 5 -7 5.672 -7 6.5
204 C-7 7.328 -6.328 8 -5.5 8
205 L4.083 8
206 C8.374 8 11.853 4.418 11.853 0
207 Z
208 """
209         )
210     }
211 val spaceshipLegs =
<lambda>null212     Path().apply {
213         parseSvgPathData(
214             """
215 M-7   -6.5
216 l-3.5  0
217 l-1   -2
218 l 0    4
219 l 1   -2
220 Z
221 M-7    6.5
222 l-3.5  0
223 l-1   -2
224 l 0    4
225 l 1   -2
226 Z
227 """
228         )
229     }
<lambda>null230 val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-5f, 0f)) }
231 
ZoomedDrawScopenull232 fun ZoomedDrawScope.drawSpacecraft(ship: Spacecraft) {
233     with(ship) {
234         rotateRad(angle, pivot = pos) {
235             translate(pos.x, pos.y) {
236                 // new in V: little landing legs
237                 ship.landing?.let {
238                     drawPath(
239                         path = spaceshipLegs,
240                         color = Color(0xFFCCCCCC),
241                         style = Stroke(width = 2f / this@drawSpacecraft.zoom)
242                     )
243                 }
244                 // draw the ship
245                 drawPath(path = spaceshipPath, color = Colors.Eigengrau) // fauxpaque
246                 drawPath(
247                     path = spaceshipPath,
248                     color = if (transit) Color.Black else Color.White,
249                     style = Stroke(width = 2f / this@drawSpacecraft.zoom)
250                 )
251                 // draw thrust
252                 if (thrust != Vec2.Zero) {
253                     drawPath(
254                         path = thrustPath,
255                         color = Color(0xFFFF8800),
256                         style =
257                             Stroke(
258                                 width = 2f / this@drawSpacecraft.zoom,
259                                 pathEffect = PathEffect.cornerPathEffect(radius = 1f)
260                             )
261                     )
262                 }
263             }
264         }
265         drawTrack(track)
266     }
267 }
268 
ZoomedDrawScopenull269 fun ZoomedDrawScope.drawLanding(landing: Landing) {
270     val v = landing.planet.pos + Vec2.makeWithAngleMag(landing.angle, landing.planet.radius)
271 
272     if (flagFlag()) {
273         val strokeWidth = 2f / zoom
274         val height = 80f
275         rotateRad(landing.angle, pivot = v) {
276             translate(v.x, v.y) {
277                 val flagPath =
278                     Path().apply {
279                         moveTo(0f, 0f)
280                         lineTo(height, 0f)
281                         lineTo(height * 0.875f, height * 0.25f)
282                         lineTo(height * 0.75f, 0f)
283                         close()
284                     }
285                 drawPath(flagPath, Colors.Flag, style = Stroke(width = strokeWidth))
286             }
287         }
288     }
289 }
290 
ZoomedDrawScopenull291 fun ZoomedDrawScope.drawSpark(spark: Spark) {
292     with(spark) {
293         if (fuse.lifetime < 0) return
294         val life = 1f - fuse.lifetime / ttl
295         when (style) {
296             Spark.Style.LINE ->
297                 if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size)
298             Spark.Style.LINE_ABSOLUTE ->
299                 if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size / zoom)
300             Spark.Style.DOT -> drawCircle(color, size, pos)
301             Spark.Style.DOT_ABSOLUTE -> drawCircle(color, size, pos / zoom)
302             Spark.Style.RING ->
303                 drawCircle(
304                     color = color.copy(alpha = color.alpha * (1f - life)),
305                     radius = exp(lerp(size, 3f * size, life)) - 1f,
306                     center = pos,
307                     style = Stroke(width = 1f / zoom)
308                 )
309         }
310     }
311 }
312 
ZoomedDrawScopenull313 fun ZoomedDrawScope.drawTrack(track: Track) {
314     with(track) {
315         if (SIMPLE_TRACK_DRAWING) {
316             drawPoints(
317                 positions,
318                 pointMode = PointMode.Lines,
319                 color = Colors.Track,
320                 strokeWidth = 1f / zoom
321             )
322         } else {
323             if (positions.size < 2) return
324             var prev: Vec2 = positions[positions.size - 1]
325             var a = 0.5f
326             positions.reversed().subList(1, positions.size).forEach { pos ->
327                 drawLine(Color(0f, 1f, 0f, a), prev, pos, strokeWidth = max(1f, 1f / zoom))
328                 prev = pos
329                 a = clamp((a - 1f / TRACK_LENGTH), 0f, 1f)
330             }
331         }
332     }
333 }
334 
ZoomedDrawScopenull335 fun ZoomedDrawScope.drawAutopilot(autopilot: Autopilot) {
336     val color = Colors.Autopilot.copy(alpha = 0.5f)
337 
338     autopilot.target?.let { target ->
339         val zoom = zoom
340         rotateRad(autopilot.universe.now * PI2f / 10f, target.pos) {
341             translate(target.pos.x, target.pos.y) {
342                 drawPath(
343                     path =
344                         createPolygon(
345                             radius = target.radius + autopilot.brakingDistance,
346                             sides = 15 // Autopilot introduced in Android 15
347                         ),
348                     color = color,
349                     style = Stroke(1f / zoom)
350                 )
351                 drawCircle(
352                     color,
353                     radius = target.radius + autopilot.landingAltitude / 2,
354                     center = Vec2.Zero,
355                     alpha = 0.25f,
356                     style = Stroke(autopilot.landingAltitude)
357                 )
358             }
359         }
360         drawLine(
361             color,
362             start = autopilot.ship.pos,
363             end = autopilot.leadingPos,
364             strokeWidth = 1f / zoom
365         )
366         drawCircle(
367             color,
368             radius = 5f / zoom,
369             center = autopilot.leadingPos,
370             style = Stroke(1f / zoom)
371         )
372     }
373 }
374