blob: 24b9c6a283c2d23f0129c9690335d33d2b6b0fb1 [file] [log] [blame]
/*
* 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)
}
}
}
}