@mumulinya167/ccm-pet-assets 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +4 -0
  2. package/assets/calico/calico-collapsing.apng +0 -0
  3. package/assets/calico/calico-dozing.apng +0 -0
  4. package/assets/calico/calico-error.apng +0 -0
  5. package/assets/calico/calico-happy.apng +0 -0
  6. package/assets/calico/calico-idle-follow.svg +324 -0
  7. package/assets/calico/calico-idle.apng +0 -0
  8. package/assets/calico/calico-mini-alert.apng +0 -0
  9. package/assets/calico/calico-mini-crabwalk.apng +0 -0
  10. package/assets/calico/calico-mini-enter.apng +0 -0
  11. package/assets/calico/calico-mini-happy.apng +0 -0
  12. package/assets/calico/calico-mini-idle.apng +0 -0
  13. package/assets/calico/calico-mini-peek.apng +0 -0
  14. package/assets/calico/calico-mini-sleep.apng +0 -0
  15. package/assets/calico/calico-notification.apng +0 -0
  16. package/assets/calico/calico-react-drag.apng +0 -0
  17. package/assets/calico/calico-react-left.apng +0 -0
  18. package/assets/calico/calico-react-poke.apng +0 -0
  19. package/assets/calico/calico-sleeping.apng +0 -0
  20. package/assets/calico/calico-thinking.apng +0 -0
  21. package/assets/calico/calico-waking.apng +0 -0
  22. package/assets/calico/calico-working-building.apng +0 -0
  23. package/assets/calico/calico-working-carrying.apng +0 -0
  24. package/assets/calico/calico-working-conducting.apng +0 -0
  25. package/assets/calico/calico-working-juggling.apng +0 -0
  26. package/assets/calico/calico-working-sweeping.apng +0 -0
  27. package/assets/calico/calico-working-typing.apng +0 -0
  28. package/assets/calico/calico-yawning.apng +0 -0
  29. package/assets/calico.svg +324 -0
  30. package/assets/clawd/LICENSE +22 -0
  31. package/assets/clawd/clawd-about-hero.svg +202 -0
  32. package/assets/clawd/clawd-collapse-sleep.svg +247 -0
  33. package/assets/clawd/clawd-error.svg +94 -0
  34. package/assets/clawd/clawd-happy.svg +161 -0
  35. package/assets/clawd/clawd-headphones-groove.svg +194 -0
  36. package/assets/clawd/clawd-idle-bubble.svg +180 -0
  37. package/assets/clawd/clawd-idle-collapse.svg +101 -0
  38. package/assets/clawd/clawd-idle-doze.svg +72 -0
  39. package/assets/clawd/clawd-idle-follow.svg +64 -0
  40. package/assets/clawd/clawd-idle-living.svg +196 -0
  41. package/assets/clawd/clawd-idle-look.svg +115 -0
  42. package/assets/clawd/clawd-idle-reading.svg +252 -0
  43. package/assets/clawd/clawd-idle-yawn.svg +158 -0
  44. package/assets/clawd/clawd-mini-alert.svg +129 -0
  45. package/assets/clawd/clawd-mini-crabwalk.svg +76 -0
  46. package/assets/clawd/clawd-mini-enter-sleep.svg +64 -0
  47. package/assets/clawd/clawd-mini-enter.svg +114 -0
  48. package/assets/clawd/clawd-mini-happy.svg +124 -0
  49. package/assets/clawd/clawd-mini-idle.svg +83 -0
  50. package/assets/clawd/clawd-mini-peek.svg +82 -0
  51. package/assets/clawd/clawd-mini-sleep.svg +112 -0
  52. package/assets/clawd/clawd-mini-typing.svg +153 -0
  53. package/assets/clawd/clawd-notification.svg +438 -0
  54. package/assets/clawd/clawd-react-annoyed.svg +167 -0
  55. package/assets/clawd/clawd-react-double-jump.svg +229 -0
  56. package/assets/clawd/clawd-react-double.svg +108 -0
  57. package/assets/clawd/clawd-react-drag.svg +102 -0
  58. package/assets/clawd/clawd-react-left.svg +102 -0
  59. package/assets/clawd/clawd-react-right.svg +102 -0
  60. package/assets/clawd/clawd-sleeping.svg +118 -0
  61. package/assets/clawd/clawd-static-base.svg +21 -0
  62. package/assets/clawd/clawd-wake.svg +277 -0
  63. package/assets/clawd/clawd-working-building.svg +329 -0
  64. package/assets/clawd/clawd-working-carrying.svg +178 -0
  65. package/assets/clawd/clawd-working-debugger.svg +245 -0
  66. package/assets/clawd/clawd-working-juggling.svg +183 -0
  67. package/assets/clawd/clawd-working-sweeping.svg +248 -0
  68. package/assets/clawd/clawd-working-thinking.svg +501 -0
  69. package/assets/clawd/clawd-working-typing-boss.svg +307 -0
  70. package/assets/clawd/clawd-working-typing.svg +273 -0
  71. package/assets/clawd/clawd-working-ultrathink.svg +166 -0
  72. package/assets/clawd/clawd-working-wizard.svg +356 -0
  73. package/assets/clawd.svg +64 -0
  74. package/assets/cloudling/cloudling/cloudling-attention.svg +380 -0
  75. package/assets/cloudling/cloudling/cloudling-building.svg +667 -0
  76. package/assets/cloudling/cloudling/cloudling-carrying.svg +604 -0
  77. package/assets/cloudling/cloudling/cloudling-conducting.svg +616 -0
  78. package/assets/cloudling/cloudling/cloudling-dozing-to-sleeping.svg +578 -0
  79. package/assets/cloudling/cloudling/cloudling-dozing.svg +358 -0
  80. package/assets/cloudling/cloudling/cloudling-error.svg +428 -0
  81. package/assets/cloudling/cloudling/cloudling-idle-reading.svg +658 -0
  82. package/assets/cloudling/cloudling/cloudling-idle-to-dozing.svg +568 -0
  83. package/assets/cloudling/cloudling/cloudling-idle-to-sleeping.svg +811 -0
  84. package/assets/cloudling/cloudling/cloudling-idle.svg +255 -0
  85. package/assets/cloudling/cloudling/cloudling-juggling.svg +648 -0
  86. package/assets/cloudling/cloudling/cloudling-mini-alert.svg +390 -0
  87. package/assets/cloudling/cloudling/cloudling-mini-crabwalk.svg +335 -0
  88. package/assets/cloudling/cloudling/cloudling-mini-enter-roll-in.svg +226 -0
  89. package/assets/cloudling/cloudling/cloudling-mini-enter-sleep.svg +232 -0
  90. package/assets/cloudling/cloudling/cloudling-mini-happy.svg +371 -0
  91. package/assets/cloudling/cloudling/cloudling-mini-idle.svg +279 -0
  92. package/assets/cloudling/cloudling/cloudling-mini-peek.svg +328 -0
  93. package/assets/cloudling/cloudling/cloudling-mini-sleep.svg +260 -0
  94. package/assets/cloudling/cloudling/cloudling-mini-typing.svg +692 -0
  95. package/assets/cloudling/cloudling/cloudling-notification.svg +494 -0
  96. package/assets/cloudling/cloudling/cloudling-react-drag.svg +83 -0
  97. package/assets/cloudling/cloudling/cloudling-sleeping-to-idle.svg +555 -0
  98. package/assets/cloudling/cloudling/cloudling-sleeping.svg +500 -0
  99. package/assets/cloudling/cloudling/cloudling-sweeping.svg +292 -0
  100. package/assets/cloudling/cloudling/cloudling-thinking.svg +720 -0
  101. package/assets/cloudling/cloudling/cloudling-typing.svg +354 -0
  102. package/assets/cloudling/cloudling-attention.svg +380 -0
  103. package/assets/cloudling/cloudling-building.svg +667 -0
  104. package/assets/cloudling/cloudling-carrying.svg +604 -0
  105. package/assets/cloudling/cloudling-conducting.svg +616 -0
  106. package/assets/cloudling/cloudling-dozing-to-sleeping.svg +578 -0
  107. package/assets/cloudling/cloudling-dozing.svg +358 -0
  108. package/assets/cloudling/cloudling-error.svg +428 -0
  109. package/assets/cloudling/cloudling-idle-reading.svg +658 -0
  110. package/assets/cloudling/cloudling-idle-to-dozing.svg +568 -0
  111. package/assets/cloudling/cloudling-idle-to-sleeping.svg +811 -0
  112. package/assets/cloudling/cloudling-idle.svg +255 -0
  113. package/assets/cloudling/cloudling-juggling.svg +648 -0
  114. package/assets/cloudling/cloudling-mini-alert.svg +390 -0
  115. package/assets/cloudling/cloudling-mini-crabwalk.svg +335 -0
  116. package/assets/cloudling/cloudling-mini-enter-roll-in.svg +226 -0
  117. package/assets/cloudling/cloudling-mini-enter-sleep.svg +232 -0
  118. package/assets/cloudling/cloudling-mini-happy.svg +371 -0
  119. package/assets/cloudling/cloudling-mini-idle.svg +279 -0
  120. package/assets/cloudling/cloudling-mini-peek.svg +328 -0
  121. package/assets/cloudling/cloudling-mini-sleep.svg +260 -0
  122. package/assets/cloudling/cloudling-mini-typing.svg +692 -0
  123. package/assets/cloudling/cloudling-notification.svg +494 -0
  124. package/assets/cloudling/cloudling-react-drag.svg +83 -0
  125. package/assets/cloudling/cloudling-sleeping-to-idle.svg +555 -0
  126. package/assets/cloudling/cloudling-sleeping.svg +500 -0
  127. package/assets/cloudling/cloudling-sweeping.svg +292 -0
  128. package/assets/cloudling/cloudling-thinking.svg +720 -0
  129. package/assets/cloudling/cloudling-typing.svg +354 -0
  130. package/assets/cloudling.svg +255 -0
  131. package/assets/ghost-attention.svg +19 -0
  132. package/assets/ghost-carrying.svg +19 -0
  133. package/assets/ghost-collapsing.svg +19 -0
  134. package/assets/ghost-dozing.svg +23 -0
  135. package/assets/ghost-error.svg +19 -0
  136. package/assets/ghost-happy.svg +23 -0
  137. package/assets/ghost-idle-action1.svg +19 -0
  138. package/assets/ghost-idle-action2.svg +19 -0
  139. package/assets/ghost-idle-action3.svg +19 -0
  140. package/assets/ghost-idle.svg +11 -0
  141. package/assets/ghost-juggling.svg +19 -0
  142. package/assets/ghost-notification.svg +19 -0
  143. package/assets/ghost-react-double.svg +23 -0
  144. package/assets/ghost-react-drag.svg +23 -0
  145. package/assets/ghost-react-left.svg +19 -0
  146. package/assets/ghost-react-right.svg +19 -0
  147. package/assets/ghost-sleeping.svg +19 -0
  148. package/assets/ghost-sweeping.svg +19 -0
  149. package/assets/ghost-thinking.svg +19 -0
  150. package/assets/ghost-waking.svg +19 -0
  151. package/assets/ghost-working.svg +19 -0
  152. package/assets/ghost-yawning.svg +23 -0
  153. package/assets/ghost.svg +11 -0
  154. package/assets/robot-attention.svg +19 -0
  155. package/assets/robot-carrying.svg +19 -0
  156. package/assets/robot-collapsing.svg +19 -0
  157. package/assets/robot-dozing.svg +23 -0
  158. package/assets/robot-error.svg +19 -0
  159. package/assets/robot-happy.svg +23 -0
  160. package/assets/robot-idle-action1.svg +19 -0
  161. package/assets/robot-idle-action2.svg +19 -0
  162. package/assets/robot-idle-action3.svg +19 -0
  163. package/assets/robot-idle.svg +11 -0
  164. package/assets/robot-juggling.svg +19 -0
  165. package/assets/robot-notification.svg +19 -0
  166. package/assets/robot-react-double.svg +23 -0
  167. package/assets/robot-react-drag.svg +23 -0
  168. package/assets/robot-react-left.svg +19 -0
  169. package/assets/robot-react-right.svg +19 -0
  170. package/assets/robot-sleeping.svg +19 -0
  171. package/assets/robot-sweeping.svg +19 -0
  172. package/assets/robot-thinking.svg +19 -0
  173. package/assets/robot-waking.svg +19 -0
  174. package/assets/robot-working.svg +19 -0
  175. package/assets/robot-yawning.svg +23 -0
  176. package/assets/robot.svg +11 -0
  177. package/manifest.json +889 -0
  178. package/package.json +18 -0
