1 /*
2  * 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 
48 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 
60     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 
68 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 
101 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 
123 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 
142 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 
164 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 =
206     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     }
227 val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-4f, 0f)) }
228 
229 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 
280 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 
286 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 
304 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