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
This commit is contained in:
@@ -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",
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
21
packages/EasterEgg/res/drawable/android14_patch_adaptive.xml
Normal file
21
packages/EasterEgg/res/drawable/android14_patch_adaptive.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
371
packages/EasterEgg/res/values/landroid_strings.xml
Normal file
371
packages/EasterEgg/res/values/landroid_strings.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
29
packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
Normal file
29
packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
543
packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
Normal file
543
packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
Normal file
34
packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
Normal 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)
|
||||
}
|
||||
96
packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
Normal file
96
packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
62
packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
Normal file
62
packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
160
packages/EasterEgg/src/com/android/egg/landroid/Physics.kt
Normal file
160
packages/EasterEgg/src/com/android/egg/landroid/Physics.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
513
packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
Normal file
513
packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
Normal 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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
65
packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt
Normal file
65
packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt
Normal 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))
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user