@@ -0,0 +1,648 @@
1
+ <svg id="stageSvg" xmlns="http://www.w3.org/2000/svg" width="880" height="720" viewBox="-32 -24 88 72" preserveAspectRatio="xMidYMid meet">
2
+ <style><![CDATA[
3
+ #stageBg { fill: #0f0f14; }
4
+ .edge-rim-tight { stroke-opacity: .65; }
5
+ .edge-rim-soft { stroke-opacity: .50; }
6
+ .edge-contact { stroke-opacity: .65; }
7
+ @media (prefers-color-scheme: light) {
8
+ #stageBg { fill: #fff; }
9
+ .edge-rim-tight { stroke-opacity: .75; }
10
+ .edge-rim-soft { stroke-opacity: .60; }
11
+ .edge-contact { stroke-opacity: .75; }
12
+ .cloud-drop-shadow { flood-opacity: .13; }
13
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
14
+ .food-cloud > path {
15
+ stroke: #5870E8 !important;
16
+ stroke-width: 1.2 !important;
17
+ stroke-opacity: .9 !important;
18
+ vector-effect: non-scaling-stroke !important;
19
+ }
20
+ }
21
+ ]]></style>
22
+ <defs>
23
+ <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
24
+ <stop offset="0" stop-color="#f0f9ff"/>
25
+ <stop offset="0.5" stop-color="#e0f2fe"/>
26
+ <stop offset="1" stop-color="#bae6fd"/>
27
+ </linearGradient>
28
+ <linearGradient id="eye-grad" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
29
+ <stop offset="0" stop-color="#B1A7FF"/>
30
+ <stop offset="0.5" stop-color="#7A9DFF"/>
31
+ <stop offset="1" stop-color="#3941FF"/>
32
+ </linearGradient>
33
+ <radialGradient id="hi-grad" cx="0.45" cy="0.25" r="0.55">
34
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.9"/>
35
+ <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
36
+ </radialGradient>
37
+ <linearGradient id="plane-white" x1="5" y1="5" x2="31" y2="25" gradientUnits="userSpaceOnUse">
38
+ <stop id="planeWhite0" offset="0" stop-color="#fff7d6"/>
39
+ <stop id="planeWhite1" offset="0.46" stop-color="#FCE3B0"/>
40
+ <stop id="planeWhite2" offset="1" stop-color="#EFB85C"/>
41
+ </linearGradient>
42
+ <linearGradient id="plane-lavender" x1="4" y1="6" x2="21" y2="32" gradientUnits="userSpaceOnUse">
43
+ <stop id="planeLavender0" offset="0" stop-color="#ffeaf3"/>
44
+ <stop id="planeLavender1" offset="0.54" stop-color="#FCC9DD"/>
45
+ <stop id="planeLavender2" offset="1" stop-color="#D768A0"/>
46
+ </linearGradient>
47
+ <linearGradient id="plane-mid" x1="8" y1="10" x2="25" y2="29" gradientUnits="userSpaceOnUse">
48
+ <stop id="planeMid0" offset="0" stop-color="#dbfff8"/>
49
+ <stop id="planeMid1" offset="0.60" stop-color="#A0E5DD"/>
50
+ <stop id="planeMid2" offset="1" stop-color="#35AFA8"/>
51
+ </linearGradient>
52
+ <linearGradient id="plane-dark" x1="5" y1="8" x2="23" y2="29" gradientUnits="userSpaceOnUse">
53
+ <stop id="planeDark0" offset="0" stop-color="#dcd5ff"/>
54
+ <stop id="planeDark1" offset="0.55" stop-color="#B1A7FF"/>
55
+ <stop id="planeDark2" offset="1" stop-color="#5F52D9"/>
56
+ </linearGradient>
57
+ <filter id="cloud-drop" x="-45%" y="-45%" width="190%" height="190%">
58
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
59
+ </filter>
60
+ <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
61
+ <feGaussianBlur stdDeviation="0.16"/>
62
+ </filter>
63
+ <filter id="plane-drop" x="-45%" y="-45%" width="190%" height="190%">
64
+ <feDropShadow id="planeShadow" dx="0" dy="0.45" stdDeviation="0.65" flood-color="#3941FF" flood-opacity="0.30"/>
65
+ </filter>
66
+ <filter id="trail-blur" x="-80%" y="-90%" width="260%" height="280%">
67
+ <feGaussianBlur stdDeviation="0.10"/>
68
+ </filter>
69
+ <clipPath id="cloud-clip">
70
+ <path id="cloud-clip-path"/>
71
+ </clipPath>
72
+ <g id="paper-plane-symbol">
73
+ <polygon points="3,6 24,23 20.6,29.5" fill="url(#plane-mid)"/>
74
+ <polygon points="3,6 20.1,25.6 20.6,29.5" fill="url(#plane-dark)"/>
75
+ <polygon points="3,6 20.1,25.6 6.7,32.2" fill="url(#plane-lavender)"/>
76
+ <polygon points="3,6 34,15.7 24,23" fill="url(#plane-white)"/>
77
+ <g id="planeFoldLines" stroke="rgba(57,65,255,.38)" stroke-width="0.34" stroke-linecap="round" fill="none">
78
+ <line x1="3" y1="6" x2="24" y2="23"/>
79
+ <line x1="3" y1="6" x2="20.6" y2="29.5"/>
80
+ <line x1="3" y1="6" x2="20.1" y2="25.6"/>
81
+ </g>
82
+ <path id="planeNoseHighlight" d="M 3 6 L 34 15.7" stroke="rgba(255,255,255,.74)" stroke-width="0.3" stroke-linecap="round" fill="none"/>
83
+ </g>
84
+ <g id="paper-plane-under-symbol">
85
+ <polygon id="planeUnderMid" points="3,6 24,23 20.6,29.5" fill="#327E86"/>
86
+ <polygon id="planeUnderDark" points="3,6 20.1,25.6 20.6,29.5" fill="#3F3595"/>
87
+ <polygon id="planeUnderLavender" points="3,6 20.1,25.6 6.7,32.2" fill="#A94B7E"/>
88
+ <polygon id="planeUnderWhite" points="3,6 34,15.7 24,23" fill="#B9863C"/>
89
+ <g id="planeUnderFoldLines" stroke="rgba(255,255,255,.28)" stroke-width="0.32" stroke-linecap="round" fill="none">
90
+ <line x1="3" y1="6" x2="24" y2="23"/>
91
+ <line x1="3" y1="6" x2="20.6" y2="29.5"/>
92
+ <line x1="3" y1="6" x2="20.1" y2="25.6"/>
93
+ </g>
94
+ <path id="planeUnderNoseHighlight" d="M 3 6 L 34 15.7" stroke="rgba(255,255,255,.34)" stroke-width="0.28" stroke-linecap="round" fill="none"/>
95
+ </g>
96
+ </defs>
97
+ <g id="trailBack" fill="none" stroke-linecap="round" stroke-linejoin="round">
98
+ <path id="trailBackSoft" filter="url(#trail-blur)"/>
99
+ <path id="trailBackMain"/>
100
+ <path id="trailBackAccent"/>
101
+ </g>
102
+ <g id="planeBack" filter="url(#plane-drop)">
103
+ <g id="planeBackUnder"><use href="#paper-plane-under-symbol"/></g>
104
+ <g id="planeBackTop"><use href="#paper-plane-symbol"/></g>
105
+ </g>
106
+
107
+ <g id="cloudEdge">
108
+ <path id="edgeContact" class="edge-contact" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#edge-blur)"/>
109
+ <path id="edgeRimSoft" class="edge-rim-soft" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#edge-blur)"/>
110
+ <path id="edgeRimTight" class="edge-rim-tight" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
111
+ </g>
112
+ <g id="cloudGroup" filter="url(#cloud-drop)">
113
+ <path id="cloudPath" fill="url(#body-grad)"/>
114
+ <rect x="1" y="2" width="22" height="11" fill="url(#hi-grad)" clip-path="url(#cloud-clip)"/>
115
+ </g>
116
+ <g id="eyeGroup">
117
+ <rect id="eyeL" x="7.7" y="9.5" width="2.6" height="5" rx="1.3" ry="1.3" fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
118
+ <rect id="eyeR" x="13.7" y="9.5" width="2.6" height="5" rx="1.3" ry="1.3" fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
119
+ </g>
120
+
121
+ <g id="trailFront" fill="none" stroke-linecap="round" stroke-linejoin="round">
122
+ <path id="trailFrontSoft" filter="url(#trail-blur)"/>
123
+ <path id="trailFrontMain"/>
124
+ <path id="trailFrontAccent"/>
125
+ </g>
126
+ <g id="planeFront" filter="url(#plane-drop)">
127
+ <g id="planeFrontUnder"><use href="#paper-plane-under-symbol"/></g>
128
+ <g id="planeFrontTop"><use href="#paper-plane-symbol"/></g>
129
+ </g>
130
+ <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
131
+ <g id="planePaletteV"></g>
132
+ <g id="readout"></g>
133
+ <g id="scrub"></g>
134
+ <g id="scrubV"></g>
135
+ </g>
136
+
137
+ <script type="application/ecmascript"><![CDATA[
138
+ const CLOUD_D = "M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388z";
139
+
140
+ const CENTER = { x: 12, y: 12 };
141
+ /* A 版配色下, 视觉机头会被读成右侧白色尖角 TR, 不是命名里的 N 点。 */
142
+ function visualForwardDeg() {
143
+ return Math.atan2(15.7 - P.pivotY, 34 - P.pivotX) * 180 / Math.PI;
144
+ }
145
+
146
+ const els = {
147
+ cloudPath: document.getElementById('cloudPath'),
148
+ cloudClipPath: document.getElementById('cloud-clip-path'),
149
+ edgeContact: document.getElementById('edgeContact'),
150
+ edgeRimSoft: document.getElementById('edgeRimSoft'),
151
+ edgeRimTight: document.getElementById('edgeRimTight'),
152
+ eyeGroup: document.getElementById('eyeGroup'),
153
+ eyeL: document.getElementById('eyeL'),
154
+ eyeR: document.getElementById('eyeR'),
155
+ eyeGrad: document.getElementById('eye-grad'),
156
+ planeBack: document.getElementById('planeBack'),
157
+ planeFront: document.getElementById('planeFront'),
158
+ planeBackTop: document.getElementById('planeBackTop'),
159
+ planeBackUnder: document.getElementById('planeBackUnder'),
160
+ planeFrontTop: document.getElementById('planeFrontTop'),
161
+ planeFrontUnder: document.getElementById('planeFrontUnder'),
162
+ trailBack: document.getElementById('trailBack'),
163
+ trailFront: document.getElementById('trailFront'),
164
+ trailBackSoft: document.getElementById('trailBackSoft'),
165
+ trailBackMain: document.getElementById('trailBackMain'),
166
+ trailBackAccent: document.getElementById('trailBackAccent'),
167
+ trailFrontSoft: document.getElementById('trailFrontSoft'),
168
+ trailFrontMain: document.getElementById('trailFrontMain'),
169
+ trailFrontAccent: document.getElementById('trailFrontAccent'),
170
+ readout: document.getElementById('readout'),
171
+ scrub: document.getElementById('scrub'),
172
+ scrubV: document.getElementById('scrubV')
173
+ };
174
+
175
+ const P = {
176
+ duration: 5.2,
177
+ rx: 17.8,
178
+ ry: 4.9,
179
+ tilt: -13,
180
+ theta: 0,
181
+ planeScale: 0.20,
182
+ planePalette: 'bluePractical',
183
+ planeOffsetX: 0,
184
+ planeOffsetY: 0,
185
+ planeRotOffset: 123,
186
+ pivotX: 19,
187
+ pivotY: 19,
188
+ backScale: 0.71,
189
+ backOpacity: 0.65,
190
+ rightRush: 0.58,
191
+ rightRushTighten: 0.28,
192
+ trailStrength: 0.90,
193
+ rollMinScale: 0.85,
194
+ rollFade: 0.18,
195
+ manualRotOverride: false,
196
+ manualPlane: false,
197
+ manualX: 26,
198
+ manualY: 24,
199
+ manualRot: -35,
200
+ manualScale: 0.22,
201
+ morphMin: 0.08,
202
+ morphMax: 0.60,
203
+ cloudR: 7.0,
204
+ eyeShift: 1.20,
205
+ eyeFlowPeriod: 5.5,
206
+ eyeFlowAmp: 4,
207
+ blinkInterval: 6.0,
208
+ blinkDur: 0.16,
209
+ showTrail: true,
210
+ pause: false
211
+ };
212
+
213
+ const PLANE_PALETTES = {
214
+ happyPop: {
215
+ label: 'Happy',
216
+ gradients: {
217
+ White: ['#fff7d6', '#FCE3B0', '#EFB85C'],
218
+ Lavender: ['#ffeaf3', '#FCC9DD', '#D768A0'],
219
+ Mid: ['#dbfff8', '#A0E5DD', '#35AFA8'],
220
+ Dark: ['#dcd5ff', '#B1A7FF', '#5F52D9']
221
+ },
222
+ under: {
223
+ Mid: '#327E86',
224
+ Dark: '#3F3595',
225
+ Lavender: '#A94B7E',
226
+ White: '#B9863C'
227
+ },
228
+ fold: 'rgba(57,65,255,.38)',
229
+ underFold: 'rgba(255,255,255,.28)',
230
+ highlight: 'rgba(255,255,255,.74)',
231
+ underHighlight: 'rgba(255,255,255,.34)',
232
+ trailBack: '#A0E5DD',
233
+ trailFront: '#FCE3B0',
234
+ shadow: '#3941FF',
235
+ shadowOpacity: '0.30'
236
+ },
237
+ happySoft: {
238
+ label: 'Soft',
239
+ gradients: {
240
+ White: ['#ffffff', '#FCE3B0', '#F4C76E'],
241
+ Lavender: ['#fff3f8', '#FCC9DD', '#E58AB7'],
242
+ Mid: ['#effffb', '#A0E5DD', '#68C9C1'],
243
+ Dark: ['#eeeaff', '#B1A7FF', '#7D71E8']
244
+ },
245
+ under: {
246
+ Mid: '#5AAEA8',
247
+ Dark: '#6558C4',
248
+ Lavender: '#C9789E',
249
+ White: '#D4A54E'
250
+ },
251
+ fold: 'rgba(57,65,255,.30)',
252
+ underFold: 'rgba(255,255,255,.24)',
253
+ highlight: 'rgba(255,255,255,.70)',
254
+ underHighlight: 'rgba(255,255,255,.30)',
255
+ trailBack: '#A0E5DD',
256
+ trailFront: '#B1A7FF',
257
+ shadow: '#7A9DFF',
258
+ shadowOpacity: '0.26'
259
+ },
260
+ ticketWarm: {
261
+ label: 'Warm',
262
+ gradients: {
263
+ White: ['#ffd28c', '#dc5a1c', '#a64214'],
264
+ Lavender: ['#ff9b45', '#dc5a1c', '#7b2f10'],
265
+ Mid: ['#d8722c', '#9d4315', '#5c260d'],
266
+ Dark: ['#743217', '#4a1f0d', '#271006']
267
+ },
268
+ under: {
269
+ Mid: '#5d260d',
270
+ Dark: '#1f0c04',
271
+ Lavender: '#713010',
272
+ White: '#8b3a13'
273
+ },
274
+ fold: 'rgba(42,18,8,.44)',
275
+ underFold: 'rgba(255,211,154,.20)',
276
+ highlight: 'rgba(255,225,190,.58)',
277
+ underHighlight: 'rgba(255,205,150,.24)',
278
+ trailBack: '#9d4315',
279
+ trailFront: '#dc5a1c',
280
+ shadow: '#dc5a1c',
281
+ shadowOpacity: '0.24'
282
+ },
283
+ bluePractical: {
284
+ label: 'Blue',
285
+ gradients: {
286
+ White: ['#ffffff', '#b8c8ff', '#6f86ff'],
287
+ Lavender: ['#e6e2ff', '#aeb8ff', '#6874f2'],
288
+ Mid: ['#9bb7ff', '#536dff', '#3941FF'],
289
+ Dark: ['#646bff', '#3037d4', '#171b82']
290
+ },
291
+ under: {
292
+ Mid: '#3037d4',
293
+ Dark: '#12165f',
294
+ Lavender: '#5664ff',
295
+ White: '#8ea1ff'
296
+ },
297
+ fold: 'rgba(17,27,140,.46)',
298
+ underFold: 'rgba(255,255,255,.24)',
299
+ highlight: 'rgba(255,255,255,.72)',
300
+ underHighlight: 'rgba(255,255,255,.36)',
301
+ trailBack: '#7A9DFF',
302
+ trailFront: '#B1A7FF',
303
+ shadow: '#3941FF',
304
+ shadowOpacity: '0.36'
305
+ }
306
+ };
307
+
308
+ const lerp = (a, b, t) => a + (b - a) * t;
309
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
310
+ const wrap01 = v => ((v % 1) + 1) % 1;
311
+ const smoothstep = t => t * t * (3 - 2 * t);
312
+ const smoothstepRange = (a, b, v) => smoothstep(clamp((v - a) / (b - a), 0, 1));
313
+ const TAU = Math.PI * 2;
314
+
315
+ function orbitProgressFor(timeProgress) {
316
+ const p = wrap01(timeProgress);
317
+ const a = TAU * p;
318
+ const warped = p
319
+ + P.rightRush * Math.sin(a) / TAU
320
+ + P.rightRushTighten * Math.sin(2 * a) / (2 * TAU);
321
+ return wrap01(warped);
322
+ }
323
+
324
+ function orbitSpeedFor(timeProgress) {
325
+ const p = wrap01(timeProgress);
326
+ const a = TAU * p;
327
+ return 1 + P.rightRush * Math.cos(a) + P.rightRushTighten * Math.cos(2 * a);
328
+ }
329
+
330
+ const EYE_BASE = {
331
+ l: { y: 9.5, h: 5, rx: 1.3 },
332
+ r: { y: 9.5, h: 5, rx: 1.3 }
333
+ };
334
+
335
+ let nextBlinkT = 2.4;
336
+ let blinkActive = false;
337
+ let blinkStartT = 0;
338
+
339
+ function getBlinkScale(t) {
340
+ if (!blinkActive && t > nextBlinkT) {
341
+ blinkActive = true;
342
+ blinkStartT = t;
343
+ }
344
+ if (blinkActive) {
345
+ const dt = t - blinkStartT;
346
+ const half = P.blinkDur / 2;
347
+ if (dt < half) return 1 - 0.94 * (dt / half);
348
+ if (dt < P.blinkDur) return 0.06 + 0.94 * ((dt - half) / half);
349
+ blinkActive = false;
350
+ nextBlinkT = t + P.blinkInterval * (0.82 + Math.random() * 0.36);
351
+ }
352
+ return 1;
353
+ }
354
+
355
+ function applyBlink(rect, base, scale) {
356
+ const h = Math.max(base.h * scale, 0.15);
357
+ const y = base.y + (base.h - h) / 2;
358
+ const rx = Math.min(base.rx, h / 2);
359
+ rect.setAttribute('height', h.toFixed(3));
360
+ rect.setAttribute('y', y.toFixed(3));
361
+ rect.setAttribute('rx', rx.toFixed(3));
362
+ rect.setAttribute('ry', rx.toFixed(3));
363
+ }
364
+
365
+ function setStop(id, color) {
366
+ document.getElementById(id).setAttribute('stop-color', color);
367
+ }
368
+
369
+ function applyPlanePalette(key) {
370
+ const palette = PLANE_PALETTES[key] || PLANE_PALETTES.happyPop;
371
+ P.planePalette = key;
372
+ for (const [name, colors] of Object.entries(palette.gradients)) {
373
+ colors.forEach((color, i) => setStop(`plane${name}${i}`, color));
374
+ }
375
+ for (const [name, color] of Object.entries(palette.under)) {
376
+ document.getElementById(`planeUnder${name}`).setAttribute('fill', color);
377
+ }
378
+ document.getElementById('planeFoldLines').setAttribute('stroke', palette.fold);
379
+ document.getElementById('planeUnderFoldLines').setAttribute('stroke', palette.underFold);
380
+ document.getElementById('planeNoseHighlight').setAttribute('stroke', palette.highlight);
381
+ document.getElementById('planeUnderNoseHighlight').setAttribute('stroke', palette.underHighlight);
382
+ [els.trailBackSoft, els.trailBackMain, els.trailBackAccent].forEach(el => el.setAttribute('stroke', palette.trailBack));
383
+ [els.trailFrontSoft, els.trailFrontMain, els.trailFrontAccent].forEach(el => el.setAttribute('stroke', palette.trailFront));
384
+ document.getElementById('planeShadow').setAttribute('flood-color', palette.shadow);
385
+ document.getElementById('planeShadow').setAttribute('flood-opacity', palette.shadowOpacity);
386
+ document.getElementById('planePaletteV').textContent = palette.label;
387
+ renderAt(currentProgress);
388
+ }
389
+
390
+ function samplePath(d, N) {
391
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
392
+ path.setAttribute('d', d);
393
+ const len = path.getTotalLength();
394
+ const pts = [];
395
+ for (let i = 0; i < N; i++) {
396
+ const p = path.getPointAtLength(i / N * len);
397
+ pts.push({ x: p.x, y: p.y });
398
+ }
399
+ return pts;
400
+ }
401
+
402
+ let basePoints = samplePath(CLOUD_D, 64);
403
+
404
+ function morphPoints(pts, R, t) {
405
+ return pts.map(p => {
406
+ const dx = p.x - CENTER.x;
407
+ const dy = p.y - CENTER.y;
408
+ const a = Math.atan2(dy, dx);
409
+ return {
410
+ x: lerp(p.x, CENTER.x + R * Math.cos(a), t),
411
+ y: lerp(p.y, CENTER.y + R * Math.sin(a), t)
412
+ };
413
+ });
414
+ }
415
+
416
+ function pointsToSmoothPath(pts) {
417
+ const n = pts.length;
418
+ let d = `M ${pts[0].x.toFixed(3)} ${pts[0].y.toFixed(3)}`;
419
+ for (let i = 0; i < n; i++) {
420
+ const p0 = pts[(i - 1 + n) % n];
421
+ const p1 = pts[i];
422
+ const p2 = pts[(i + 1) % n];
423
+ const p3 = pts[(i + 2) % n];
424
+ const c1x = p1.x + (p2.x - p0.x) / 6;
425
+ const c1y = p1.y + (p2.y - p0.y) / 6;
426
+ const c2x = p2.x - (p3.x - p1.x) / 6;
427
+ const c2y = p2.y - (p3.y - p1.y) / 6;
428
+ d += ` C ${c1x.toFixed(3)} ${c1y.toFixed(3)} ${c2x.toFixed(3)} ${c2y.toFixed(3)} ${p2.x.toFixed(3)} ${p2.y.toFixed(3)}`;
429
+ }
430
+ return d + ' Z';
431
+ }
432
+
433
+ function orbitPoint(thetaDeg) {
434
+ const t = thetaDeg * Math.PI / 180;
435
+ const r = P.tilt * Math.PI / 180;
436
+ const ct = Math.cos(t), st = Math.sin(t);
437
+ const cr = Math.cos(r), sr = Math.sin(r);
438
+ return {
439
+ x: CENTER.x + P.rx * ct * cr - P.ry * st * sr,
440
+ y: CENTER.y + P.rx * ct * sr + P.ry * st * cr
441
+ };
442
+ }
443
+
444
+ function orbitDerivative(thetaDeg) {
445
+ const t = thetaDeg * Math.PI / 180;
446
+ const r = P.tilt * Math.PI / 180;
447
+ const cr = Math.cos(r), sr = Math.sin(r);
448
+ const dx = -P.rx * Math.sin(t) * cr - P.ry * Math.cos(t) * sr;
449
+ const dy = -P.rx * Math.sin(t) * sr + P.ry * Math.cos(t) * cr;
450
+ return { x: dx, y: dy };
451
+ }
452
+
453
+ function getPlaneState(progress) {
454
+ const orbitProgress = orbitProgressFor(progress);
455
+ const theta = P.theta + orbitProgress * 360;
456
+ const orbitDepth = Math.sin(theta * Math.PI / 180);
457
+ return {
458
+ phase: 'orbit',
459
+ theta,
460
+ orbitDepth,
461
+ orbitProximity: smoothstepRange(0.22, 0.90, Math.abs(orbitDepth)),
462
+ p: orbitPoint(theta),
463
+ d: orbitDerivative(theta),
464
+ orbitT: orbitProgress,
465
+ timeT: wrap01(progress),
466
+ orbitSpeed: orbitSpeedFor(progress),
467
+ frontOverride: false,
468
+ opacity: 1
469
+ };
470
+ }
471
+
472
+ function cloudMorphFor(orbitProximity) {
473
+ return lerp(P.morphMin, P.morphMax, orbitProximity);
474
+ }
475
+
476
+ function trailPathD(timeProgress, fromLag, toLag, samples) {
477
+ let d = '';
478
+ for (let i = 0; i <= samples; i++) {
479
+ const lag = lerp(fromLag, toLag, i / samples);
480
+ const sample = getAutoPlaneVisual(getPlaneState(timeProgress - lag));
481
+ d += i === 0
482
+ ? `M ${sample.p.x.toFixed(3)} ${sample.p.y.toFixed(3)}`
483
+ : ` L ${sample.p.x.toFixed(3)} ${sample.p.y.toFixed(3)}`;
484
+ }
485
+ return d;
486
+ }
487
+
488
+ function setTrailLayer(el, d, opacity, width, dash = 'none', dashOffset = 0) {
489
+ el.setAttribute('d', d);
490
+ el.setAttribute('opacity', opacity.toFixed(3));
491
+ el.setAttribute('stroke-width', width.toFixed(3));
492
+ el.setAttribute('stroke-dasharray', dash);
493
+ el.setAttribute('stroke-dashoffset', dashOffset.toFixed(3));
494
+ }
495
+
496
+ function renderTrailLines(timeProgress, orbitSpeed, plane) {
497
+ const enabled = P.showTrail && !P.manualPlane;
498
+ const speedBurst = clamp((orbitSpeed - 1) / 0.86, 0, 1);
499
+ const base = enabled ? P.trailStrength * lerp(0.82, 1.24, speedBurst) : 0;
500
+ const frontBase = base * plane.frontness;
501
+ const backBase = base * (1 - plane.frontness) * P.backOpacity;
502
+ const drift = timeProgress * P.duration;
503
+ const mainD = trailPathD(timeProgress, 0.145, 0.010, 18);
504
+ const shortD = trailPathD(timeProgress, 0.090, 0.006, 12);
505
+ const longD = trailPathD(timeProgress, 0.190, 0.016, 22);
506
+ const mainOffset = -drift * 0.36;
507
+ const accentOffset = -drift * 0.58;
508
+
509
+ setTrailLayer(els.trailBackSoft, longD, backBase * 0.12, 0.62, 'none', 0);
510
+ setTrailLayer(els.trailBackMain, mainD, backBase * 0.42, 0.30, '0.72 0.62', mainOffset);
511
+ setTrailLayer(els.trailBackAccent, shortD, backBase * 0.22, 0.20, '1.16 0.55', accentOffset);
512
+ setTrailLayer(els.trailFrontSoft, longD, frontBase * 0.12, 0.62, 'none', 0);
513
+ setTrailLayer(els.trailFrontMain, mainD, frontBase * 0.42, 0.30, '0.72 0.62', mainOffset);
514
+ setTrailLayer(els.trailFrontAccent, shortD, frontBase * 0.22, 0.20, '1.16 0.55', accentOffset);
515
+ }
516
+
517
+ function getAutoPlaneVisual(s) {
518
+ const p = {
519
+ x: s.p.x + P.planeOffsetX,
520
+ y: s.p.y + P.planeOffsetY
521
+ };
522
+ const tangentDeg = Math.atan2(s.d.y, s.d.x) * 180 / Math.PI;
523
+ const orbitFrontness = clamp((p.y - CENTER.y + 1.3) / 4.0, 0, 1);
524
+ const frontness = s.frontOverride ? 1 : orbitFrontness;
525
+ const depthScale = lerp(P.backScale, 1, frontness);
526
+ const totalScale = P.planeScale * depthScale;
527
+ const bank = (frontness - 0.5) * 7;
528
+ const rot = tangentDeg - visualForwardDeg() + bank + P.planeRotOffset;
529
+ const faceMix = smoothstepRange(-P.rollFade, P.rollFade, s.orbitDepth);
530
+ const rollStrength = smoothstepRange(0.10, 0.88, Math.abs(s.orbitDepth));
531
+ return {
532
+ phase: s.phase,
533
+ p,
534
+ tangentDeg,
535
+ frontness,
536
+ orbitT: s.orbitT,
537
+ orbitDepth: s.orbitDepth,
538
+ orbitProximity: s.orbitProximity,
539
+ topFaceOpacity: faceMix,
540
+ underFaceOpacity: 1 - faceMix,
541
+ rollForeshorten: lerp(P.rollMinScale, 1, rollStrength),
542
+ totalScale,
543
+ rot,
544
+ frontOpacity: s.opacity * frontness,
545
+ backOpacity: s.opacity * (1 - frontness) * P.backOpacity
546
+ };
547
+ }
548
+
549
+ function getPlaneVisual(s) {
550
+ if (!P.manualPlane) {
551
+ const auto = getAutoPlaneVisual(s);
552
+ if (!P.manualRotOverride) return auto;
553
+ return {
554
+ ...auto,
555
+ phase: 'orbit+manual-rot',
556
+ tangentDeg: P.manualRot,
557
+ rot: P.manualRot
558
+ };
559
+ }
560
+ return {
561
+ phase: 'manual',
562
+ p: { x: P.manualX, y: P.manualY },
563
+ tangentDeg: P.manualRot,
564
+ frontness: 1,
565
+ orbitDepth: 1,
566
+ orbitProximity: 1,
567
+ topFaceOpacity: 1,
568
+ underFaceOpacity: 0,
569
+ rollForeshorten: 1,
570
+ totalScale: P.manualScale,
571
+ rot: P.manualRot,
572
+ frontOpacity: 1,
573
+ backOpacity: 0
574
+ };
575
+ }
576
+
577
+ function renderAt(progress) {
578
+ const s = getPlaneState(progress);
579
+ const plane = getPlaneVisual(s);
580
+ const morph = cloudMorphFor(s.orbitProximity);
581
+ const cloudD = pointsToSmoothPath(morphPoints(basePoints, P.cloudR, morph));
582
+ els.cloudPath.setAttribute('d', cloudD);
583
+ els.cloudClipPath.setAttribute('d', cloudD);
584
+ els.edgeContact.setAttribute('d', cloudD);
585
+ els.edgeRimSoft.setAttribute('d', cloudD);
586
+ els.edgeRimTight.setAttribute('d', cloudD);
587
+
588
+ const lookDx = plane.p.x - CENTER.x;
589
+ const lookDy = plane.p.y - CENTER.y;
590
+ const lookLen = Math.max(Math.hypot(lookDx, lookDy), 0.001);
591
+ const eyeDrive = P.eyeShift;
592
+ const eyeX = lookDx / lookLen * eyeDrive;
593
+ const eyeY = lookDy / lookLen * eyeDrive * 0.55;
594
+ const eyeScale = lerp(1, 0.82, morph / Math.max(P.morphMax, 0.001));
595
+ els.eyeGroup.setAttribute('transform', `translate(${eyeX.toFixed(3)} ${eyeY.toFixed(3)}) translate(12 12) scale(${eyeScale.toFixed(3)}) translate(-12 -12)`);
596
+ els.eyeL.setAttribute('opacity', lerp(1, 0.88, morph).toFixed(3));
597
+ els.eyeR.setAttribute('opacity', lerp(1, 0.88, morph).toFixed(3));
598
+ const tNow = performance.now() / 1000;
599
+ const blinkScale = getBlinkScale(tNow);
600
+ applyBlink(els.eyeL, EYE_BASE.l, blinkScale);
601
+ applyBlink(els.eyeR, EYE_BASE.r, blinkScale);
602
+ const flowOffset = Math.sin(2 * Math.PI * tNow / P.eyeFlowPeriod) * P.eyeFlowAmp;
603
+ els.eyeGrad.setAttribute('y1', (3 + flowOffset).toFixed(2));
604
+ els.eyeGrad.setAttribute('y2', (21 + flowOffset).toFixed(2));
605
+
606
+ const transform = `translate(${plane.p.x.toFixed(3)} ${plane.p.y.toFixed(3)}) rotate(${plane.rot.toFixed(3)}) scale(${plane.totalScale.toFixed(4)} ${(plane.totalScale * plane.rollForeshorten).toFixed(4)}) translate(${(-P.pivotX).toFixed(3)} ${(-P.pivotY).toFixed(3)})`;
607
+ els.planeFront.setAttribute('transform', transform);
608
+ els.planeBack.setAttribute('transform', transform);
609
+ els.planeFront.setAttribute('opacity', plane.frontOpacity.toFixed(3));
610
+ els.planeBack.setAttribute('opacity', plane.backOpacity.toFixed(3));
611
+ els.planeFrontTop.setAttribute('opacity', plane.topFaceOpacity.toFixed(3));
612
+ els.planeBackTop.setAttribute('opacity', plane.topFaceOpacity.toFixed(3));
613
+ els.planeFrontUnder.setAttribute('opacity', plane.underFaceOpacity.toFixed(3));
614
+ els.planeBackUnder.setAttribute('opacity', plane.underFaceOpacity.toFixed(3));
615
+
616
+ renderTrailLines(progress, s.orbitSpeed, plane);
617
+
618
+ els.scrub.value = progress.toFixed(3);
619
+ els.scrubV.textContent = `${Math.round(progress * 100)}%`;
620
+ els.readout.textContent =
621
+ `phase=${plane.phase} progress=${progress.toFixed(3)}
622
+ plane=(${plane.p.x.toFixed(2)}, ${plane.p.y.toFixed(2)}) rot=${plane.rot.toFixed(1)}deg tangent=${plane.tangentDeg.toFixed(1)}deg frontness=${plane.frontness.toFixed(2)}
623
+ cloud morph=${Math.round(morph * 100)}% sidePass=${s.orbitProximity.toFixed(2)} depth=${s.orbitDepth.toFixed(2)} range=${Math.round(P.morphMin * 100)}-${Math.round(P.morphMax * 100)}%
624
+ roll top=${plane.topFaceOpacity.toFixed(2)} under=${plane.underFaceOpacity.toFixed(2)} squash=${plane.rollForeshorten.toFixed(2)}
625
+ eyes dx=${eyeX.toFixed(2)} dy=${eyeY.toFixed(2)} blink=${blinkScale.toFixed(2)} flow=${flowOffset.toFixed(1)}
626
+ orbit t=${s.orbitT.toFixed(3)} speed=${s.orbitSpeed.toFixed(2)}x rx=${P.rx.toFixed(1)} ry=${P.ry.toFixed(1)} tilt=${P.tilt.toFixed(0)}deg start=${P.theta.toFixed(0)}deg
627
+ plane global offset=(${P.planeOffsetX.toFixed(1)}, ${P.planeOffsetY.toFixed(1)}) rotOffset=${P.planeRotOffset.toFixed(0)}deg pivot=(${P.pivotX.toFixed(1)}, ${P.pivotY.toFixed(1)})
628
+ palette=${P.planePalette} (${PLANE_PALETTES[P.planePalette]?.label || 'custom'})
629
+ manual=${P.manualPlane ? `position on x=${P.manualX.toFixed(1)} y=${P.manualY.toFixed(1)} rot=${P.manualRot.toFixed(0)} scale=${P.manualScale.toFixed(2)}` : `position off rotOverride=${P.manualRotOverride ? `on rot=${P.manualRot.toFixed(0)}` : 'off'}`}
630
+ trail style=short dash
631
+ note: right side has a short rush; trail variants are line-based for review.`;
632
+ }
633
+
634
+ let startTime = performance.now();
635
+ let currentProgress = 0;
636
+
637
+ function loop(now) {
638
+ if (!P.pause) {
639
+ currentProgress = ((now - startTime) / 1000 / P.duration) % 1;
640
+ }
641
+ renderAt(currentProgress);
642
+ requestAnimationFrame(loop);
643
+ }
644
+
645
+ applyPlanePalette(P.planePalette);
646
+ requestAnimationFrame(loop);
647
+ ]]></script>
648
+ </svg>