ANDROID 14 -- SPACE PROGRAM -- LAUNCH IMMINENT
EASTEREGG.APK IS AN EXPLORATION OF NEW TECHNOLOGY.
// MISSION "UPSIDE DOWN CAKE" //
+ JETPACK COMPOSURE
+ SPACE FOLDING
+ ASTRAL PROJECTION
+ VECTORPUNK
// PILOT ADVISORIES //
+ STABLE ORBITS ARE TRICKY BUT POSSIBLE.
+ EACH DAY PRESENTS NEW DISCOVERIES.
+ ATTEMPT ALL LANDINGS.
GODSPEED AND GOOD LUCK.
A PRODUCT OF YOYODROID PROPULSION SYSTEMS
"WHERE INTEGRATION COMPLETE IS TOMORROW!"
Fixes: 267678663 // REMOVE BEFORE FLIGHT
Test: adb shell am start -a android.intent.action.MAIN -c com.android.internal.category.PLATLOGO
Change-Id: Ia06422bdc2ec22e8e64b189ca2cc02def03d84f3
diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp
index e88410c..8699f59 100644
--- a/packages/EasterEgg/Android.bp
+++ b/packages/EasterEgg/Android.bp
@@ -26,7 +26,10 @@
android_app {
// the build system in pi-dev can't quite handle R.java in kt
// so we will have a mix of java and kotlin files
- srcs: ["src/**/*.java", "src/**/*.kt"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
resource_dirs: ["res"],
@@ -36,17 +39,34 @@
certificate: "platform",
optimize: {
+ enabled: true,
+ optimize: true,
+ shrink: true,
+ shrink_resources: true,
+ proguard_compatibility: false,
proguard_flags_files: ["proguard.flags"],
},
- static_libs: [
- "androidx.core_core",
- "androidx.recyclerview_recyclerview",
+ static_libs: [
+ "androidx.core_core",
"androidx.annotation_annotation",
- "kotlinx-coroutines-android",
- "kotlinx-coroutines-core",
- //"kotlinx-coroutines-reactive",
- ],
+ "androidx.recyclerview_recyclerview",
+ "kotlinx-coroutines-android",
+ "kotlinx-coroutines-core",
+
+ "androidx.core_core-ktx",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
+ "androidx.activity_activity-compose",
+ "androidx.compose.ui_ui",
+ "androidx.compose.ui_ui-util",
+ "androidx.compose.ui_ui-tooling-preview",
+ "androidx.compose.material_material",
+ "androidx.window_window",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.activity_activity-compose",
+ "androidx.compose.ui_ui",
+ ],
manifest: "AndroidManifest.xml",
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index cc7bb4a..d1db237 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -1,4 +1,19 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?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.
+-->
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.egg"
android:versionCode="12"
@@ -18,8 +33,27 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
- android:icon="@drawable/icon"
+ android:icon="@drawable/android14_patch_adaptive"
android:label="@string/app_name">
+
+ <!-- Android U easter egg -->
+
+ <activity
+ android:name=".landroid.MainActivity"
+ android:exported="true"
+ android:label="@string/u_egg_name"
+ android:icon="@drawable/android14_patch_adaptive"
+ android:configChanges="orientation|screenLayout|screenSize|density"
+ android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="com.android.internal.category.PLATLOGO" />
+ </intent-filter>
+ </activity>
+
+
+ <!-- Android Q easter egg -->
<activity
android:name=".quares.QuaresActivity"
android:exported="true"
@@ -69,7 +103,7 @@
android:exported="true"
android:showOnLockScreen="true"
android:theme="@android:style/Theme.Material.Light.Dialog.NoActionBar" />
- <!-- Used to enable easter egg -->
+ <!-- Used to enable easter egg components for earlier easter eggs. -->
<activity
android:name=".ComponentActivationActivity"
android:excludeFromRecents="true"
@@ -79,7 +113,6 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
- <category android:name="com.android.internal.category.PLATLOGO" />
</intent-filter>
</activity>
diff --git a/packages/EasterEgg/res/drawable/android14_patch_adaptive.xml b/packages/EasterEgg/res/drawable/android14_patch_adaptive.xml
new file mode 100644
index 0000000..423e351
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/android14_patch_adaptive.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/android14_patch_adaptive_background"/>
+ <foreground android:drawable="@drawable/android14_patch_adaptive_foreground"/>
+ <monochrome android:drawable="@drawable/android14_patch_monochrome"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/packages/EasterEgg/res/drawable/android14_patch_adaptive_background.xml b/packages/EasterEgg/res/drawable/android14_patch_adaptive_background.xml
new file mode 100644
index 0000000..c31aa7b
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/android14_patch_adaptive_background.xml
@@ -0,0 +1,85 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:pathData="M0,0 L108,0 L108,108 L0,108 z"
+ android:fillColor="#FF073042"/>
+ <path
+ android:pathData="M44.51,43.32L44.86,42.27C47.04,54.48 52.81,86.71 52.81,50.14C52.81,49.99 52.92,49.86 53.06,49.86H55.04C55.18,49.86 55.3,49.98 55.3,50.14C55.27,114.18 44.51,43.32 44.51,43.32Z"
+ android:fillColor="#3DDC84"/>
+ <path
+ android:name="planetary head"
+ android:pathData="M38.81,42.23L33.63,51.21C33.33,51.72 33.51,52.38 34.02,52.68C34.54,52.98 35.2,52.8 35.49,52.28L40.74,43.2C49.22,47 58.92,47 67.4,43.2L72.65,52.28C72.96,52.79 73.62,52.96 74.13,52.65C74.62,52.35 74.79,51.71 74.51,51.21L69.33,42.23C78.23,37.39 84.32,28.38 85.21,17.74H22.93C23.82,28.38 29.91,37.39 38.81,42.23Z"
+ android:fillColor="#ffffff"/>
+ <!-- yes it's an easter egg in a vector drawable -->
+ <path
+ android:name="planetary body"
+ android:pathData="M22.9,0 L85.1,0 L85.1,15.5 L22.9,15.5 z"
+ android:fillColor="#ffffff" />
+ <path
+ android:pathData="M54.96,43.32H53.1C52.92,43.32 52.77,43.47 52.77,43.65V48.04C52.77,48.22 52.92,48.37 53.1,48.37H54.96C55.15,48.37 55.3,48.22 55.3,48.04V43.65C55.3,43.47 55.15,43.32 54.96,43.32Z"
+ android:fillColor="#3DDC84"/>
+ <path
+ android:pathData="M54.99,40.61H53.08C52.91,40.61 52.77,40.75 52.77,40.92V41.56C52.77,41.73 52.91,41.87 53.08,41.87H54.99C55.16,41.87 55.3,41.73 55.3,41.56V40.92C55.3,40.75 55.16,40.61 54.99,40.61Z"
+ android:fillColor="#3DDC84"/>
+ <path
+ android:pathData="M41.49,47.88H40.86V48.51H41.49V47.88Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M44.13,57.08H43.5V57.71H44.13V57.08Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M72.29,66.76H71.66V67.39H72.29V66.76Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M59.31,53.41H58.68V54.04H59.31V53.41Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M64.47,48.19H63.84V48.83H64.47V48.19Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M60.58,59.09H59.95V59.72H60.58V59.09Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M66.95,56.7H65.69V57.97H66.95V56.7Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M44.13,60.71H43.5V61.34H44.13V60.71Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M49.66,51.33H48.4V52.6H49.66V51.33Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M57.78,63.83H56.52V65.09H57.78V63.83Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M61.1,68.57H59.83V69.83H61.1V68.57Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M40.43,53.73H39.16V54.99H40.43V53.73Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M74.47,44H73.21V45.26H74.47V44Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M36.8,64.58H35.54V65.84H36.8V64.58Z"
+ android:fillColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/android14_patch_adaptive_foreground.xml b/packages/EasterEgg/res/drawable/android14_patch_adaptive_foreground.xml
new file mode 100644
index 0000000..391d515
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/android14_patch_adaptive_foreground.xml
@@ -0,0 +1,60 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:pathData="M54.03,33.03C52.99,33.03 52.14,33.86 52.14,34.87V37.14C52.14,37.34 52.3,37.5 52.5,37.5C52.69,37.5 52.85,37.34 52.85,37.14V36.53C52.85,36.14 53.17,35.82 53.56,35.82H54.51C54.9,35.82 55.22,36.14 55.22,36.53V37.14C55.22,37.34 55.38,37.5 55.57,37.5C55.77,37.5 55.93,37.34 55.93,37.14V34.87C55.93,33.86 55.08,33.03 54.03,33.03H54.03Z"
+ android:fillColor="#3DDC84"/>
+ <path
+ android:pathData="M108,0H0V108H108V0ZM54,80.67C68.73,80.67 80.67,68.73 80.67,54C80.67,39.27 68.73,27.33 54,27.33C39.27,27.33 27.33,39.27 27.33,54C27.33,68.73 39.27,80.67 54,80.67Z"
+ android:fillColor="#F86734"
+ android:fillType="evenOdd"/>
+ <group>
+ <!-- the text doesn't look great everywhere but you can remove the clip to try it out. -->
+ <clip-path />
+ <path
+ android:pathData="M28.58,32.18L29.18,31.5L33.82,33.02L33.12,33.81L32.15,33.48L30.92,34.87L31.37,35.8L30.68,36.58L28.58,32.18L28.58,32.18ZM31.25,33.18L29.87,32.71L30.51,34.02L31.25,33.18V33.18Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M38,29.76L34.61,28.79L36.23,31.04L35.42,31.62L32.8,27.99L33.5,27.48L36.88,28.45L35.26,26.21L36.08,25.62L38.7,29.25L38,29.76Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M39.23,23.87L40.63,23.27C41.79,22.77 43.13,23.28 43.62,24.43C44.11,25.57 43.56,26.89 42.4,27.39L40.99,27.99L39.23,23.87ZM42.03,26.54C42.73,26.24 42.96,25.49 42.68,24.83C42.4,24.17 41.69,23.82 41,24.11L40.51,24.32L41.55,26.75L42.03,26.54Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M45.91,21.43L47.64,21.09C48.47,20.93 49.12,21.41 49.27,22.15C49.38,22.72 49.15,23.14 48.63,23.45L50.57,25.08L49.39,25.31L47.57,23.79L47.41,23.82L47.76,25.63L46.78,25.83L45.91,21.43H45.91ZM47.87,22.86C48.16,22.8 48.34,22.59 48.29,22.34C48.24,22.07 48,21.96 47.71,22.02L47.07,22.14L47.24,22.98L47.87,22.86Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M52.17,22.69C52.19,21.41 53.24,20.39 54.52,20.41C55.8,20.43 56.82,21.49 56.8,22.76C56.78,24.04 55.72,25.06 54.45,25.04C53.17,25.02 52.15,23.96 52.17,22.69ZM55.79,22.75C55.8,22.02 55.23,21.39 54.51,21.38C53.78,21.37 53.19,21.98 53.18,22.7C53.17,23.43 53.73,24.06 54.47,24.07C55.19,24.08 55.78,23.47 55.79,22.75H55.79Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M60,21.01L60.98,21.2L60.12,25.6L59.14,25.41L60,21.01Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M64.3,22.03L65.73,22.58C66.91,23.03 67.51,24.32 67.07,25.49C66.62,26.65 65.31,27.22 64.13,26.77L62.71,26.22L64.3,22.03L64.3,22.03ZM64.46,25.9C65.17,26.17 65.86,25.8 66.12,25.12C66.37,24.45 66.11,23.71 65.4,23.44L64.91,23.25L63.97,25.72L64.46,25.9Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M73.59,27.94L72.94,27.44L73.51,26.69L74.92,27.77L72.2,31.34L71.45,30.76L73.59,27.94Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M76.18,33.75L74.69,32.14L75.25,31.62L78.81,31.42L79.4,32.05L77.47,33.85L77.86,34.27L77.22,34.86L76.83,34.44L76.12,35.11L75.47,34.41L76.18,33.75ZM77.72,32.31L76.12,32.4L76.82,33.15L77.72,32.31Z"
+ android:fillColor="#ffffff"/>
+ </group>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/android14_patch_monochrome.xml b/packages/EasterEgg/res/drawable/android14_patch_monochrome.xml
new file mode 100644
index 0000000..beef85c
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/android14_patch_monochrome.xml
@@ -0,0 +1,84 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <group>
+ <clip-path
+ android:pathData="M0,0h108v108h-108z"/>
+ <group>
+ <clip-path
+ android:pathData="M22,22h64v64h-64z"/>
+ <path
+ android:pathData="M54,78C67.25,78 78,67.25 78,54C78,40.75 67.25,30 54,30C40.75,30 30,40.75 30,54C30,67.25 40.75,78 54,78Z"
+ android:strokeWidth="5"
+ android:fillColor="#00000000"
+ android:strokeColor="#000000"/>
+ <group>
+ <clip-path
+ android:pathData="M77.5,54C77.5,66.98 66.98,77.5 54,77.5C41.02,77.5 30.5,66.98 30.5,54C30.5,41.02 41.02,30.5 54,30.5C66.98,30.5 77.5,41.02 77.5,54Z"/>
+ <path
+ android:pathData="M61.5,46.06C56.7,47.89 51.4,47.89 46.61,46.06L44.04,50.51C43.49,51.46 42.28,51.79 41.33,51.24C40.39,50.69 40.06,49.48 40.61,48.53L43.06,44.28C37.97,41.03 34.54,35.56 34,29.19L33.88,27.74H74.22L74.1,29.19C73.57,35.56 70.14,41.03 65.04,44.28L67.51,48.56C68.03,49.49 67.71,50.66 66.8,51.21C65.87,51.77 64.65,51.47 64.08,50.54L64.07,50.51L61.5,46.06Z"
+ android:fillColor="#000000"/>
+ </group>
+ <path
+ android:pathData="M51.33,67.33h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M48.67,62h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M56.67,70h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M56.67,62h2.67v2.67h-2.67z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M67.33,62h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M59.33,51.33h2.67v2.67h-2.67z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M62,59.33h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M70,54h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M35.33,56.67h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M35.33,48.67h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M40.67,59.33h2.67v2.67h-2.67z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M46,51.33h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M43.33,67.33h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M54,54h1.33v1.33h-1.33z"
+ android:fillColor="#000000"/>
+ </group>
+ </group>
+</vector>
diff --git a/packages/EasterEgg/res/values/landroid_strings.xml b/packages/EasterEgg/res/values/landroid_strings.xml
new file mode 100644
index 0000000..1394f2f
--- /dev/null
+++ b/packages/EasterEgg/res/values/landroid_strings.xml
@@ -0,0 +1,371 @@
+<?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.
+-->
+
+<resources>
+ <string name="u_egg_name" translatable="false">Android 14 Easter Egg</string>
+
+ <string-array name="planet_descriptors" translatable="false">
+ <item>earthy</item>
+ <item>swamp</item>
+ <item>frozen</item>
+ <item>grassy</item>
+ <item>arid</item>
+ <item>crowded</item>
+ <item>ancient</item>
+ <item>lively</item>
+ <item>homey</item>
+ <item>modern</item>
+ <item>boring</item>
+ <item>compact</item>
+ <item>expensive</item>
+ <item>polluted</item>
+ <item>rusty</item>
+ <item>sandy</item>
+ <item>undulating</item>
+ <item>verdant</item>
+ <item>tessellated</item>
+ <item>hollow</item>
+ <item>scalding</item>
+ <item>hemispherical</item>
+ <item>oblong</item>
+ <item>oblate</item>
+ <item>vacuum</item>
+ <item>high-pressure</item>
+ <item>low-pressure</item>
+ <item>plastic</item>
+ <item>metallic</item>
+ <item>burned-out</item>
+ <item>bucolic</item>
+ </string-array>
+
+ <string-array name="life_descriptors" translatable="false">
+ <item>aggressive</item>
+ <item>passive-aggressive</item>
+ <item>shy</item>
+ <item>timid</item>
+ <item>nasty</item>
+ <item>brutish</item>
+ <item>short</item>
+ <item>absent</item>
+ <item>teen-aged</item>
+ <item>confused</item>
+ <item>transparent</item>
+ <item>cubic</item>
+ <item>quadratic</item>
+ <item>higher-order</item>
+ <item>huge</item>
+ <item>tall</item>
+ <item>wary</item>
+ <item>loud</item>
+ <item>yodeling</item>
+ <item>purring</item>
+ <item>slender</item>
+ <item>cats</item>
+ <item>adorable</item>
+ <item>eclectic</item>
+ <item>electric</item>
+ <item>microscopic</item>
+ <item>trunkless</item>
+ <item>myriad</item>
+ <item>cantankerous</item>
+ <item>gargantuan</item>
+ <item>contagious</item>
+ <item>fungal</item>
+ <item>cattywampus</item>
+ <item>spatchcocked</item>
+ <item>rotisserie</item>
+ <item>farm-to-table</item>
+ <item>organic</item>
+ <item>synthetic</item>
+ <item>unfocused</item>
+ <item>focused</item>
+ <item>capitalist</item>
+ <item>communal</item>
+ <item>bossy</item>
+ <item>malicious</item>
+ <item>compliant</item>
+ <item>psychic</item>
+ <item>oblivious</item>
+ <item>passive</item>
+ <item>bonsai</item>
+ </string-array>
+
+ <string-array name="any_descriptors" translatable="false">
+ <item>silly</item>
+ <item>dangerous</item>
+ <item>vast</item>
+ <item>invisible</item>
+ <item>superfluous</item>
+ <item>superconducting</item>
+ <item>superior</item>
+ <item>alien</item>
+ <item>phantom</item>
+ <item>friendly</item>
+ <item>peaceful</item>
+ <item>lonely</item>
+ <item>uncomfortable</item>
+ <item>charming</item>
+ <item>fractal</item>
+ <item>imaginary</item>
+ <item>forgotten</item>
+ <item>tardy</item>
+ <item>gassy</item>
+ <item>fungible</item>
+ <item>bespoke</item>
+ <item>artisanal</item>
+ <item>exceptional</item>
+ <item>puffy</item>
+ <item>rusty</item>
+ <item>fresh</item>
+ <item>crusty</item>
+ <item>glossy</item>
+ <item>lovely</item>
+ <item>processed</item>
+ <item>macabre</item>
+ <item>reticulated</item>
+ <item>shocking</item>
+ <item>void</item>
+ <item>undefined</item>
+ <item>gothic</item>
+ <item>beige</item>
+ <item>mid</item>
+ <item>milquetoast</item>
+ <item>melancholy</item>
+ <item>unnerving</item>
+ <item>cheery</item>
+ <item>vibrant</item>
+ <item>heliotrope</item>
+ <item>psychedelic</item>
+ <item>nondescript</item>
+ <item>indescribable</item>
+ <item>tubular</item>
+ <item>toroidal</item>
+ <item>voxellated</item>
+ <item>low-poly</item>
+ <item>low-carb</item>
+ <item>100% cotton</item>
+ <item>synthetic</item>
+ <item>boot-cut</item>
+ <item>bell-bottom</item>
+ <item>bumpy</item>
+ <item>fluffy</item>
+ <item>sous-vide</item>
+ <item>tepid</item>
+ <item>upcycled</item>
+ <item>sous-vide</item>
+ <item>bedazzled</item>
+ <item>ancient</item>
+ <item>inexplicable</item>
+ <item>sparkling</item>
+ <item>still</item>
+ <item>lemon-scented</item>
+ <item>eccentric</item>
+ <item>tilted</item>
+ <item>pungent</item>
+ <item>pine-scented</item>
+ <item>corduroy</item>
+ <item>overengineered</item>
+ <item>bioengineered</item>
+ <item>impossible</item>
+ </string-array>
+
+ <string-array name="constellations" translatable="false">
+ <item>Aries</item>
+ <item>Taurus</item>
+ <item>Gemini</item>
+ <item>Cancer</item>
+ <item>Leo</item>
+ <item>Virgo</item>
+ <item>Libra</item>
+ <item>Scorpio</item>
+ <item>Sagittarius</item>
+ <item>Capricorn</item>
+ <item>Aquarius</item>
+ <item>Pisces</item>
+ <item>Andromeda</item>
+ <item>Cygnus</item>
+ <item>Draco</item>
+ <item>Alcor</item>
+ <item>Calamari</item>
+ <item>Cuckoo</item>
+ <item>Neko</item>
+ <item>Monoceros</item>
+ <item>Norma</item>
+ <item>Abnorma</item>
+ <item>Morel</item>
+ <item>Redlands</item>
+ <item>Cupcake</item>
+ <item>Donut</item>
+ <item>Eclair</item>
+ <item>Froyo</item>
+ <item>Gingerbread</item>
+ <item>Honeycomb</item>
+ <item>Icecreamsandwich</item>
+ <item>Jellybean</item>
+ <item>Kitkat</item>
+ <item>Lollipop</item>
+ <item>Marshmallow</item>
+ <item>Nougat</item>
+ <item>Oreo</item>
+ <item>Pie</item>
+ <item>Quincetart</item>
+ <item>Redvelvetcake</item>
+ <item>Snowcone</item>
+ <item>Tiramisu</item>
+ <item>Upsidedowncake</item>
+ <item>Vanillaicecream</item>
+ <item>Android</item>
+ <item>Binder</item>
+ <item>Campanile</item>
+ <item>Dread</item>
+ </string-array>
+
+ <!-- prob: 5% -->
+ <string-array name="constellations_rare" translatable="false">
+ <item>Jandycane</item>
+ <item>Zombiegingerbread</item>
+ <item>Astro</item>
+ <item>Bender</item>
+ <item>Flan</item>
+ <item>Untitled-1</item>
+ <item>Expedit</item>
+ <item>Petit Four</item>
+ <item>Worcester</item>
+ <item>Xylophone</item>
+ <item>Yellowpeep</item>
+ <item>Zebraball</item>
+ <item>Hutton</item>
+ <item>Klang</item>
+ <item>Frogblast</item>
+ <item>Exo</item>
+ <item>Keylimepie</item>
+ <item>Nat</item>
+ <item>Nrp</item>
+ </string-array>
+
+ <!-- prob: 75% -->
+ <string-array name="star_suffixes" translatable="false">
+ <item>Alpha</item>
+ <item>Beta</item>
+ <item>Gamma</item>
+ <item>Delta</item>
+ <item>Epsilon</item>
+ <item>Zeta</item>
+ <item>Eta</item>
+ <item>Theta</item>
+ <item>Iota</item>
+ <item>Kappa</item>
+ <item>Lambda</item>
+ <item>Mu</item>
+ <item>Nu</item>
+ <item>Xi</item>
+ <item>Omicron</item>
+ <item>Pi</item>
+ <item>Rho</item>
+ <item>Sigma</item>
+ <item>Tau</item>
+ <item>Upsilon</item>
+ <item>Phi</item>
+ <item>Chi</item>
+ <item>Psi</item>
+ <item>Omega</item>
+
+ <item>Prime</item>
+ <item>Secundo</item>
+ <item>Major</item>
+ <item>Minor</item>
+ <item>Diminished</item>
+ <item>Augmented</item>
+ <item>Ultima</item>
+ <item>Penultima</item>
+ <item>Mid</item>
+
+ <item>Proxima</item>
+ <item>Novis</item>
+
+ <item>Plus</item>
+ </string-array>
+
+ <!-- prob: 5% -->
+ <!-- more than one can be appended, with very low prob -->
+ <string-array name="star_suffixes_rare" translatable="false">
+ <item>Serif</item>
+ <item>Sans</item>
+ <item>Oblique</item>
+ <item>Grotesque</item>
+ <item>Handtooled</item>
+ <item>III “Trey”</item>
+ <item>Alfredo</item>
+ <item>2.0</item>
+ <item>(Final)</item>
+ <item>(Final (Final))</item>
+ <item>(Draft)</item>
+ <item>Con Carne</item>
+ </string-array>
+
+ <string-array name="planet_types" translatable="false">
+ <item>planet</item>
+ <item>planetoid</item>
+ <item>moon</item>
+ <item>moonlet</item>
+ <item>centaur</item>
+ <item>asteroid</item>
+ <item>space garbage</item>
+ <item>detritus</item>
+ <item>satellite</item>
+ <item>core</item>
+ <item>giant</item>
+ <item>body</item>
+ <item>slab</item>
+ <item>rock</item>
+ <item>husk</item>
+ <item>planemo</item>
+ <item>object</item>
+ <item>planetesimal</item>
+ <item>exoplanet</item>
+ <item>ploonet</item>
+ </string-array>
+
+ <string-array name="atmo_descriptors" translatable="false">
+ <item>toxic</item>
+ <item>breathable</item>
+ <item>radioactive</item>
+ <item>clear</item>
+ <item>calm</item>
+ <item>peaceful</item>
+ <item>vacuum</item>
+ <item>stormy</item>
+ <item>freezing</item>
+ <item>burning</item>
+ <item>humid</item>
+ <item>tropical</item>
+ <item>cloudy</item>
+ <item>obscured</item>
+ <item>damp</item>
+ <item>dank</item>
+ <item>clammy</item>
+ <item>frozen</item>
+ <item>contaminated</item>
+ <item>temperate</item>
+ <item>moist</item>
+ <item>minty</item>
+ <item>relaxed</item>
+ <item>skunky</item>
+ <item>breezy</item>
+ <item>soup </item>
+ </string-array>
+
+</resources>
diff --git a/packages/EasterEgg/res/values/strings.xml b/packages/EasterEgg/res/values/strings.xml
index 743947a..79957df 100644
--- a/packages/EasterEgg/res/values/strings.xml
+++ b/packages/EasterEgg/res/values/strings.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <string name="app_name" translatable="false">Android S Easter Egg</string>
+ <string name="app_name" translatable="false">Android Easter Egg</string>
<!-- name of the Q easter egg, a nonogram-style icon puzzle -->
<string name="q_egg_name" translatable="false">Icon Quiz</string>
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
new file mode 100644
index 0000000..f5657ae
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.ui.graphics.Color
+
+/** Various UI colors. */
+object Colors {
+ val Eigengrau = Color(0xFF16161D)
+ val Eigengrau2 = Color(0xFF292936)
+ val Eigengrau3 = Color(0xFF3C3C4F)
+ val Eigengrau4 = Color(0xFFA7A7CA)
+
+ val Console = Color(0xFFB7B7FF)
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
new file mode 100644
index 0000000..d040fba
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import kotlin.random.Random
+
+@Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() }
+
+operator fun Easing.times(next: Easing) = { x: Float -> next.transform(transform(x)) }
+
+fun flickerFadeEasing(rng: Random) = Easing { frac -> if (rng.nextFloat() < frac) 1f else 0f }
+
+val flickerFadeIn =
+ fadeIn(
+ animationSpec =
+ tween(
+ durationMillis = 1000,
+ easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random)
+ )
+ )
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
new file mode 100644
index 0000000..5a9b814
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
@@ -0,0 +1,543 @@
+/*
+ * 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.content.res.Resources
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+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
+import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.gestures.rememberTransformableState
+import androidx.compose.foundation.gestures.transformable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.AbsoluteAlignment.Left
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.drawscope.Stroke
+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.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.math.MathUtils.clamp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowInfoTracker
+import java.lang.Float.max
+import java.lang.Float.min
+import java.util.Calendar
+import java.util.GregorianCalendar
+import kotlin.math.absoluteValue
+import kotlin.math.floor
+import kotlin.math.sqrt
+import kotlin.random.Random
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+enum class RandomSeedType {
+ Fixed,
+ Daily,
+ Evergreen
+}
+
+const val TEST_UNIVERSE = false
+
+val RANDOM_SEED_TYPE = RandomSeedType.Daily
+
+const val FIXED_RANDOM_SEED = 5038L
+const val DEFAULT_CAMERA_ZOOM = 0.25f
+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
+
+fun dailySeed(): Long {
+ val today = GregorianCalendar()
+ return today.get(Calendar.YEAR) * 10_000L +
+ today.get(Calendar.MONTH) * 100L +
+ today.get(Calendar.DAY_OF_MONTH)
+}
+
+fun randomSeed(): Long {
+ return when (RANDOM_SEED_TYPE) {
+ RandomSeedType.Fixed -> FIXED_RANDOM_SEED
+ RandomSeedType.Daily -> dailySeed()
+ else -> Random.Default.nextLong().mod(10_000_000).toLong()
+ }.absoluteValue
+}
+
+val DEBUG_TEXT = mutableStateOf("Hello Universe")
+const val SHOW_DEBUG_TEXT = false
+
+@Composable
+fun DebugText(text: MutableState<String>) {
+ if (SHOW_DEBUG_TEXT) {
+ Text(
+ modifier = Modifier.fillMaxWidth().border(0.5.dp, color = Color.Yellow).padding(2.dp),
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Medium,
+ fontSize = 9.sp,
+ color = Color.Yellow,
+ text = text.value
+ )
+ }
+}
+
+@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) }
+
+ LaunchedEffect("blah") {
+ delay(1000)
+ bottomVisible = true
+ delay(1000)
+ topVisible = true
+ }
+
+ Column(modifier = Modifier.fillMaxSize().padding(6.dp)) {
+ universe.triggerDraw.value // recompose on every frame
+ val explored = universe.planets.filter { it.explored }
+
+ 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")
+
+ // TODO: different colors, highlight latest discovery
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ AnimatedVisibility(modifier = Modifier, visible = bottomVisible, enter = flickerFadeIn) {
+ Text(
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.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")
+ }
+ )
+ }
+ }
+}
+
+class MainActivity : ComponentActivity() {
+ private var foldState = mutableStateOf<FoldingFeature?>(null)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ onWindowLayoutInfoChange()
+
+ val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed())
+
+ if (TEST_UNIVERSE) {
+ universe.initTest()
+ } else {
+ universe.initRandom()
+ }
+
+ setContent {
+ Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
+ DebugText(DEBUG_TEXT)
+
+ val minRadius = 50.dp.toLocalPx()
+ val maxRadius = 100.dp.toLocalPx()
+ FlightStick(
+ modifier = Modifier.fillMaxSize(),
+ minRadius = minRadius,
+ maxRadius = maxRadius,
+ color = Color.Green
+ ) { vec ->
+ (universe.follow as? Spacecraft)?.let { ship ->
+ if (vec == Vec2.Zero) {
+ ship.thrust = Vec2.Zero
+ } else {
+ val a = vec.angle()
+ ship.angle = a
+
+ val m = vec.mag()
+ if (m < minRadius) {
+ // within this radius, just reorient
+ ship.thrust = Vec2.Zero
+ } else {
+ ship.thrust =
+ Vec2.makeWithAngleMag(
+ a,
+ lexp(minRadius, maxRadius, m).coerceIn(0f, 1f)
+ )
+ }
+ }
+ }
+ }
+ Telemetry(universe)
+ }
+ }
+
+ private fun onWindowLayoutInfoChange() {
+ val windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ windowInfoTracker.windowLayoutInfo(this@MainActivity).collect { layoutInfo ->
+ foldState.value =
+ layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
+ Log.v("Landroid", "fold updated: $foldState")
+ }
+ }
+ }
+ }
+}
+
+@Preview(name = "phone", device = Devices.PHONE)
+@Preview(name = "fold", device = Devices.FOLDABLE)
+@Preview(name = "tablet", device = Devices.TABLET)
+@Composable
+fun MainActivityPreview() {
+ val universe = VisibleUniverse(namer = Namer(Resources.getSystem()), randomSeed = randomSeed())
+
+ universe.initTest()
+
+ Spaaaace(modifier = Modifier.fillMaxSize(), universe)
+ DebugText(DEBUG_TEXT)
+ Telemetry(universe)
+}
+
+@Composable
+fun FlightStick(
+ modifier: Modifier,
+ minRadius: Float = 0f,
+ maxRadius: Float = 1000f,
+ color: Color = Color.Green,
+ onStickChanged: (vector: Vec2) -> Unit
+) {
+ val origin = remember { mutableStateOf(Vec2.Zero) }
+ val target = remember { mutableStateOf(Vec2.Zero) }
+
+ Box(
+ modifier =
+ modifier
+ .pointerInput(Unit) {
+ forEachGesture {
+ awaitPointerEventScope {
+ // ACTION_DOWN
+ val down = awaitFirstDown(requireUnconsumed = false)
+ origin.value = down.position
+ target.value = down.position
+
+ do {
+ // ACTION_MOVE
+ val event: PointerEvent = awaitPointerEvent()
+ target.value = event.changes[0].position
+
+ onStickChanged(target.value - origin.value)
+ } while (
+ !event.changes.any { it.isConsumed } &&
+ event.changes.count { it.pressed } == 1
+ )
+
+ // ACTION_UP / CANCEL
+ target.value = Vec2.Zero
+ origin.value = Vec2.Zero
+
+ onStickChanged(Vec2.Zero)
+ }
+ }
+ }
+ .drawBehind {
+ if (origin.value != Vec2.Zero) {
+ val delta = target.value - origin.value
+ val mag = min(maxRadius, delta.mag())
+ val r = max(minRadius, mag)
+ val a = delta.angle()
+ drawCircle(
+ color = color,
+ center = origin.value,
+ radius = r,
+ style =
+ Stroke(
+ width = 2f,
+ pathEffect =
+ if (mag < minRadius)
+ PathEffect.dashPathEffect(
+ floatArrayOf(this.density * 1f, this.density * 2f)
+ )
+ else null
+ )
+ )
+ drawLine(
+ color = color,
+ start = origin.value,
+ end = origin.value + Vec2.makeWithAngleMag(a, mag),
+ strokeWidth = 2f
+ )
+ }
+ }
+ )
+}
+
+@Composable
+fun Spaaaace(
+ modifier: Modifier,
+ u: VisibleUniverse,
+ foldState: MutableState<FoldingFeature?> = mutableStateOf(null)
+) {
+ LaunchedEffect(u) {
+ while (true) withInfiniteAnimationFrameNanos { frameTimeNanos ->
+ u.simulateAndDrawFrame(frameTimeNanos)
+ }
+ }
+
+ var cameraZoom by remember { mutableStateOf(1f) }
+ var cameraOffset by remember { mutableStateOf(Offset.Zero) }
+
+ val transformableState =
+ rememberTransformableState { zoomChange, offsetChange, rotationChange ->
+ if (TOUCH_CAMERA_PAN) cameraOffset += offsetChange / cameraZoom
+ if (TOUCH_CAMERA_ZOOM)
+ cameraZoom = clamp(cameraZoom * zoomChange, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM)
+ }
+
+ var canvasModifier = modifier
+
+ if (TOUCH_CAMERA_PAN || TOUCH_CAMERA_ZOOM) {
+ canvasModifier = canvasModifier.transformable(transformableState)
+ }
+
+ val halfFolded = foldState.value?.let { it.state == FoldingFeature.State.HALF_OPENED } ?: false
+ val horizontalFold =
+ foldState.value?.let { it.orientation == FoldingFeature.Orientation.HORIZONTAL } ?: false
+
+ val centerFracX: Float by
+ animateFloatAsState(if (halfFolded && !horizontalFold) 0.25f else 0.5f, label = "centerX")
+ val centerFracY: Float by
+ animateFloatAsState(if (halfFolded && horizontalFold) 0.25f else 0.5f, label = "centerY")
+
+ Canvas(modifier = canvasModifier) {
+ drawRect(Colors.Eigengrau, Offset.Zero, size)
+
+ val closest = u.closestPlanet()
+ 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)
+ } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM
+ if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f
+
+ // cameraZoom: metersToPixels
+ // visibleSpaceSizeMeters: meters
+ // cameraOffset: meters ≈ vector pointing from ship to (0,0) (e.g. -pos)
+ val visibleSpaceSizeMeters = size / cameraZoom // meters x meters
+ val visibleSpaceRectMeters =
+ Rect(
+ -cameraOffset -
+ Offset(
+ visibleSpaceSizeMeters.width * centerFracX,
+ visibleSpaceSizeMeters.height * centerFracY
+ ),
+ visibleSpaceSizeMeters
+ )
+
+ var gridStep = 1000f
+ while (gridStep * cameraZoom < 32.dp.toPx()) gridStep *= 10
+
+ DEBUG_TEXT.value =
+ ("SIMULATION //\n" +
+ // "normalizedDist=${normalizedDist} \n" +
+ "entities: ${u.entities.size} // " +
+ "zoom: ${"%.4f".format(cameraZoom)}x // " +
+ "fps: ${"%3.0f".format(1f / u.dt)} " +
+ "dt: ${u.dt}\n" +
+ ((u.follow as? Spacecraft)?.let {
+ "ship: p=%s v=%7.2f a=%6.3f t=%s\n".format(
+ it.pos.str("%+7.1f"),
+ it.velocity.mag(),
+ it.angle,
+ it.thrust.str("%+5.2f")
+ )
+ }
+ ?: "") +
+ "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()}"
+ })
+
+ zoom(cameraZoom) {
+ // All coordinates are space coordinates now.
+
+ translate(
+ -visibleSpaceRectMeters.center.x + size.width * 0.5f,
+ -visibleSpaceRectMeters.center.y + size.height * 0.5f
+ ) {
+ // debug outer frame
+ // drawRect(
+ // Colors.Eigengrau2,
+ // visibleSpaceRectMeters.topLeft,
+ // visibleSpaceRectMeters.size,
+ // style = Stroke(width = 10f / cameraZoom)
+ // )
+
+ var x = floor(visibleSpaceRectMeters.left / gridStep) * gridStep
+ while (x < visibleSpaceRectMeters.right) {
+ drawLine(
+ color = Colors.Eigengrau2,
+ start = Offset(x, visibleSpaceRectMeters.top),
+ end = Offset(x, visibleSpaceRectMeters.bottom),
+ strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+ )
+ x += gridStep
+ }
+
+ var y = floor(visibleSpaceRectMeters.top / gridStep) * gridStep
+ while (y < visibleSpaceRectMeters.bottom) {
+ drawLine(
+ color = Colors.Eigengrau2,
+ start = Offset(visibleSpaceRectMeters.left, y),
+ end = Offset(visibleSpaceRectMeters.right, y),
+ strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+ )
+ y += gridStep
+ }
+
+ this@zoom.drawUniverse(u)
+ }
+ }
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
new file mode 100644
index 0000000..fdf29f7
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
@@ -0,0 +1,34 @@
+/*
+ * 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 kotlin.math.pow
+
+/** smoothstep. Ken Perlin's version */
+fun smooth(x: Float): Float {
+ return x * x * x * (x * (x * 6 - 15) + 10)
+}
+
+/** Kind of like an inverted smoothstep, but */
+fun invsmoothish(x: Float): Float {
+ return 0.25f * ((2f * x - 1f).pow(5f) + 1f) + 0.5f * x
+}
+
+/** Compute the fraction that progress represents between start and end (inverse of lerp). */
+fun lexp(start: Float, end: Float, progress: Float): Float {
+ return (progress - start) / (end - start)
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
new file mode 100644
index 0000000..67d536e
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.content.res.Resources
+import kotlin.random.Random
+
+import com.android.egg.R
+
+const val SUFFIX_PROB = 0.75f
+const val LETTER_PROB = 0.3f
+const val NUMBER_PROB = 0.3f
+const val RARE_PROB = 0.05f
+
+class Namer(resources: Resources) {
+ private val planetDescriptors = Bag(resources.getStringArray(R.array.planet_descriptors))
+ private val lifeDescriptors = Bag(resources.getStringArray(R.array.life_descriptors))
+ private val anyDescriptors = Bag(resources.getStringArray(R.array.any_descriptors))
+ private val atmoDescriptors = Bag(resources.getStringArray(R.array.atmo_descriptors))
+
+ private val planetTypes = Bag(resources.getStringArray(R.array.planet_types))
+ private val constellations = Bag(resources.getStringArray(R.array.constellations))
+ private val constellationsRare = Bag(resources.getStringArray(R.array.constellations_rare))
+ private val suffixes = Bag(resources.getStringArray(R.array.star_suffixes))
+ private val suffixesRare = Bag(resources.getStringArray(R.array.star_suffixes_rare))
+
+ private val planetTable = RandomTable(0.75f to planetDescriptors, 0.25f to anyDescriptors)
+
+ private var lifeTable = RandomTable(0.75f to lifeDescriptors, 0.25f to anyDescriptors)
+
+ private var constellationsTable =
+ RandomTable(RARE_PROB to constellationsRare, 1f - RARE_PROB to constellations)
+
+ private var suffixesTable = RandomTable(RARE_PROB to suffixesRare, 1f - RARE_PROB to suffixes)
+
+ private var atmoTable = RandomTable(0.75f to atmoDescriptors, 0.25f to anyDescriptors)
+
+ private var delimiterTable =
+ RandomTable(
+ 15f to " ",
+ 3f to "-",
+ 1f to "_",
+ 1f to "/",
+ 1f to ".",
+ 1f to "*",
+ 1f to "^",
+ 1f to "#",
+ 0.1f to "(^*!%@##!!"
+ )
+
+ fun describePlanet(rng: Random): String {
+ return planetTable.roll(rng).pull(rng) + " " + planetTypes.pull(rng)
+ }
+
+ fun describeLife(rng: Random): String {
+ return lifeTable.roll(rng).pull(rng)
+ }
+
+ fun nameSystem(rng: Random): String {
+ val parts = StringBuilder()
+ parts.append(constellationsTable.roll(rng).pull(rng))
+ if (rng.nextFloat() <= SUFFIX_PROB) {
+ parts.append(delimiterTable.roll(rng))
+ parts.append(suffixesTable.roll(rng).pull(rng))
+ if (rng.nextFloat() <= RARE_PROB) parts.append(' ').append(suffixesRare.pull(rng))
+ }
+ if (rng.nextFloat() <= LETTER_PROB) {
+ parts.append(delimiterTable.roll(rng))
+ parts.append('A' + rng.nextInt(0, 26))
+ if (rng.nextFloat() <= RARE_PROB) parts.append(delimiterTable.roll(rng))
+ }
+ if (rng.nextFloat() <= NUMBER_PROB) {
+ parts.append(delimiterTable.roll(rng))
+ parts.append(rng.nextInt(2, 5039))
+ }
+ return parts.toString()
+ }
+
+ fun describeAtmo(rng: Random): String {
+ return atmoTable.roll(rng).pull(rng)
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
new file mode 100644
index 0000000..8510640
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.util.Log
+import androidx.compose.ui.graphics.Path
+import kotlin.math.cos
+import kotlin.math.sin
+
+fun createPolygon(radius: Float, sides: Int): Path {
+ return Path().apply {
+ moveTo(radius, 0f)
+ val angleStep = PI2f / sides
+ for (i in 1 until sides) {
+ lineTo(radius * cos(angleStep * i), radius * sin(angleStep * i))
+ }
+ close()
+ }
+}
+
+fun createStar(radius1: Float, radius2: Float, points: Int): Path {
+ return Path().apply {
+ val angleStep = PI2f / points
+ moveTo(radius1, 0f)
+ lineTo(radius2 * cos(angleStep * (0.5f)), radius2 * sin(angleStep * (0.5f)))
+ for (i in 1 until points) {
+ lineTo(radius1 * cos(angleStep * i), radius1 * sin(angleStep * i))
+ lineTo(radius2 * cos(angleStep * (i + 0.5f)), radius2 * sin(angleStep * (i + 0.5f)))
+ }
+ close()
+ }
+}
+
+fun Path.parseSvgPathData(d: String) {
+ Regex("([A-Z])([-.,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(","))
+ 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])
+ "Z" -> close()
+ else -> Log.v("Landroid", "unsupported SVG command: $cmd")
+ }
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Physics.kt b/packages/EasterEgg/src/com/android/egg/landroid/Physics.kt
new file mode 100644
index 0000000..fc66ad6
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Physics.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.util.ArraySet
+import kotlin.random.Random
+
+// artificially speed up or slow down the simulation
+const val TIME_SCALE = 1f
+
+// if it's been over 1 real second since our last timestep, don't simulate that elapsed time.
+// this allows the simulation to "pause" when, for example, the activity pauses
+const val MAX_VALID_DT = 1f
+
+interface Entity {
+ // Integrate.
+ // Compute accelerations from forces, add accelerations to velocity, save old position,
+ // add velocity to position.
+ fun update(sim: Simulator, dt: Float)
+
+ // Post-integration step, after constraints are satisfied.
+ fun postUpdate(sim: Simulator, dt: Float)
+}
+
+open class Body(var name: String = "Unknown") : Entity {
+ var pos = Vec2.Zero
+ var opos = Vec2.Zero
+ var velocity = Vec2.Zero
+
+ var mass = 0f
+ var angle = 0f
+ var radius = 0f
+
+ var collides = true
+
+ var omega: Float
+ get() = angle - oangle
+ set(value) {
+ oangle = angle - value
+ }
+
+ var oangle = 0f
+
+ override fun update(sim: Simulator, dt: Float) {
+ if (dt <= 0) return
+
+ // integrate velocity
+ val vscaled = velocity * dt
+ opos = pos
+ pos += vscaled
+
+ // integrate angular velocity
+ // val wscaled = omega * timescale
+ // oangle = angle
+ // angle = (angle + wscaled) % PI2f
+ }
+
+ override fun postUpdate(sim: Simulator, dt: Float) {
+ if (dt <= 0) return
+ velocity = (pos - opos) / dt
+ }
+}
+
+interface Constraint {
+ // Solve constraints. Pick up objects and put them where they are "supposed" to be.
+ fun solve(sim: Simulator, dt: Float)
+}
+
+open class Container(val radius: Float) : Constraint {
+ private val list = ArraySet<Body>()
+ private val softness = 0.0f
+
+ override fun toString(): String {
+ return "Container($radius)"
+ }
+
+ fun add(p: Body) {
+ list.add(p)
+ }
+
+ fun remove(p: Body) {
+ list.remove(p)
+ }
+
+ override fun solve(sim: Simulator, dt: Float) {
+ for (p in list) {
+ if ((p.pos.mag() + p.radius) > radius) {
+ p.pos =
+ p.pos * (softness) +
+ Vec2.makeWithAngleMag(p.pos.angle(), radius - p.radius) * (1f - softness)
+ }
+ }
+ }
+}
+
+open class Simulator(val randomSeed: Long) {
+ private var wallClockNanos: Long = 0L
+ var now: Float = 0f
+ var dt: Float = 0f
+ val rng = Random(randomSeed)
+ val entities = ArraySet<Entity>(1000)
+ val constraints = ArraySet<Constraint>(100)
+
+ fun add(e: Entity) = entities.add(e)
+ fun remove(e: Entity) = entities.remove(e)
+ fun add(c: Constraint) = constraints.add(c)
+ fun remove(c: Constraint) = constraints.remove(c)
+
+ open fun updateAll(dt: Float, entities: ArraySet<Entity>) {
+ entities.forEach { it.update(this, dt) }
+ }
+
+ open fun solveAll(dt: Float, constraints: ArraySet<Constraint>) {
+ constraints.forEach { it.solve(this, dt) }
+ }
+
+ open fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) {
+ entities.forEach { it.postUpdate(this, dt) }
+ }
+
+ fun step(nanos: Long) {
+ val firstFrame = (wallClockNanos == 0L)
+
+ dt = (nanos - wallClockNanos) / 1_000_000_000f * TIME_SCALE
+ this.wallClockNanos = nanos
+
+ // we start the simulation on the next frame
+ if (firstFrame || dt > MAX_VALID_DT) return
+
+ // simulation is running; we start accumulating simulation time
+ this.now += dt
+
+ val localEntities = ArraySet(entities)
+ val localConstraints = ArraySet(constraints)
+
+ // position-based dynamics approach:
+ // 1. apply acceleration to velocity, save positions, apply velocity to position
+ updateAll(dt, localEntities)
+
+ // 2. solve all constraints
+ solveAll(dt, localConstraints)
+
+ // 3. compute new velocities from updated positions and saved positions
+ postUpdateAll(dt, localEntities)
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
new file mode 100644
index 0000000..ebbb2bd
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 kotlin.random.Random
+
+/**
+ * A bag of stones. Each time you pull one out it is not replaced, preventing duplicates. When the
+ * bag is exhausted, all the stones are replaced and reshuffled.
+ */
+class Bag<T>(items: Array<T>) {
+ private val remaining = items.copyOf()
+ private var next = remaining.size // will cause a shuffle on first pull()
+
+ /** Return the next random item from the bag, without replacing it. */
+ fun pull(rng: Random): T {
+ if (next >= remaining.size) {
+ remaining.shuffle(rng)
+ next = 0
+ }
+ return remaining[next++]
+ }
+}
+
+/**
+ * A loot table. The weight of each possibility is in the first of the pair; the value to be
+ * returned in the second. They need not add up to 1f (we will do that for you, free of charge).
+ */
+class RandomTable<T>(private vararg val pairs: Pair<Float, T>) {
+ private val total = pairs.map { it.first }.sum()
+
+ /** Select a random value from the weighted table. */
+ fun roll(rng: Random): T {
+ var x = rng.nextFloatInRange(0f, total)
+ for ((weight, result) in pairs) {
+ x -= weight
+ if (x < 0f) return result
+ }
+ return pairs.last().second
+ }
+}
+
+/** Return a random float in the range [from, until). */
+fun Random.nextFloatInRange(from: Float, until: Float): Float =
+ from + ((until - from) * nextFloat())
+
+/** 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)
+
+/** Choose a random element from an array. */
+fun <T> Random.choose(array: Array<T>) = array[nextInt(array.size)]
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
new file mode 100644
index 0000000..fec3ab3
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
@@ -0,0 +1,513 @@
+/*
+ * 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.util.ArraySet
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.util.lerp
+import kotlin.math.absoluteValue
+import kotlin.math.pow
+import kotlin.math.sqrt
+
+const val UNIVERSE_RANGE = 200_000f
+
+val NUM_PLANETS_RANGE = 1..10
+val STAR_RADIUS_RANGE = (1_000f..8_000f)
+val PLANET_RADIUS_RANGE = (50f..2_000f)
+val PLANET_ORBIT_RANGE = (STAR_RADIUS_RANGE.endInclusive * 2f)..(UNIVERSE_RANGE * 0.75f)
+
+const val GRAVITATION = 1e-2f
+const val KEPLER_CONSTANT = 50f // * 4f * PIf * PIf / GRAVITATION
+
+// m = d * r
+const val PLANETARY_DENSITY = 2.5f
+const val STELLAR_DENSITY = 0.5f
+
+const val SPACECRAFT_MASS = 10f
+
+const val CRAFT_SPEED_LIMIT = 5_000f
+const val MAIN_ENGINE_ACCEL = 1000f // thrust effect, pixels per second squared
+const val LAUNCH_MECO = 2f // how long to suspend gravity when launching
+
+const val SCALED_THRUST = true
+
+interface Removable {
+ fun canBeRemoved(): Boolean
+}
+
+open class Planet(
+ val orbitCenter: Vec2,
+ radius: Float,
+ pos: Vec2,
+ var speed: Float,
+ var color: Color = Color.White
+) : Body() {
+ var atmosphere = ""
+ var description = ""
+ var flora = ""
+ var fauna = ""
+ var explored = false
+ private val orbitRadius: Float
+ init {
+ this.radius = radius
+ this.pos = pos
+ orbitRadius = pos.distance(orbitCenter)
+ mass = 4 / 3 * PIf * radius.pow(3) * PLANETARY_DENSITY
+ }
+
+ override fun update(sim: Simulator, dt: Float) {
+ val orbitAngle = (pos - orbitCenter).angle()
+ // constant linear velocity
+ velocity = Vec2.makeWithAngleMag(orbitAngle + PIf / 2f, speed)
+
+ super.update(sim, dt)
+ }
+
+ override fun postUpdate(sim: Simulator, dt: Float) {
+ // This is kind of like a constraint, but whatever.
+ val orbitAngle = (pos - orbitCenter).angle()
+ pos = orbitCenter + Vec2.makeWithAngleMag(orbitAngle, orbitRadius)
+ super.postUpdate(sim, dt)
+ }
+}
+
+enum class StarClass {
+ O,
+ B,
+ A,
+ F,
+ G,
+ K,
+ M
+}
+
+fun starColor(cls: StarClass) =
+ when (cls) {
+ StarClass.O -> Color(0xFF6666FF)
+ StarClass.B -> Color(0xFFCCCCFF)
+ StarClass.A -> Color(0xFFEEEEFF)
+ StarClass.F -> Color(0xFFFFFFFF)
+ StarClass.G -> Color(0xFFFFFF66)
+ StarClass.K -> Color(0xFFFFCC33)
+ StarClass.M -> Color(0xFFFF8800)
+ }
+
+class Star(val cls: StarClass, radius: Float) :
+ Planet(orbitCenter = Vec2.Zero, radius = radius, pos = Vec2.Zero, speed = 0f) {
+ init {
+ pos = Vec2.Zero
+ mass = 4 / 3 * PIf * radius.pow(3) * STELLAR_DENSITY
+ color = starColor(cls)
+ collides = false
+ }
+ var anim = 0f
+ override fun update(sim: Simulator, dt: Float) {
+ anim += dt
+ }
+}
+
+open class Universe(val namer: Namer, randomSeed: Long) : Simulator(randomSeed) {
+ var latestDiscovery: Planet? = null
+ lateinit var star: Star
+ lateinit var ship: Spacecraft
+ val planets: MutableList<Planet> = mutableListOf()
+ var follow: Body? = null
+ val ringfence = Container(UNIVERSE_RANGE)
+
+ fun initTest() {
+ val systemName = "TEST SYSTEM"
+ star =
+ Star(
+ cls = StarClass.A,
+ radius = STAR_RADIUS_RANGE.endInclusive,
+ )
+ .apply { name = "TEST SYSTEM" }
+
+ repeat(NUM_PLANETS_RANGE.last) {
+ val thisPlanetFrac = it.toFloat() / (NUM_PLANETS_RANGE.last - 1)
+ val radius =
+ lerp(PLANET_RADIUS_RANGE.start, PLANET_RADIUS_RANGE.endInclusive, thisPlanetFrac)
+ val orbitRadius =
+ lerp(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive, thisPlanetFrac)
+
+ val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT
+ val speed = 2f * PIf * orbitRadius / period
+
+ val p =
+ Planet(
+ orbitCenter = star.pos,
+ radius = radius,
+ pos = star.pos + Vec2.makeWithAngleMag(thisPlanetFrac * PI2f, orbitRadius),
+ speed = speed,
+ color = Colors.Eigengrau4
+ )
+ 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"
+ p.flora = "mass=${p.mass}"
+ p.fauna = "speed=$speed"
+ planets.add(p)
+ add(p)
+ }
+
+ planets.sortBy { it.pos.distance(star.pos) }
+ planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" }
+ add(star)
+
+ ship = Spacecraft()
+
+ ship.pos = star.pos + Vec2.makeWithAngleMag(PIf / 4, PLANET_ORBIT_RANGE.start)
+ ship.angle = 0f
+ add(ship)
+
+ ringfence.add(ship)
+ add(ringfence)
+
+ follow = ship
+ }
+
+ fun initRandom() {
+ val systemName = namer.nameSystem(rng)
+ star =
+ Star(
+ cls = rng.choose(StarClass.values()),
+ radius = rng.nextFloatInRange(STAR_RADIUS_RANGE)
+ )
+ star.name = systemName
+ repeat(rng.nextInt(NUM_PLANETS_RANGE.first, NUM_PLANETS_RANGE.last + 1)) {
+ val radius = rng.nextFloatInRange(PLANET_RADIUS_RANGE)
+ val orbitRadius =
+ lerp(
+ PLANET_ORBIT_RANGE.start,
+ PLANET_ORBIT_RANGE.endInclusive,
+ rng.nextFloat().pow(1f)
+ )
+
+ // Kepler's third law
+ val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT
+ val speed = 2f * PIf * orbitRadius / period
+
+ val p =
+ Planet(
+ orbitCenter = star.pos,
+ radius = radius,
+ pos = star.pos + Vec2.makeWithAngleMag(rng.nextFloat() * PI2f, orbitRadius),
+ speed = speed,
+ color = Colors.Eigengrau4
+ )
+ 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)
+ p.fauna = namer.describeLife(rng)
+ planets.add(p)
+ add(p)
+ }
+ planets.sortBy { it.pos.distance(star.pos) }
+ planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" }
+ add(star)
+
+ ship = Spacecraft()
+
+ ship.pos =
+ star.pos +
+ Vec2.makeWithAngleMag(
+ rng.nextFloat() * PI2f,
+ rng.nextFloatInRange(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive)
+ )
+ ship.angle = rng.nextFloat() * PI2f
+ add(ship)
+
+ ringfence.add(ship)
+ add(ringfence)
+
+ follow = ship
+ }
+
+ override fun updateAll(dt: Float, entities: ArraySet<Entity>) {
+ // check for passing in front of the sun
+ ship.transit = false
+
+ (planets + star).forEach { planet ->
+ val vector = planet.pos - ship.pos
+ val d = vector.mag()
+ if (d < planet.radius) {
+ if (planet is Star) ship.transit = true
+ } else if (
+ now > ship.launchClock + LAUNCH_MECO
+ ) { // within MECO sec of launch, no gravity at all
+ // simulate gravity: $ f_g = G * m1 * m2 * 1/d^2 $
+ ship.velocity =
+ ship.velocity +
+ Vec2.makeWithAngleMag(
+ vector.angle(),
+ GRAVITATION * (ship.mass * planet.mass) / d.pow(2)
+ ) * dt
+ }
+ }
+
+ super.updateAll(dt, entities)
+ }
+
+ fun closestPlanet(): Planet {
+ val bodiesByDist =
+ (planets + star)
+ .map { planet -> (planet.pos - ship.pos) to planet }
+ .sortedBy { it.first.mag() }
+
+ return bodiesByDist[0].second
+ }
+
+ override fun solveAll(dt: Float, constraints: ArraySet<Constraint>) {
+ if (ship.landing == null) {
+ val planet = closestPlanet()
+
+ if (planet.collides) {
+ val d = (ship.pos - planet.pos).mag() - ship.radius - planet.radius
+ val a = (ship.pos - planet.pos).angle()
+
+ if (d < 0) {
+ // landing, or impact?
+
+ // 1. relative speed
+ val vDiff = (ship.velocity - planet.velocity).mag()
+ // 2. landing angle
+ val aDiff = (ship.angle - a).absoluteValue
+
+ // landing criteria
+ if (aDiff < PIf / 4
+ // &&
+ // vDiff < 100f
+ ) {
+ val landing = Landing(ship, planet, a)
+ ship.landing = landing
+ ship.velocity = planet.velocity
+ add(landing)
+
+ planet.explored = true
+ latestDiscovery = planet
+ } else {
+ val impact = planet.pos + Vec2.makeWithAngleMag(a, planet.radius)
+ ship.pos =
+ planet.pos + Vec2.makeWithAngleMag(a, planet.radius + ship.radius - d)
+
+ // add(Spark(
+ // lifetime = 1f,
+ // style = Spark.Style.DOT,
+ // color = Color.Yellow,
+ // size = 10f
+ // ).apply {
+ // pos = impact
+ // opos = impact
+ // velocity = Vec2.Zero
+ // })
+ //
+ (1..10).forEach {
+ Spark(
+ lifetime = rng.nextFloatInRange(0.5f, 2f),
+ style = Spark.Style.DOT,
+ color = Color.White,
+ size = 1f
+ )
+ .apply {
+ pos =
+ impact +
+ Vec2.makeWithAngleMag(
+ rng.nextFloatInRange(0f, 2 * PIf),
+ rng.nextFloatInRange(0.1f, 0.5f)
+ )
+ opos = pos
+ velocity =
+ ship.velocity * 0.8f +
+ Vec2.makeWithAngleMag(
+ // a +
+ // rng.nextFloatInRange(-PIf, PIf),
+ rng.nextFloatInRange(0f, 2 * PIf),
+ rng.nextFloatInRange(0.1f, 0.5f)
+ )
+ add(this)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ super.solveAll(dt, constraints)
+ }
+
+ override fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) {
+ super.postUpdateAll(dt, entities)
+
+ entities
+ .filterIsInstance<Removable>()
+ .filter(predicate = Removable::canBeRemoved)
+ .filterIsInstance<Entity>()
+ .forEach { remove(it) }
+ }
+}
+
+class Landing(val ship: Spacecraft, val planet: Planet, val angle: Float) : Constraint {
+ private val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius)
+ 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
+ }
+}
+
+class Spark(
+ var lifetime: Float,
+ collides: Boolean = false,
+ mass: Float = 0f,
+ val style: Style = Style.LINE,
+ val color: Color = Color.Gray,
+ val size: Float = 2f
+) : Removable, Body() {
+ enum class Style {
+ LINE,
+ LINE_ABSOLUTE,
+ DOT,
+ DOT_ABSOLUTE,
+ RING
+ }
+
+ init {
+ this.collides = collides
+ this.mass = mass
+ }
+ override fun update(sim: Simulator, dt: Float) {
+ super.update(sim, dt)
+ lifetime -= dt
+ }
+ override fun canBeRemoved(): Boolean {
+ return lifetime < 0
+ }
+}
+
+const val TRACK_LENGTH = 10_000
+const val SIMPLE_TRACK_DRAWING = true
+
+class Track {
+ val positions = ArrayDeque<Vec2>(TRACK_LENGTH)
+ private val angles = ArrayDeque<Float>(TRACK_LENGTH)
+ fun add(x: Float, y: Float, a: Float) {
+ if (positions.size >= (TRACK_LENGTH - 1)) {
+ positions.removeFirst()
+ angles.removeFirst()
+ positions.removeFirst()
+ angles.removeFirst()
+ }
+ positions.addLast(Vec2(x, y))
+ angles.addLast(a)
+ }
+}
+
+class Spacecraft : Body() {
+ var thrust = Vec2.Zero
+ var launchClock = 0f
+
+ var transit = false
+
+ val track = Track()
+
+ var landing: Landing? = null
+
+ init {
+ mass = SPACECRAFT_MASS
+ radius = 12f
+ }
+
+ override fun update(sim: Simulator, dt: Float) {
+ // check for thrusters
+ val thrustMag = thrust.mag()
+ if (thrustMag > 0) {
+ 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 */
+
+ if (sim.now > launchClock) {
+ // first-stage to orbit has 1000x power
+ // deltaV *= 1000f
+ sim.remove(landing)
+ this.landing = null
+ } else {
+ deltaV = 0f
+ }
+ }
+
+ // this is it. impart thrust to the ship.
+ // note that we always thrust in the forward direction
+ velocity += Vec2.makeWithAngleMag(angle, deltaV)
+ } else {
+ if (launchClock != 0f) launchClock = 0f
+ }
+
+ // apply global speed limit
+ if (velocity.mag() > CRAFT_SPEED_LIMIT)
+ velocity = Vec2.makeWithAngleMag(velocity.angle(), CRAFT_SPEED_LIMIT)
+
+ super.update(sim, dt)
+ }
+
+ override fun postUpdate(sim: Simulator, dt: Float) {
+ super.postUpdate(sim, dt)
+
+ // special effects all need to be added after the simulation step so they have
+ // the correct position of the ship.
+ track.add(pos.x, pos.y, angle)
+
+ val mag = thrust.mag()
+ if (sim.rng.nextFloat() < mag) {
+ // exhaust
+ sim.add(
+ Spark(
+ lifetime = sim.rng.nextFloatInRange(0.5f, 1f),
+ collides = true,
+ mass = 1f,
+ style = Spark.Style.RING,
+ size = 3f,
+ color = Color(0x40FFFFFF)
+ )
+ .also { spark ->
+ spark.pos = pos
+ spark.opos = pos
+ spark.velocity =
+ velocity +
+ Vec2.makeWithAngleMag(
+ angle + sim.rng.nextFloatInRange(-0.2f, 0.2f),
+ -MAIN_ENGINE_ACCEL * mag * 10f * dt
+ )
+ }
+ )
+ }
+ }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt b/packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt
new file mode 100644
index 0000000..82bae75
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.ui.geometry.Offset
+import kotlin.math.PI
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+
+const val PIf = PI.toFloat()
+const val PI2f = (2 * PI).toFloat()
+
+typealias Vec2 = Offset
+
+fun Vec2.str(fmt: String = "%+.2f"): String = "<$fmt,$fmt>".format(x, y)
+
+fun Vec2(x: Float, y: Float): Vec2 = Offset(x, y)
+
+fun Vec2.mag(): Float {
+ return getDistance()
+}
+
+fun Vec2.distance(other: Vec2): Float {
+ return (this - other).mag()
+}
+
+fun Vec2.angle(): Float {
+ return atan2(y, x)
+}
+
+fun Vec2.dot(o: Vec2): Float {
+ return x * o.x + y * o.y
+}
+
+fun Vec2.product(f: Float): Vec2 {
+ return Vec2(x * f, y * f)
+}
+
+fun Offset.Companion.makeWithAngleMag(a: Float, m: Float): Vec2 {
+ return Vec2(m * cos(a), m * sin(a))
+}
+
+fun Vec2.rotate(angle: Float, origin: Vec2 = Vec2.Zero): Offset {
+ val translated = this - origin
+ return origin +
+ Offset(
+ (translated.x * cos(angle) - translated.y * sin(angle)),
+ (translated.x * sin(angle) + translated.y * cos(angle))
+ )
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
new file mode 100644
index 0000000..24b9c6a
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
@@ -0,0 +1,334 @@
+/*
+ * 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)
+ }
+ }
+ }
+}