@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,578 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="stage" xmlns="http://www.w3.org/2000/svg" viewBox="-6 -6 36 36" preserveAspectRatio="xMidYMid meet">
3
+ <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
4
+ <g id="btn-play"></g>
5
+ <g id="phase"></g>
6
+ <g id="scrubber"></g>
7
+ <g id="time"></g>
8
+ <g id="timing-controls"></g>
9
+ </g>
10
+ <script type="application/ecmascript"><![CDATA[
11
+ 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";
12
+
13
+ const SOFT_CLOUD_POINTS = [
14
+ {x:9.064,y:3.344},{x:9.825,y:3.106},{x:10.616,y:3.004},{x:11.413,y:3.040},
15
+ {x:12.192,y:3.210},{x:12.928,y:3.518},{x:13.599,y:3.948},{x:14.255,y:4.291},
16
+ {x:15.046,y:4.188},{x:15.843,y:4.224},{x:16.621,y:4.398},{x:17.357,y:4.706},
17
+ {x:18.029,y:5.134},{x:18.617,y:5.673},{x:19.103,y:6.306},{x:19.471,y:7.013},
18
+ {x:19.715,y:7.772},{x:19.812,y:8.564},{x:19.784,y:9.361},{x:19.797,y:10.105},
19
+ {x:20.287,y:10.735},{x:20.656,y:11.442},{x:20.897,y:12.202},{x:20.995,y:12.994},
20
+ {x:20.964,y:13.791},{x:20.791,y:14.569},{x:20.480,y:15.304},{x:20.052,y:15.977},
21
+ {x:19.516,y:16.567},{x:18.882,y:17.052},{x:18.174,y:17.418},{x:17.539,y:17.808},
22
+ {x:17.231,y:18.545},{x:16.805,y:19.218},{x:16.264,y:19.805},{x:15.633,y:20.292},
23
+ {x:14.925,y:20.660},{x:14.163,y:20.895},{x:13.372,y:20.996},{x:12.575,y:20.964},
24
+ {x:11.796,y:20.793},{x:11.061,y:20.483},{x:10.392,y:20.049},{x:9.736,y:19.714},
25
+ {x:8.946,y:19.811},{x:8.148,y:19.777},{x:7.370,y:19.606},{x:6.634,y:19.299},
26
+ {x:5.962,y:18.868},{x:5.377,y:18.326},{x:4.885,y:17.698},{x:4.522,y:16.989},
27
+ {x:4.283,y:16.228},{x:4.182,y:15.437},{x:4.220,y:14.640},{x:4.179,y:13.907},
28
+ {x:3.696,y:13.273},{x:3.332,y:12.563},{x:3.092,y:11.803},{x:3.003,y:11.010},
29
+ {x:3.038,y:10.213},{x:3.202,y:9.433},{x:3.514,y:8.699},{x:3.944,y:8.027},
30
+ {x:4.484,y:7.440},{x:5.114,y:6.951},{x:5.822,y:6.586},{x:6.445,y:6.191},
31
+ {x:6.752,y:5.455},{x:7.185,y:4.785},{x:7.723,y:4.197},{x:8.356,y:3.711},
32
+ ];
33
+
34
+ const DOZE = {
35
+ slump: 0.55,
36
+ topSag: 1.42,
37
+ shoulderSag: 0.34,
38
+ spread: 0.82,
39
+ floorY: 19.92,
40
+ floorFlatten: 0.56,
41
+ bottomSquish: 0.82,
42
+ leftLean: -0.18,
43
+ bodyTy: 1.15,
44
+ bodyRot: -1.5,
45
+ bodyScale: 1.04,
46
+ eyeY: 13.0,
47
+ eyeGap: 6.12,
48
+ eyeW: 2.70,
49
+ eyeH: 1.28,
50
+ eyeTilt: 7.7,
51
+ eyeStroke: 0.80,
52
+ haloY: 20.62,
53
+ haloRx: 10.8,
54
+ haloRy: 1.28,
55
+ };
56
+
57
+ const SLEEP = {
58
+ bodyTilt: -6,
59
+ breathScale: 1.02,
60
+ hatTx: -2,
61
+ hatTy: -0.4,
62
+ hatRot: -10,
63
+ hatScale: 0.98,
64
+ maskTx: 0,
65
+ maskTy: -0.6,
66
+ maskRot: -9,
67
+ maskScale: 1.00,
68
+ };
69
+
70
+ const TIMING = {
71
+ hold: 0.54,
72
+ unwind: 1.95,
73
+ hat: 1.42,
74
+ mask: 1.30,
75
+ settle: 0.88,
76
+ };
77
+
78
+ const CONTROLS = [
79
+ { key: 'hold', label: 'doze hold', min: 0.10, max: 1.00, step: 0.01 },
80
+ { key: 'unwind', label: 'sleep inhale', min: 0.90, max: 2.40, step: 0.01 },
81
+ { key: 'hat', label: 'hat fall', min: 0.70, max: 1.80, step: 0.01 },
82
+ { key: 'mask', label: 'mask settle', min: 0.60, max: 1.70, step: 0.01 },
83
+ { key: 'settle', label: 'final quiet', min: 0.20, max: 1.20, step: 0.01 },
84
+ ];
85
+
86
+ const SLEEP_MASK_PATH = {
87
+ d: "M488.728,341.587l-7.846-2.599c-0.405-16.992-7.605-47.34-48.625-70.941c-38.301-22.036-96.652-33.215-173.431-33.224c-76.822,0-135.146,11.165-173.438,33.183c-42.335,24.344-48.659,55.883-48.661,72.516c0,2.608,0.135,5.183,0.352,7.735h-2.649c-0.001,0-0.001,0-0.002,0c-9.635,0-17.446,7.811-17.448,17.445c-0.001,9.636,7.81,17.449,17.445,17.45l13,0.001c15.257,28.43,45.267,47.814,79.728,47.818c20.016-0.001,36.647-11.511,54.256-23.695c20.65-14.29,44.048-30.481,77.388-30.481c33.359,0.003,56.757,16.204,77.402,30.497c17.61,12.192,34.24,23.707,54.251,23.709h0.012c24.153,0,46.861-9.404,63.941-26.481c8.937-8.936,15.751-19.422,20.239-30.839l3.111,1.031c1.82,0.604,3.67,0.89,5.489,0.89c7.318,0,14.133-4.64,16.56-11.966C502.835,354.489,497.876,344.617,488.728,341.587Z",
88
+ bboxX: 15, bboxY: 232, bboxW: 487, bboxH: 200
89
+ };
90
+
91
+ const stage = document.getElementById('stage');
92
+ const scrubber = document.getElementById('scrubber');
93
+ const timeOut = document.getElementById('time');
94
+ const phaseEl = document.getElementById('phase');
95
+ const timingControls = document.getElementById('timing-controls');
96
+ let playing = true;
97
+ let start = performance.now();
98
+ let manualT = 0;
99
+ let DURATION = 4.70;
100
+
101
+ function clamp01(v) { return Math.max(0, Math.min(1, v)); }
102
+ function lerp(a, b, t) { return a + (b - a) * t; }
103
+ function progress(t, a, b) { return clamp01((t - a) / Math.max(0.0001, b - a)); }
104
+ function smoothstep(a, b, t) {
105
+ const x = progress(t, a, b);
106
+ return x * x * (3 - 2 * x);
107
+ }
108
+ function easeInOut(t) {
109
+ return t < .5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
110
+ }
111
+ function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
112
+ function easeOutBackSoft(t) {
113
+ const c1 = 0.74;
114
+ const c3 = c1 + 1;
115
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
116
+ }
117
+
118
+ function buildTimeline() {
119
+ const tl = {};
120
+ let t = 0;
121
+ tl.holdEnd = t += TIMING.hold;
122
+ tl.unwindStart = tl.holdEnd;
123
+ tl.unwindEnd = t += TIMING.unwind;
124
+ tl.hatStart = tl.unwindStart + TIMING.unwind * 0.68;
125
+ tl.hatEnd = tl.hatStart + TIMING.hat;
126
+ tl.maskStart = tl.hatStart + TIMING.hat * 0.46;
127
+ tl.maskEnd = tl.maskStart + TIMING.mask;
128
+ tl.eyeFadeStart = tl.maskStart + TIMING.mask * 0.22;
129
+ tl.eyeFadeEnd = tl.maskStart + TIMING.mask * 0.82;
130
+ tl.duration = Math.max(tl.maskEnd, tl.hatEnd, tl.unwindEnd) + TIMING.settle;
131
+ return tl;
132
+ }
133
+
134
+ function syncTimeline() {
135
+ const tl = buildTimeline();
136
+ DURATION = tl.duration;
137
+ scrubber.max = DURATION.toFixed(2);
138
+ if (manualT > DURATION) manualT = DURATION;
139
+ return tl;
140
+ }
141
+
142
+ function catmullRomClosedPath(points, tension = 1) {
143
+ const q = (n) => n.toFixed(3);
144
+ let d = `M ${q(points[0].x)} ${q(points[0].y)}`;
145
+ const n = points.length;
146
+ for (let i = 0; i < n; i++) {
147
+ const p0 = points[(i - 1 + n) % n];
148
+ const p1 = points[i];
149
+ const p2 = points[(i + 1) % n];
150
+ const p3 = points[(i + 2) % n];
151
+ const c1x = p1.x + (p2.x - p0.x) * tension / 6;
152
+ const c1y = p1.y + (p2.y - p0.y) * tension / 6;
153
+ const c2x = p2.x - (p3.x - p1.x) * tension / 6;
154
+ const c2y = p2.y - (p3.y - p1.y) * tension / 6;
155
+ d += ` C ${q(c1x)} ${q(c1y)} ${q(c2x)} ${q(c2y)} ${q(p2.x)} ${q(p2.y)}`;
156
+ }
157
+ return `${d} Z`;
158
+ }
159
+
160
+ function makeDozeCloudPath(amount) {
161
+ const mix = clamp01(amount);
162
+ if (mix <= 0.0001) return CLOUD_D;
163
+ const slump = DOZE.slump * mix;
164
+ const points = SOFT_CLOUD_POINTS.map((pt) => {
165
+ const dx = pt.x - 12;
166
+ const distX = Math.abs(dx);
167
+ const sign = dx === 0 ? 0 : Math.sign(dx);
168
+ const topWeight = Math.pow(clamp01((8.2 - pt.y) / 5.2), 0.95);
169
+ const crownCenter = clamp01(1 - distX / 6.6);
170
+ const shoulderWeight = clamp01((pt.y - 6.0) / 8.5) * clamp01((18.6 - pt.y) / 10.5);
171
+ const lowerWeight = Math.pow(clamp01((pt.y - 15.4) / 5.8), 0.78);
172
+ const sideWeight = clamp01((distX - 2.2) / 7.5) * clamp01((20.2 - pt.y) / 16.2);
173
+
174
+ const topSag = slump * topWeight * DOZE.topSag * (0.70 + crownCenter * 0.42);
175
+ const shoulderSag = slump * shoulderWeight * DOZE.shoulderSag;
176
+ const floorCurve = DOZE.floorY + Math.pow(distX / 10, 2) * 0.46 - crownCenter * 0.04;
177
+ const floorPull = (floorCurve - pt.y) * lowerWeight * DOZE.floorFlatten * slump;
178
+ const sideOut = sign * slump * DOZE.spread * (0.18 + sideWeight * 0.76 + lowerWeight * 0.42);
179
+ const bottomInhale = lowerWeight * DOZE.bottomSquish * slump * -0.34;
180
+ const lean = DOZE.leftLean * slump * (0.15 + topWeight * 0.50 + shoulderWeight * 0.30);
181
+
182
+ return { x: pt.x + sideOut + lean, y: pt.y + topSag + shoulderSag + floorPull + bottomInhale };
183
+ });
184
+ return catmullRomClosedPath(points, lerp(0.95, 0.68, mix));
185
+ }
186
+
187
+ function defs(p, cloudD) {
188
+ return `
189
+ <linearGradient id="${p}-body" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
190
+ <stop offset="0" stop-color="#93c5fd"/>
191
+ <stop offset="0.5" stop-color="#60a5fa"/>
192
+ <stop offset="1" stop-color="#3b82f6"/>
193
+ </linearGradient>
194
+ <linearGradient id="${p}-eye" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
195
+ <stop offset="0" stop-color="#B1A7FF"/>
196
+ <stop offset="0.5" stop-color="#7A9DFF"/>
197
+ <stop offset="1" stop-color="#3941FF"/>
198
+ </linearGradient>
199
+ <radialGradient id="${p}-hi" cx="0.45" cy="0.25" r="0.55">
200
+ <stop offset="0" stop-color="#93c5fd" stop-opacity="0.9"/>
201
+ <stop offset="1" stop-color="#93c5fd" stop-opacity="0"/>
202
+ </radialGradient>
203
+ <radialGradient id="${p}-halo" cx="50%" cy="50%" r="50%">
204
+ <stop offset="0" stop-color="#7A9DFF" stop-opacity="0.55"/>
205
+ <stop offset="0.54" stop-color="#3941FF" stop-opacity="0.24"/>
206
+ <stop offset="1" stop-color="#3941FF" stop-opacity="0"/>
207
+ </radialGradient>
208
+ <linearGradient id="${p}-hat" x1="11" y1="-3" x2="14" y2="6" gradientUnits="userSpaceOnUse">
209
+ <stop offset="0" stop-color="#7AA0FF"/>
210
+ <stop offset="0.45" stop-color="#3941FF"/>
211
+ <stop offset="1" stop-color="#1F2BC2"/>
212
+ </linearGradient>
213
+ <linearGradient id="${p}-cuff" x1="12" y1="3.0" x2="12" y2="8.0" gradientUnits="userSpaceOnUse">
214
+ <stop offset="0" stop-color="#93c5fd"/>
215
+ <stop offset="0.5" stop-color="#fafbff"/>
216
+ <stop offset="1" stop-color="#e8ecf6"/>
217
+ </linearGradient>
218
+ <linearGradient id="${p}-star-yellow" x1="0" y1="-2" x2="0" y2="2" gradientUnits="userSpaceOnUse">
219
+ <stop offset="0" stop-color="#FFE9A8"/>
220
+ <stop offset="0.5" stop-color="#FFD360"/>
221
+ <stop offset="1" stop-color="#F5A623"/>
222
+ </linearGradient>
223
+ <filter id="${p}-star-glow" x="-80%" y="-80%" width="260%" height="260%">
224
+ <feDropShadow dx="0" dy="0.4" stdDeviation="0.5" flood-color="#F5A623" flood-opacity="0.32"/>
225
+ </filter>
226
+ <filter id="${p}-shadow" x="-50%" y="-50%" width="200%" height="200%">
227
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.35"/>
228
+ </filter>
229
+ <filter id="${p}-edge-blur" x="-40%" y="-40%" width="180%" height="180%">
230
+ <feGaussianBlur stdDeviation="0.16"/>
231
+ </filter>
232
+ <filter id="${p}-soft" x="-30%" y="-30%" width="160%" height="160%">
233
+ <feGaussianBlur stdDeviation="0.18"/>
234
+ </filter>
235
+ <filter id="${p}-halo-blur" x="-80%" y="-120%" width="260%" height="340%">
236
+ <feGaussianBlur stdDeviation="0.42"/>
237
+ </filter>
238
+ <clipPath id="${p}-clip"><path d="${cloudD}"/></clipPath>
239
+ `;
240
+ }
241
+
242
+ function closedEyeStrokePath(cx, cy, w, h, tiltDeg, flip) {
243
+ const x1 = cx - w / 2;
244
+ const x2 = cx + w / 2;
245
+ const mid = cy + h * 0.56;
246
+ const c1x = cx - w * 0.18;
247
+ const c2x = cx + w * 0.18;
248
+ const tilt = flip * tiltDeg * 0.018;
249
+ return `M ${x1.toFixed(2)} ${(cy - tilt).toFixed(2)} C ${c1x.toFixed(2)} ${mid.toFixed(2)} ${c2x.toFixed(2)} ${mid.toFixed(2)} ${x2.toFixed(2)} ${(cy + tilt).toFixed(2)}`;
250
+ }
251
+
252
+ function makeDozeEyes(opacity) {
253
+ const left = 12 - DOZE.eyeGap / 2;
254
+ const right = 12 + DOZE.eyeGap / 2;
255
+ return `
256
+ <g fill="none" stroke="url(#dz-eye)" stroke-width="${DOZE.eyeStroke}" stroke-linecap="round" stroke-linejoin="round" opacity="${opacity.toFixed(3)}">
257
+ <path d="${closedEyeStrokePath(left, DOZE.eyeY, DOZE.eyeW, DOZE.eyeH, DOZE.eyeTilt, -1)}"/>
258
+ <path d="${closedEyeStrokePath(right, DOZE.eyeY, DOZE.eyeW, DOZE.eyeH, DOZE.eyeTilt, 1)}"/>
259
+ </g>
260
+ `;
261
+ }
262
+
263
+ function makeFluffyCuff(p, cx, cy, rx, ry, asym, bumps) {
264
+ const parts = [];
265
+ const circles = [];
266
+ for (let i = 0; i < bumps; i++) {
267
+ const ang = (i * 2 * Math.PI) / bumps - Math.PI / 2;
268
+ const x = cx + Math.cos(ang) * rx;
269
+ const y = cy + Math.sin(ang) * ry;
270
+ const sizeY = -Math.sin(ang);
271
+ const sizeBoost = 0.18 * (sizeY + 1) / 2 + 0.10;
272
+ const jitter = ((i * 13) % 7) / 35;
273
+ let r = 0.62 + sizeBoost + jitter;
274
+ if (asym !== 0) r *= 1 + asym * Math.cos(ang) * 0.28;
275
+ circles.push({ x, y, r });
276
+ }
277
+ const innerRy = ry + 0.40;
278
+ parts.push(`<g filter="url(#${p}-soft)">`);
279
+ parts.push(`<ellipse cx="${cx}" cy="${cy}" rx="${(rx + 0.30).toFixed(2)}" ry="${(innerRy + 0.30).toFixed(2)}" fill="#7A9DFF" opacity="0.50"/>`);
280
+ circles.forEach(c => parts.push(`<circle cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${(c.r + 0.30).toFixed(2)}" fill="#7A9DFF" opacity="0.50"/>`));
281
+ parts.push(`</g>`);
282
+ parts.push(`<ellipse cx="${cx}" cy="${cy}" rx="${(rx + 0.15).toFixed(2)}" ry="${(innerRy + 0.15).toFixed(2)}" fill="#aab3c6" opacity="0.65"/>`);
283
+ circles.forEach(c => parts.push(`<circle cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${(c.r + 0.15).toFixed(2)}" fill="#aab3c6" opacity="0.65"/>`));
284
+ parts.push(`<ellipse cx="${cx}" cy="${cy}" rx="${rx.toFixed(2)}" ry="${innerRy.toFixed(2)}" fill="url(#${p}-cuff)"/>`);
285
+ circles.forEach(c => parts.push(`<circle cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${c.r.toFixed(2)}" fill="url(#${p}-cuff)"/>`));
286
+ parts.push(`<ellipse cx="${cx}" cy="${(cy - ry * 0.45).toFixed(2)}" rx="${(rx * 0.78).toFixed(2)}" ry="${(ry * 0.32).toFixed(2)}" fill="none" stroke="#ffffff" stroke-opacity="0.55" stroke-width="0.14"/>`);
287
+ return parts.join('');
288
+ }
289
+
290
+ function makeBentHat(bend) {
291
+ const baseLeft = { x: 8.5, y: 5.0 };
292
+ const baseRight = { x: 15.5, y: 5.0 };
293
+ const elbowX = 12 + bend * 2.2;
294
+ const elbowY = -0.5 + bend * 2.3;
295
+ const tipX = 11 - bend * 5.5;
296
+ const tipY = 0.0 + bend * 0.5;
297
+ const tipBottomX = tipX + 1.2 + bend * 0.4;
298
+ const tipBottomY = tipY + 1.4 + bend * 0.3;
299
+ const c1 = { x: baseRight.x + 0.5, y: baseRight.y - 1.5 };
300
+ const c2 = { x: elbowX + 0.8, y: elbowY + 1.0 };
301
+ const c3 = { x: elbowX - 0.5, y: elbowY - 0.8 };
302
+ const c4 = { x: tipX + 1.5, y: tipY - 0.5 };
303
+ const c5 = { x: tipX - 0.3, y: tipY + 0.6 };
304
+ const c6 = { x: tipBottomX - 1.0, y: tipBottomY };
305
+ const c7 = { x: 8.5, y: 4.0 };
306
+ return `M ${baseLeft.x} ${baseLeft.y} Q 12 5.55 ${baseRight.x} ${baseRight.y} C ${c1.x.toFixed(2)} ${c1.y.toFixed(2)} ${c2.x.toFixed(2)} ${c2.y.toFixed(2)} ${elbowX.toFixed(2)} ${elbowY.toFixed(2)} C ${c3.x.toFixed(2)} ${c3.y.toFixed(2)} ${c4.x.toFixed(2)} ${c4.y.toFixed(2)} ${tipX.toFixed(2)} ${tipY.toFixed(2)} C ${c5.x.toFixed(2)} ${c5.y.toFixed(2)} ${c6.x.toFixed(2)} ${c6.y.toFixed(2)} ${tipBottomX.toFixed(2)} ${tipBottomY.toFixed(2)} C ${c7.x.toFixed(2)} ${c7.y.toFixed(2)} 8.0 4.5 ${baseLeft.x} ${baseLeft.y} Z`;
307
+ }
308
+
309
+ function makeStar(cx, cy, r, rotationDeg, cornerRound, innerRatio) {
310
+ const innerR = r * innerRatio;
311
+ const rotRad = (rotationDeg * Math.PI) / 180;
312
+ const verts = [];
313
+ for (let i = 0; i < 10; i++) {
314
+ const ang = (i * Math.PI / 5) + rotRad - Math.PI / 2;
315
+ const radius = i % 2 === 0 ? r : innerR;
316
+ verts.push({ x: cx + Math.cos(ang) * radius, y: cy + Math.sin(ang) * radius });
317
+ }
318
+ const inP = [], outP = [];
319
+ for (let i = 0; i < 10; i++) {
320
+ const prev = verts[(i - 1 + 10) % 10];
321
+ const cur = verts[i];
322
+ const next = verts[(i + 1) % 10];
323
+ const v1x = prev.x - cur.x, v1y = prev.y - cur.y;
324
+ const v2x = next.x - cur.x, v2y = next.y - cur.y;
325
+ const v1Len = Math.hypot(v1x, v1y);
326
+ const v2Len = Math.hypot(v2x, v2y);
327
+ const cr = Math.min(cornerRound, v1Len * 0.48, v2Len * 0.48);
328
+ inP[i] = { x: cur.x + (v1x / v1Len) * cr, y: cur.y + (v1y / v1Len) * cr };
329
+ outP[i] = { x: cur.x + (v2x / v2Len) * cr, y: cur.y + (v2y / v2Len) * cr };
330
+ }
331
+ let path = `M ${inP[0].x.toFixed(2)} ${inP[0].y.toFixed(2)} `;
332
+ for (let i = 0; i < 10; i++) {
333
+ path += `Q ${verts[i].x.toFixed(2)} ${verts[i].y.toFixed(2)} ${outP[i].x.toFixed(2)} ${outP[i].y.toFixed(2)} `;
334
+ const nextI = (i + 1) % 10;
335
+ path += `L ${inP[nextI].x.toFixed(2)} ${inP[nextI].y.toFixed(2)} `;
336
+ }
337
+ return `${path}Z`;
338
+ }
339
+
340
+ function makeHat(p) {
341
+ const hatPath = makeBentHat(0.4);
342
+ const cuff = makeFluffyCuff(p, 12, 5.4, 3.7, 0.85, 0, 18);
343
+ const star = makeStar(8.55, 0.35, 1.00, 30, 0.27, 0.56);
344
+ return `
345
+ <g filter="url(#${p}-shadow)"><path d="${hatPath}" fill="url(#${p}-hat)"/></g>
346
+ <g>${cuff}</g>
347
+ <g filter="url(#${p}-star-glow)">
348
+ <path d="${star}" fill="url(#${p}-star-yellow)" stroke="#ffffff" stroke-width="0.06" stroke-opacity="0.7" stroke-linejoin="round"/>
349
+ </g>
350
+ `;
351
+ }
352
+
353
+ function makeSleepMask(p, tx, ty, rot, scale) {
354
+ const cx = 12 + tx;
355
+ const cy = 12 + ty;
356
+ const W = 16.61 * scale;
357
+ const H = 7.55 * scale;
358
+ const eyeScale = 1.18;
359
+ const eyeRot = 5;
360
+ const eyeOffsetY = -0.25;
361
+ const lashW = 2.45;
362
+ const lashH = 0.70;
363
+ const lashStroke = 0.45;
364
+ const lashGap = 6.00 * scale;
365
+ const lashHairLen = 0.66;
366
+ const sx = W / SLEEP_MASK_PATH.bboxW;
367
+ const sy = H / SLEEP_MASK_PATH.bboxH;
368
+ const cxOrig = SLEEP_MASK_PATH.bboxX + SLEEP_MASK_PATH.bboxW / 2;
369
+ const cyOrig = SLEEP_MASK_PATH.bboxY + SLEEP_MASK_PATH.bboxH / 2;
370
+ const body = `<g transform="translate(${cx} ${cy}) scale(${sx.toFixed(5)} ${sy.toFixed(5)}) translate(${(-cxOrig).toFixed(2)} ${(-cyOrig).toFixed(2)})">
371
+ <path d="${SLEEP_MASK_PATH.d}" fill="#22305A" stroke="#000000" stroke-width="0.22" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/>
372
+ </g>`;
373
+ const arcLocal = `<path d="M ${(-lashW/2).toFixed(2)} 0 Q 0 ${(lashH * 2).toFixed(2)} ${(lashW/2).toFixed(2)} 0" fill="none" stroke="#FFFFFF" stroke-width="${lashStroke}" stroke-linecap="round"/>`;
374
+ const hairs = [];
375
+ for (let i = 0; i < 3; i++) {
376
+ const t = 0.18 + (i / 2) * 0.64;
377
+ const sxh = (1-t)*(1-t)*(-lashW/2) + t*t*(lashW/2);
378
+ const syh = 2*(1-t)*t*(2*lashH);
379
+ const tangX = 2*(1-t)*(lashW/2) + 2*t*(lashW/2);
380
+ const tangY = 2*(1-t)*(2*lashH) + 2*t*(-2*lashH);
381
+ const len = Math.hypot(tangX, tangY);
382
+ const nx1 = -tangY / len, ny1 = tangX / len;
383
+ const nx2 = tangY / len, ny2 = -tangX / len;
384
+ const nx = ny1 > 0 ? nx1 : nx2;
385
+ const ny = ny1 > 0 ? ny1 : ny2;
386
+ hairs.push(`<line x1="${sxh.toFixed(2)}" y1="${syh.toFixed(2)}" x2="${(sxh + nx * lashHairLen).toFixed(2)}" y2="${(syh + ny * lashHairLen).toFixed(2)}" stroke="#FFFFFF" stroke-width="0.38" stroke-linecap="round" opacity="0.92"/>`);
387
+ }
388
+ const eye = (ecx, sign) => `<g transform="translate(${ecx.toFixed(2)} ${(cy + eyeOffsetY).toFixed(2)}) rotate(${(sign * eyeRot).toFixed(2)}) scale(${eyeScale.toFixed(3)})">${arcLocal}${hairs.join('')}</g>`;
389
+ return `
390
+ <g transform="rotate(${rot} ${cx} ${cy})" filter="url(#${p}-shadow)">
391
+ ${body}
392
+ ${eye(cx - lashGap / 2, -1)}
393
+ ${eye(cx + lashGap / 2, 1)}
394
+ </g>
395
+ `;
396
+ }
397
+
398
+ function computeFrame(t) {
399
+ const tl = syncTimeline();
400
+ const unwindU = easeInOut(progress(t, tl.unwindStart, tl.unwindEnd));
401
+ const breathArc = Math.sin(unwindU * Math.PI);
402
+ const pathU = 1 - unwindU;
403
+ const sceneRot = lerp(DOZE.bodyRot, SLEEP.bodyTilt, easeInOut(progress(t, tl.unwindStart + 0.18, tl.unwindEnd + 0.32)));
404
+ const bodyTy = lerp(DOZE.bodyTy, 0, unwindU) + breathArc * 0.18;
405
+ const scale = lerp(DOZE.bodyScale, SLEEP.breathScale, unwindU) - breathArc * 0.028;
406
+ const haloOpacity = (1 - smoothstep(tl.unwindStart, tl.unwindEnd, t)) * 0.34;
407
+
408
+ const hatU = easeOutBackSoft(progress(t, tl.hatStart, tl.hatEnd));
409
+ const hatOpacity = smoothstep(tl.hatStart, tl.hatStart + 0.24, t);
410
+ const hatY = lerp(-6.3, SLEEP.hatTy, clamp01(hatU)) + Math.sin(clamp01(hatU) * Math.PI) * 0.16;
411
+ const hatRot = lerp(-18, SLEEP.hatRot, clamp01(hatU));
412
+ const hatScale = lerp(0.88, SLEEP.hatScale, clamp01(hatU));
413
+
414
+ const maskU = easeOutCubic(progress(t, tl.maskStart, tl.maskEnd));
415
+ const maskOpacity = smoothstep(tl.maskStart, tl.maskStart + 0.26, t);
416
+ const maskY = lerp(-4.4, SLEEP.maskTy, maskU) + Math.sin(maskU * Math.PI) * 0.10;
417
+ const maskRot = lerp(-2, SLEEP.maskRot, maskU);
418
+ const eyeOpacity = 0.92 * (1 - smoothstep(tl.eyeFadeStart, tl.eyeFadeEnd, t));
419
+
420
+ return {
421
+ pathU,
422
+ sceneRot,
423
+ bodyTy,
424
+ scale,
425
+ haloOpacity,
426
+ hat: { opacity: hatOpacity, y: hatY, rot: hatRot, scale: hatScale },
427
+ mask: { opacity: maskOpacity, y: maskY, rot: maskRot },
428
+ eyeOpacity,
429
+ phase: phaseName(t, tl),
430
+ };
431
+ }
432
+
433
+ function phaseName(t, tl) {
434
+ if (t < tl.holdEnd) return 'dozing hold';
435
+ if (t < tl.unwindEnd) return 'sleeping inhale';
436
+ if (t < tl.hatEnd) return 'hat settling';
437
+ if (t < tl.maskEnd) return 'mask settling';
438
+ return 'sleeping loop handoff';
439
+ }
440
+
441
+ function renderFrame(t) {
442
+ const f = computeFrame(t);
443
+ const p = 'dz';
444
+ const cloudD = makeDozeCloudPath(f.pathU);
445
+ const hat = makeHat(p);
446
+ const mask = makeSleepMask(p, SLEEP.maskTx, f.mask.y, f.mask.rot, SLEEP.maskScale);
447
+ const hatTransform = `translate(${SLEEP.hatTx} ${f.hat.y.toFixed(3)}) rotate(${f.hat.rot.toFixed(3)} 12 5.4) scale(${f.hat.scale.toFixed(4)})`;
448
+
449
+ stage.innerHTML = `
450
+ <style>
451
+ .edge-rim-tight { stroke-opacity: .9; }
452
+ .edge-rim-soft { stroke-opacity: .75; }
453
+ .edge-contact { stroke-opacity: .9; }
454
+ .wrap {
455
+ width: min(960px, 100%);
456
+ display: grid;
457
+ gap: 18px;
458
+ }
459
+ .header {
460
+ display: flex;
461
+ justify-content: space-between;
462
+ gap: 14px;
463
+ align-items: end;
464
+ }
465
+ #stage {
466
+ width: min(520px, 82vw);
467
+ aspect-ratio: 1;
468
+ display: grid;
469
+ place-items: center;
470
+ }
471
+ output {
472
+ min-width: 76px;
473
+ color: rgba(255,255,255,.70);
474
+ font-size: 12px;
475
+ text-align: right;
476
+ }
477
+ .phase {
478
+ display: flex;
479
+ flex-wrap: wrap;
480
+ gap: 9px 16px;
481
+ color: rgba(255,255,255,.62);
482
+ font-size: 12px;
483
+ }
484
+ .phase b { color: rgba(255,255,255,.88); font-weight: 650; }
485
+ .grid {
486
+ display: grid;
487
+ grid-template-columns: repeat(4, minmax(120px, 1fr));
488
+ gap: 10px 14px;
489
+ }
490
+ .field {
491
+ display: grid;
492
+ grid-template-columns: 1fr auto;
493
+ gap: 6px;
494
+ align-items: center;
495
+ color: rgba(255,255,255,.58);
496
+ font-size: 12px;
497
+ }
498
+ .field input { grid-column: 1 / -1; }
499
+ .field output { min-width: 46px; }
500
+ @media (prefers-color-scheme: light) {
501
+ .edge-rim-tight { stroke-opacity: .95; }
502
+ .edge-rim-soft { stroke-opacity: .85; }
503
+ .edge-contact { stroke-opacity: .95; }
504
+ .cloud-drop-shadow { flood-opacity: .13; }
505
+ .phase, output, .field { color: rgba(21,33,63,.62); }
506
+ .phase b { color: rgba(21,33,63,.86); }
507
+ .cloud-drop-shadow { flood-opacity: .13; }
508
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
509
+ .food-cloud > path {
510
+ stroke: #5870E8 !important;
511
+ stroke-width: 1.2 !important;
512
+ stroke-opacity: .9 !important;
513
+ vector-effect: non-scaling-stroke !important;
514
+ }
515
+ }
516
+ </style>
517
+ <defs>
518
+ ${defs(p, cloudD)}
519
+ <path id="${p}-cloud-shape" d="${cloudD}"/>
520
+ </defs>
521
+ <g opacity="${f.haloOpacity.toFixed(3)}">
522
+ <ellipse cx="12" cy="${DOZE.haloY}" rx="${DOZE.haloRx}" ry="${DOZE.haloRy}" fill="url(#${p}-halo)" filter="url(#${p}-halo-blur)"/>
523
+ </g>
524
+ <g class="scene" transform="rotate(${f.sceneRot.toFixed(3)} 12 12)">
525
+ <g id="breath-group" transform="translate(12 12) scale(${f.scale.toFixed(4)}) translate(-12 -12) translate(0 ${f.bodyTy.toFixed(3)})">
526
+ <g class="edge-system">
527
+ <use class="edge-contact" href="#${p}-cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.4" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#${p}-edge-blur)"/>
528
+ <use class="edge-rim-soft" href="#${p}-cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="1.2" stroke-linejoin="round" filter="url(#${p}-edge-blur)"/>
529
+ <use class="edge-rim-tight" href="#${p}-cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.8" stroke-linejoin="round"/>
530
+ </g>
531
+ <g filter="url(#${p}-shadow)">
532
+ <use href="#${p}-cloud-shape" fill="url(#${p}-body)"/>
533
+ <rect x="1" y="2" width="22" height="11" fill="url(#${p}-hi)" clip-path="url(#${p}-clip)"/>
534
+ </g>
535
+ ${makeDozeEyes(f.eyeOpacity)}
536
+ <g opacity="${f.hat.opacity.toFixed(3)}" transform="${hatTransform}">
537
+ ${hat}
538
+ </g>
539
+ <g opacity="${f.mask.opacity.toFixed(3)}">
540
+ ${mask}
541
+ </g>
542
+ </g>
543
+ </g>
544
+ `;
545
+ scrubber.value = t.toFixed(2);
546
+ timeOut.value = `${t.toFixed(2)} s`;
547
+ phaseEl.innerHTML = `<span>phase: <b>${f.phase}</b></span><span>cloud slump ${(f.pathU * 100).toFixed(0)}%</span><span>tilt ${f.sceneRot.toFixed(1)}deg</span><span>scale ${f.scale.toFixed(3)}</span><span>eyes ${(f.eyeOpacity * 100).toFixed(0)}%</span>`;
548
+ }
549
+
550
+ function currentTime(now) {
551
+ if (!playing) return manualT;
552
+ const t = (now - start) / 1000;
553
+ return Math.min(t, DURATION);
554
+ }
555
+
556
+ function loop(now) {
557
+ const t = currentTime(now);
558
+ renderFrame(t);
559
+ if (playing && t >= DURATION) {
560
+ playing = false;
561
+ manualT = DURATION;
562
+ { const __btn = document.getElementById('btn-play'); if (__btn) __btn.textContent = 'Play'; }
563
+ }
564
+ requestAnimationFrame(loop);
565
+ }
566
+
567
+ function restart() {
568
+ syncTimeline();
569
+ playing = true;
570
+ manualT = 0;
571
+ start = performance.now();
572
+ { const __btn = document.getElementById('btn-play'); if (__btn) __btn.textContent = 'Pause'; }
573
+ }
574
+
575
+ renderFrame(0);
576
+ requestAnimationFrame(loop);
577
+ ]]></script>
578
+ </svg>