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