ANDROID 15 MISSION UPDATE -- "VANILLA ICE CREAM"
// UPDATES
+ IDLE DIVERSIONS
+ "AI" AUTOPILOT
+ TYPOGRAPHIC COMPRESSION
+ OCCUPATIONAL DESCRIPTIONS
+ RENDERING TO THE EDGE OF SPACE
+ FLAG SUPPORT: LANDED
// TECHNICAL NOTE //
This brings back the old enable-components trick; once
the egg has been invoked, the daydream mode will be
unlocked for use and will appear in Settings > Display >
Screen saver, or wherever you get your Dreams.
A PRODUCT OF YOYODROID PROPULSION SYSTEMS
"WHERE INTEGRATION COMPLETE IS TOMORROW!"
Fixes: 330919557
Bug: 320695719
Flag: com.android.egg.flags.flag_flag
Test: easter egg main entry point:
adb shell am start -n android/com.android.internal.app.PlatLogoActivity
Test: interactive mode:
adb shell am start -n com.android.egg/.landroid.MainActivity
Test: screensaver mode:
# first, enable the component if you haven't gone
# through the main entry point
adb root ; adb shell pm enable com.android.egg/.landroid.DreamUniverse
# select the screensaver
adb shell settings put secure screensaver_components \
com.android.egg.landroid/com.android.egg.landroid.DreamUniverse
# start dreaming
adb shell service call dreams 1
Change-Id: I57a8834ce7f3d41aacaa56133e6f512cf81f5346
diff --git a/core/java/com/android/internal/app/PlatLogoActivity.java b/core/java/com/android/internal/app/PlatLogoActivity.java
index 71bbccb..b8f7a3d 100644
--- a/core/java/com/android/internal/app/PlatLogoActivity.java
+++ b/core/java/com/android/internal/app/PlatLogoActivity.java
@@ -69,7 +69,7 @@
private static final long LAUNCH_TIME = 5000L;
- private static final String U_EGG_UNLOCK_SETTING = "egg_mode_u";
+ private static final String EGG_UNLOCK_SETTING = "egg_mode_v";
private static final float MIN_WARP = 1f;
private static final float MAX_WARP = 10f; // after all these years
@@ -309,13 +309,12 @@
private void launchNextStage(boolean locked) {
final ContentResolver cr = getContentResolver();
-
try {
if (shouldWriteSettings()) {
Log.v(TAG, "Saving egg locked=" + locked);
syncTouchPressure();
Settings.System.putLong(cr,
- U_EGG_UNLOCK_SETTING,
+ EGG_UNLOCK_SETTING,
locked ? 0 : System.currentTimeMillis());
}
} catch (RuntimeException e) {
@@ -499,4 +498,4 @@
mDt = dt;
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index d1db237..1500583 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -36,8 +36,28 @@
android:icon="@drawable/android14_patch_adaptive"
android:label="@string/app_name">
- <!-- Android U easter egg -->
+ <!-- Android V easter egg: Daydream version of Landroid
+ (must be enabled by unlocking the egg) -->
+ <service
+ android:name=".landroid.DreamUniverse"
+ android:exported="true"
+ android:icon="@drawable/android14_patch_adaptive"
+ android:label="@string/v_egg_name"
+ android:description="@string/dream_description"
+ android:enabled="false"
+ android:permission="android.permission.BIND_DREAM_SERVICE"
+ >
+ <intent-filter>
+ <action android:name="android.service.dreams.DreamService" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data
+ android:name="android.service.dream"
+ android:resource="@xml/landroid_dream"/>
+ </service>
+
+ <!-- Android U easter egg -->
<activity
android:name=".landroid.MainActivity"
android:exported="true"
@@ -52,7 +72,6 @@
</intent-filter>
</activity>
-
<!-- Android Q easter egg -->
<activity
android:name=".quares.QuaresActivity"
diff --git a/packages/EasterEgg/res/values/landroid_strings.xml b/packages/EasterEgg/res/values/landroid_strings.xml
index 1394f2f..1bbfcca 100644
--- a/packages/EasterEgg/res/values/landroid_strings.xml
+++ b/packages/EasterEgg/res/values/landroid_strings.xml
@@ -1,21 +1,13 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- 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.
--->
-
+<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string name="u_egg_name" translatable="false">Android 14 Easter Egg</string>
+
+ <!-- No Android's Sky -->
+ <!-- Char Star Field -->
+ <!-- V-leet: Harmless -->
+ <!-- Contemplating My Orbital Mechanics -->
+ <string name="u_egg_name" translatable="false">Landroid</string>
+ <string name="v_egg_name" translatable="false">Landroid</string>
+ <string name="dream_description" translatable="false">---- AUTOPILOT ENGAGED ----</string>
<string-array name="planet_descriptors" translatable="false">
<item>earthy</item>
@@ -365,7 +357,64 @@
<item>relaxed</item>
<item>skunky</item>
<item>breezy</item>
- <item>soup </item>
+ <item>soup</item>
+ </string-array>
+
+ <string-array name="fauna_generic_plurals" translatable="false">
+ <item>fauna</item>
+ <item>animals</item>
+ <item>locals</item>
+ <item>creatures</item>
+ <item>critters</item>
+ <item>wildlife</item>
+ <item>specimens</item>
+ <item>life</item>
+ <item>cells</item>
+ </string-array>
+
+ <string-array name="flora_generic_plurals" translatable="false">
+ <item>flora</item>
+ <item>plants</item>
+ <item>flowers</item>
+ <item>trees</item>
+ <item>mosses</item>
+ <item>specimens</item>
+ <item>life</item>
+ <item>cells</item>
+ </string-array>
+
+ <string-array name="atmo_generic_plurals" translatable="false">
+ <item>air</item>
+ <item>atmosphere</item>
+ <item>clouds</item>
+ <item>atmo</item>
+ <item>gases</item>
+ </string-array>
+
+ <string-array name="activities" translatable="false">
+ <item>refueling</item>
+ <item>sightseeing</item>
+ <item>vacationing</item>
+ <item>luncheoning</item>
+ <item>recharging</item>
+ <item>taking up space</item>
+ <item>reticulating space splines</item>
+ <item>using facilities</item>
+ <item>spelunking</item>
+ <item>repairing</item>
+ <item>herding {fauna}</item>
+ <item>taming {fauna}</item>
+ <item>breeding {fauna}</item>
+ <item>singing lullabies to {fauna}</item>
+ <item>singing lullabies to {flora}</item>
+ <item>singing lullabies to the {planet}</item>
+ <item>gardening {flora}</item>
+ <item>collecting {flora}</item>
+ <item>surveying the {planet}</item>
+ <item>mapping the {planet}</item>
+ <item>breathing {atmo}</item>
+ <item>reprocessing {atmo}</item>
+ <item>bottling {atmo}</item>
</string-array>
</resources>
diff --git a/packages/EasterEgg/res/xml/landroid_dream.xml b/packages/EasterEgg/res/xml/landroid_dream.xml
new file mode 100644
index 0000000..adf82bd
--- /dev/null
+++ b/packages/EasterEgg/res/xml/landroid_dream.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2024 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.
+-->
+<dream xmlns:android="http://schemas.android.com/apk/res/android"
+ android:previewImage="@*android:drawable/platlogo" />
diff --git a/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java b/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java
index 5820b5a..30320d6 100644
--- a/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java
+++ b/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java
@@ -18,11 +18,14 @@
import android.app.Activity;
import android.content.ComponentName;
+import android.content.Context;
import android.content.pm.PackageManager;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
+import com.android.egg.flags.Flags;
+import com.android.egg.landroid.DreamUniverse;
import com.android.egg.neko.NekoControlsService;
import com.android.egg.widget.PaintChipsActivity;
import com.android.egg.widget.PaintChipsWidget;
@@ -33,7 +36,9 @@
public class ComponentActivationActivity extends Activity {
private static final String TAG = "EasterEgg";
+ // check PlatLogoActivity.java for these
private static final String S_EGG_UNLOCK_SETTING = "egg_mode_s";
+ private static final String V_EGG_UNLOCK_SETTING = "egg_mode_v";
private void toastUp(String s) {
Toast toast = Toast.makeText(this, s, Toast.LENGTH_SHORT);
@@ -44,14 +49,39 @@
public void onStart() {
super.onStart();
- final PackageManager pm = getPackageManager();
- final ComponentName[] cns = new ComponentName[] {
- new ComponentName(this, NekoControlsService.class),
- new ComponentName(this, PaintChipsActivity.class),
- new ComponentName(this, PaintChipsWidget.class)
- };
- final long unlockValue = Settings.System.getLong(getContentResolver(),
- S_EGG_UNLOCK_SETTING, 0);
+ lockUnlockComponents(this);
+
+ finish();
+ }
+
+ /**
+ * Check easter egg unlock state and update unlockable components to match.
+ */
+ public static void lockUnlockComponents(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ final ComponentName[] cns;
+ final String unlockSettingsKey;
+ final boolean shouldReLock;
+ final long unlockValue;
+ if (Flags.flagFlag()) {
+ unlockSettingsKey = V_EGG_UNLOCK_SETTING;
+ unlockValue = 1; // since we're not toggling we actually don't need to check the setting
+ shouldReLock = false;
+ cns = new ComponentName[]{
+ new ComponentName(context, DreamUniverse.class)
+ };
+ } else {
+ unlockSettingsKey = S_EGG_UNLOCK_SETTING;
+ unlockValue = Settings.System.getLong(context.getContentResolver(),
+ unlockSettingsKey, 0);
+ shouldReLock = true;
+ cns = new ComponentName[]{
+ new ComponentName(context, NekoControlsService.class),
+ new ComponentName(context, PaintChipsActivity.class),
+ new ComponentName(context, PaintChipsWidget.class),
+ new ComponentName(context, DreamUniverse.class)
+ };
+ }
for (ComponentName cn : cns) {
final boolean componentEnabled = pm.getComponentEnabledSetting(cn)
== PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
@@ -77,7 +107,5 @@
}
}
}
-
- finish();
}
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
new file mode 100644
index 0000000..f71abee
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 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 kotlin.math.min
+import kotlin.math.sign
+
+class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity {
+ val BRAKING_TIME = 5f
+ val SIGHTSEEING_TIME = 10f
+ val STRATEGY_MIN_TIME = 0.5f
+
+ var enabled = false
+
+ var target: Planet? = null
+
+ var landingAltitude = 0f
+
+ var nextStrategyTime = 0f
+
+ var brakingDistance = 0f
+
+ // used by rendering
+ var leadingPos = Vec2.Zero
+ var leadingVector = Vec2.Zero
+
+ val telemetry: String
+ get() =
+ listOf(
+ "---- AUTOPILOT ENGAGED ----",
+ "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
+ "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
+ )
+ .joinToString("\n")
+
+ private var strategy: String = "NONE"
+ private var debug: String = ""
+
+ override fun update(sim: Simulator, dt: Float) {
+ if (!enabled) return
+
+ if (sim.now < nextStrategyTime) {
+ return
+ }
+
+ val currentStrategy = strategy
+
+ if (ship.landing != null) {
+ if (target != null) {
+ strategy = "LANDED"
+ debug = ""
+ // we just got here. see the sights.
+ target = null
+ landingAltitude = 0f
+ nextStrategyTime = sim.now + SIGHTSEEING_TIME
+ } else {
+ // full power until we blast off
+ ship.thrust = Vec2.makeWithAngleMag(ship.angle, 1f)
+
+ strategy = "LAUNCHING"
+ debug = ""
+ nextStrategyTime = sim.now + 2f
+ }
+ } else {
+ // select new target
+
+ if (target == null) {
+ // testing: target the first planet
+ // target = universe.planets[0]
+
+ // target the nearest unexplored planet
+ target =
+ universe.planets
+ .sortedBy { (it.pos - ship.pos).mag() }
+ .firstOrNull { !it.explored }
+ brakingDistance = 0f
+
+ // if we've explored them all, pick one at random
+ if (target == null) target = universe.planets.random()
+ }
+
+ target?.let { target -> // should be nonnull
+ val shipV = ship.velocity
+ val targetV = target.velocity
+ val targetVector = (target.pos - ship.pos)
+ val altitude = targetVector.mag() - target.radius
+
+ landingAltitude = min(target.radius, 100f)
+
+ // the following is in the moving reference frame of the target
+ val relativeV: Vec2 = shipV - targetV
+ val projection = relativeV.dot(targetVector / targetVector.mag())
+ val relativeSpeed = relativeV.mag() * projection.sign
+ val timeToTarget = if (relativeSpeed != 0f) altitude / relativeSpeed else 1_000f
+
+ val newBrakingDistance =
+ BRAKING_TIME * if (relativeSpeed > 0) relativeSpeed else MAIN_ENGINE_ACCEL
+ brakingDistance =
+ expSmooth(brakingDistance, newBrakingDistance, dt = sim.dt, speed = 5f)
+
+ // We're going to aim at where the target will be, but we want to make sure to
+ // compute
+ leadingPos =
+ target.pos +
+ Vec2.makeWithAngleMag(
+ target.velocity.angle(),
+ min(altitude / 2, target.velocity.mag())
+ )
+ leadingVector = leadingPos - ship.pos
+
+ if (altitude < landingAltitude) {
+ strategy = "LANDING"
+ // Strategy: zero thrust, face away, prepare for landing
+
+ ship.angle = (ship.pos - target.pos).angle() // point away from ground
+ ship.thrust = Vec2.Zero
+ } else {
+ if (relativeSpeed < 0 || altitude > brakingDistance) {
+ strategy = "CHASING"
+ // Strategy: Make tracks. We are either a long way away, or falling behind.
+ ship.angle = leadingVector.angle()
+
+ ship.thrust = Vec2.makeWithAngleMag(ship.angle, 1.0f)
+ } else {
+ strategy = "APPROACHING"
+ // Strategy: Just slow down. If we get caught in the gravity well, it will
+ // gradually start pulling us more in the direction of the planet, which
+ // will create a graceful deceleration
+ ship.angle = (-ship.velocity).angle()
+
+ // We want to bleed off velocity over time. Specifically, relativeSpeed px/s
+ // over timeToTarget seconds.
+ val decel = relativeSpeed / timeToTarget
+ val decelThrust =
+ decel / MAIN_ENGINE_ACCEL * 0.9f // not quite slowing down enough
+ ship.thrust = Vec2.makeWithAngleMag(ship.angle, decelThrust)
+ }
+ }
+ debug = ("DV=%.0f D=%.0f T%+.1f").format(relativeSpeed, altitude, timeToTarget)
+ }
+ if (strategy != currentStrategy) {
+ nextStrategyTime = sim.now + STRATEGY_MIN_TIME
+ }
+ }
+ }
+
+ override fun postUpdate(sim: Simulator, dt: Float) {
+ if (!enabled) return
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
index f5657ae..24c4975 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
@@ -19,11 +19,22 @@
import androidx.compose.ui.graphics.Color
/** Various UI colors. */
-object Colors {
- val Eigengrau = Color(0xFF16161D)
- val Eigengrau2 = Color(0xFF292936)
- val Eigengrau3 = Color(0xFF3C3C4F)
- val Eigengrau4 = Color(0xFFA7A7CA)
+class Colors {
+ object Android {
+ val Green = Color(0xFF34A853)
+ val Blue = Color(0xFF4285F4)
+ val Mint = Color(0xFFE8F5E9)
+ val Chartreuse = Color(0xFFC6FF00)
+ }
+ companion object {
+ val Eigengrau = Color(0xFF16161D)
+ val Eigengrau2 = Color(0xFF292936)
+ val Eigengrau3 = Color(0xFF3C3C4F)
+ val Eigengrau4 = Color(0xFFA7A7CA)
- val Console = Color(0xFFB7B7FF)
+ val Console = Color(0xFFB7B7FF)
+ val Autopilot = Android.Blue
+ val Track = Android.Green
+ val Flag = Android.Chartreuse
+ }
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
new file mode 100644
index 0000000..8c87c5d
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
@@ -0,0 +1,120 @@
+/*
+ * 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 android.service.dreams.DreamService
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.SavedStateRegistryController
+import androidx.savedstate.SavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import androidx.window.layout.FoldingFeature
+import kotlin.random.Random
+
+class DreamUniverse : DreamService() {
+ private var foldState = mutableStateOf<FoldingFeature?>(null) // unused
+
+ private val lifecycleOwner =
+ object : SavedStateRegistryOwner {
+ override val lifecycle = LifecycleRegistry(this)
+ override val savedStateRegistry
+ get() = savedStateRegistryController.savedStateRegistry
+
+ private val savedStateRegistryController =
+ SavedStateRegistryController.create(this).apply { performAttach() }
+
+ fun onCreate() {
+ savedStateRegistryController.performRestore(null)
+ lifecycle.currentState = Lifecycle.State.CREATED
+ }
+
+ fun onStart() {
+ lifecycle.currentState = Lifecycle.State.STARTED
+ }
+
+ fun onStop() {
+ lifecycle.currentState = Lifecycle.State.CREATED
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+
+ val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed())
+
+ isInteractive = false
+
+ if (TEST_UNIVERSE) {
+ universe.initTest()
+ } else {
+ universe.initRandom()
+
+ // We actually don't want the deterministic random position of the ship, we want
+ // true randomness to keep things interesting. So use Random (not universe.rng).
+ universe.ship.pos =
+ universe.star.pos +
+ Vec2.makeWithAngleMag(
+ Random.nextFloat() * PI2f,
+ Random.nextFloatInRange(
+ PLANET_ORBIT_RANGE.start,
+ PLANET_ORBIT_RANGE.endInclusive
+ )
+ )
+ }
+
+ // enable autopilot in screensaver mode
+ val autopilot = Autopilot(universe.ship, universe)
+ universe.ship.autopilot = autopilot
+ universe.add(autopilot)
+ autopilot.enabled = true
+
+ // much more visually interesting in a screensaver context
+ DYNAMIC_ZOOM = true
+
+ val composeView = ComposeView(this)
+ composeView.setContent {
+ Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
+ DebugText(DEBUG_TEXT)
+ Telemetry(universe)
+ }
+
+ composeView.setViewTreeLifecycleOwner(lifecycleOwner)
+ composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
+
+ setContentView(composeView)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ lifecycleOwner.onCreate()
+ }
+
+ override fun onDreamingStarted() {
+ super.onDreamingStarted()
+ lifecycleOwner.onStart()
+ }
+
+ override fun onDreamingStopped() {
+ super.onDreamingStopped()
+ lifecycleOwner.onStop()
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
index 5a9b814..79f8b5fc 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
@@ -21,12 +21,10 @@
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
import androidx.compose.animation.core.withInfiniteAnimationFrameNanos
-import androidx.compose.animation.fadeIn
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.awaitFirstDown
@@ -34,12 +32,14 @@
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -49,6 +49,7 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.AbsoluteAlignment.Left
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
@@ -59,8 +60,10 @@
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -94,12 +97,12 @@
val RANDOM_SEED_TYPE = RandomSeedType.Daily
const val FIXED_RANDOM_SEED = 5038L
-const val DEFAULT_CAMERA_ZOOM = 0.25f
+const val DEFAULT_CAMERA_ZOOM = 1f
const val MIN_CAMERA_ZOOM = 250f / UNIVERSE_RANGE // 0.0025f
const val MAX_CAMERA_ZOOM = 5f
-const val TOUCH_CAMERA_PAN = false
-const val TOUCH_CAMERA_ZOOM = true
-const val DYNAMIC_ZOOM = false // @@@ FIXME
+var TOUCH_CAMERA_PAN = false
+var TOUCH_CAMERA_ZOOM = false
+var DYNAMIC_ZOOM = false
fun dailySeed(): Long {
val today = GregorianCalendar()
@@ -134,39 +137,21 @@
}
@Composable
-fun ColumnScope.ConsoleText(
- modifier: Modifier = Modifier,
- visible: Boolean = true,
- random: Random = Random.Default,
- text: String
-) {
- AnimatedVisibility(
- modifier = modifier,
- visible = visible,
- enter =
- fadeIn(
- animationSpec =
- tween(
- durationMillis = 1000,
- easing = flickerFadeEasing(random) * CubicBezierEasing(0f, 1f, 1f, 0f)
- )
- )
- ) {
- Text(
- fontFamily = FontFamily.Monospace,
- fontWeight = FontWeight.Medium,
- fontSize = 12.sp,
- color = Color(0xFFFF8000),
- text = text
- )
- }
-}
-
-@Composable
fun Telemetry(universe: VisibleUniverse) {
var topVisible by remember { mutableStateOf(false) }
var bottomVisible by remember { mutableStateOf(false) }
+ var catalogFontSize by remember { mutableStateOf(9.sp) }
+
+ val textStyle =
+ TextStyle(
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ letterSpacing = 1.sp,
+ lineHeight = 12.sp,
+ )
+
LaunchedEffect("blah") {
delay(1000)
bottomVisible = true
@@ -174,65 +159,109 @@
topVisible = true
}
- Column(modifier = Modifier.fillMaxSize().padding(6.dp)) {
- universe.triggerDraw.value // recompose on every frame
- val explored = universe.planets.filter { it.explored }
+ universe.triggerDraw.value // recompose on every frame
- AnimatedVisibility(modifier = Modifier, visible = topVisible, enter = flickerFadeIn) {
- Text(
- fontFamily = FontFamily.Monospace,
- fontWeight = FontWeight.Medium,
- fontSize = 12.sp,
- color = Colors.Console,
- modifier = Modifier.align(Left),
- text =
- with(universe.star) {
- " STAR: $name (UDC-${universe.randomSeed % 100_000})\n" +
- " CLASS: ${cls.name}\n" +
- "RADIUS: ${radius.toInt()}\n" +
- " MASS: %.3g\n".format(mass) +
- "BODIES: ${explored.size} / ${universe.planets.size}\n" +
- "\n"
- } +
- explored
- .map {
- " BODY: ${it.name}\n" +
- " TYPE: ${it.description.capitalize()}\n" +
- " ATMO: ${it.atmosphere.capitalize()}\n" +
- " FAUNA: ${it.fauna.capitalize()}\n" +
- " FLORA: ${it.flora.capitalize()}\n"
- }
- .joinToString("\n")
+ val explored = universe.planets.filter { it.explored }
- // TODO: different colors, highlight latest discovery
+ BoxWithConstraints(
+ modifier =
+ Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent),
+ ) {
+ val wide = maxWidth > maxHeight
+ Column(
+ modifier =
+ Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart)
+ .fillMaxWidth(if (wide) 0.45f else 1.0f)
+ ) {
+ universe.ship.autopilot?.let { autopilot ->
+ if (autopilot.enabled) {
+ AnimatedVisibility(
+ modifier = Modifier,
+ visible = bottomVisible,
+ enter = flickerFadeIn
+ ) {
+ Text(
+ style = textStyle,
+ color = Colors.Autopilot,
+ modifier = Modifier.align(Left),
+ text = autopilot.telemetry
+ )
+ }
+ }
+ }
+
+ AnimatedVisibility(
+ modifier = Modifier,
+ visible = bottomVisible,
+ enter = flickerFadeIn
+ ) {
+ Text(
+ style = textStyle,
+ color = Colors.Console,
+ modifier = Modifier.align(Left),
+ text =
+ with(universe.ship) {
+ val closest = universe.closestPlanet()
+ val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt()
+ listOfNotNull(
+ landing?.let {
+ "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}"
+ }
+ ?: if (distToClosest < 10_000) {
+ "ALT: $distToClosest"
+ } else null,
+ "THR: %.0f%%".format(thrust.mag() * 100f),
+ "POS: %s".format(pos.str("%+7.0f")),
+ "VEL: %.0f".format(velocity.mag())
+ )
+ .joinToString("\n")
+ }
)
+ }
}
- Spacer(modifier = Modifier.weight(1f))
-
- AnimatedVisibility(modifier = Modifier, visible = bottomVisible, enter = flickerFadeIn) {
+ AnimatedVisibility(
+ modifier = Modifier.align(Alignment.TopStart),
+ visible = topVisible,
+ enter = flickerFadeIn
+ ) {
Text(
- fontFamily = FontFamily.Monospace,
- fontWeight = FontWeight.Medium,
- fontSize = 12.sp,
+ style = textStyle,
+ fontSize = catalogFontSize,
+ lineHeight = catalogFontSize,
+ letterSpacing = 1.sp,
color = Colors.Console,
- modifier = Modifier.align(Left),
- text =
- with(universe.ship) {
- val closest = universe.closestPlanet()
- val distToClosest = (closest.pos - pos).mag().toInt()
- listOfNotNull(
- landing?.let { "LND: ${it.planet.name}" }
- ?: if (distToClosest < 10_000) {
- "ALT: $distToClosest"
- } else null,
- if (thrust != Vec2.Zero) "THR: %.0f%%".format(thrust.mag() * 100f)
- else null,
- "POS: %s".format(pos.str("%+7.0f")),
- "VEL: %.0f".format(velocity.mag())
- )
- .joinToString("\n")
+ onTextLayout = { textLayoutResult ->
+ if (textLayoutResult.didOverflowHeight) {
+ catalogFontSize = 8.sp
}
+ },
+ text =
+ (with(universe.star) {
+ listOf(
+ " STAR: $name (UDC-${universe.randomSeed % 100_000})",
+ " CLASS: ${cls.name}",
+ "RADIUS: ${radius.toInt()}",
+ " MASS: %.3g".format(mass),
+ "BODIES: ${explored.size} / ${universe.planets.size}",
+ ""
+ )
+ } +
+ explored
+ .map {
+ listOf(
+ " BODY: ${it.name}",
+ " TYPE: ${it.description.capitalize()}",
+ " ATMO: ${it.atmosphere.capitalize()}",
+ " FAUNA: ${it.fauna.capitalize()}",
+ " FLORA: ${it.flora.capitalize()}",
+ ""
+ )
+ }
+ .flatten())
+ .joinToString("\n")
+
+ // TODO: different colors, highlight latest discovery
)
}
}
@@ -246,6 +275,8 @@
onWindowLayoutInfoChange()
+ enableEdgeToEdge()
+
val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed())
if (TEST_UNIVERSE) {
@@ -254,6 +285,15 @@
universe.initRandom()
}
+ com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext)
+
+ // for autopilot testing in the activity
+ // val autopilot = Autopilot(universe.ship, universe)
+ // universe.ship.autopilot = autopilot
+ // universe.add(autopilot)
+ // autopilot.enabled = true
+ // DYNAMIC_ZOOM = autopilot.enabled
+
setContent {
Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
DebugText(DEBUG_TEXT)
@@ -437,8 +477,13 @@
val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f)
// val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f
if (DYNAMIC_ZOOM) {
- // cameraZoom = lerp(0.1f, 5f, smooth(1f-normalizedDist))
- cameraZoom = clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM)
+ cameraZoom =
+ expSmooth(
+ cameraZoom,
+ clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM),
+ dt = u.dt,
+ speed = 1.5f
+ )
} else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM
if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f
@@ -478,26 +523,26 @@
"star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " +
"class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" +
"planets: ${u.planets.size}\n" +
- u.planets.joinToString("\n") {
- val range = (u.ship.pos - it.pos).mag()
- val vorbit = sqrt(GRAVITATION * it.mass / range)
- val vescape = sqrt(2 * GRAVITATION * it.mass / it.radius)
- " * ${it.name}:\n" +
- if (it.explored) {
- " TYPE: ${it.description.capitalize()}\n" +
- " ATMO: ${it.atmosphere.capitalize()}\n" +
- " FAUNA: ${it.fauna.capitalize()}\n" +
- " FLORA: ${it.flora.capitalize()}\n"
- } else {
- " (Unexplored)\n"
- } +
- " orbit=${(it.pos - it.orbitCenter).mag().toInt()}" +
- " radius=${it.radius.toInt()}" +
- " mass=${"%g".format(it.mass)}" +
- " vel=${(it.speed).toInt()}" +
- " // range=${"%.0f".format(range)}" +
- " vorbit=${vorbit.toInt()} vescape=${vescape.toInt()}"
- })
+ u.planets.joinToString("\n") {
+ val range = (u.ship.pos - it.pos).mag()
+ val vorbit = sqrt(GRAVITATION * it.mass / range)
+ val vescape = sqrt(2 * GRAVITATION * it.mass / it.radius)
+ " * ${it.name}:\n" +
+ if (it.explored) {
+ " TYPE: ${it.description.capitalize()}\n" +
+ " ATMO: ${it.atmosphere.capitalize()}\n" +
+ " FAUNA: ${it.fauna.capitalize()}\n" +
+ " FLORA: ${it.flora.capitalize()}\n"
+ } else {
+ " (Unexplored)\n"
+ } +
+ " orbit=${(it.pos - it.orbitCenter).mag().toInt()}" +
+ " radius=${it.radius.toInt()}" +
+ " mass=${"%g".format(it.mass)}" +
+ " vel=${(it.speed).toInt()}" +
+ " // range=${"%.0f".format(range)}" +
+ " vorbit=${vorbit.toInt()} vescape=${vescape.toInt()}"
+ })
zoom(cameraZoom) {
// All coordinates are space coordinates now.
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
index fdf29f7..a1e8212 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
@@ -16,6 +16,7 @@
package com.android.egg.landroid
+import kotlin.math.exp
import kotlin.math.pow
/** smoothstep. Ken Perlin's version */
@@ -32,3 +33,8 @@
fun lexp(start: Float, end: Float, progress: Float): Float {
return (progress - start) / (end - start)
}
+
+/** Exponentially smooth current toward target by a factor of speed. */
+fun expSmooth(current: Float, target: Float, dt: Float = 1f / 60, speed: Float = 5f): Float {
+ return current + (target - current) * (1 - exp(-dt * speed))
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
index 67d536e..7331807 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
@@ -17,9 +17,8 @@
package com.android.egg.landroid
import android.content.res.Resources
-import kotlin.random.Random
-
import com.android.egg.R
+import kotlin.random.Random
const val SUFFIX_PROB = 0.75f
const val LETTER_PROB = 0.3f
@@ -62,6 +61,11 @@
0.1f to "(^*!%@##!!"
)
+ private var activities = Bag(resources.getStringArray(R.array.activities))
+ private var floraGenericPlurals = Bag(resources.getStringArray(R.array.flora_generic_plurals))
+ private var faunaGenericPlurals = Bag(resources.getStringArray(R.array.fauna_generic_plurals))
+ private var atmoGenericPlurals = Bag(resources.getStringArray(R.array.atmo_generic_plurals))
+
fun describePlanet(rng: Random): String {
return planetTable.roll(rng).pull(rng) + " " + planetTypes.pull(rng)
}
@@ -93,4 +97,30 @@
fun describeAtmo(rng: Random): String {
return atmoTable.roll(rng).pull(rng)
}
+
+ fun floraPlural(rng: Random): String {
+ return floraGenericPlurals.pull(rng)
+ }
+ fun faunaPlural(rng: Random): String {
+ return faunaGenericPlurals.pull(rng)
+ }
+ fun atmoPlural(rng: Random): String {
+ return atmoGenericPlurals.pull(rng)
+ }
+
+ val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""")
+ fun describeActivity(rng: Random, target: Planet?): String {
+ return activities
+ .pull(rng)
+ .replace(TEMPLATE_REGEX) {
+ when (it.groupValues[1]) {
+ "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
+ "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
+ "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
+ "planet" -> (target?.description ?: "SOME BODY") // once told me
+ else -> "unknown template tag: ${it.groupValues[0]}"
+ }
+ }
+ .toUpperCase()
+ }
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
index 8510640..cd87335 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
@@ -32,6 +32,13 @@
}
}
+fun createPolygonPoints(radius: Float, sides: Int): List<Vec2> {
+ val angleStep = PI2f / sides
+ return (0 until sides).map { i ->
+ Vec2(radius * cos(angleStep * i), radius * sin(angleStep * i))
+ }
+}
+
fun createStar(radius1: Float, radius2: Float, points: Int): Path {
return Path().apply {
val angleStep = PI2f / points
@@ -46,15 +53,16 @@
}
fun Path.parseSvgPathData(d: String) {
- Regex("([A-Z])([-.,0-9e ]+)").findAll(d.trim()).forEach {
+ Regex("([A-Za-z])\\s*([-.,0-9e ]+)").findAll(d.trim()).forEach {
val cmd = it.groups[1]!!.value
val args =
it.groups[2]?.value?.split(Regex("\\s+"))?.map { v -> v.toFloat() } ?: emptyList()
- Log.d("Landroid", "cmd = $cmd, args = " + args.joinToString(","))
+ // Log.d("Landroid", "cmd = $cmd, args = " + args.joinToString(","))
when (cmd) {
"M" -> moveTo(args[0], args[1])
"C" -> cubicTo(args[0], args[1], args[2], args[3], args[4], args[5])
"L" -> lineTo(args[0], args[1])
+ "l" -> relativeLineTo(args[0], args[1])
"Z" -> close()
else -> Log.v("Landroid", "unsupported SVG command: $cmd")
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
index ebbb2bd..2903534 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
@@ -61,6 +61,7 @@
/** Return a random float in the range [start, end). */
fun Random.nextFloatInRange(fromUntil: ClosedFloatingPointRange<Float>): Float =
nextFloatInRange(fromUntil.start, fromUntil.endInclusive)
+
/** Return a random float in the range [first, second). */
fun Random.nextFloatInRange(fromUntil: Pair<Float, Float>): Float =
nextFloatInRange(fromUntil.first, fromUntil.second)
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
index 11dce61..1e54569 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
@@ -155,10 +155,7 @@
speed = speed,
color = Colors.Eigengrau4
)
- android.util.Log.v(
- "Landroid",
- "created planet $p with period $period and vel $speed"
- )
+ android.util.Log.v("Landroid", "created planet $p with period $period and vel $speed")
val num = it + 1
p.description = "TEST PLANET #$num"
p.atmosphere = "radius=$radius"
@@ -215,10 +212,7 @@
speed = speed,
color = Colors.Eigengrau4
)
- android.util.Log.v(
- "Landroid",
- "created planet $p with period $period and vel $speed"
- )
+ android.util.Log.v("Landroid", "created planet $p with period $period and vel $speed")
p.description = namer.describePlanet(rng)
p.atmosphere = namer.describeAtmo(rng)
p.flora = namer.describeLife(rng)
@@ -302,7 +296,7 @@
// &&
// vDiff < 100f
) {
- val landing = Landing(ship, planet, a)
+ val landing = Landing(ship, planet, a, namer.describeActivity(rng, planet))
ship.landing = landing
ship.velocity = planet.velocity
add(landing)
@@ -370,12 +364,15 @@
}
}
-class Landing(val ship: Spacecraft, val planet: Planet, val angle: Float) : Constraint {
- private val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius)
+class Landing(var ship: Spacecraft?, val planet: Planet, val angle: Float, val text: String = "") :
+ Constraint {
override fun solve(sim: Simulator, dt: Float) {
- val desiredPos = planet.pos + landingVector
- ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME
- ship.angle = angle
+ ship?.let { ship ->
+ val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius)
+ val desiredPos = planet.pos + landingVector
+ ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME
+ ship.angle = angle
+ }
}
}
@@ -435,6 +432,7 @@
val track = Track()
var landing: Landing? = null
+ var autopilot: Autopilot? = null
init {
mass = SPACECRAFT_MASS
@@ -448,23 +446,19 @@
var deltaV = MAIN_ENGINE_ACCEL * dt
if (SCALED_THRUST) deltaV *= thrustMag.coerceIn(0f, 1f)
- if (landing == null) {
- // we are free in space, so we attempt to pivot toward the desired direction
- // NOTE: no longer required thanks to FlightStick
- // angle = thrust.angle()
- } else
- landing?.let { landing ->
- if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */
+ // check if we are currently attached to a landing
+ landing?.let { landing ->
+ // launch clock is 1 second long
+ if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */
- if (sim.now > launchClock) {
- // first-stage to orbit has 1000x power
- // deltaV *= 1000f
- sim.remove(landing)
- this.landing = null
- } else {
- deltaV = 0f
- }
+ if (sim.now > launchClock) {
+ // detach from landing site
+ landing.ship = null
+ this.landing = null
+ } else {
+ deltaV = 0f
}
+ }
// this is it. impart thrust to the ship.
// note that we always thrust in the forward direction
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
index 6baf36e..974784d 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
@@ -28,11 +28,10 @@
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.util.lerp
import androidx.core.math.MathUtils.clamp
+import com.android.egg.flags.Flags.flagFlag
import java.lang.Float.max
import kotlin.math.sqrt
-import com.android.egg.flags.Flags.flagFlag
-
const val DRAW_ORBITS = true
const val DRAW_GRAVITATIONAL_FIELDS = true
const val DRAW_STAR_GRAVITATIONAL_FIELDS = true
@@ -71,16 +70,6 @@
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)
@@ -89,13 +78,14 @@
}
drawStar(star)
entities.forEach {
- if (it === ship || it === star) return@forEach // draw the ship last
+ if (it === star) return@forEach // don't draw the star as a planet
when (it) {
- is Spacecraft -> drawSpacecraft(it)
is Spark -> drawSpark(it)
is Planet -> drawPlanet(it)
+ else -> Unit // draw these at a different time, or not at all
}
}
+ ship.autopilot?.let { drawAutopilot(it) }
drawSpacecraft(ship)
}
}
@@ -111,15 +101,6 @@
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) {
@@ -226,23 +207,47 @@
"""
)
}
-val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-4f, 0f)) }
+val spaceshipLegs =
+ Path().apply {
+ parseSvgPathData(
+ """
+M-7 -6.5
+l-3.5 0
+l-1 -2
+l 0 4
+l 1 -2
+Z
+M-7 6.5
+l-3.5 0
+l-1 -2
+l 0 4
+l 1 -2
+Z
+"""
+ )
+ }
+val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-5f, 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)
- // )
+ // new in V: little landing legs
+ ship.landing?.let {
+ drawPath(
+ path = spaceshipLegs,
+ color = Color(0xFFCCCCCC),
+ style = Stroke(width = 2f / this@drawSpacecraft.zoom)
+ )
+ }
+ // draw the ship
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)
)
+ // draw thrust
if (thrust != Vec2.Zero) {
drawPath(
path = thrustPath,
@@ -254,27 +259,8 @@
)
)
}
- // 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)
}
}
@@ -287,14 +273,15 @@
val height = 80f
rotateRad(landing.angle, pivot = v) {
translate(v.x, v.y) {
- drawPath(
+ val flagPath =
Path().apply {
moveTo(0f, 0f)
lineTo(height, 0f)
lineTo(height * 0.875f, height * 0.25f)
lineTo(height * 0.75f, 0f)
close()
- }, Color.Yellow, style = Stroke(width = strokeWidth))
+ }
+ drawPath(flagPath, Colors.Flag, style = Stroke(width = strokeWidth))
}
}
}
@@ -311,10 +298,7 @@
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)
}
}
@@ -324,19 +308,9 @@
drawPoints(
positions,
pointMode = PointMode.Lines,
- color = Color.Green,
+ color = Colors.Track,
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]
@@ -349,3 +323,43 @@
}
}
}
+
+fun ZoomedDrawScope.drawAutopilot(autopilot: Autopilot) {
+ val color = Colors.Autopilot.copy(alpha = 0.5f)
+
+ autopilot.target?.let { target ->
+ val zoom = zoom
+ rotateRad(autopilot.universe.now * PI2f / 10f, target.pos) {
+ translate(target.pos.x, target.pos.y) {
+ drawPath(
+ path =
+ createPolygon(
+ radius = target.radius + autopilot.brakingDistance,
+ sides = 15 // Autopilot introduced in Android 15
+ ),
+ color = color,
+ style = Stroke(1f / zoom)
+ )
+ drawCircle(
+ color,
+ radius = target.radius + autopilot.landingAltitude / 2,
+ center = Vec2.Zero,
+ alpha = 0.25f,
+ style = Stroke(autopilot.landingAltitude)
+ )
+ }
+ }
+ drawLine(
+ color,
+ start = autopilot.ship.pos,
+ end = autopilot.leadingPos,
+ strokeWidth = 1f / zoom
+ )
+ drawCircle(
+ color,
+ radius = 5f / zoom,
+ center = autopilot.leadingPos,
+ style = Stroke(1f / zoom)
+ )
+ }
+}