Merge "ANDROID 14 -- SPACE PROGRAM -- LAUNCH IMMINENT" into udc-dev

This commit is contained in:
Daniel Sandler
2023-06-21 19:12:32 +00:00
committed by Android (Google) Code Review
19 changed files with 2633 additions and 13 deletions

View File

@@ -26,7 +26,10 @@ package {
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 @@ android_app {
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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -14,7 +14,7 @@ Copyright (C) 2018 The Android Open Source Project
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>

View File

@@ -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)
}

View File

@@ -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)
)
)

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)]

View File

@@ -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
)
}
)
}
}
}

View File

@@ -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))
)
}

View File

@@ -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)
}
}
}
}