@mumulinya167/ccm-pet-assets 2.0.0 → 2.0.2

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 (141) hide show
  1. package/assets/calico/calico-idle-follow.svg +324 -324
  2. package/assets/calico.svg +324 -324
  3. package/assets/clawd/LICENSE +22 -22
  4. package/assets/clawd/clawd-about-hero.svg +202 -202
  5. package/assets/clawd/clawd-collapse-sleep.svg +247 -247
  6. package/assets/clawd/clawd-error.svg +94 -94
  7. package/assets/clawd/clawd-happy.svg +160 -160
  8. package/assets/clawd/clawd-headphones-groove.svg +194 -194
  9. package/assets/clawd/clawd-idle-bubble.svg +180 -180
  10. package/assets/clawd/clawd-idle-collapse.svg +101 -101
  11. package/assets/clawd/clawd-idle-doze.svg +72 -72
  12. package/assets/clawd/clawd-idle-follow.svg +64 -64
  13. package/assets/clawd/clawd-idle-living.svg +195 -195
  14. package/assets/clawd/clawd-idle-look.svg +115 -115
  15. package/assets/clawd/clawd-idle-reading.svg +252 -252
  16. package/assets/clawd/clawd-idle-yawn.svg +158 -158
  17. package/assets/clawd/clawd-mini-alert.svg +129 -129
  18. package/assets/clawd/clawd-mini-crabwalk.svg +76 -76
  19. package/assets/clawd/clawd-mini-enter-sleep.svg +64 -64
  20. package/assets/clawd/clawd-mini-enter.svg +114 -114
  21. package/assets/clawd/clawd-mini-happy.svg +124 -124
  22. package/assets/clawd/clawd-mini-idle.svg +83 -83
  23. package/assets/clawd/clawd-mini-peek.svg +82 -82
  24. package/assets/clawd/clawd-mini-sleep.svg +112 -112
  25. package/assets/clawd/clawd-mini-typing.svg +153 -153
  26. package/assets/clawd/clawd-notification.svg +438 -438
  27. package/assets/clawd/clawd-react-annoyed.svg +167 -167
  28. package/assets/clawd/clawd-react-double-jump.svg +229 -229
  29. package/assets/clawd/clawd-react-double.svg +108 -108
  30. package/assets/clawd/clawd-react-drag.svg +102 -102
  31. package/assets/clawd/clawd-react-left.svg +102 -102
  32. package/assets/clawd/clawd-react-right.svg +102 -102
  33. package/assets/clawd/clawd-sleeping.svg +117 -117
  34. package/assets/clawd/clawd-static-base.svg +20 -20
  35. package/assets/clawd/clawd-wake.svg +277 -277
  36. package/assets/clawd/clawd-working-building.svg +329 -329
  37. package/assets/clawd/clawd-working-carrying.svg +178 -178
  38. package/assets/clawd/clawd-working-debugger.svg +245 -245
  39. package/assets/clawd/clawd-working-juggling.svg +183 -183
  40. package/assets/clawd/clawd-working-sweeping.svg +248 -248
  41. package/assets/clawd/clawd-working-thinking.svg +501 -501
  42. package/assets/clawd/clawd-working-typing-boss.svg +307 -307
  43. package/assets/clawd/clawd-working-typing.svg +273 -273
  44. package/assets/clawd/clawd-working-ultrathink.svg +166 -166
  45. package/assets/clawd/clawd-working-wizard.svg +356 -356
  46. package/assets/clawd.svg +64 -64
  47. package/assets/cloudling/cloudling/cloudling-attention.svg +379 -379
  48. package/assets/cloudling/cloudling/cloudling-building.svg +667 -667
  49. package/assets/cloudling/cloudling/cloudling-carrying.svg +604 -604
  50. package/assets/cloudling/cloudling/cloudling-conducting.svg +616 -616
  51. package/assets/cloudling/cloudling/cloudling-dozing-to-sleeping.svg +578 -578
  52. package/assets/cloudling/cloudling/cloudling-dozing.svg +358 -358
  53. package/assets/cloudling/cloudling/cloudling-error.svg +428 -428
  54. package/assets/cloudling/cloudling/cloudling-idle-reading.svg +658 -658
  55. package/assets/cloudling/cloudling/cloudling-idle-to-dozing.svg +568 -568
  56. package/assets/cloudling/cloudling/cloudling-idle-to-sleeping.svg +811 -811
  57. package/assets/cloudling/cloudling/cloudling-idle.svg +255 -255
  58. package/assets/cloudling/cloudling/cloudling-juggling.svg +648 -648
  59. package/assets/cloudling/cloudling/cloudling-mini-alert.svg +389 -389
  60. package/assets/cloudling/cloudling/cloudling-mini-crabwalk.svg +335 -335
  61. package/assets/cloudling/cloudling/cloudling-mini-enter-roll-in.svg +225 -225
  62. package/assets/cloudling/cloudling/cloudling-mini-enter-sleep.svg +231 -231
  63. package/assets/cloudling/cloudling/cloudling-mini-happy.svg +370 -370
  64. package/assets/cloudling/cloudling/cloudling-mini-idle.svg +279 -279
  65. package/assets/cloudling/cloudling/cloudling-mini-peek.svg +327 -327
  66. package/assets/cloudling/cloudling/cloudling-mini-sleep.svg +260 -260
  67. package/assets/cloudling/cloudling/cloudling-mini-typing.svg +691 -691
  68. package/assets/cloudling/cloudling/cloudling-notification.svg +493 -493
  69. package/assets/cloudling/cloudling/cloudling-react-drag.svg +83 -83
  70. package/assets/cloudling/cloudling/cloudling-sleeping-to-idle.svg +555 -555
  71. package/assets/cloudling/cloudling/cloudling-sleeping.svg +500 -500
  72. package/assets/cloudling/cloudling/cloudling-sweeping.svg +292 -292
  73. package/assets/cloudling/cloudling/cloudling-thinking.svg +719 -719
  74. package/assets/cloudling/cloudling/cloudling-typing.svg +354 -354
  75. package/assets/cloudling/cloudling-attention.svg +379 -379
  76. package/assets/cloudling/cloudling-building.svg +667 -667
  77. package/assets/cloudling/cloudling-carrying.svg +604 -604
  78. package/assets/cloudling/cloudling-conducting.svg +616 -616
  79. package/assets/cloudling/cloudling-dozing-to-sleeping.svg +578 -578
  80. package/assets/cloudling/cloudling-dozing.svg +358 -358
  81. package/assets/cloudling/cloudling-error.svg +428 -428
  82. package/assets/cloudling/cloudling-idle-reading.svg +658 -658
  83. package/assets/cloudling/cloudling-idle-to-dozing.svg +568 -568
  84. package/assets/cloudling/cloudling-idle-to-sleeping.svg +811 -811
  85. package/assets/cloudling/cloudling-idle.svg +255 -255
  86. package/assets/cloudling/cloudling-juggling.svg +648 -648
  87. package/assets/cloudling/cloudling-mini-alert.svg +389 -389
  88. package/assets/cloudling/cloudling-mini-crabwalk.svg +335 -335
  89. package/assets/cloudling/cloudling-mini-enter-roll-in.svg +225 -225
  90. package/assets/cloudling/cloudling-mini-enter-sleep.svg +231 -231
  91. package/assets/cloudling/cloudling-mini-happy.svg +370 -370
  92. package/assets/cloudling/cloudling-mini-idle.svg +279 -279
  93. package/assets/cloudling/cloudling-mini-peek.svg +327 -327
  94. package/assets/cloudling/cloudling-mini-sleep.svg +260 -260
  95. package/assets/cloudling/cloudling-mini-typing.svg +691 -691
  96. package/assets/cloudling/cloudling-notification.svg +493 -493
  97. package/assets/cloudling/cloudling-react-drag.svg +83 -83
  98. package/assets/cloudling/cloudling-sleeping-to-idle.svg +555 -555
  99. package/assets/cloudling/cloudling-sleeping.svg +500 -500
  100. package/assets/cloudling/cloudling-sweeping.svg +292 -292
  101. package/assets/cloudling/cloudling-thinking.svg +719 -719
  102. package/assets/cloudling/cloudling-typing.svg +354 -354
  103. package/assets/cloudling.svg +255 -255
  104. package/assets/ghost-attention.svg +19 -19
  105. package/assets/ghost-carrying.svg +19 -19
  106. package/assets/ghost-collapsing.svg +19 -19
  107. package/assets/ghost-dozing.svg +23 -23
  108. package/assets/ghost-error.svg +19 -19
  109. package/assets/ghost-happy.svg +23 -23
  110. package/assets/ghost-idle.svg +11 -11
  111. package/assets/ghost-juggling.svg +19 -19
  112. package/assets/ghost-notification.svg +19 -19
  113. package/assets/ghost-react-double.svg +23 -23
  114. package/assets/ghost-react-drag.svg +23 -23
  115. package/assets/ghost-react-left.svg +19 -19
  116. package/assets/ghost-react-right.svg +19 -19
  117. package/assets/ghost-sleeping.svg +19 -19
  118. package/assets/ghost-sweeping.svg +19 -19
  119. package/assets/ghost-waking.svg +19 -19
  120. package/assets/ghost-yawning.svg +23 -23
  121. package/assets/ghost.svg +11 -11
  122. package/assets/robot-attention.svg +19 -19
  123. package/assets/robot-carrying.svg +19 -19
  124. package/assets/robot-collapsing.svg +19 -19
  125. package/assets/robot-dozing.svg +23 -23
  126. package/assets/robot-error.svg +19 -19
  127. package/assets/robot-happy.svg +23 -23
  128. package/assets/robot-idle.svg +11 -11
  129. package/assets/robot-juggling.svg +19 -19
  130. package/assets/robot-notification.svg +19 -19
  131. package/assets/robot-react-double.svg +23 -23
  132. package/assets/robot-react-drag.svg +23 -23
  133. package/assets/robot-react-left.svg +19 -19
  134. package/assets/robot-react-right.svg +19 -19
  135. package/assets/robot-sleeping.svg +19 -19
  136. package/assets/robot-sweeping.svg +19 -19
  137. package/assets/robot-waking.svg +19 -19
  138. package/assets/robot-yawning.svg +23 -23
  139. package/assets/robot.svg +11 -11
  140. package/manifest.json +280 -280
  141. package/package.json +1 -1
