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