| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.egg.landroid |
| |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.graphics.Path |
| import androidx.compose.ui.graphics.PathEffect |
| import androidx.compose.ui.graphics.PointMode |
| import androidx.compose.ui.graphics.drawscope.DrawScope |
| import androidx.compose.ui.graphics.drawscope.Stroke |
| import androidx.compose.ui.graphics.drawscope.rotateRad |
| import androidx.compose.ui.graphics.drawscope.scale |
| import androidx.compose.ui.graphics.drawscope.translate |
| import androidx.compose.ui.util.lerp |
| import androidx.core.math.MathUtils.clamp |
| import java.lang.Float.max |
| import kotlin.math.sqrt |
| |
| const val DRAW_ORBITS = true |
| const val DRAW_GRAVITATIONAL_FIELDS = true |
| const val DRAW_STAR_GRAVITATIONAL_FIELDS = true |
| |
| val STAR_POINTS = android.os.Build.VERSION.SDK_INT.takeIf { it in 1..99 } ?: 31 |
| |
| /** |
| * A zoomedDrawScope is one that is scaled, but remembers its zoom level, so you can correct for it |
| * if you want to draw single-pixel lines. Which we do. |
| */ |
| interface ZoomedDrawScope : DrawScope { |
| val zoom: Float |
| } |
| |
| fun DrawScope.zoom(zoom: Float, block: ZoomedDrawScope.() -> Unit) { |
| val ds = |
| object : ZoomedDrawScope, DrawScope by this { |
| override var zoom = zoom |
| } |
| ds.scale(zoom) { block(ds) } |
| } |
| |
| class VisibleUniverse(namer: Namer, randomSeed: Long) : Universe(namer, randomSeed) { |
| // Magic variable. Every time we update it, Compose will notice and redraw the universe. |
| val triggerDraw = mutableStateOf(0L) |
| |
| fun simulateAndDrawFrame(nanos: Long) { |
| // By writing this value, Compose will look for functions that read it (like drawZoomed). |
| triggerDraw.value = nanos |
| |
| step(nanos) |
| } |
| } |
| |
| fun ZoomedDrawScope.drawUniverse(universe: VisibleUniverse) { |
| with(universe) { |
| triggerDraw.value // Please recompose when this value changes. |
| |
| // star.drawZoomed(ds, zoom) |
| // planets.forEach { p -> |
| // p.drawZoomed(ds, zoom) |
| // if (p == follow) { |
| // drawCircle(Color.Red, 20f / zoom, p.pos) |
| // } |
| // } |
| // |
| // ship.drawZoomed(ds, zoom) |
| |
| constraints.forEach { |
| when (it) { |
| is Landing -> drawLanding(it) |
| is Container -> drawContainer(it) |
| } |
| } |
| drawStar(star) |
| entities.forEach { |
| if (it === ship || it === star) return@forEach // draw the ship last |
| when (it) { |
| is Spacecraft -> drawSpacecraft(it) |
| is Spark -> drawSpark(it) |
| is Planet -> drawPlanet(it) |
| } |
| } |
| drawSpacecraft(ship) |
| } |
| } |
| |
| fun ZoomedDrawScope.drawContainer(container: Container) { |
| drawCircle( |
| color = Color(0xFF800000), |
| radius = container.radius, |
| center = Vec2.Zero, |
| style = |
| Stroke( |
| width = 1f / zoom, |
| pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f / zoom, 8f / zoom), 0f) |
| ) |
| ) |
| // val path = Path().apply { |
| // fillType = PathFillType.EvenOdd |
| // addOval(Rect(center = Vec2.Zero, radius = container.radius)) |
| // addOval(Rect(center = Vec2.Zero, radius = container.radius + 10_000)) |
| // } |
| // drawPath( |
| // path = path, |
| // |
| // ) |
| } |
| |
| fun ZoomedDrawScope.drawGravitationalField(planet: Planet) { |
| val rings = 8 |
| for (i in 0 until rings) { |
| val force = |
| lerp( |
| 200f, |
| 0.01f, |
| i.toFloat() / rings |
| ) // first rings at force = 1N, dropping off after that |
| val r = sqrt(GRAVITATION * planet.mass * SPACECRAFT_MASS / force) |
| drawCircle( |
| color = Color(1f, 0f, 0f, lerp(0.5f, 0.1f, i.toFloat() / rings)), |
| center = planet.pos, |
| style = Stroke(2f / zoom), |
| radius = r |
| ) |
| } |
| } |
| |
| fun ZoomedDrawScope.drawPlanet(planet: Planet) { |
| with(planet) { |
| if (DRAW_ORBITS) |
| drawCircle( |
| color = Color(0x8000FFFF), |
| radius = pos.distance(orbitCenter), |
| center = orbitCenter, |
| style = |
| Stroke( |
| width = 1f / zoom, |
| ) |
| ) |
| |
| if (DRAW_GRAVITATIONAL_FIELDS) { |
| drawGravitationalField(this) |
| } |
| |
| drawCircle(color = Colors.Eigengrau, radius = radius, center = pos) |
| drawCircle(color = color, radius = radius, center = pos, style = Stroke(2f / zoom)) |
| } |
| } |
| |
| fun ZoomedDrawScope.drawStar(star: Star) { |
| translate(star.pos.x, star.pos.y) { |
| drawCircle(color = star.color, radius = star.radius, center = Vec2.Zero) |
| |
| if (DRAW_STAR_GRAVITATIONAL_FIELDS) this@drawStar.drawGravitationalField(star) |
| |
| rotateRad(radians = star.anim / 23f * PI2f, pivot = Vec2.Zero) { |
| drawPath( |
| path = |
| createStar( |
| radius1 = star.radius + 80, |
| radius2 = star.radius + 250, |
| points = STAR_POINTS |
| ), |
| color = star.color, |
| style = |
| Stroke( |
| width = 3f / this@drawStar.zoom, |
| pathEffect = PathEffect.cornerPathEffect(radius = 200f) |
| ) |
| ) |
| } |
| rotateRad(radians = star.anim / -19f * PI2f, pivot = Vec2.Zero) { |
| drawPath( |
| path = |
| createStar( |
| radius1 = star.radius + 20, |
| radius2 = star.radius + 200, |
| points = STAR_POINTS + 1 |
| ), |
| color = star.color, |
| style = |
| Stroke( |
| width = 3f / this@drawStar.zoom, |
| pathEffect = PathEffect.cornerPathEffect(radius = 200f) |
| ) |
| ) |
| } |
| } |
| } |
| |
| val spaceshipPath = |
| Path().apply { |
| parseSvgPathData( |
| """ |
| M11.853 0 |
| C11.853 -4.418 8.374 -8 4.083 -8 |
| L-5.5 -8 |
| C-6.328 -8 -7 -7.328 -7 -6.5 |
| C-7 -5.672 -6.328 -5 -5.5 -5 |
| L-2.917 -5 |
| C-1.26 -5 0.083 -3.657 0.083 -2 |
| L0.083 2 |
| C0.083 3.657 -1.26 5 -2.917 5 |
| L-5.5 5 |
| C-6.328 5 -7 5.672 -7 6.5 |
| C-7 7.328 -6.328 8 -5.5 8 |
| L4.083 8 |
| C8.374 8 11.853 4.418 11.853 0 |
| Z |
| """ |
| ) |
| } |
| val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-4f, 0f)) } |
| |
| fun ZoomedDrawScope.drawSpacecraft(ship: Spacecraft) { |
| with(ship) { |
| rotateRad(angle, pivot = pos) { |
| translate(pos.x, pos.y) { |
| // drawPath( |
| // path = createStar(200f, 100f, 3), |
| // color = Color.White, |
| // style = Stroke(width = 2f / zoom) |
| // ) |
| drawPath(path = spaceshipPath, color = Colors.Eigengrau) // fauxpaque |
| drawPath( |
| path = spaceshipPath, |
| color = if (transit) Color.Black else Color.White, |
| style = Stroke(width = 2f / this@drawSpacecraft.zoom) |
| ) |
| if (thrust != Vec2.Zero) { |
| drawPath( |
| path = thrustPath, |
| color = Color(0xFFFF8800), |
| style = |
| Stroke( |
| width = 2f / this@drawSpacecraft.zoom, |
| pathEffect = PathEffect.cornerPathEffect(radius = 1f) |
| ) |
| ) |
| } |
| // drawRect( |
| // topLeft = Offset(-1f, -1f), |
| // size = Size(2f, 2f), |
| // color = Color.Cyan, |
| // style = Stroke(width = 2f / zoom) |
| // ) |
| // drawLine( |
| // start = Vec2.Zero, |
| // end = Vec2(20f, 0f), |
| // color = Color.Cyan, |
| // strokeWidth = 2f / zoom |
| // ) |
| } |
| } |
| // // DEBUG: draw velocity vector |
| // drawLine( |
| // start = pos, |
| // end = pos + velocity, |
| // color = Color.Red, |
| // strokeWidth = 3f / zoom |
| // ) |
| drawTrack(track) |
| } |
| } |
| |
| fun ZoomedDrawScope.drawLanding(landing: Landing) { |
| val v = landing.planet.pos + Vec2.makeWithAngleMag(landing.angle, landing.planet.radius) |
| drawLine(Color.Red, v + Vec2(-5f, -5f), v + Vec2(5f, 5f), strokeWidth = 1f / zoom) |
| drawLine(Color.Red, v + Vec2(5f, -5f), v + Vec2(-5f, 5f), strokeWidth = 1f / zoom) |
| } |
| |
| fun ZoomedDrawScope.drawSpark(spark: Spark) { |
| with(spark) { |
| if (lifetime < 0) return |
| when (style) { |
| Spark.Style.LINE -> |
| if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size) |
| Spark.Style.LINE_ABSOLUTE -> |
| if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size / zoom) |
| Spark.Style.DOT -> drawCircle(color, size, pos) |
| Spark.Style.DOT_ABSOLUTE -> drawCircle(color, size, pos / zoom) |
| Spark.Style.RING -> drawCircle(color, size, pos, style = Stroke(width = 1f / zoom)) |
| // drawPoints(listOf(pos), PointMode.Points, color, strokeWidth = 2f/zoom) |
| // drawCircle(color, 2f/zoom, pos) |
| } |
| // drawCircle(Color.Gray, center = pos, radius = 1.5f / zoom) |
| } |
| } |
| |
| fun ZoomedDrawScope.drawTrack(track: Track) { |
| with(track) { |
| if (SIMPLE_TRACK_DRAWING) { |
| drawPoints( |
| positions, |
| pointMode = PointMode.Lines, |
| color = Color.Green, |
| strokeWidth = 1f / zoom |
| ) |
| // if (positions.size < 2) return |
| // drawPath(Path() |
| // .apply { |
| // val p = positions[positions.size - 1] |
| // moveTo(p.x, p.y) |
| // positions.reversed().subList(1, positions.size).forEach { p -> |
| // lineTo(p.x, p.y) |
| // } |
| // }, |
| // color = Color.Green, style = Stroke(1f/zoom)) |
| } else { |
| if (positions.size < 2) return |
| var prev: Vec2 = positions[positions.size - 1] |
| var a = 0.5f |
| positions.reversed().subList(1, positions.size).forEach { pos -> |
| drawLine(Color(0f, 1f, 0f, a), prev, pos, strokeWidth = max(1f, 1f / zoom)) |
| prev = pos |
| a = clamp((a - 1f / TRACK_LENGTH), 0f, 1f) |
| } |
| } |
| } |
| } |