@@ -1,568 +1,568 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg id="stage" xmlns="http://www.w3.org/2000/svg" width="880" height="720" viewBox="-32 -24 88 72" 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="duration-total"></g>
6
- <g id="motion-controls"></g>
7
- <g id="motion-summary"></g>
8
- <g id="phase"></g>
9
- <g id="scrubber"></g>
10
- <g id="time"></g>
11
- <g id="timing-controls"></g>
12
- </g>
13
- <script type="application/ecmascript"><![CDATA[
14
- 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";
15
-
16
- const SOFT_CLOUD_POINTS = [
17
- {x:9.064,y:3.344},{x:9.825,y:3.106},{x:10.616,y:3.004},{x:11.413,y:3.040},
18
- {x:12.192,y:3.210},{x:12.928,y:3.518},{x:13.599,y:3.948},{x:14.255,y:4.291},
19
- {x:15.046,y:4.188},{x:15.843,y:4.224},{x:16.621,y:4.398},{x:17.357,y:4.706},
20
- {x:18.029,y:5.134},{x:18.617,y:5.673},{x:19.103,y:6.306},{x:19.471,y:7.013},
21
- {x:19.715,y:7.772},{x:19.812,y:8.564},{x:19.784,y:9.361},{x:19.797,y:10.105},
22
- {x:20.287,y:10.735},{x:20.656,y:11.442},{x:20.897,y:12.202},{x:20.995,y:12.994},
23
- {x:20.964,y:13.791},{x:20.791,y:14.569},{x:20.480,y:15.304},{x:20.052,y:15.977},
24
- {x:19.516,y:16.567},{x:18.882,y:17.052},{x:18.174,y:17.418},{x:17.539,y:17.808},
25
- {x:17.231,y:18.545},{x:16.805,y:19.218},{x:16.264,y:19.805},{x:15.633,y:20.292},
26
- {x:14.925,y:20.660},{x:14.163,y:20.895},{x:13.372,y:20.996},{x:12.575,y:20.964},
27
- {x:11.796,y:20.793},{x:11.061,y:20.483},{x:10.392,y:20.049},{x:9.736,y:19.714},
28
- {x:8.946,y:19.811},{x:8.148,y:19.777},{x:7.370,y:19.606},{x:6.634,y:19.299},
29
- {x:5.962,y:18.868},{x:5.377,y:18.326},{x:4.885,y:17.698},{x:4.522,y:16.989},
30
- {x:4.283,y:16.228},{x:4.182,y:15.437},{x:4.220,y:14.640},{x:4.179,y:13.907},
31
- {x:3.696,y:13.273},{x:3.332,y:12.563},{x:3.092,y:11.803},{x:3.003,y:11.010},
32
- {x:3.038,y:10.213},{x:3.202,y:9.433},{x:3.514,y:8.699},{x:3.944,y:8.027},
33
- {x:4.484,y:7.440},{x:5.114,y:6.951},{x:5.822,y:6.586},{x:6.445,y:6.191},
34
- {x:6.752,y:5.455},{x:7.185,y:4.785},{x:7.723,y:4.197},{x:8.356,y:3.711},
35
- ];
36
-
37
- let DURATION = 9.03;
38
- const DOZE = {
39
- slump: 0.55,
40
- topSag: 1.42,
41
- shoulderSag: 0.34,
42
- spread: 0.82,
43
- floorY: 19.92,
44
- floorFlatten: 0.56,
45
- bottomSquish: 0.82,
46
- leftLean: -0.18,
47
- bodyTx: 0,
48
- bodyTy: 1.15,
49
- bodyRot: -1.5,
50
- bodyScale: 1.04,
51
- eyeY: 13.0,
52
- eyeGap: 6.12,
53
- eyeW: 2.70,
54
- eyeH: 1.28,
55
- eyeTilt: 7.7,
56
- eyeStroke: 0.80,
57
- eyeOpacity: 0.92,
58
- haloY: 20.62,
59
- haloRx: 10.8,
60
- haloRy: 1.28,
61
- haloOpacity: 0.34,
62
- glowOpacity: 0.18,
63
- };
64
-
65
- const stage = document.getElementById('stage');
66
- const scrubber = document.getElementById('scrubber');
67
- const timeOut = document.getElementById('time');
68
- const phaseEl = document.getElementById('phase');
69
- const timingControls = document.getElementById('timing-controls');
70
- const durationTotal = document.getElementById('duration-total');
71
- const motionControls = document.getElementById('motion-controls');
72
- const motionSummary = document.getElementById('motion-summary');
73
- let playing = true;
74
- let start = performance.now();
75
- let pausedAt = 0;
76
- let manualT = 0;
77
-
78
- const TIMING_CONTROLS = [
79
- { key: 'idleHold', label: '起始停顿', min: 0.10, max: 1.20, step: 0.01 },
80
- { key: 'halfClose1', label: '半闭 1', min: 0.25, max: 1.60, step: 0.01 },
81
- { key: 'wake1', label: '睁眼 1', min: 0.12, max: 0.80, step: 0.01 },
82
- { key: 'hold1', label: '撑住 1', min: 0.10, max: 1.40, step: 0.01 },
83
- { key: 'halfClose2', label: '半闭 2', min: 0.25, max: 1.80, step: 0.01 },
84
- { key: 'wake2', label: '睁眼 2', min: 0.12, max: 0.90, step: 0.01 },
85
- { key: 'hold2', label: '撑住 2', min: 0.10, max: 1.50, step: 0.01 },
86
- { key: 'finalClose', label: '最终闭眼', min: 0.25, max: 1.70, step: 0.01 },
87
- { key: 'slump', label: '软塌下移', min: 0.45, max: 2.20, step: 0.01 },
88
- { key: 'finalHold', label: '末尾停顿', min: 0.05, max: 0.80, step: 0.01 },
89
- ];
90
-
91
- const TIMING = {
92
- idleHold: 0.35,
93
- halfClose1: 1.00,
94
- wake1: 0.30,
95
- hold1: 0.80,
96
- halfClose2: 1.59,
97
- wake2: 0.55,
98
- hold2: 1.00,
99
- finalClose: 1.32,
100
- slump: 1.88,
101
- finalHold: 0.24,
102
- };
103
-
104
- const MOTION_CONTROLS = [
105
- { key: 'breathAmp', label: '呼吸幅度', min: 0, max: 0.04, step: 0.001, format: (v) => `${(v * 100).toFixed(1)}%` },
106
- { key: 'breathPeriod', label: '呼吸周期', min: 1.80, max: 6.00, step: 0.05, format: (v) => `${v.toFixed(2)}s` },
107
- ];
108
-
109
- const MOTION = {
110
- breathAmp: 0.018,
111
- breathPeriod: 3.80,
112
- };
113
-
114
- function clamp01(v) { return Math.max(0, Math.min(1, v)); }
115
- function lerp(a, b, t) { return a + (b - a) * t; }
116
- function progress(t, a, b) { return clamp01((t - a) / (b - a)); }
117
- function smoothstep(a, b, t) {
118
- const x = progress(t, a, b);
119
- return x * x * (3 - 2 * x);
120
- }
121
- function easeInOut(t) {
122
- return t < .5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
123
- }
124
- function easeOutBack(t) {
125
- const c1 = 1.18;
126
- const c3 = c1 + 1;
127
- return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
128
- }
129
-
130
- function buildTimeline() {
131
- let t = 0;
132
- const tl = {};
133
- tl.idleEnd = t += TIMING.idleHold;
134
- tl.half1Start = tl.idleEnd;
135
- tl.half1Peak = t += TIMING.halfClose1;
136
- tl.wake1End = t += TIMING.wake1;
137
- tl.hold1End = t += TIMING.hold1;
138
- tl.half2Start = tl.hold1End;
139
- tl.half2Peak = t += TIMING.halfClose2;
140
- tl.wake2End = t += TIMING.wake2;
141
- tl.hold2End = t += TIMING.hold2;
142
- tl.closeStart = tl.hold2End;
143
- tl.closeEnd = t += TIMING.finalClose;
144
- tl.slumpStart = tl.closeEnd;
145
- tl.slumpEnd = t += TIMING.slump;
146
- tl.duration = t += TIMING.finalHold;
147
- tl.haloStart = tl.slumpStart + TIMING.slump * 0.13;
148
- tl.dropStart = tl.slumpStart + TIMING.slump * 0.03;
149
- return tl;
150
- }
151
-
152
- let timeline = buildTimeline();
153
- DURATION = timeline.duration;
154
-
155
- function syncDuration() {
156
- timeline = buildTimeline();
157
- DURATION = timeline.duration;
158
- scrubber.max = DURATION.toFixed(2);
159
- if (manualT > DURATION) manualT = DURATION;
160
- durationTotal.textContent = `total ${DURATION.toFixed(2)} s`;
161
- }
162
-
163
- function catmullRomClosedPath(points, tension = 1) {
164
- const q = (n) => n.toFixed(3);
165
- let d = `M ${q(points[0].x)} ${q(points[0].y)}`;
166
- const n = points.length;
167
- for (let i = 0; i < n; i++) {
168
- const p0 = points[(i - 1 + n) % n];
169
- const p1 = points[i];
170
- const p2 = points[(i + 1) % n];
171
- const p3 = points[(i + 2) % n];
172
- const c1x = p1.x + (p2.x - p0.x) * tension / 6;
173
- const c1y = p1.y + (p2.y - p0.y) * tension / 6;
174
- const c2x = p2.x - (p3.x - p1.x) * tension / 6;
175
- const c2y = p2.y - (p3.y - p1.y) * tension / 6;
176
- d += ` C ${q(c1x)} ${q(c1y)} ${q(c2x)} ${q(c2y)} ${q(p2.x)} ${q(p2.y)}`;
177
- }
178
- return `${d} Z`;
179
- }
180
-
181
- function makeDozeCloudPath(amount) {
182
- const mix = clamp01(amount);
183
- if (mix <= 0.0001) return CLOUD_D;
184
- const slump = DOZE.slump * mix;
185
- const points = SOFT_CLOUD_POINTS.map((pt) => {
186
- const dx = pt.x - 12;
187
- const distX = Math.abs(dx);
188
- const sign = dx === 0 ? 0 : Math.sign(dx);
189
- const topWeight = Math.pow(clamp01((8.2 - pt.y) / 5.2), 0.95);
190
- const crownCenter = clamp01(1 - distX / 6.6);
191
- const shoulderWeight = clamp01((pt.y - 6.0) / 8.5) * clamp01((18.6 - pt.y) / 10.5);
192
- const lowerWeight = Math.pow(clamp01((pt.y - 15.4) / 5.8), 0.78);
193
- const sideWeight = clamp01((distX - 2.2) / 7.5) * clamp01((20.2 - pt.y) / 16.2);
194
-
195
- const topSag = slump * topWeight * DOZE.topSag * (0.70 + crownCenter * 0.42);
196
- const shoulderSag = slump * shoulderWeight * DOZE.shoulderSag;
197
- const floorCurve = DOZE.floorY + Math.pow(distX / 10, 2) * 0.46 - crownCenter * 0.04;
198
- const floorPull = (floorCurve - pt.y) * lowerWeight * DOZE.floorFlatten * slump;
199
- const sideOut = sign * slump * DOZE.spread * (0.18 + sideWeight * 0.76 + lowerWeight * 0.42);
200
- const bottomInhale = lowerWeight * DOZE.bottomSquish * slump * -0.34;
201
- const lean = DOZE.leftLean * slump * (0.15 + topWeight * 0.50 + shoulderWeight * 0.30);
202
-
203
- return {
204
- x: pt.x + sideOut + lean,
205
- y: pt.y + topSag + shoulderSag + floorPull + bottomInhale
206
- };
207
- });
208
- return catmullRomClosedPath(points, lerp(0.95, 0.68, mix));
209
- }
210
-
211
- function defs(cloudD) {
212
- return `
213
- <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
214
- <stop offset="0" stop-color="#f0f9ff"/>
215
- <stop offset="0.48" stop-color="#f5f6fa"/>
216
- <stop offset="1" stop-color="#dde2ec"/>
217
- </linearGradient>
218
- <linearGradient id="eye-grad" x1="12" y1="8" x2="12" y2="15" gradientUnits="userSpaceOnUse">
219
- <stop offset="0" stop-color="#B1A7FF"/>
220
- <stop offset="0.55" stop-color="#7A9DFF"/>
221
- <stop offset="1" stop-color="#3941FF"/>
222
- </linearGradient>
223
- <radialGradient id="hi-grad" cx="0.43" cy="0.22" r="0.62">
224
- <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.82"/>
225
- <stop offset="0.58" stop-color="#f0f9ff" stop-opacity="0.28"/>
226
- <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
227
- </radialGradient>
228
- <radialGradient id="halo-grad" cx="50%" cy="50%" r="50%">
229
- <stop offset="0" stop-color="#7A9DFF" stop-opacity="0.55"/>
230
- <stop offset="0.54" stop-color="#3941FF" stop-opacity="0.24"/>
231
- <stop offset="1" stop-color="#3941FF" stop-opacity="0"/>
232
- </radialGradient>
233
- <filter id="drop" x="-45%" y="-45%" width="190%" height="190%">
234
- <feDropShadow dx="0" dy="0.72" stdDeviation="0.92" flood-color="#3941FF" flood-opacity="0.18"/>
235
- </filter>
236
- <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
237
- <feGaussianBlur stdDeviation="0.16"/>
238
- </filter>
239
- <filter id="halo-blur" x="-80%" y="-120%" width="260%" height="340%">
240
- <feGaussianBlur stdDeviation="0.42"/>
241
- </filter>
242
- <clipPath id="cloud-clip"><path d="${cloudD}"/></clipPath>
243
- `;
244
- }
245
-
246
- function capsulePath(cx, cy, w, h) {
247
- const r = w / 2;
248
- const top = cy - h / 2;
249
- const bottom = cy + h / 2;
250
- return [
251
- `M ${cx.toFixed(2)} ${top.toFixed(2)}`,
252
- `C ${(cx + r).toFixed(2)} ${top.toFixed(2)} ${(cx + r).toFixed(2)} ${(top + r).toFixed(2)} ${(cx + r).toFixed(2)} ${(top + r).toFixed(2)}`,
253
- `L ${(cx + r).toFixed(2)} ${(bottom - r).toFixed(2)}`,
254
- `C ${(cx + r).toFixed(2)} ${bottom.toFixed(2)} ${cx.toFixed(2)} ${bottom.toFixed(2)} ${cx.toFixed(2)} ${bottom.toFixed(2)}`,
255
- `C ${(cx - r).toFixed(2)} ${bottom.toFixed(2)} ${(cx - r).toFixed(2)} ${(bottom - r).toFixed(2)} ${(cx - r).toFixed(2)} ${(bottom - r).toFixed(2)}`,
256
- `L ${(cx - r).toFixed(2)} ${(top + r).toFixed(2)}`,
257
- `C ${(cx - r).toFixed(2)} ${top.toFixed(2)} ${cx.toFixed(2)} ${top.toFixed(2)} ${cx.toFixed(2)} ${top.toFixed(2)} Z`
258
- ].join(' ');
259
- }
260
-
261
- function roundedRectPath(cx, cy, w, h) {
262
- const r = Math.min(w, h) / 2;
263
- const k = 0.5522847498;
264
- const left = cx - w / 2;
265
- const right = cx + w / 2;
266
- const top = cy - h / 2;
267
- const bottom = cy + h / 2;
268
- const c = r * k;
269
- return [
270
- `M ${(left + r).toFixed(2)} ${top.toFixed(2)}`,
271
- `L ${(right - r).toFixed(2)} ${top.toFixed(2)}`,
272
- `C ${(right - r + c).toFixed(2)} ${top.toFixed(2)} ${right.toFixed(2)} ${(top + r - c).toFixed(2)} ${right.toFixed(2)} ${(top + r).toFixed(2)}`,
273
- `L ${right.toFixed(2)} ${(bottom - r).toFixed(2)}`,
274
- `C ${right.toFixed(2)} ${(bottom - r + c).toFixed(2)} ${(right - r + c).toFixed(2)} ${bottom.toFixed(2)} ${(right - r).toFixed(2)} ${bottom.toFixed(2)}`,
275
- `L ${(left + r).toFixed(2)} ${bottom.toFixed(2)}`,
276
- `C ${(left + r - c).toFixed(2)} ${bottom.toFixed(2)} ${left.toFixed(2)} ${(bottom - r + c).toFixed(2)} ${left.toFixed(2)} ${(bottom - r).toFixed(2)}`,
277
- `L ${left.toFixed(2)} ${(top + r).toFixed(2)}`,
278
- `C ${left.toFixed(2)} ${(top + r - c).toFixed(2)} ${(left + r - c).toFixed(2)} ${top.toFixed(2)} ${(left + r).toFixed(2)} ${top.toFixed(2)} Z`
279
- ].join(' ');
280
- }
281
-
282
- function closedEyeStrokePath(cx, cy, w, h, tiltDeg, flip) {
283
- const x1 = cx - w / 2;
284
- const x2 = cx + w / 2;
285
- const mid = cy + h * 0.56;
286
- const c1x = cx - w * 0.18;
287
- const c2x = cx + w * 0.18;
288
- const tilt = flip * tiltDeg * 0.018;
289
- 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)}`;
290
- }
291
-
292
- function makeClippedCapsuleEye(cx, cy, w, h, lid, clipId) {
293
- const d = capsulePath(cx, cy, w, h);
294
- const cut = clamp01(lid);
295
- if (cut <= 0.012) return `<path d="${d}"/>`;
296
-
297
- const cutY = cy - h / 2 + h * cut;
298
- return `
299
- <clipPath id="${clipId}" clipPathUnits="userSpaceOnUse">
300
- <rect x="${(cx - w / 2 - 0.18).toFixed(2)}" y="${cutY.toFixed(2)}" width="${(w + 0.36).toFixed(2)}" height="${(h + 0.48).toFixed(2)}"/>
301
- </clipPath>
302
- <path d="${d}" clip-path="url(#${clipId})"/>`;
303
- }
304
-
305
- function makeClosingSlitEye(cx, cy, w, h, close, clipId) {
306
- const u = clamp01(close);
307
- const squeeze = smoothstep(0.42, 0.84, u);
308
- if (squeeze <= 0.001) return makeClippedCapsuleEye(cx, cy, w, h, u * 0.96, clipId);
309
-
310
- const cutY = cy - h / 2 + h * u * 0.96;
311
- const bottomY = cy + h / 2;
312
- const visibleH = Math.max(0.34, bottomY - cutY);
313
- const visibleCy = cutY + visibleH / 2;
314
- const slitH = lerp(visibleH, 0.34, squeeze);
315
- const slitW = lerp(w, w * 0.90, squeeze);
316
- const slitCy = lerp(visibleCy, cy + 0.25, squeeze);
317
- return `<path d="${roundedRectPath(cx, slitCy, slitW, slitH)}"/>`;
318
- }
319
-
320
- function makeEyes(eye) {
321
- const fillGap = 6.00;
322
- const fillCy = 12.00;
323
- const fillW = 2.60;
324
- const fillH = 5.00;
325
- const fillLeft = 12 - fillGap / 2;
326
- const fillRight = 12 + fillGap / 2;
327
-
328
- const closedGap = lerp(5.84, DOZE.eyeGap, eye.drop);
329
- const closedCy = lerp(12.12, DOZE.eyeY, eye.drop);
330
- const closedW = lerp(2.28, DOZE.eyeW, eye.drop);
331
- const closedH = lerp(0.72, DOZE.eyeH, eye.drop);
332
- const closedTilt = lerp(2.8, DOZE.eyeTilt, eye.drop);
333
- const closedStroke = lerp(0.62, DOZE.eyeStroke, eye.drop);
334
- const closedLeft = 12 - closedGap / 2;
335
- const closedRight = 12 + closedGap / 2;
336
-
337
- return `
338
- <g id="eyes-morph">
339
- <g fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7" opacity="${eye.fillOpacity.toFixed(3)}">
340
- ${eye.finalClose > 0.001
341
- ? makeClosingSlitEye(fillLeft, fillCy, fillW, fillH, eye.finalClose, 'eye-l-clip')
342
- : makeClippedCapsuleEye(fillLeft, fillCy, fillW, fillH, eye.lid, 'eye-l-clip')}
343
- ${eye.finalClose > 0.001
344
- ? makeClosingSlitEye(fillRight, fillCy, fillW, fillH, eye.finalClose, 'eye-r-clip')
345
- : makeClippedCapsuleEye(fillRight, fillCy, fillW, fillH, eye.lid, 'eye-r-clip')}
346
- </g>
347
- <g id="eyes-doze" fill="none" stroke="url(#eye-grad)" stroke-width="${closedStroke.toFixed(2)}" stroke-linecap="round" stroke-linejoin="round" opacity="${(eye.closedOpacity * DOZE.eyeOpacity).toFixed(3)}">
348
- <path d="${closedEyeStrokePath(closedLeft, closedCy, closedW, closedH, closedTilt, -1)}"/>
349
- <path d="${closedEyeStrokePath(closedRight, closedCy, closedW, closedH, closedTilt, 1)}"/>
350
- </g>
351
- </g>
352
- `;
353
- }
354
-
355
- function dozeBeat(t, start, peak, end) {
356
- return Math.min(smoothstep(start, peak, t), 1 - smoothstep(peak, end, t));
357
- }
358
-
359
- function computeFrame(t) {
360
- const tl = timeline;
361
- const beatA = dozeBeat(t, tl.half1Start, tl.half1Peak, tl.wake1End);
362
- const beatB = dozeBeat(t, tl.half2Start, tl.half2Peak, tl.wake2End);
363
- const closeU = smoothstep(tl.closeStart, tl.closeEnd, t);
364
- const pathU = easeInOut(progress(t, tl.slumpStart, tl.slumpEnd));
365
- const settleU = easeInOut(progress(t, tl.dropStart, tl.slumpEnd));
366
- const wakeA = Math.sin(progress(t, tl.half1Peak, tl.wake1End) * Math.PI) * 0.42;
367
- const wakeB = Math.sin(progress(t, tl.half2Peak, tl.wake2End) * Math.PI) * 0.34;
368
- const nodU = Math.max(beatA * 0.72, beatB * 1.05);
369
- const reboundU = wakeA + wakeB;
370
- const haloU = smoothstep(tl.haloStart, tl.slumpEnd, t);
371
- const eyeDropU = easeInOut(progress(t, tl.dropStart, tl.slumpEnd));
372
- const breathWave = Math.sin((t / MOTION.breathPeriod) * Math.PI * 2);
373
- const breathFade = lerp(1, 0.72, pathU);
374
- const breath = breathWave * MOTION.breathAmp * breathFade;
375
- const sleepyU = Math.max(beatA * 0.82, beatB, closeU);
376
- const lid = beatA * 0.56 + beatB * 0.66 + closeU * 0.96;
377
- const fillOpacity = 1 - smoothstep(0.58, 0.88, closeU);
378
- const closedOpacity = smoothstep(0.58, 1.0, closeU);
379
-
380
- return {
381
- pathU,
382
- haloU,
383
- eye: {
384
- sleepy: sleepyU,
385
- lid,
386
- finalClose: closeU,
387
- fillOpacity,
388
- closedOpacity,
389
- drop: eyeDropU,
390
- },
391
- bodyTx: lerp(0, DOZE.bodyTx, settleU) - 0.05 * nodU,
392
- bodyTy: lerp(0, DOZE.bodyTy, settleU) + 0.74 * nodU - 0.18 * reboundU,
393
- bodyRot: lerp(0, DOZE.bodyRot, settleU) - 2.1 * nodU + 0.42 * reboundU,
394
- bodySx: lerp(1, DOZE.bodyScale, settleU) + 0.025 * nodU + breath * 0.58,
395
- bodySy: lerp(1, DOZE.bodyScale, settleU) - 0.045 * nodU + breath,
396
- breath,
397
- };
398
- }
399
-
400
- function phaseName(t) {
401
- const tl = timeline;
402
- if (t < tl.idleEnd) return 'idle';
403
- if (t < tl.half1Peak) return 'half close 1';
404
- if (t < tl.wake1End) return 'wake 1';
405
- if (t < tl.hold1End) return 'holding on';
406
- if (t < tl.half2Peak) return 'half close 2';
407
- if (t < tl.wake2End) return 'wake 2';
408
- if (t < tl.hold2End) return 'holding on';
409
- if (t < tl.closeEnd) return 'natural close';
410
- if (t < tl.slumpEnd) return 'soft slump';
411
- return 'dozing hold';
412
- }
413
-
414
- function renderFrame(t) {
415
- const f = computeFrame(t);
416
- const cloudD = makeDozeCloudPath(f.pathU);
417
- const bodyTransform = `translate(${f.bodyTx.toFixed(3)} ${f.bodyTy.toFixed(3)}) translate(12 12) rotate(${f.bodyRot.toFixed(3)}) scale(${f.bodySx.toFixed(4)} ${f.bodySy.toFixed(4)}) translate(-12 -12)`;
418
- stage.innerHTML = `
419
- <style>
420
- .edge-rim-tight { stroke-opacity: .65; }
421
- .edge-rim-soft { stroke-opacity: .50; }
422
- .edge-contact { stroke-opacity: .65; }
423
- .page {
424
- width: min(1040px, 100%);
425
- display: grid;
426
- gap: 18px;
427
- }
428
- .header {
429
- display: flex;
430
- align-items: baseline;
431
- justify-content: space-between;
432
- gap: 16px;
433
- }
434
- .timeline, .tuning {
435
- border: 1px solid rgba(149, 170, 255, .22);
436
- background: rgba(5, 10, 24, .46);
437
- box-shadow: 0 20px 80px rgba(0, 0, 0, .34);
438
- border-radius: 8px;
439
- }
440
- .timeline {
441
- padding: 13px;
442
- display: grid;
443
- grid-template-columns: 70px 1fr 72px auto auto auto;
444
- gap: 10px;
445
- align-items: center;
446
- }
447
- .timeline span, .timeline output {
448
- color: rgba(226, 234, 255, .76);
449
- font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
450
- }
451
- .phase {
452
- display: flex;
453
- gap: 10px;
454
- flex-wrap: wrap;
455
- color: rgba(226, 234, 255, .54);
456
- font-size: 11px;
457
- }
458
- .phase b {
459
- color: rgba(235, 240, 255, .84);
460
- font-weight: 650;
461
- }
462
- .tuning {
463
- padding: 14px;
464
- display: grid;
465
- gap: 12px;
466
- }
467
- .tuning-head {
468
- display: flex;
469
- align-items: center;
470
- justify-content: space-between;
471
- gap: 12px;
472
- color: rgba(226, 234, 255, .82);
473
- font-size: 12px;
474
- font-weight: 650;
475
- }
476
- .tuning-head output {
477
- color: rgba(226, 234, 255, .62);
478
- font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
479
- font-weight: 500;
480
- }
481
- .timing-controls {
482
- display: grid;
483
- grid-template-columns: repeat(2, minmax(0, 1fr));
484
- gap: 11px 16px;
485
- }
486
- .timing-row {
487
- display: grid;
488
- grid-template-columns: 92px 1fr 56px;
489
- gap: 10px;
490
- align-items: center;
491
- }
492
- .timing-row label {
493
- color: rgba(226, 234, 255, .72);
494
- font-size: 12px;
495
- }
496
- .timing-row output {
497
- color: rgba(226, 234, 255, .66);
498
- font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
499
- text-align: right;
500
- }
501
- @media (prefers-color-scheme: light) {
502
- .edge-rim-tight { stroke-opacity: .75; }
503
- .edge-rim-soft { stroke-opacity: .60; }
504
- .edge-contact { stroke-opacity: .75; }
505
- .cloud-drop-shadow { flood-opacity: .13; }
506
- .timeline, .tuning {
507
- background: rgba(255, 255, 255, .68);
508
- border-color: rgba(82, 111, 190, .22);
509
- box-shadow: 0 20px 70px rgba(58, 78, 130, .14);
510
- }
511
- .phase, .timeline span, .timeline output, .tuning-head output, .timing-row label, .timing-row output { color: rgba(21, 33, 63, .66); }
512
- .phase b { color: rgba(21, 33, 63, .86); }
513
- .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
514
- .food-cloud > path {
515
- stroke: #5870E8 !important;
516
- stroke-width: 1.2 !important;
517
- stroke-opacity: .9 !important;
518
- vector-effect: non-scaling-stroke !important;
519
- }
520
- }
521
- </style>
522
- <defs>
523
- ${defs(cloudD)}
524
- <path id="cloud-shape" d="${cloudD}"/>
525
- </defs>
526
- <g id="halo-layer" opacity="${f.haloU.toFixed(3)}">
527
- <ellipse cx="12" cy="${DOZE.haloY}" rx="${lerp(7.8, DOZE.haloRx, f.haloU).toFixed(2)}" ry="${lerp(.72, DOZE.haloRy, f.haloU).toFixed(2)}" fill="url(#halo-grad)" opacity="${DOZE.haloOpacity}" filter="url(#halo-blur)"/>
528
- <ellipse cx="12" cy="${(DOZE.haloY + 0.05).toFixed(2)}" rx="${(DOZE.haloRx * 0.68).toFixed(2)}" ry="${(DOZE.haloRy * 0.46).toFixed(2)}" fill="#7A9DFF" opacity="${DOZE.glowOpacity}" filter="url(#halo-blur)"/>
529
- </g>
530
- <g id="body-pose" transform="${bodyTransform}">
531
- <g id="edge-system">
532
- <use class="edge-contact" href="#cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.00" stroke-linejoin="round" transform="translate(0 0.16)" filter="url(#edge-blur)"/>
533
- <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.88" stroke-linejoin="round" filter="url(#edge-blur)"/>
534
- <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.40" stroke-linejoin="round"/>
535
- </g>
536
- <g filter="url(#drop)">
537
- <use href="#cloud-shape" fill="url(#body-grad)"/>
538
- <rect x="1" y="2.2" width="22" height="10.4" fill="url(#hi-grad)" clip-path="url(#cloud-clip)" opacity=".88"/>
539
- </g>
540
- ${makeEyes(f.eye)}
541
- </g>
542
- `;
543
- scrubber.value = t.toFixed(2);
544
- timeOut.value = `${t.toFixed(2)} s`;
545
- phaseEl.innerHTML = `<span>phase: <b>${phaseName(t)}</b></span><span>path ${(f.pathU * 100).toFixed(0)}%</span><span>halo ${(f.haloU * 100).toFixed(0)}%</span><span>breath ${(f.breath * 100).toFixed(1)}%</span>`;
546
- }
547
-
548
- function currentTime(now) {
549
- if (!playing) return manualT;
550
- const t = ((now - start) / 1000);
551
- return Math.min(t, DURATION);
552
- }
553
-
554
- function loop(now) {
555
- const t = currentTime(now);
556
- renderFrame(t);
557
- if (playing && t >= DURATION) {
558
- playing = false;
559
- manualT = DURATION;
560
- { const __btn = document.getElementById('btn-play'); if (__btn) __btn.textContent = 'Play'; }
561
- }
562
- requestAnimationFrame(loop);
563
- }
564
-
565
- renderFrame(0);
566
- requestAnimationFrame(loop);
567
- ]]></script>
568
- </svg>
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="stage" xmlns="http://www.w3.org/2000/svg" width="880" height="720" viewBox="-32 -24 88 72" 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="duration-total"></g>
6
+ <g id="motion-controls"></g>
7
+ <g id="motion-summary"></g>
8
+ <g id="phase"></g>
9
+ <g id="scrubber"></g>
10
+ <g id="time"></g>
11
+ <g id="timing-controls"></g>
12
+ </g>
13
+ <script type="application/ecmascript"><![CDATA[
14
+ 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";
15
+
16
+ const SOFT_CLOUD_POINTS = [
17
+ {x:9.064,y:3.344},{x:9.825,y:3.106},{x:10.616,y:3.004},{x:11.413,y:3.040},
18
+ {x:12.192,y:3.210},{x:12.928,y:3.518},{x:13.599,y:3.948},{x:14.255,y:4.291},
19
+ {x:15.046,y:4.188},{x:15.843,y:4.224},{x:16.621,y:4.398},{x:17.357,y:4.706},
20
+ {x:18.029,y:5.134},{x:18.617,y:5.673},{x:19.103,y:6.306},{x:19.471,y:7.013},
21
+ {x:19.715,y:7.772},{x:19.812,y:8.564},{x:19.784,y:9.361},{x:19.797,y:10.105},
22
+ {x:20.287,y:10.735},{x:20.656,y:11.442},{x:20.897,y:12.202},{x:20.995,y:12.994},
23
+ {x:20.964,y:13.791},{x:20.791,y:14.569},{x:20.480,y:15.304},{x:20.052,y:15.977},
24
+ {x:19.516,y:16.567},{x:18.882,y:17.052},{x:18.174,y:17.418},{x:17.539,y:17.808},
25
+ {x:17.231,y:18.545},{x:16.805,y:19.218},{x:16.264,y:19.805},{x:15.633,y:20.292},
26
+ {x:14.925,y:20.660},{x:14.163,y:20.895},{x:13.372,y:20.996},{x:12.575,y:20.964},
27
+ {x:11.796,y:20.793},{x:11.061,y:20.483},{x:10.392,y:20.049},{x:9.736,y:19.714},
28
+ {x:8.946,y:19.811},{x:8.148,y:19.777},{x:7.370,y:19.606},{x:6.634,y:19.299},
29
+ {x:5.962,y:18.868},{x:5.377,y:18.326},{x:4.885,y:17.698},{x:4.522,y:16.989},
30
+ {x:4.283,y:16.228},{x:4.182,y:15.437},{x:4.220,y:14.640},{x:4.179,y:13.907},
31
+ {x:3.696,y:13.273},{x:3.332,y:12.563},{x:3.092,y:11.803},{x:3.003,y:11.010},
32
+ {x:3.038,y:10.213},{x:3.202,y:9.433},{x:3.514,y:8.699},{x:3.944,y:8.027},
33
+ {x:4.484,y:7.440},{x:5.114,y:6.951},{x:5.822,y:6.586},{x:6.445,y:6.191},
34
+ {x:6.752,y:5.455},{x:7.185,y:4.785},{x:7.723,y:4.197},{x:8.356,y:3.711},
35
+ ];
36
+
37
+ let DURATION = 9.03;
38
+ const DOZE = {
39
+ slump: 0.55,
40
+ topSag: 1.42,
41
+ shoulderSag: 0.34,
42
+ spread: 0.82,
43
+ floorY: 19.92,
44
+ floorFlatten: 0.56,
45
+ bottomSquish: 0.82,
46
+ leftLean: -0.18,
47
+ bodyTx: 0,
48
+ bodyTy: 1.15,
49
+ bodyRot: -1.5,
50
+ bodyScale: 1.04,
51
+ eyeY: 13.0,
52
+ eyeGap: 6.12,
53
+ eyeW: 2.70,
54
+ eyeH: 1.28,
55
+ eyeTilt: 7.7,
56
+ eyeStroke: 0.80,
57
+ eyeOpacity: 0.92,
58
+ haloY: 20.62,
59
+ haloRx: 10.8,
60
+ haloRy: 1.28,
61
+ haloOpacity: 0.34,
62
+ glowOpacity: 0.18,
63
+ };
64
+
65
+ const stage = document.getElementById('stage');
66
+ const scrubber = document.getElementById('scrubber');
67
+ const timeOut = document.getElementById('time');
68
+ const phaseEl = document.getElementById('phase');
69
+ const timingControls = document.getElementById('timing-controls');
70
+ const durationTotal = document.getElementById('duration-total');
71
+ const motionControls = document.getElementById('motion-controls');
72
+ const motionSummary = document.getElementById('motion-summary');
73
+ let playing = true;
74
+ let start = performance.now();
75
+ let pausedAt = 0;
76
+ let manualT = 0;
77
+
78
+ const TIMING_CONTROLS = [
79
+ { key: 'idleHold', label: '起始停顿', min: 0.10, max: 1.20, step: 0.01 },
80
+ { key: 'halfClose1', label: '半闭 1', min: 0.25, max: 1.60, step: 0.01 },
81
+ { key: 'wake1', label: '睁眼 1', min: 0.12, max: 0.80, step: 0.01 },
82
+ { key: 'hold1', label: '撑住 1', min: 0.10, max: 1.40, step: 0.01 },
83
+ { key: 'halfClose2', label: '半闭 2', min: 0.25, max: 1.80, step: 0.01 },
84
+ { key: 'wake2', label: '睁眼 2', min: 0.12, max: 0.90, step: 0.01 },
85
+ { key: 'hold2', label: '撑住 2', min: 0.10, max: 1.50, step: 0.01 },
86
+ { key: 'finalClose', label: '最终闭眼', min: 0.25, max: 1.70, step: 0.01 },
87
+ { key: 'slump', label: '软塌下移', min: 0.45, max: 2.20, step: 0.01 },
88
+ { key: 'finalHold', label: '末尾停顿', min: 0.05, max: 0.80, step: 0.01 },
89
+ ];
90
+
91
+ const TIMING = {
92
+ idleHold: 0.35,
93
+ halfClose1: 1.00,
94
+ wake1: 0.30,
95
+ hold1: 0.80,
96
+ halfClose2: 1.59,
97
+ wake2: 0.55,
98
+ hold2: 1.00,
99
+ finalClose: 1.32,
100
+ slump: 1.88,
101
+ finalHold: 0.24,
102
+ };
103
+
104
+ const MOTION_CONTROLS = [
105
+ { key: 'breathAmp', label: '呼吸幅度', min: 0, max: 0.04, step: 0.001, format: (v) => `${(v * 100).toFixed(1)}%` },
106
+ { key: 'breathPeriod', label: '呼吸周期', min: 1.80, max: 6.00, step: 0.05, format: (v) => `${v.toFixed(2)}s` },
107
+ ];
108
+
109
+ const MOTION = {
110
+ breathAmp: 0.018,
111
+ breathPeriod: 3.80,
112
+ };
113
+
114
+ function clamp01(v) { return Math.max(0, Math.min(1, v)); }
115
+ function lerp(a, b, t) { return a + (b - a) * t; }
116
+ function progress(t, a, b) { return clamp01((t - a) / (b - a)); }
117
+ function smoothstep(a, b, t) {
118
+ const x = progress(t, a, b);
119
+ return x * x * (3 - 2 * x);
120
+ }
121
+ function easeInOut(t) {
122
+ return t < .5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
123
+ }
124
+ function easeOutBack(t) {
125
+ const c1 = 1.18;
126
+ const c3 = c1 + 1;
127
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
128
+ }
129
+
130
+ function buildTimeline() {
131
+ let t = 0;
132
+ const tl = {};
133
+ tl.idleEnd = t += TIMING.idleHold;
134
+ tl.half1Start = tl.idleEnd;
135
+ tl.half1Peak = t += TIMING.halfClose1;
136
+ tl.wake1End = t += TIMING.wake1;
137
+ tl.hold1End = t += TIMING.hold1;
138
+ tl.half2Start = tl.hold1End;
139
+ tl.half2Peak = t += TIMING.halfClose2;
140
+ tl.wake2End = t += TIMING.wake2;
141
+ tl.hold2End = t += TIMING.hold2;
142
+ tl.closeStart = tl.hold2End;
143
+ tl.closeEnd = t += TIMING.finalClose;
144
+ tl.slumpStart = tl.closeEnd;
145
+ tl.slumpEnd = t += TIMING.slump;
146
+ tl.duration = t += TIMING.finalHold;
147
+ tl.haloStart = tl.slumpStart + TIMING.slump * 0.13;
148
+ tl.dropStart = tl.slumpStart + TIMING.slump * 0.03;
149
+ return tl;
150
+ }
151
+
152
+ let timeline = buildTimeline();
153
+ DURATION = timeline.duration;
154
+
155
+ function syncDuration() {
156
+ timeline = buildTimeline();
157
+ DURATION = timeline.duration;
158
+ scrubber.max = DURATION.toFixed(2);
159
+ if (manualT > DURATION) manualT = DURATION;
160
+ durationTotal.textContent = `total ${DURATION.toFixed(2)} s`;
161
+ }
162
+
163
+ function catmullRomClosedPath(points, tension = 1) {
164
+ const q = (n) => n.toFixed(3);
165
+ let d = `M ${q(points[0].x)} ${q(points[0].y)}`;
166
+ const n = points.length;
167
+ for (let i = 0; i < n; i++) {
168
+ const p0 = points[(i - 1 + n) % n];
169
+ const p1 = points[i];
170
+ const p2 = points[(i + 1) % n];
171
+ const p3 = points[(i + 2) % n];
172
+ const c1x = p1.x + (p2.x - p0.x) * tension / 6;
173
+ const c1y = p1.y + (p2.y - p0.y) * tension / 6;
174
+ const c2x = p2.x - (p3.x - p1.x) * tension / 6;
175
+ const c2y = p2.y - (p3.y - p1.y) * tension / 6;
176
+ d += ` C ${q(c1x)} ${q(c1y)} ${q(c2x)} ${q(c2y)} ${q(p2.x)} ${q(p2.y)}`;
177
+ }
178
+ return `${d} Z`;
179
+ }
180
+
181
+ function makeDozeCloudPath(amount) {
182
+ const mix = clamp01(amount);
183
+ if (mix <= 0.0001) return CLOUD_D;
184
+ const slump = DOZE.slump * mix;
185
+ const points = SOFT_CLOUD_POINTS.map((pt) => {
186
+ const dx = pt.x - 12;
187
+ const distX = Math.abs(dx);
188
+ const sign = dx === 0 ? 0 : Math.sign(dx);
189
+ const topWeight = Math.pow(clamp01((8.2 - pt.y) / 5.2), 0.95);
190
+ const crownCenter = clamp01(1 - distX / 6.6);
191
+ const shoulderWeight = clamp01((pt.y - 6.0) / 8.5) * clamp01((18.6 - pt.y) / 10.5);
192
+ const lowerWeight = Math.pow(clamp01((pt.y - 15.4) / 5.8), 0.78);
193
+ const sideWeight = clamp01((distX - 2.2) / 7.5) * clamp01((20.2 - pt.y) / 16.2);
194
+
195
+ const topSag = slump * topWeight * DOZE.topSag * (0.70 + crownCenter * 0.42);
196
+ const shoulderSag = slump * shoulderWeight * DOZE.shoulderSag;
197
+ const floorCurve = DOZE.floorY + Math.pow(distX / 10, 2) * 0.46 - crownCenter * 0.04;
198
+ const floorPull = (floorCurve - pt.y) * lowerWeight * DOZE.floorFlatten * slump;
199
+ const sideOut = sign * slump * DOZE.spread * (0.18 + sideWeight * 0.76 + lowerWeight * 0.42);
200
+ const bottomInhale = lowerWeight * DOZE.bottomSquish * slump * -0.34;
201
+ const lean = DOZE.leftLean * slump * (0.15 + topWeight * 0.50 + shoulderWeight * 0.30);
202
+
203
+ return {
204
+ x: pt.x + sideOut + lean,
205
+ y: pt.y + topSag + shoulderSag + floorPull + bottomInhale
206
+ };
207
+ });
208
+ return catmullRomClosedPath(points, lerp(0.95, 0.68, mix));
209
+ }
210
+
211
+ function defs(cloudD) {
212
+ return `
213
+ <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
214
+ <stop offset="0" stop-color="#f0f9ff"/>
215
+ <stop offset="0.48" stop-color="#f5f6fa"/>
216
+ <stop offset="1" stop-color="#dde2ec"/>
217
+ </linearGradient>
218
+ <linearGradient id="eye-grad" x1="12" y1="8" x2="12" y2="15" gradientUnits="userSpaceOnUse">
219
+ <stop offset="0" stop-color="#B1A7FF"/>
220
+ <stop offset="0.55" stop-color="#7A9DFF"/>
221
+ <stop offset="1" stop-color="#3941FF"/>
222
+ </linearGradient>
223
+ <radialGradient id="hi-grad" cx="0.43" cy="0.22" r="0.62">
224
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.82"/>
225
+ <stop offset="0.58" stop-color="#f0f9ff" stop-opacity="0.28"/>
226
+ <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
227
+ </radialGradient>
228
+ <radialGradient id="halo-grad" cx="50%" cy="50%" r="50%">
229
+ <stop offset="0" stop-color="#7A9DFF" stop-opacity="0.55"/>
230
+ <stop offset="0.54" stop-color="#3941FF" stop-opacity="0.24"/>
231
+ <stop offset="1" stop-color="#3941FF" stop-opacity="0"/>
232
+ </radialGradient>
233
+ <filter id="drop" x="-45%" y="-45%" width="190%" height="190%">
234
+ <feDropShadow dx="0" dy="0.72" stdDeviation="0.92" flood-color="#3941FF" flood-opacity="0.18"/>
235
+ </filter>
236
+ <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
237
+ <feGaussianBlur stdDeviation="0.16"/>
238
+ </filter>
239
+ <filter id="halo-blur" x="-80%" y="-120%" width="260%" height="340%">
240
+ <feGaussianBlur stdDeviation="0.42"/>
241
+ </filter>
242
+ <clipPath id="cloud-clip"><path d="${cloudD}"/></clipPath>
243
+ `;
244
+ }
245
+
246
+ function capsulePath(cx, cy, w, h) {
247
+ const r = w / 2;
248
+ const top = cy - h / 2;
249
+ const bottom = cy + h / 2;
250
+ return [
251
+ `M ${cx.toFixed(2)} ${top.toFixed(2)}`,
252
+ `C ${(cx + r).toFixed(2)} ${top.toFixed(2)} ${(cx + r).toFixed(2)} ${(top + r).toFixed(2)} ${(cx + r).toFixed(2)} ${(top + r).toFixed(2)}`,
253
+ `L ${(cx + r).toFixed(2)} ${(bottom - r).toFixed(2)}`,
254
+ `C ${(cx + r).toFixed(2)} ${bottom.toFixed(2)} ${cx.toFixed(2)} ${bottom.toFixed(2)} ${cx.toFixed(2)} ${bottom.toFixed(2)}`,
255
+ `C ${(cx - r).toFixed(2)} ${bottom.toFixed(2)} ${(cx - r).toFixed(2)} ${(bottom - r).toFixed(2)} ${(cx - r).toFixed(2)} ${(bottom - r).toFixed(2)}`,
256
+ `L ${(cx - r).toFixed(2)} ${(top + r).toFixed(2)}`,
257
+ `C ${(cx - r).toFixed(2)} ${top.toFixed(2)} ${cx.toFixed(2)} ${top.toFixed(2)} ${cx.toFixed(2)} ${top.toFixed(2)} Z`
258
+ ].join(' ');
259
+ }
260
+
261
+ function roundedRectPath(cx, cy, w, h) {
262
+ const r = Math.min(w, h) / 2;
263
+ const k = 0.5522847498;
264
+ const left = cx - w / 2;
265
+ const right = cx + w / 2;
266
+ const top = cy - h / 2;
267
+ const bottom = cy + h / 2;
268
+ const c = r * k;
269
+ return [
270
+ `M ${(left + r).toFixed(2)} ${top.toFixed(2)}`,
271
+ `L ${(right - r).toFixed(2)} ${top.toFixed(2)}`,
272
+ `C ${(right - r + c).toFixed(2)} ${top.toFixed(2)} ${right.toFixed(2)} ${(top + r - c).toFixed(2)} ${right.toFixed(2)} ${(top + r).toFixed(2)}`,
273
+ `L ${right.toFixed(2)} ${(bottom - r).toFixed(2)}`,
274
+ `C ${right.toFixed(2)} ${(bottom - r + c).toFixed(2)} ${(right - r + c).toFixed(2)} ${bottom.toFixed(2)} ${(right - r).toFixed(2)} ${bottom.toFixed(2)}`,
275
+ `L ${(left + r).toFixed(2)} ${bottom.toFixed(2)}`,
276
+ `C ${(left + r - c).toFixed(2)} ${bottom.toFixed(2)} ${left.toFixed(2)} ${(bottom - r + c).toFixed(2)} ${left.toFixed(2)} ${(bottom - r).toFixed(2)}`,
277
+ `L ${left.toFixed(2)} ${(top + r).toFixed(2)}`,
278
+ `C ${left.toFixed(2)} ${(top + r - c).toFixed(2)} ${(left + r - c).toFixed(2)} ${top.toFixed(2)} ${(left + r).toFixed(2)} ${top.toFixed(2)} Z`
279
+ ].join(' ');
280
+ }
281
+
282
+ function closedEyeStrokePath(cx, cy, w, h, tiltDeg, flip) {
283
+ const x1 = cx - w / 2;
284
+ const x2 = cx + w / 2;
285
+ const mid = cy + h * 0.56;
286
+ const c1x = cx - w * 0.18;
287
+ const c2x = cx + w * 0.18;
288
+ const tilt = flip * tiltDeg * 0.018;
289
+ 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)}`;
290
+ }
291
+
292
+ function makeClippedCapsuleEye(cx, cy, w, h, lid, clipId) {
293
+ const d = capsulePath(cx, cy, w, h);
294
+ const cut = clamp01(lid);
295
+ if (cut <= 0.012) return `<path d="${d}"/>`;
296
+
297
+ const cutY = cy - h / 2 + h * cut;
298
+ return `
299
+ <clipPath id="${clipId}" clipPathUnits="userSpaceOnUse">
300
+ <rect x="${(cx - w / 2 - 0.18).toFixed(2)}" y="${cutY.toFixed(2)}" width="${(w + 0.36).toFixed(2)}" height="${(h + 0.48).toFixed(2)}"/>
301
+ </clipPath>
302
+ <path d="${d}" clip-path="url(#${clipId})"/>`;
303
+ }
304
+
305
+ function makeClosingSlitEye(cx, cy, w, h, close, clipId) {
306
+ const u = clamp01(close);
307
+ const squeeze = smoothstep(0.42, 0.84, u);
308
+ if (squeeze <= 0.001) return makeClippedCapsuleEye(cx, cy, w, h, u * 0.96, clipId);
309
+
310
+ const cutY = cy - h / 2 + h * u * 0.96;
311
+ const bottomY = cy + h / 2;
312
+ const visibleH = Math.max(0.34, bottomY - cutY);
313
+ const visibleCy = cutY + visibleH / 2;
314
+ const slitH = lerp(visibleH, 0.34, squeeze);
315
+ const slitW = lerp(w, w * 0.90, squeeze);
316
+ const slitCy = lerp(visibleCy, cy + 0.25, squeeze);
317
+ return `<path d="${roundedRectPath(cx, slitCy, slitW, slitH)}"/>`;
318
+ }
319
+
320
+ function makeEyes(eye) {
321
+ const fillGap = 6.00;
322
+ const fillCy = 12.00;
323
+ const fillW = 2.60;
324
+ const fillH = 5.00;
325
+ const fillLeft = 12 - fillGap / 2;
326
+ const fillRight = 12 + fillGap / 2;
327
+
328
+ const closedGap = lerp(5.84, DOZE.eyeGap, eye.drop);
329
+ const closedCy = lerp(12.12, DOZE.eyeY, eye.drop);
330
+ const closedW = lerp(2.28, DOZE.eyeW, eye.drop);
331
+ const closedH = lerp(0.72, DOZE.eyeH, eye.drop);
332
+ const closedTilt = lerp(2.8, DOZE.eyeTilt, eye.drop);
333
+ const closedStroke = lerp(0.62, DOZE.eyeStroke, eye.drop);
334
+ const closedLeft = 12 - closedGap / 2;
335
+ const closedRight = 12 + closedGap / 2;
336
+
337
+ return `
338
+ <g id="eyes-morph">
339
+ <g fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7" opacity="${eye.fillOpacity.toFixed(3)}">
340
+ ${eye.finalClose > 0.001
341
+ ? makeClosingSlitEye(fillLeft, fillCy, fillW, fillH, eye.finalClose, 'eye-l-clip')
342
+ : makeClippedCapsuleEye(fillLeft, fillCy, fillW, fillH, eye.lid, 'eye-l-clip')}
343
+ ${eye.finalClose > 0.001
344
+ ? makeClosingSlitEye(fillRight, fillCy, fillW, fillH, eye.finalClose, 'eye-r-clip')
345
+ : makeClippedCapsuleEye(fillRight, fillCy, fillW, fillH, eye.lid, 'eye-r-clip')}
346
+ </g>
347
+ <g id="eyes-doze" fill="none" stroke="url(#eye-grad)" stroke-width="${closedStroke.toFixed(2)}" stroke-linecap="round" stroke-linejoin="round" opacity="${(eye.closedOpacity * DOZE.eyeOpacity).toFixed(3)}">
348
+ <path d="${closedEyeStrokePath(closedLeft, closedCy, closedW, closedH, closedTilt, -1)}"/>
349
+ <path d="${closedEyeStrokePath(closedRight, closedCy, closedW, closedH, closedTilt, 1)}"/>
350
+ </g>
351
+ </g>
352
+ `;
353
+ }
354
+
355
+ function dozeBeat(t, start, peak, end) {
356
+ return Math.min(smoothstep(start, peak, t), 1 - smoothstep(peak, end, t));
357
+ }
358
+
359
+ function computeFrame(t) {
360
+ const tl = timeline;
361
+ const beatA = dozeBeat(t, tl.half1Start, tl.half1Peak, tl.wake1End);
362
+ const beatB = dozeBeat(t, tl.half2Start, tl.half2Peak, tl.wake2End);
363
+ const closeU = smoothstep(tl.closeStart, tl.closeEnd, t);
364
+ const pathU = easeInOut(progress(t, tl.slumpStart, tl.slumpEnd));
365
+ const settleU = easeInOut(progress(t, tl.dropStart, tl.slumpEnd));
366
+ const wakeA = Math.sin(progress(t, tl.half1Peak, tl.wake1End) * Math.PI) * 0.42;
367
+ const wakeB = Math.sin(progress(t, tl.half2Peak, tl.wake2End) * Math.PI) * 0.34;
368
+ const nodU = Math.max(beatA * 0.72, beatB * 1.05);
369
+ const reboundU = wakeA + wakeB;
370
+ const haloU = smoothstep(tl.haloStart, tl.slumpEnd, t);
371
+ const eyeDropU = easeInOut(progress(t, tl.dropStart, tl.slumpEnd));
372
+ const breathWave = Math.sin((t / MOTION.breathPeriod) * Math.PI * 2);
373
+ const breathFade = lerp(1, 0.72, pathU);
374
+ const breath = breathWave * MOTION.breathAmp * breathFade;
375
+ const sleepyU = Math.max(beatA * 0.82, beatB, closeU);
376
+ const lid = beatA * 0.56 + beatB * 0.66 + closeU * 0.96;
377
+ const fillOpacity = 1 - smoothstep(0.58, 0.88, closeU);
378
+ const closedOpacity = smoothstep(0.58, 1.0, closeU);
379
+
380
+ return {
381
+ pathU,
382
+ haloU,
383
+ eye: {
384
+ sleepy: sleepyU,
385
+ lid,
386
+ finalClose: closeU,
387
+ fillOpacity,
388
+ closedOpacity,
389
+ drop: eyeDropU,
390
+ },
391
+ bodyTx: lerp(0, DOZE.bodyTx, settleU) - 0.05 * nodU,
392
+ bodyTy: lerp(0, DOZE.bodyTy, settleU) + 0.74 * nodU - 0.18 * reboundU,
393
+ bodyRot: lerp(0, DOZE.bodyRot, settleU) - 2.1 * nodU + 0.42 * reboundU,
394
+ bodySx: lerp(1, DOZE.bodyScale, settleU) + 0.025 * nodU + breath * 0.58,
395
+ bodySy: lerp(1, DOZE.bodyScale, settleU) - 0.045 * nodU + breath,
396
+ breath,
397
+ };
398
+ }
399
+
400
+ function phaseName(t) {
401
+ const tl = timeline;
402
+ if (t < tl.idleEnd) return 'idle';
403
+ if (t < tl.half1Peak) return 'half close 1';
404
+ if (t < tl.wake1End) return 'wake 1';
405
+ if (t < tl.hold1End) return 'holding on';
406
+ if (t < tl.half2Peak) return 'half close 2';
407
+ if (t < tl.wake2End) return 'wake 2';
408
+ if (t < tl.hold2End) return 'holding on';
409
+ if (t < tl.closeEnd) return 'natural close';
410
+ if (t < tl.slumpEnd) return 'soft slump';
411
+ return 'dozing hold';
412
+ }
413
+
414
+ function renderFrame(t) {
415
+ const f = computeFrame(t);
416
+ const cloudD = makeDozeCloudPath(f.pathU);
417
+ const bodyTransform = `translate(${f.bodyTx.toFixed(3)} ${f.bodyTy.toFixed(3)}) translate(12 12) rotate(${f.bodyRot.toFixed(3)}) scale(${f.bodySx.toFixed(4)} ${f.bodySy.toFixed(4)}) translate(-12 -12)`;
418
+ stage.innerHTML = `
419
+ <style>
420
+ .edge-rim-tight { stroke-opacity: .65; }
421
+ .edge-rim-soft { stroke-opacity: .50; }
422
+ .edge-contact { stroke-opacity: .65; }
423
+ .page {
424
+ width: min(1040px, 100%);
425
+ display: grid;
426
+ gap: 18px;
427
+ }
428
+ .header {
429
+ display: flex;
430
+ align-items: baseline;
431
+ justify-content: space-between;
432
+ gap: 16px;
433
+ }
434
+ .timeline, .tuning {
435
+ border: 1px solid rgba(149, 170, 255, .22);
436
+ background: rgba(5, 10, 24, .46);
437
+ box-shadow: 0 20px 80px rgba(0, 0, 0, .34);
438
+ border-radius: 8px;
439
+ }
440
+ .timeline {
441
+ padding: 13px;
442
+ display: grid;
443
+ grid-template-columns: 70px 1fr 72px auto auto auto;
444
+ gap: 10px;
445
+ align-items: center;
446
+ }
447
+ .timeline span, .timeline output {
448
+ color: rgba(226, 234, 255, .76);
449
+ font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
450
+ }
451
+ .phase {
452
+ display: flex;
453
+ gap: 10px;
454
+ flex-wrap: wrap;
455
+ color: rgba(226, 234, 255, .54);
456
+ font-size: 11px;
457
+ }
458
+ .phase b {
459
+ color: rgba(235, 240, 255, .84);
460
+ font-weight: 650;
461
+ }
462
+ .tuning {
463
+ padding: 14px;
464
+ display: grid;
465
+ gap: 12px;
466
+ }
467
+ .tuning-head {
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: space-between;
471
+ gap: 12px;
472
+ color: rgba(226, 234, 255, .82);
473
+ font-size: 12px;
474
+ font-weight: 650;
475
+ }
476
+ .tuning-head output {
477
+ color: rgba(226, 234, 255, .62);
478
+ font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
479
+ font-weight: 500;
480
+ }
481
+ .timing-controls {
482
+ display: grid;
483
+ grid-template-columns: repeat(2, minmax(0, 1fr));
484
+ gap: 11px 16px;
485
+ }
486
+ .timing-row {
487
+ display: grid;
488
+ grid-template-columns: 92px 1fr 56px;
489
+ gap: 10px;
490
+ align-items: center;
491
+ }
492
+ .timing-row label {
493
+ color: rgba(226, 234, 255, .72);
494
+ font-size: 12px;
495
+ }
496
+ .timing-row output {
497
+ color: rgba(226, 234, 255, .66);
498
+ font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
499
+ text-align: right;
500
+ }
501
+ @media (prefers-color-scheme: light) {
502
+ .edge-rim-tight { stroke-opacity: .75; }
503
+ .edge-rim-soft { stroke-opacity: .60; }
504
+ .edge-contact { stroke-opacity: .75; }
505
+ .cloud-drop-shadow { flood-opacity: .13; }
506
+ .timeline, .tuning {
507
+ background: rgba(255, 255, 255, .68);
508
+ border-color: rgba(82, 111, 190, .22);
509
+ box-shadow: 0 20px 70px rgba(58, 78, 130, .14);
510
+ }
511
+ .phase, .timeline span, .timeline output, .tuning-head output, .timing-row label, .timing-row output { color: rgba(21, 33, 63, .66); }
512
+ .phase b { color: rgba(21, 33, 63, .86); }
513
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
514
+ .food-cloud > path {
515
+ stroke: #5870E8 !important;
516
+ stroke-width: 1.2 !important;
517
+ stroke-opacity: .9 !important;
518
+ vector-effect: non-scaling-stroke !important;
519
+ }
520
+ }
521
+ </style>
522
+ <defs>
523
+ ${defs(cloudD)}
524
+ <path id="cloud-shape" d="${cloudD}"/>
525
+ </defs>
526
+ <g id="halo-layer" opacity="${f.haloU.toFixed(3)}">
527
+ <ellipse cx="12" cy="${DOZE.haloY}" rx="${lerp(7.8, DOZE.haloRx, f.haloU).toFixed(2)}" ry="${lerp(.72, DOZE.haloRy, f.haloU).toFixed(2)}" fill="url(#halo-grad)" opacity="${DOZE.haloOpacity}" filter="url(#halo-blur)"/>
528
+ <ellipse cx="12" cy="${(DOZE.haloY + 0.05).toFixed(2)}" rx="${(DOZE.haloRx * 0.68).toFixed(2)}" ry="${(DOZE.haloRy * 0.46).toFixed(2)}" fill="#7A9DFF" opacity="${DOZE.glowOpacity}" filter="url(#halo-blur)"/>
529
+ </g>
530
+ <g id="body-pose" transform="${bodyTransform}">
531
+ <g id="edge-system">
532
+ <use class="edge-contact" href="#cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.00" stroke-linejoin="round" transform="translate(0 0.16)" filter="url(#edge-blur)"/>
533
+ <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.88" stroke-linejoin="round" filter="url(#edge-blur)"/>
534
+ <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.40" stroke-linejoin="round"/>
535
+ </g>
536
+ <g filter="url(#drop)">
537
+ <use href="#cloud-shape" fill="url(#body-grad)"/>
538
+ <rect x="1" y="2.2" width="22" height="10.4" fill="url(#hi-grad)" clip-path="url(#cloud-clip)" opacity=".88"/>
539
+ </g>
540
+ ${makeEyes(f.eye)}
541
+ </g>
542
+ `;
543
+ scrubber.value = t.toFixed(2);
544
+ timeOut.value = `${t.toFixed(2)} s`;
545
+ phaseEl.innerHTML = `<span>phase: <b>${phaseName(t)}</b></span><span>path ${(f.pathU * 100).toFixed(0)}%</span><span>halo ${(f.haloU * 100).toFixed(0)}%</span><span>breath ${(f.breath * 100).toFixed(1)}%</span>`;
546
+ }
547
+
548
+ function currentTime(now) {
549
+ if (!playing) return manualT;
550
+ const t = ((now - start) / 1000);
551
+ return Math.min(t, DURATION);
552
+ }
553
+
554
+ function loop(now) {
555
+ const t = currentTime(now);
556
+ renderFrame(t);
557
+ if (playing && t >= DURATION) {
558
+ playing = false;
559
+ manualT = DURATION;
560
+ { const __btn = document.getElementById('btn-play'); if (__btn) __btn.textContent = 'Play'; }
561
+ }
562
+ requestAnimationFrame(loop);
563
+ }
564
+
565
+ renderFrame(0);
566
+ requestAnimationFrame(loop);
567
+ ]]></script>
568
+ </svg>