@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,667 +1,667 @@
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="scrubber"></g>
5
- <g id="t-display"></g>
6
- </g>
7
- <script type="application/ecmascript"><![CDATA[
8
- /* =========================================================================
9
- * Cloudling 主体几何 (沿用 V2 视觉定稿, 跟 carrying-v5 / idle-to-sleeping-v1 同源)
10
- * ========================================================================= */
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
- function lerp(a, b, t) { return a + (b - a) * t; }
35
- function clamp01(t) { return Math.max(0, Math.min(1, t)); }
36
- function easeIn(t) { return t * t; }
37
- function easeOut(t) { return 1 - Math.pow(1 - t, 2); }
38
- function easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2; }
39
- function easeOutBack(t) { const c=1.70158; return 1 + (c+1)*Math.pow(t-1,3) + c*Math.pow(t-1,2); }
40
- function progress(t, begin, end) { return clamp01((t - begin) / (end - begin)); }
41
-
42
- function catmullRomClosedPath(points, tension = 1) {
43
- const q = (n) => n.toFixed(3);
44
- let d = `M ${q(points[0].x)} ${q(points[0].y)}`;
45
- const n = points.length;
46
- for (let i = 0; i < n; i++) {
47
- const p0 = points[(i - 1 + n) % n];
48
- const p1 = points[i];
49
- const p2 = points[(i + 1) % n];
50
- const p3 = points[(i + 2) % n];
51
- const c1x = p1.x + (p2.x - p0.x) * tension / 6;
52
- const c1y = p1.y + (p2.y - p0.y) * tension / 6;
53
- const c2x = p2.x - (p3.x - p1.x) * tension / 6;
54
- const c2y = p2.y - (p3.y - p1.y) * tension / 6;
55
- d += ` C ${q(c1x)} ${q(c1y)} ${q(c2x)} ${q(c2y)} ${q(p2.x)} ${q(p2.y)}`;
56
- }
57
- return `${d} Z`;
58
- }
59
-
60
- /* =========================================================================
61
- * 径向射线投影: 从中心 (cx,cy) 沿角度 angle 射出, 与圆角矩形边界求交.
62
- * 这是 building 通用 morph 工具 — 后续 7 个形状只要换边界几何就能复用.
63
- * ========================================================================= */
64
- function rayToRoundedRect(angle, cx, cy, halfW, halfH, r) {
65
- const dx = Math.cos(angle), dy = Math.sin(angle);
66
- // 1. 先求射线与无圆角矩形的最近交点
67
- let t = Infinity;
68
- if (Math.abs(dx) > 1e-9) t = Math.min(t, halfW / Math.abs(dx));
69
- if (Math.abs(dy) > 1e-9) t = Math.min(t, halfH / Math.abs(dy));
70
- let px = cx + dx * t;
71
- let py = cy + dy * t;
72
- // 2. 检查交点是否落在圆角象限内
73
- const lx = Math.abs(px - cx);
74
- const ly = Math.abs(py - cy);
75
- if (lx > halfW - r - 1e-6 && ly > halfH - r - 1e-6) {
76
- // 落在圆角象限 → 用射线与圆求交
77
- const sx = px >= cx ? 1 : -1;
78
- const sy = py >= cy ? 1 : -1;
79
- const ccx = cx + sx * (halfW - r);
80
- const ccy = cy + sy * (halfH - r);
81
- const ex = cx - ccx;
82
- const ey = cy - ccy;
83
- const a = dx*dx + dy*dy;
84
- const b = 2*(ex*dx + ey*dy);
85
- const c = ex*ex + ey*ey - r*r;
86
- const disc = b*b - 4*a*c;
87
- if (disc >= 0) {
88
- const s = (-b + Math.sqrt(disc)) / (2*a);
89
- px = cx + dx * s;
90
- py = cy + dy * s;
91
- }
92
- }
93
- return {x: px, y: py};
94
- }
95
-
96
- function makeRoundedRectTargetPoints(halfW, halfH, r) {
97
- const cx = 12, cy = 12;
98
- return SOFT_CLOUD_POINTS.map(cp => {
99
- const angle = Math.atan2(cp.y - cy, cp.x - cx);
100
- return rayToRoundedRect(angle, cx, cy, halfW, halfH, r);
101
- });
102
- }
103
-
104
- /* =========================================================================
105
- * 多形态 body path: cloud / terminal / module / monitor / envelope
106
- * ========================================================================= */
107
- function softenTargetPoints(targetPoints, amount) {
108
- return SOFT_CLOUD_POINTS.map((cp, i) => ({
109
- x: lerp(cp.x, targetPoints[i].x, amount),
110
- y: lerp(cp.y, targetPoints[i].y, amount),
111
- }));
112
- }
113
-
114
- function mixPointSets(a, b, t) {
115
- const u = clamp01(t);
116
- return a.map((p, i) => ({
117
- x: lerp(p.x, b[i].x, u),
118
- y: lerp(p.y, b[i].y, u),
119
- }));
120
- }
121
-
122
- function squashPoints(points, squashY) {
123
- if (squashY === 1) return points;
124
- const cy = 12;
125
- return points.map(p => ({ x: p.x, y: cy + (p.y - cy) * squashY }));
126
- }
127
-
128
- function makePathFromPoints(points, sharpness = 0) {
129
- return catmullRomClosedPath(points, lerp(1.0, 0.76, clamp01(sharpness)));
130
- }
131
-
132
- function makeShapePointSets() {
133
- return {
134
- cloud: SOFT_CLOUD_POINTS,
135
- terminal: softenTargetPoints(makeRoundedRectTargetPoints(params.terminalHalfW, params.terminalHalfH, params.terminalCornerR), params.terminalMorphAmount),
136
- module: softenTargetPoints(makeRoundedRectTargetPoints(params.squareHalfW, params.squareHalfH, params.squareCornerR), params.moduleMorphAmount),
137
- monitor: softenTargetPoints(makeRoundedRectTargetPoints(params.monitorHalfW, params.monitorHalfH, params.monitorCornerR), params.monitorMorphAmount),
138
- envelope: softenTargetPoints(makeRoundedRectTargetPoints(params.envHalfW, params.envHalfH, params.envCornerR), params.envelopeMorphAmount),
139
- };
140
- }
141
-
142
- /* =========================================================================
143
- * 默认参数
144
- * ========================================================================= */
145
- const DEFAULTS = {
146
- T_CYCLE: 5.4,
147
- T_CLOUD_HOLD: 0.42,
148
- T_TERMINAL_HOLD: 1.32,
149
- T_MODULE_HOLD: 2.18,
150
- T_MONITOR_HOLD: 3.04,
151
- T_ENVELOPE_HOLD: 3.96,
152
- T_SMILE_HOLD: 4.78,
153
- blinkClose: 0.12,
154
- blinkSwitch: 0.08,
155
- blinkOpen: 0.18,
156
-
157
- // morph
158
- squashY: 0.86,
159
- blinkSquashY: 0.84,
160
- terminalMorphAmount: 0.93,
161
- moduleMorphAmount: 0.94,
162
- monitorMorphAmount: 0.93,
163
- envelopeMorphAmount: 0.93,
164
-
165
- // 终端窗口几何
166
- terminalHalfW: 11.2,
167
- terminalHalfH: 7.3,
168
- terminalCornerR: 1.15,
169
-
170
- // 终端装饰
171
- titleBarH: 1.35, // 标题栏高度
172
- screenInset: 1.20, // 屏幕距主体边距, 留出更厚白色边框
173
-
174
- // 模块方块几何
175
- squareHalfW: 9.4,
176
- squareHalfH: 9.0,
177
- squareCornerR: 2.35,
178
- moduleDepth: 1.15,
179
-
180
- // 显示器
181
- monitorHalfW: 10.8,
182
- monitorHalfH: 6.25,
183
- monitorCornerR: 1.0,
184
- monitorScreenInset: 1.0,
185
- monitorScreenCornerR: 0.65,
186
- standNeckW: 2.0,
187
- standNeckH: 1.6,
188
- standBaseW: 6.2,
189
- standBaseH: 0.9,
190
- standBaseR: 0.38,
191
-
192
- // 信封
193
- envHalfW: 11.0,
194
- envHalfH: 6.6,
195
- envCornerR: 1.05,
196
- flapDepth: 3.4,
197
- flapInset: 0.85,
198
- flapFillOpacity: 0.08,
199
- mStrokeW: 1.12,
200
- mLegBoost: 1.28,
201
-
202
- // duang spring
203
- springAmp: 0.075,
204
- springFreq: 4.0,
205
- springDamping: 5.5,
206
- springDuration: 0.65,
207
-
208
- // 呼吸
209
- breathLow: 0.97,
210
- breathHigh: 1.02,
211
-
212
- // 眼睛
213
- eyeLeftX: 9,
214
- eyeRightX: 15,
215
- eyeWidth: 2.6,
216
- eyeHeight: 5,
217
- eyeY: 9.5,
218
- eyeFlashStrength: 0.5, // 变身瞬间眼睛亮度增强 (0-1)
219
-
220
- // 阴影
221
- cloudShadowOpacity: 0.28,
222
- };
223
-
224
- let params = { ...DEFAULTS };
225
- let paused = false;
226
- let scrubbing = false;
227
-
228
- /* =========================================================================
229
- * defs (复用 cloudling 标准件)
230
- * ========================================================================= */
231
- function commonDefs(p, cloudD) {
232
- return `
233
- <linearGradient id="${p}-body" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
234
- <stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="#e0f2fe"/><stop offset="1" stop-color="#bae6fd"/>
235
- </linearGradient>
236
- <linearGradient id="${p}-eye" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
237
- <stop offset="0" stop-color="#B1A7FF"/><stop offset="0.5" stop-color="#7A9DFF"/><stop offset="1" stop-color="#3941FF"/>
238
- </linearGradient>
239
- <linearGradient id="${p}-eye-flash" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
240
- <stop offset="0" stop-color="#E8E2FF"/><stop offset="0.5" stop-color="#B6CBFF"/><stop offset="1" stop-color="#7A8AFF"/>
241
- </linearGradient>
242
- <radialGradient id="${p}-hi" cx="0.45" cy="0.25" r="0.55">
243
- <stop offset="0" stop-color="#fff" stop-opacity="0.9"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
244
- </radialGradient>
245
- <filter id="${p}-shadow" x="-50%" y="-50%" width="200%" height="200%">
246
- <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="${params.cloudShadowOpacity}"/>
247
- </filter>
248
- <filter id="${p}-edge-blur" x="-40%" y="-40%" width="180%" height="180%">
249
- <feGaussianBlur stdDeviation="0.16"/>
250
- </filter>
251
- <filter id="${p}-eye-glow" x="-80%" y="-80%" width="260%" height="260%">
252
- <feGaussianBlur stdDeviation="0.45" result="b"/>
253
- <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
254
- </filter>
255
- <clipPath id="${p}-clip"><path d="${cloudD}"/></clipPath>
256
- `;
257
- }
258
-
259
- /* =========================================================================
260
- * cloudling 主体 (edge-system + drop shadow + 高光) — 跟 carrying-v5 同款
261
- * ========================================================================= */
262
- function makeCloudlingBody(p, f) {
263
- const terminalOpacity = f.opacities.terminal;
264
- const moduleOpacity = f.opacities.module;
265
- const monitorOpacity = f.opacities.monitor;
266
- const envelopeOpacity = f.opacities.envelope;
267
-
268
- const moduleDepth = params.moduleDepth * clamp01(moduleOpacity);
269
- const moduleSide = moduleOpacity > 0.01 ? `
270
- <use href="#${p}-cloud-shape"
271
- transform="translate(${moduleDepth.toFixed(2)} ${moduleDepth.toFixed(2)})"
272
- fill="#8EA4FF"
273
- opacity="${(0.24 * moduleOpacity).toFixed(3)}"/>
274
- ` : '';
275
-
276
- const tHalfW = params.terminalHalfW;
277
- const tHalfH = params.terminalHalfH;
278
- const titleH = params.titleBarH;
279
- const inset = params.screenInset;
280
- const titleX1 = 12 - tHalfW + 0.5;
281
- const titleX2 = 12 + tHalfW - 0.5;
282
- const titleY1 = 12 - tHalfH + 0.5;
283
- const titleW = titleX2 - titleX1;
284
- const screenX1 = titleX1 + inset;
285
- const screenX2 = titleX2 - inset;
286
- const screenY1 = titleY1 + titleH + 0.3;
287
- const screenY2 = 12 + tHalfH - 0.5 - 0.5;
288
- const dotY = titleY1 + titleH * 0.5;
289
- const dotR = Math.min(titleH * 0.28, 0.48);
290
- const dotX1 = titleX1 + 0.75;
291
- const dotX2 = dotX1 + 1.20;
292
- const dotX3 = dotX2 + 1.20;
293
- const promptSize = Math.max(2.4, Math.min(3.6, (screenY2 - screenY1) * 0.34));
294
- const promptX = screenX1 + 0.5;
295
- const promptY = (screenY1 + screenY2) * 0.5 + promptSize * 0.32;
296
-
297
- const terminalDecor = terminalOpacity > 0.01 ? `
298
- <g class="terminal-decor" opacity="${terminalOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
299
- <rect x="${titleX1.toFixed(2)}" y="${titleY1.toFixed(2)}" width="${titleW.toFixed(2)}" height="${titleH.toFixed(2)}" rx="0.45" fill="#EEF2FF" fill-opacity="0.72"/>
300
- <circle cx="${dotX1.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#96A9E8"/>
301
- <circle cx="${dotX2.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#B7C3F6"/>
302
- <circle cx="${dotX3.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#D3DAFF"/>
303
- <rect x="${screenX1.toFixed(2)}" y="${screenY1.toFixed(2)}" width="${(screenX2-screenX1).toFixed(2)}" height="${(screenY2-screenY1).toFixed(2)}" rx="0.65" fill="#171D2D"/>
304
- <text x="${promptX.toFixed(2)}" y="${promptY.toFixed(2)}" font-family="ui-monospace, monospace" font-size="${promptSize.toFixed(2)}" font-weight="700" fill="#7AC8FF" opacity="0.95">&gt;</text>
305
- </g>
306
- ` : '';
307
-
308
- const mHalfW = params.monitorHalfW;
309
- const mHalfH = params.monitorHalfH;
310
- const mInset = params.monitorScreenInset;
311
- const monX1 = 12 - mHalfW + 0.5 + mInset;
312
- const monX2 = 12 + mHalfW - 0.5 - mInset;
313
- const monY1 = 12 - mHalfH + 0.5 + mInset;
314
- const monY2 = 12 + mHalfH - 0.5 - mInset;
315
- const monBottom = 12 + mHalfH - 0.5;
316
- const neckW = params.standNeckW;
317
- const neckH = params.standNeckH;
318
- const neckX = 12 - neckW * 0.5;
319
- const neckY = monBottom - 0.05;
320
- const baseW = params.standBaseW;
321
- const baseH = params.standBaseH;
322
- const baseX = 12 - baseW * 0.5;
323
- const baseY = neckY + neckH;
324
- const monitorStand = monitorOpacity > 0.01 ? `
325
- <g class="stand-decor" opacity="${monitorOpacity.toFixed(3)}" filter="url(#${p}-shadow)">
326
- <rect x="${neckX.toFixed(2)}" y="${neckY.toFixed(2)}" width="${neckW.toFixed(2)}" height="${neckH.toFixed(2)}" rx="0.18" fill="url(#${p}-body)"/>
327
- <rect x="${baseX.toFixed(2)}" y="${baseY.toFixed(2)}" width="${baseW.toFixed(2)}" height="${baseH.toFixed(2)}" rx="${params.standBaseR}" fill="url(#${p}-body)"/>
328
- </g>
329
- <g class="stand-edge" opacity="${monitorOpacity.toFixed(3)}">
330
- <rect x="${neckX.toFixed(2)}" y="${(neckY + 0.05).toFixed(2)}" width="${neckW.toFixed(2)}" height="${(neckH - 0.05).toFixed(2)}" rx="0.18" fill="none" stroke="#aab3c6" stroke-width="0.34" stroke-opacity="0.55"/>
331
- <rect x="${baseX.toFixed(2)}" y="${baseY.toFixed(2)}" width="${baseW.toFixed(2)}" height="${baseH.toFixed(2)}" rx="${params.standBaseR}" fill="none" stroke="#aab3c6" stroke-width="0.34" stroke-opacity="0.55"/>
332
- </g>
333
- ` : '';
334
- const monitorScreen = monitorOpacity > 0.01 ? `
335
- <g class="screen-decor" opacity="${monitorOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
336
- <rect x="${monX1.toFixed(2)}" y="${monY1.toFixed(2)}" width="${(monX2-monX1).toFixed(2)}" height="${(monY2-monY1).toFixed(2)}" rx="${params.monitorScreenCornerR}" fill="#171D2D"/>
337
- <rect x="${(monX1 + 0.20).toFixed(2)}" y="${(monY1 + 0.20).toFixed(2)}" width="${(monX2-monX1 - 0.40).toFixed(2)}" height="${(monY2-monY1 - 0.40).toFixed(2)}" rx="${Math.max(0.2, params.monitorScreenCornerR - 0.16).toFixed(2)}" fill="none" stroke="#7A9DFF" stroke-width="0.18" stroke-opacity="0.34"/>
338
- </g>
339
- ` : '';
340
-
341
- const eHalfW = params.envHalfW;
342
- const eHalfH = params.envHalfH;
343
- const eInset = params.flapInset;
344
- const flapX1 = 12 - eHalfW + 0.5 + eInset;
345
- const flapX2 = 12 + eHalfW - 0.5 - eInset;
346
- const flapY = 12 - eHalfH + 0.5 + eInset;
347
- const flapBottomY = flapY + params.flapDepth;
348
- const mBottomY = 12 + eHalfH - 1.18;
349
- const mStrokeW = params.mStrokeW;
350
- const mLegStrokeW = mStrokeW * params.mLegBoost;
351
- const envelopeDecor = envelopeOpacity > 0.01 ? `
352
- <g class="envelope-decor" opacity="${envelopeOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
353
- <path d="M ${flapX1.toFixed(2)} ${flapY.toFixed(2)} L 12 ${flapBottomY.toFixed(2)} L ${flapX2.toFixed(2)} ${flapY.toFixed(2)} Z"
354
- fill="#B1A7FF" fill-opacity="${params.flapFillOpacity.toFixed(3)}"/>
355
- <path d="M ${flapX1.toFixed(2)} ${mBottomY.toFixed(2)} L ${flapX1.toFixed(2)} ${flapY.toFixed(2)}"
356
- fill="none" stroke="#6EA8FF" stroke-width="${mLegStrokeW.toFixed(2)}" stroke-linecap="round"/>
357
- <path d="M ${flapX1.toFixed(2)} ${flapY.toFixed(2)} L 12 ${flapBottomY.toFixed(2)}"
358
- fill="none" stroke="#FF6B7A" stroke-width="${mStrokeW.toFixed(2)}" stroke-linecap="round"/>
359
- <path d="M 12 ${flapBottomY.toFixed(2)} L ${flapX2.toFixed(2)} ${flapY.toFixed(2)}"
360
- fill="none" stroke="#FFD45C" stroke-width="${mStrokeW.toFixed(2)}" stroke-linecap="round"/>
361
- <path d="M ${flapX2.toFixed(2)} ${flapY.toFixed(2)} L ${flapX2.toFixed(2)} ${mBottomY.toFixed(2)}"
362
- fill="none" stroke="#55D08A" stroke-width="${mLegStrokeW.toFixed(2)}" stroke-linecap="round"/>
363
- </g>
364
- ` : '';
365
-
366
- return `
367
- <g class="cloudling-body">
368
- ${moduleSide}
369
- ${monitorStand}
370
- <g class="edge-system">
371
- <use class="edge-contact" href="#${p}-cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#${p}-edge-blur)"/>
372
- <use class="edge-rim-soft" href="#${p}-cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#${p}-edge-blur)"/>
373
- <use class="edge-rim-tight" href="#${p}-cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
374
- </g>
375
- <g filter="url(#${p}-shadow)">
376
- <use href="#${p}-cloud-shape" fill="url(#${p}-body)"/>
377
- <rect x="1" y="2" width="22" height="11" fill="url(#${p}-hi)" clip-path="url(#${p}-clip)"/>
378
- </g>
379
- ${terminalDecor}
380
- ${monitorScreen}
381
- ${envelopeDecor}
382
- </g>
383
- `;
384
- }
385
-
386
- /* =========================================================================
387
- * 眼睛 (capsule + 闭眼线 + smile, 锚点固定 9,12 / 15,12)
388
- * ========================================================================= */
389
- function makeEyes(p, f) {
390
- const mode = f.eyeMode;
391
- const openness = f.eyeOpenness;
392
- const flashStrength = f.eyeFlash;
393
- const lcx = f.eyeLeftX ?? params.eyeLeftX ?? 9;
394
- const rcx = f.eyeRightX ?? params.eyeRightX ?? 15;
395
- const useFlash = flashStrength > 0.05;
396
- const eyeFill = useFlash ? `url(#${p}-eye-flash)` : `url(#${p}-eye)`;
397
- const filter = useFlash ? `filter="url(#${p}-eye-glow)"` : '';
398
-
399
- if (mode === 'closed') {
400
- const eyeW = f.eyeWidth ?? params.eyeWidth ?? 2.6;
401
- const cy = (f.eyeY ?? params.eyeY ?? 9.5) + ((f.eyeHeight ?? params.eyeHeight ?? 5) * 0.5);
402
- const linePath = (cx) => `M ${(cx-eyeW*0.48).toFixed(2)} ${cy.toFixed(2)} Q ${cx.toFixed(2)} ${(cy+0.18).toFixed(2)} ${(cx+eyeW*0.48).toFixed(2)} ${cy.toFixed(2)}`;
403
- return `
404
- <g class="eyes" ${filter}>
405
- <path d="${linePath(lcx)}" fill="none" stroke="${eyeFill}" stroke-width="0.62" stroke-linecap="round"/>
406
- <path d="${linePath(rcx)}" fill="none" stroke="${eyeFill}" stroke-width="0.62" stroke-linecap="round"/>
407
- </g>
408
- `;
409
- }
410
-
411
- if (mode === 'smile') {
412
- const w = 3.5, h = 1.0, smileCY = 12.7;
413
- const smilePath = (cx) => `M ${(cx-w/2).toFixed(2)} ${smileCY} Q ${cx} ${(smileCY-2*h).toFixed(2)} ${(cx+w/2).toFixed(2)} ${smileCY}`;
414
- return `
415
- <g class="eyes">
416
- <path d="${smilePath(lcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>
417
- <path d="${smilePath(rcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>
418
- </g>
419
- `;
420
- }
421
-
422
- const eyeW = f.eyeWidth ?? params.eyeWidth ?? 2.6;
423
- const baseH = f.eyeHeight ?? params.eyeHeight ?? 5;
424
- const h = Math.max(0.3, baseH * openness);
425
- const y = (f.eyeY ?? params.eyeY ?? 9.5) + (baseH - h) / 2;
426
- const rx = Math.min(1.3, h / 2);
427
- const leftX = lcx - eyeW / 2;
428
- const rightX = rcx - eyeW / 2;
429
- return `
430
- <g class="eyes" ${filter}>
431
- <rect x="${leftX.toFixed(2)}" y="${y.toFixed(2)}" width="${eyeW.toFixed(2)}" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="${eyeFill}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>
432
- <rect x="${rightX.toFixed(2)}" y="${y.toFixed(2)}" width="${eyeW.toFixed(2)}" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="${eyeFill}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>
433
- </g>
434
- `;
435
- }
436
-
437
- /* =========================================================================
438
- * duang duang spring overshoot (跟 carrying-v5 同款)
439
- * ========================================================================= */
440
- function springOvershoot(tLocal, amp, freq, damping) {
441
- if (tLocal < 0) return 0;
442
- return amp * Math.cos(2 * Math.PI * freq * tLocal) * Math.exp(-damping * tLocal);
443
- }
444
-
445
- /* =========================================================================
446
- * 时间线计算
447
- * ========================================================================= */
448
- const EYE_STYLES = {
449
- cloud: { left: 9.00, right: 15.00, width: 2.60, height: 5.00, y: 9.50 },
450
- terminal: { left: 9.35, right: 14.85, width: 2.35, height: 4.20, y: 10.15 },
451
- module: { left: 8.85, right: 15.15, width: 2.65, height: 4.85, y: 9.65 },
452
- monitor: { left: 8.85, right: 15.15, width: 2.25, height: 4.05, y: 10.25 },
453
- envelope: { left: 8.75, right: 15.25, width: 2.40, height: 4.20, y: 10.20 },
454
- };
455
-
456
- function mixEyeStyle(from, to, u) {
457
- const a = EYE_STYLES[from];
458
- const b = EYE_STYLES[to];
459
- return {
460
- left: lerp(a.left, b.left, u),
461
- right: lerp(a.right, b.right, u),
462
- width: lerp(a.width, b.width, u),
463
- height: lerp(a.height, b.height, u),
464
- y: lerp(a.y, b.y, u),
465
- };
466
- }
467
-
468
- function setShape(f, from, to, uRaw) {
469
- const u = clamp01(uRaw);
470
- f.fromShape = from;
471
- f.toShape = to;
472
- f.shapeU = u;
473
- f.opacities = { terminal: 0, module: 0, monitor: 0, envelope: 0 };
474
- if (from !== 'cloud') f.opacities[from] += 1 - u;
475
- if (to !== 'cloud') f.opacities[to] += u;
476
-
477
- const eye = mixEyeStyle(from, to, u);
478
- f.eyeLeftX = eye.left;
479
- f.eyeRightX = eye.right;
480
- f.eyeWidth = eye.width;
481
- f.eyeHeight = eye.height;
482
- f.eyeY = eye.y;
483
- }
484
-
485
- function holdShape(f, shape, eyeMode = 'capsule') {
486
- setShape(f, shape, shape, 0);
487
- f.eyeMode = eyeMode;
488
- }
489
-
490
- function blinkSwitchFrame(f, tLocal, from, to, options = {}) {
491
- const closeDur = Math.max(0.04, params.blinkClose);
492
- const switchDur = Math.max(0.04, params.blinkSwitch);
493
- const openDur = Math.max(0.06, params.blinkOpen);
494
- const total = closeDur + switchDur + openDur;
495
- const targetEyeMode = options.smile ? 'smile' : 'capsule';
496
-
497
- if (tLocal < closeDur) {
498
- const u = easeIn(progress(tLocal, 0, closeDur));
499
- setShape(f, from, from, 0);
500
- f.eyeMode = 'closed';
501
- f.eyeOpenness = lerp(1, 0.08, u);
502
- f.squashY = lerp(1, params.blinkSquashY, u);
503
- f.eyeFlash = u * params.eyeFlashStrength * 0.55;
504
- } else if (tLocal < closeDur + switchDur) {
505
- const u = easeInOut(progress(tLocal, closeDur, closeDur + switchDur));
506
- setShape(f, from, to, u);
507
- f.eyeMode = 'closed';
508
- f.eyeOpenness = 0.08;
509
- f.squashY = params.blinkSquashY;
510
- f.eyeFlash = params.eyeFlashStrength * 0.35;
511
- } else if (tLocal < total) {
512
- const u = easeOutBack(progress(tLocal, closeDur + switchDur, total));
513
- setShape(f, to, to, 0);
514
- f.eyeOpenness = lerp(0.08, 1, clamp01(u));
515
- f.squashY = lerp(params.blinkSquashY, 1, clamp01(u));
516
- f.eyeFlash = lerp(params.eyeFlashStrength * 0.35, 0, clamp01(u));
517
- f.springScale = 1 + Math.sin(Math.PI * clamp01(u)) * 0.018;
518
- if (options.smile && u > 0.28) f.eyeMode = 'smile';
519
- } else {
520
- holdShape(f, to, targetEyeMode);
521
- }
522
- }
523
-
524
- function computeFrame(t) {
525
- const T = params.T_CYCLE;
526
- const tMod = ((t % T) + T) % T;
527
- const f = {
528
- tMod,
529
- fromShape: 'cloud',
530
- toShape: 'cloud',
531
- shapeU: 0,
532
- squashY: 1,
533
- springScale: 1,
534
- eyeMode: 'capsule',
535
- eyeOpenness: 1,
536
- eyeFlash: 0,
537
- opacities: { terminal: 0, module: 0, monitor: 0, envelope: 0 },
538
- };
539
-
540
- const blinkTotal = params.blinkClose + params.blinkSwitch + params.blinkOpen;
541
- const b1 = params.T_CLOUD_HOLD;
542
- const hTerminal = b1 + blinkTotal;
543
- const b2 = params.T_TERMINAL_HOLD;
544
- const hModule = b2 + blinkTotal;
545
- const b3 = params.T_MODULE_HOLD;
546
- const hMonitor = b3 + blinkTotal;
547
- const b4 = params.T_MONITOR_HOLD;
548
- const hEnvelope = b4 + blinkTotal;
549
- const b5 = params.T_ENVELOPE_HOLD;
550
- const hSmile = b5 + blinkTotal;
551
- const b6 = Math.max(params.T_SMILE_HOLD, hSmile + 0.05);
552
- const hCloud = b6 + blinkTotal;
553
-
554
- if (tMod < b1) {
555
- holdShape(f, 'cloud');
556
- } else if (tMod < hTerminal) {
557
- blinkSwitchFrame(f, tMod - b1, 'cloud', 'terminal');
558
- } else if (tMod < b2) {
559
- holdShape(f, 'terminal');
560
- } else if (tMod < hModule) {
561
- blinkSwitchFrame(f, tMod - b2, 'terminal', 'module');
562
- } else if (tMod < b3) {
563
- holdShape(f, 'module');
564
- } else if (tMod < hMonitor) {
565
- blinkSwitchFrame(f, tMod - b3, 'module', 'monitor');
566
- } else if (tMod < b4) {
567
- holdShape(f, 'monitor');
568
- } else if (tMod < hEnvelope) {
569
- blinkSwitchFrame(f, tMod - b4, 'monitor', 'envelope');
570
- } else if (tMod < b5) {
571
- holdShape(f, 'envelope');
572
- } else if (tMod < hSmile) {
573
- blinkSwitchFrame(f, tMod - b5, 'envelope', 'cloud', { smile: true });
574
- } else if (tMod < b6) {
575
- holdShape(f, 'cloud', 'smile');
576
- } else if (tMod < hCloud) {
577
- blinkSwitchFrame(f, tMod - b6, 'cloud', 'cloud');
578
- } else {
579
- holdShape(f, 'cloud');
580
- }
581
-
582
- const breathPhase = (Math.cos(2 * Math.PI * tMod / params.T_CYCLE) + 1) / 2;
583
- f.breathScale = lerp(params.breathLow, params.breathHigh, breathPhase);
584
- return f;
585
- }
586
-
587
- /* =========================================================================
588
- * 渲染
589
- * ========================================================================= */
590
- const stage = document.getElementById('stage');
591
-
592
- function renderFrame(t) {
593
- const f = computeFrame(t);
594
- const p = 'bld';
595
-
596
- const pointSets = makeShapePointSets();
597
- let bodyPoints = mixPointSets(pointSets[f.fromShape], pointSets[f.toShape], f.shapeU);
598
- bodyPoints = squashPoints(bodyPoints, f.squashY);
599
- const nonCloudness = 1 - (f.fromShape === 'cloud' && f.toShape === 'cloud' ? 1 : 0);
600
- const sharpness = Math.max(f.opacities.terminal, f.opacities.module, f.opacities.monitor, f.opacities.envelope, nonCloudness * f.shapeU);
601
- const cloudD = makePathFromPoints(bodyPoints, sharpness);
602
- const cloudlingBody = makeCloudlingBody(p, f);
603
- const eyes = makeEyes(p, f);
604
-
605
- stage.innerHTML = `
606
- <style>
607
- .edge-rim-tight { stroke-opacity: .65; }
608
- .edge-rim-soft { stroke-opacity: .50; }
609
- .edge-contact { stroke-opacity: .65; }
610
- @media (prefers-color-scheme: light) {
611
- .edge-rim-tight { stroke-opacity: .75; }
612
- .edge-rim-soft { stroke-opacity: .60; }
613
- .edge-contact { stroke-opacity: .75; }
614
- .cloud-drop-shadow { flood-opacity: .13; }
615
- .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
616
- .food-cloud > path {
617
- stroke: #5870E8 !important;
618
- stroke-width: 1.2 !important;
619
- stroke-opacity: .9 !important;
620
- vector-effect: non-scaling-stroke !important;
621
- }
622
- }
623
- </style>
624
- <defs>
625
- ${commonDefs(p, cloudD)}
626
- <path id="${p}-cloud-shape" d="${cloudD}"/>
627
- </defs>
628
- <g class="scene">
629
- <g class="spring" transform="translate(12 12) scale(${f.springScale.toFixed(4)}) translate(-12 -12)">
630
- <g class="breath" transform="translate(12 12) scale(${f.breathScale.toFixed(4)}) translate(-12 -12)">
631
- ${cloudlingBody}
632
- ${eyes}
633
- </g>
634
- </g>
635
- </g>
636
- `;
637
- }
638
-
639
- /* =========================================================================
640
- * 主循环 + 控件 (复用 carrying-v5 模板)
641
- * ========================================================================= */
642
- let startT = performance.now();
643
- let elapsedOffset = 0;
644
- let pausedAt = 0;
645
-
646
- const scrubberEl = document.getElementById('scrubber');
647
- const tDisplayEl = document.getElementById('t-display');
648
-
649
- function getCurrentT() {
650
- if (scrubbing) return Number(scrubberEl.value);
651
- if (paused) return elapsedOffset + (pausedAt - startT) / 1000;
652
- return elapsedOffset + (performance.now() - startT) / 1000;
653
- }
654
-
655
- function loop() {
656
- const t = getCurrentT();
657
- scrubberEl.max = params.T_CYCLE.toFixed(2);
658
- const tMod = ((t % params.T_CYCLE) + params.T_CYCLE) % params.T_CYCLE;
659
- if (!scrubbing) scrubberEl.value = tMod.toFixed(3);
660
- tDisplayEl.textContent = `${tMod.toFixed(2)} s`;
661
- renderFrame(t);
662
- requestAnimationFrame(loop);
663
- }
664
- renderFrame(0);
665
- requestAnimationFrame(loop);
666
- ]]></script>
667
- </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="scrubber"></g>
5
+ <g id="t-display"></g>
6
+ </g>
7
+ <script type="application/ecmascript"><![CDATA[
8
+ /* =========================================================================
9
+ * Cloudling 主体几何 (沿用 V2 视觉定稿, 跟 carrying-v5 / idle-to-sleeping-v1 同源)
10
+ * ========================================================================= */
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
+ function lerp(a, b, t) { return a + (b - a) * t; }
35
+ function clamp01(t) { return Math.max(0, Math.min(1, t)); }
36
+ function easeIn(t) { return t * t; }
37
+ function easeOut(t) { return 1 - Math.pow(1 - t, 2); }
38
+ function easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2; }
39
+ function easeOutBack(t) { const c=1.70158; return 1 + (c+1)*Math.pow(t-1,3) + c*Math.pow(t-1,2); }
40
+ function progress(t, begin, end) { return clamp01((t - begin) / (end - begin)); }
41
+
42
+ function catmullRomClosedPath(points, tension = 1) {
43
+ const q = (n) => n.toFixed(3);
44
+ let d = `M ${q(points[0].x)} ${q(points[0].y)}`;
45
+ const n = points.length;
46
+ for (let i = 0; i < n; i++) {
47
+ const p0 = points[(i - 1 + n) % n];
48
+ const p1 = points[i];
49
+ const p2 = points[(i + 1) % n];
50
+ const p3 = points[(i + 2) % n];
51
+ const c1x = p1.x + (p2.x - p0.x) * tension / 6;
52
+ const c1y = p1.y + (p2.y - p0.y) * tension / 6;
53
+ const c2x = p2.x - (p3.x - p1.x) * tension / 6;
54
+ const c2y = p2.y - (p3.y - p1.y) * tension / 6;
55
+ d += ` C ${q(c1x)} ${q(c1y)} ${q(c2x)} ${q(c2y)} ${q(p2.x)} ${q(p2.y)}`;
56
+ }
57
+ return `${d} Z`;
58
+ }
59
+
60
+ /* =========================================================================
61
+ * 径向射线投影: 从中心 (cx,cy) 沿角度 angle 射出, 与圆角矩形边界求交.
62
+ * 这是 building 通用 morph 工具 — 后续 7 个形状只要换边界几何就能复用.
63
+ * ========================================================================= */
64
+ function rayToRoundedRect(angle, cx, cy, halfW, halfH, r) {
65
+ const dx = Math.cos(angle), dy = Math.sin(angle);
66
+ // 1. 先求射线与无圆角矩形的最近交点
67
+ let t = Infinity;
68
+ if (Math.abs(dx) > 1e-9) t = Math.min(t, halfW / Math.abs(dx));
69
+ if (Math.abs(dy) > 1e-9) t = Math.min(t, halfH / Math.abs(dy));
70
+ let px = cx + dx * t;
71
+ let py = cy + dy * t;
72
+ // 2. 检查交点是否落在圆角象限内
73
+ const lx = Math.abs(px - cx);
74
+ const ly = Math.abs(py - cy);
75
+ if (lx > halfW - r - 1e-6 && ly > halfH - r - 1e-6) {
76
+ // 落在圆角象限 → 用射线与圆求交
77
+ const sx = px >= cx ? 1 : -1;
78
+ const sy = py >= cy ? 1 : -1;
79
+ const ccx = cx + sx * (halfW - r);
80
+ const ccy = cy + sy * (halfH - r);
81
+ const ex = cx - ccx;
82
+ const ey = cy - ccy;
83
+ const a = dx*dx + dy*dy;
84
+ const b = 2*(ex*dx + ey*dy);
85
+ const c = ex*ex + ey*ey - r*r;
86
+ const disc = b*b - 4*a*c;
87
+ if (disc >= 0) {
88
+ const s = (-b + Math.sqrt(disc)) / (2*a);
89
+ px = cx + dx * s;
90
+ py = cy + dy * s;
91
+ }
92
+ }
93
+ return {x: px, y: py};
94
+ }
95
+
96
+ function makeRoundedRectTargetPoints(halfW, halfH, r) {
97
+ const cx = 12, cy = 12;
98
+ return SOFT_CLOUD_POINTS.map(cp => {
99
+ const angle = Math.atan2(cp.y - cy, cp.x - cx);
100
+ return rayToRoundedRect(angle, cx, cy, halfW, halfH, r);
101
+ });
102
+ }
103
+
104
+ /* =========================================================================
105
+ * 多形态 body path: cloud / terminal / module / monitor / envelope
106
+ * ========================================================================= */
107
+ function softenTargetPoints(targetPoints, amount) {
108
+ return SOFT_CLOUD_POINTS.map((cp, i) => ({
109
+ x: lerp(cp.x, targetPoints[i].x, amount),
110
+ y: lerp(cp.y, targetPoints[i].y, amount),
111
+ }));
112
+ }
113
+
114
+ function mixPointSets(a, b, t) {
115
+ const u = clamp01(t);
116
+ return a.map((p, i) => ({
117
+ x: lerp(p.x, b[i].x, u),
118
+ y: lerp(p.y, b[i].y, u),
119
+ }));
120
+ }
121
+
122
+ function squashPoints(points, squashY) {
123
+ if (squashY === 1) return points;
124
+ const cy = 12;
125
+ return points.map(p => ({ x: p.x, y: cy + (p.y - cy) * squashY }));
126
+ }
127
+
128
+ function makePathFromPoints(points, sharpness = 0) {
129
+ return catmullRomClosedPath(points, lerp(1.0, 0.76, clamp01(sharpness)));
130
+ }
131
+
132
+ function makeShapePointSets() {
133
+ return {
134
+ cloud: SOFT_CLOUD_POINTS,
135
+ terminal: softenTargetPoints(makeRoundedRectTargetPoints(params.terminalHalfW, params.terminalHalfH, params.terminalCornerR), params.terminalMorphAmount),
136
+ module: softenTargetPoints(makeRoundedRectTargetPoints(params.squareHalfW, params.squareHalfH, params.squareCornerR), params.moduleMorphAmount),
137
+ monitor: softenTargetPoints(makeRoundedRectTargetPoints(params.monitorHalfW, params.monitorHalfH, params.monitorCornerR), params.monitorMorphAmount),
138
+ envelope: softenTargetPoints(makeRoundedRectTargetPoints(params.envHalfW, params.envHalfH, params.envCornerR), params.envelopeMorphAmount),
139
+ };
140
+ }
141
+
142
+ /* =========================================================================
143
+ * 默认参数
144
+ * ========================================================================= */
145
+ const DEFAULTS = {
146
+ T_CYCLE: 5.4,
147
+ T_CLOUD_HOLD: 0.42,
148
+ T_TERMINAL_HOLD: 1.32,
149
+ T_MODULE_HOLD: 2.18,
150
+ T_MONITOR_HOLD: 3.04,
151
+ T_ENVELOPE_HOLD: 3.96,
152
+ T_SMILE_HOLD: 4.78,
153
+ blinkClose: 0.12,
154
+ blinkSwitch: 0.08,
155
+ blinkOpen: 0.18,
156
+
157
+ // morph
158
+ squashY: 0.86,
159
+ blinkSquashY: 0.84,
160
+ terminalMorphAmount: 0.93,
161
+ moduleMorphAmount: 0.94,
162
+ monitorMorphAmount: 0.93,
163
+ envelopeMorphAmount: 0.93,
164
+
165
+ // 终端窗口几何
166
+ terminalHalfW: 11.2,
167
+ terminalHalfH: 7.3,
168
+ terminalCornerR: 1.15,
169
+
170
+ // 终端装饰
171
+ titleBarH: 1.35, // 标题栏高度
172
+ screenInset: 1.20, // 屏幕距主体边距, 留出更厚白色边框
173
+
174
+ // 模块方块几何
175
+ squareHalfW: 9.4,
176
+ squareHalfH: 9.0,
177
+ squareCornerR: 2.35,
178
+ moduleDepth: 1.15,
179
+
180
+ // 显示器
181
+ monitorHalfW: 10.8,
182
+ monitorHalfH: 6.25,
183
+ monitorCornerR: 1.0,
184
+ monitorScreenInset: 1.0,
185
+ monitorScreenCornerR: 0.65,
186
+ standNeckW: 2.0,
187
+ standNeckH: 1.6,
188
+ standBaseW: 6.2,
189
+ standBaseH: 0.9,
190
+ standBaseR: 0.38,
191
+
192
+ // 信封
193
+ envHalfW: 11.0,
194
+ envHalfH: 6.6,
195
+ envCornerR: 1.05,
196
+ flapDepth: 3.4,
197
+ flapInset: 0.85,
198
+ flapFillOpacity: 0.08,
199
+ mStrokeW: 1.12,
200
+ mLegBoost: 1.28,
201
+
202
+ // duang spring
203
+ springAmp: 0.075,
204
+ springFreq: 4.0,
205
+ springDamping: 5.5,
206
+ springDuration: 0.65,
207
+
208
+ // 呼吸
209
+ breathLow: 0.97,
210
+ breathHigh: 1.02,
211
+
212
+ // 眼睛
213
+ eyeLeftX: 9,
214
+ eyeRightX: 15,
215
+ eyeWidth: 2.6,
216
+ eyeHeight: 5,
217
+ eyeY: 9.5,
218
+ eyeFlashStrength: 0.5, // 变身瞬间眼睛亮度增强 (0-1)
219
+
220
+ // 阴影
221
+ cloudShadowOpacity: 0.28,
222
+ };
223
+
224
+ let params = { ...DEFAULTS };
225
+ let paused = false;
226
+ let scrubbing = false;
227
+
228
+ /* =========================================================================
229
+ * defs (复用 cloudling 标准件)
230
+ * ========================================================================= */
231
+ function commonDefs(p, cloudD) {
232
+ return `
233
+ <linearGradient id="${p}-body" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
234
+ <stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="#e0f2fe"/><stop offset="1" stop-color="#bae6fd"/>
235
+ </linearGradient>
236
+ <linearGradient id="${p}-eye" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
237
+ <stop offset="0" stop-color="#B1A7FF"/><stop offset="0.5" stop-color="#7A9DFF"/><stop offset="1" stop-color="#3941FF"/>
238
+ </linearGradient>
239
+ <linearGradient id="${p}-eye-flash" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
240
+ <stop offset="0" stop-color="#E8E2FF"/><stop offset="0.5" stop-color="#B6CBFF"/><stop offset="1" stop-color="#7A8AFF"/>
241
+ </linearGradient>
242
+ <radialGradient id="${p}-hi" cx="0.45" cy="0.25" r="0.55">
243
+ <stop offset="0" stop-color="#fff" stop-opacity="0.9"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
244
+ </radialGradient>
245
+ <filter id="${p}-shadow" x="-50%" y="-50%" width="200%" height="200%">
246
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="${params.cloudShadowOpacity}"/>
247
+ </filter>
248
+ <filter id="${p}-edge-blur" x="-40%" y="-40%" width="180%" height="180%">
249
+ <feGaussianBlur stdDeviation="0.16"/>
250
+ </filter>
251
+ <filter id="${p}-eye-glow" x="-80%" y="-80%" width="260%" height="260%">
252
+ <feGaussianBlur stdDeviation="0.45" result="b"/>
253
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
254
+ </filter>
255
+ <clipPath id="${p}-clip"><path d="${cloudD}"/></clipPath>
256
+ `;
257
+ }
258
+
259
+ /* =========================================================================
260
+ * cloudling 主体 (edge-system + drop shadow + 高光) — 跟 carrying-v5 同款
261
+ * ========================================================================= */
262
+ function makeCloudlingBody(p, f) {
263
+ const terminalOpacity = f.opacities.terminal;
264
+ const moduleOpacity = f.opacities.module;
265
+ const monitorOpacity = f.opacities.monitor;
266
+ const envelopeOpacity = f.opacities.envelope;
267
+
268
+ const moduleDepth = params.moduleDepth * clamp01(moduleOpacity);
269
+ const moduleSide = moduleOpacity > 0.01 ? `
270
+ <use href="#${p}-cloud-shape"
271
+ transform="translate(${moduleDepth.toFixed(2)} ${moduleDepth.toFixed(2)})"
272
+ fill="#8EA4FF"
273
+ opacity="${(0.24 * moduleOpacity).toFixed(3)}"/>
274
+ ` : '';
275
+
276
+ const tHalfW = params.terminalHalfW;
277
+ const tHalfH = params.terminalHalfH;
278
+ const titleH = params.titleBarH;
279
+ const inset = params.screenInset;
280
+ const titleX1 = 12 - tHalfW + 0.5;
281
+ const titleX2 = 12 + tHalfW - 0.5;
282
+ const titleY1 = 12 - tHalfH + 0.5;
283
+ const titleW = titleX2 - titleX1;
284
+ const screenX1 = titleX1 + inset;
285
+ const screenX2 = titleX2 - inset;
286
+ const screenY1 = titleY1 + titleH + 0.3;
287
+ const screenY2 = 12 + tHalfH - 0.5 - 0.5;
288
+ const dotY = titleY1 + titleH * 0.5;
289
+ const dotR = Math.min(titleH * 0.28, 0.48);
290
+ const dotX1 = titleX1 + 0.75;
291
+ const dotX2 = dotX1 + 1.20;
292
+ const dotX3 = dotX2 + 1.20;
293
+ const promptSize = Math.max(2.4, Math.min(3.6, (screenY2 - screenY1) * 0.34));
294
+ const promptX = screenX1 + 0.5;
295
+ const promptY = (screenY1 + screenY2) * 0.5 + promptSize * 0.32;
296
+
297
+ const terminalDecor = terminalOpacity > 0.01 ? `
298
+ <g class="terminal-decor" opacity="${terminalOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
299
+ <rect x="${titleX1.toFixed(2)}" y="${titleY1.toFixed(2)}" width="${titleW.toFixed(2)}" height="${titleH.toFixed(2)}" rx="0.45" fill="#EEF2FF" fill-opacity="0.72"/>
300
+ <circle cx="${dotX1.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#96A9E8"/>
301
+ <circle cx="${dotX2.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#B7C3F6"/>
302
+ <circle cx="${dotX3.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#D3DAFF"/>
303
+ <rect x="${screenX1.toFixed(2)}" y="${screenY1.toFixed(2)}" width="${(screenX2-screenX1).toFixed(2)}" height="${(screenY2-screenY1).toFixed(2)}" rx="0.65" fill="#171D2D"/>
304
+ <text x="${promptX.toFixed(2)}" y="${promptY.toFixed(2)}" font-family="ui-monospace, monospace" font-size="${promptSize.toFixed(2)}" font-weight="700" fill="#7AC8FF" opacity="0.95">&gt;</text>
305
+ </g>
306
+ ` : '';
307
+
308
+ const mHalfW = params.monitorHalfW;
309
+ const mHalfH = params.monitorHalfH;
310
+ const mInset = params.monitorScreenInset;
311
+ const monX1 = 12 - mHalfW + 0.5 + mInset;
312
+ const monX2 = 12 + mHalfW - 0.5 - mInset;
313
+ const monY1 = 12 - mHalfH + 0.5 + mInset;
314
+ const monY2 = 12 + mHalfH - 0.5 - mInset;
315
+ const monBottom = 12 + mHalfH - 0.5;
316
+ const neckW = params.standNeckW;
317
+ const neckH = params.standNeckH;
318
+ const neckX = 12 - neckW * 0.5;
319
+ const neckY = monBottom - 0.05;
320
+ const baseW = params.standBaseW;
321
+ const baseH = params.standBaseH;
322
+ const baseX = 12 - baseW * 0.5;
323
+ const baseY = neckY + neckH;
324
+ const monitorStand = monitorOpacity > 0.01 ? `
325
+ <g class="stand-decor" opacity="${monitorOpacity.toFixed(3)}" filter="url(#${p}-shadow)">
326
+ <rect x="${neckX.toFixed(2)}" y="${neckY.toFixed(2)}" width="${neckW.toFixed(2)}" height="${neckH.toFixed(2)}" rx="0.18" fill="url(#${p}-body)"/>
327
+ <rect x="${baseX.toFixed(2)}" y="${baseY.toFixed(2)}" width="${baseW.toFixed(2)}" height="${baseH.toFixed(2)}" rx="${params.standBaseR}" fill="url(#${p}-body)"/>
328
+ </g>
329
+ <g class="stand-edge" opacity="${monitorOpacity.toFixed(3)}">
330
+ <rect x="${neckX.toFixed(2)}" y="${(neckY + 0.05).toFixed(2)}" width="${neckW.toFixed(2)}" height="${(neckH - 0.05).toFixed(2)}" rx="0.18" fill="none" stroke="#aab3c6" stroke-width="0.34" stroke-opacity="0.55"/>
331
+ <rect x="${baseX.toFixed(2)}" y="${baseY.toFixed(2)}" width="${baseW.toFixed(2)}" height="${baseH.toFixed(2)}" rx="${params.standBaseR}" fill="none" stroke="#aab3c6" stroke-width="0.34" stroke-opacity="0.55"/>
332
+ </g>
333
+ ` : '';
334
+ const monitorScreen = monitorOpacity > 0.01 ? `
335
+ <g class="screen-decor" opacity="${monitorOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
336
+ <rect x="${monX1.toFixed(2)}" y="${monY1.toFixed(2)}" width="${(monX2-monX1).toFixed(2)}" height="${(monY2-monY1).toFixed(2)}" rx="${params.monitorScreenCornerR}" fill="#171D2D"/>
337
+ <rect x="${(monX1 + 0.20).toFixed(2)}" y="${(monY1 + 0.20).toFixed(2)}" width="${(monX2-monX1 - 0.40).toFixed(2)}" height="${(monY2-monY1 - 0.40).toFixed(2)}" rx="${Math.max(0.2, params.monitorScreenCornerR - 0.16).toFixed(2)}" fill="none" stroke="#7A9DFF" stroke-width="0.18" stroke-opacity="0.34"/>
338
+ </g>
339
+ ` : '';
340
+
341
+ const eHalfW = params.envHalfW;
342
+ const eHalfH = params.envHalfH;
343
+ const eInset = params.flapInset;
344
+ const flapX1 = 12 - eHalfW + 0.5 + eInset;
345
+ const flapX2 = 12 + eHalfW - 0.5 - eInset;
346
+ const flapY = 12 - eHalfH + 0.5 + eInset;
347
+ const flapBottomY = flapY + params.flapDepth;
348
+ const mBottomY = 12 + eHalfH - 1.18;
349
+ const mStrokeW = params.mStrokeW;
350
+ const mLegStrokeW = mStrokeW * params.mLegBoost;
351
+ const envelopeDecor = envelopeOpacity > 0.01 ? `
352
+ <g class="envelope-decor" opacity="${envelopeOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
353
+ <path d="M ${flapX1.toFixed(2)} ${flapY.toFixed(2)} L 12 ${flapBottomY.toFixed(2)} L ${flapX2.toFixed(2)} ${flapY.toFixed(2)} Z"
354
+ fill="#B1A7FF" fill-opacity="${params.flapFillOpacity.toFixed(3)}"/>
355
+ <path d="M ${flapX1.toFixed(2)} ${mBottomY.toFixed(2)} L ${flapX1.toFixed(2)} ${flapY.toFixed(2)}"
356
+ fill="none" stroke="#6EA8FF" stroke-width="${mLegStrokeW.toFixed(2)}" stroke-linecap="round"/>
357
+ <path d="M ${flapX1.toFixed(2)} ${flapY.toFixed(2)} L 12 ${flapBottomY.toFixed(2)}"
358
+ fill="none" stroke="#FF6B7A" stroke-width="${mStrokeW.toFixed(2)}" stroke-linecap="round"/>
359
+ <path d="M 12 ${flapBottomY.toFixed(2)} L ${flapX2.toFixed(2)} ${flapY.toFixed(2)}"
360
+ fill="none" stroke="#FFD45C" stroke-width="${mStrokeW.toFixed(2)}" stroke-linecap="round"/>
361
+ <path d="M ${flapX2.toFixed(2)} ${flapY.toFixed(2)} L ${flapX2.toFixed(2)} ${mBottomY.toFixed(2)}"
362
+ fill="none" stroke="#55D08A" stroke-width="${mLegStrokeW.toFixed(2)}" stroke-linecap="round"/>
363
+ </g>
364
+ ` : '';
365
+
366
+ return `
367
+ <g class="cloudling-body">
368
+ ${moduleSide}
369
+ ${monitorStand}
370
+ <g class="edge-system">
371
+ <use class="edge-contact" href="#${p}-cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#${p}-edge-blur)"/>
372
+ <use class="edge-rim-soft" href="#${p}-cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#${p}-edge-blur)"/>
373
+ <use class="edge-rim-tight" href="#${p}-cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
374
+ </g>
375
+ <g filter="url(#${p}-shadow)">
376
+ <use href="#${p}-cloud-shape" fill="url(#${p}-body)"/>
377
+ <rect x="1" y="2" width="22" height="11" fill="url(#${p}-hi)" clip-path="url(#${p}-clip)"/>
378
+ </g>
379
+ ${terminalDecor}
380
+ ${monitorScreen}
381
+ ${envelopeDecor}
382
+ </g>
383
+ `;
384
+ }
385
+
386
+ /* =========================================================================
387
+ * 眼睛 (capsule + 闭眼线 + smile, 锚点固定 9,12 / 15,12)
388
+ * ========================================================================= */
389
+ function makeEyes(p, f) {
390
+ const mode = f.eyeMode;
391
+ const openness = f.eyeOpenness;
392
+ const flashStrength = f.eyeFlash;
393
+ const lcx = f.eyeLeftX ?? params.eyeLeftX ?? 9;
394
+ const rcx = f.eyeRightX ?? params.eyeRightX ?? 15;
395
+ const useFlash = flashStrength > 0.05;
396
+ const eyeFill = useFlash ? `url(#${p}-eye-flash)` : `url(#${p}-eye)`;
397
+ const filter = useFlash ? `filter="url(#${p}-eye-glow)"` : '';
398
+
399
+ if (mode === 'closed') {
400
+ const eyeW = f.eyeWidth ?? params.eyeWidth ?? 2.6;
401
+ const cy = (f.eyeY ?? params.eyeY ?? 9.5) + ((f.eyeHeight ?? params.eyeHeight ?? 5) * 0.5);
402
+ const linePath = (cx) => `M ${(cx-eyeW*0.48).toFixed(2)} ${cy.toFixed(2)} Q ${cx.toFixed(2)} ${(cy+0.18).toFixed(2)} ${(cx+eyeW*0.48).toFixed(2)} ${cy.toFixed(2)}`;
403
+ return `
404
+ <g class="eyes" ${filter}>
405
+ <path d="${linePath(lcx)}" fill="none" stroke="${eyeFill}" stroke-width="0.62" stroke-linecap="round"/>
406
+ <path d="${linePath(rcx)}" fill="none" stroke="${eyeFill}" stroke-width="0.62" stroke-linecap="round"/>
407
+ </g>
408
+ `;
409
+ }
410
+
411
+ if (mode === 'smile') {
412
+ const w = 3.5, h = 1.0, smileCY = 12.7;
413
+ const smilePath = (cx) => `M ${(cx-w/2).toFixed(2)} ${smileCY} Q ${cx} ${(smileCY-2*h).toFixed(2)} ${(cx+w/2).toFixed(2)} ${smileCY}`;
414
+ return `
415
+ <g class="eyes">
416
+ <path d="${smilePath(lcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>
417
+ <path d="${smilePath(rcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>
418
+ </g>
419
+ `;
420
+ }
421
+
422
+ const eyeW = f.eyeWidth ?? params.eyeWidth ?? 2.6;
423
+ const baseH = f.eyeHeight ?? params.eyeHeight ?? 5;
424
+ const h = Math.max(0.3, baseH * openness);
425
+ const y = (f.eyeY ?? params.eyeY ?? 9.5) + (baseH - h) / 2;
426
+ const rx = Math.min(1.3, h / 2);
427
+ const leftX = lcx - eyeW / 2;
428
+ const rightX = rcx - eyeW / 2;
429
+ return `
430
+ <g class="eyes" ${filter}>
431
+ <rect x="${leftX.toFixed(2)}" y="${y.toFixed(2)}" width="${eyeW.toFixed(2)}" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="${eyeFill}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>
432
+ <rect x="${rightX.toFixed(2)}" y="${y.toFixed(2)}" width="${eyeW.toFixed(2)}" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="${eyeFill}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>
433
+ </g>
434
+ `;
435
+ }
436
+
437
+ /* =========================================================================
438
+ * duang duang spring overshoot (跟 carrying-v5 同款)
439
+ * ========================================================================= */
440
+ function springOvershoot(tLocal, amp, freq, damping) {
441
+ if (tLocal < 0) return 0;
442
+ return amp * Math.cos(2 * Math.PI * freq * tLocal) * Math.exp(-damping * tLocal);
443
+ }
444
+
445
+ /* =========================================================================
446
+ * 时间线计算
447
+ * ========================================================================= */
448
+ const EYE_STYLES = {
449
+ cloud: { left: 9.00, right: 15.00, width: 2.60, height: 5.00, y: 9.50 },
450
+ terminal: { left: 9.35, right: 14.85, width: 2.35, height: 4.20, y: 10.15 },
451
+ module: { left: 8.85, right: 15.15, width: 2.65, height: 4.85, y: 9.65 },
452
+ monitor: { left: 8.85, right: 15.15, width: 2.25, height: 4.05, y: 10.25 },
453
+ envelope: { left: 8.75, right: 15.25, width: 2.40, height: 4.20, y: 10.20 },
454
+ };
455
+
456
+ function mixEyeStyle(from, to, u) {
457
+ const a = EYE_STYLES[from];
458
+ const b = EYE_STYLES[to];
459
+ return {
460
+ left: lerp(a.left, b.left, u),
461
+ right: lerp(a.right, b.right, u),
462
+ width: lerp(a.width, b.width, u),
463
+ height: lerp(a.height, b.height, u),
464
+ y: lerp(a.y, b.y, u),
465
+ };
466
+ }
467
+
468
+ function setShape(f, from, to, uRaw) {
469
+ const u = clamp01(uRaw);
470
+ f.fromShape = from;
471
+ f.toShape = to;
472
+ f.shapeU = u;
473
+ f.opacities = { terminal: 0, module: 0, monitor: 0, envelope: 0 };
474
+ if (from !== 'cloud') f.opacities[from] += 1 - u;
475
+ if (to !== 'cloud') f.opacities[to] += u;
476
+
477
+ const eye = mixEyeStyle(from, to, u);
478
+ f.eyeLeftX = eye.left;
479
+ f.eyeRightX = eye.right;
480
+ f.eyeWidth = eye.width;
481
+ f.eyeHeight = eye.height;
482
+ f.eyeY = eye.y;
483
+ }
484
+
485
+ function holdShape(f, shape, eyeMode = 'capsule') {
486
+ setShape(f, shape, shape, 0);
487
+ f.eyeMode = eyeMode;
488
+ }
489
+
490
+ function blinkSwitchFrame(f, tLocal, from, to, options = {}) {
491
+ const closeDur = Math.max(0.04, params.blinkClose);
492
+ const switchDur = Math.max(0.04, params.blinkSwitch);
493
+ const openDur = Math.max(0.06, params.blinkOpen);
494
+ const total = closeDur + switchDur + openDur;
495
+ const targetEyeMode = options.smile ? 'smile' : 'capsule';
496
+
497
+ if (tLocal < closeDur) {
498
+ const u = easeIn(progress(tLocal, 0, closeDur));
499
+ setShape(f, from, from, 0);
500
+ f.eyeMode = 'closed';
501
+ f.eyeOpenness = lerp(1, 0.08, u);
502
+ f.squashY = lerp(1, params.blinkSquashY, u);
503
+ f.eyeFlash = u * params.eyeFlashStrength * 0.55;
504
+ } else if (tLocal < closeDur + switchDur) {
505
+ const u = easeInOut(progress(tLocal, closeDur, closeDur + switchDur));
506
+ setShape(f, from, to, u);
507
+ f.eyeMode = 'closed';
508
+ f.eyeOpenness = 0.08;
509
+ f.squashY = params.blinkSquashY;
510
+ f.eyeFlash = params.eyeFlashStrength * 0.35;
511
+ } else if (tLocal < total) {
512
+ const u = easeOutBack(progress(tLocal, closeDur + switchDur, total));
513
+ setShape(f, to, to, 0);
514
+ f.eyeOpenness = lerp(0.08, 1, clamp01(u));
515
+ f.squashY = lerp(params.blinkSquashY, 1, clamp01(u));
516
+ f.eyeFlash = lerp(params.eyeFlashStrength * 0.35, 0, clamp01(u));
517
+ f.springScale = 1 + Math.sin(Math.PI * clamp01(u)) * 0.018;
518
+ if (options.smile && u > 0.28) f.eyeMode = 'smile';
519
+ } else {
520
+ holdShape(f, to, targetEyeMode);
521
+ }
522
+ }
523
+
524
+ function computeFrame(t) {
525
+ const T = params.T_CYCLE;
526
+ const tMod = ((t % T) + T) % T;
527
+ const f = {
528
+ tMod,
529
+ fromShape: 'cloud',
530
+ toShape: 'cloud',
531
+ shapeU: 0,
532
+ squashY: 1,
533
+ springScale: 1,
534
+ eyeMode: 'capsule',
535
+ eyeOpenness: 1,
536
+ eyeFlash: 0,
537
+ opacities: { terminal: 0, module: 0, monitor: 0, envelope: 0 },
538
+ };
539
+
540
+ const blinkTotal = params.blinkClose + params.blinkSwitch + params.blinkOpen;
541
+ const b1 = params.T_CLOUD_HOLD;
542
+ const hTerminal = b1 + blinkTotal;
543
+ const b2 = params.T_TERMINAL_HOLD;
544
+ const hModule = b2 + blinkTotal;
545
+ const b3 = params.T_MODULE_HOLD;
546
+ const hMonitor = b3 + blinkTotal;
547
+ const b4 = params.T_MONITOR_HOLD;
548
+ const hEnvelope = b4 + blinkTotal;
549
+ const b5 = params.T_ENVELOPE_HOLD;
550
+ const hSmile = b5 + blinkTotal;
551
+ const b6 = Math.max(params.T_SMILE_HOLD, hSmile + 0.05);
552
+ const hCloud = b6 + blinkTotal;
553
+
554
+ if (tMod < b1) {
555
+ holdShape(f, 'cloud');
556
+ } else if (tMod < hTerminal) {
557
+ blinkSwitchFrame(f, tMod - b1, 'cloud', 'terminal');
558
+ } else if (tMod < b2) {
559
+ holdShape(f, 'terminal');
560
+ } else if (tMod < hModule) {
561
+ blinkSwitchFrame(f, tMod - b2, 'terminal', 'module');
562
+ } else if (tMod < b3) {
563
+ holdShape(f, 'module');
564
+ } else if (tMod < hMonitor) {
565
+ blinkSwitchFrame(f, tMod - b3, 'module', 'monitor');
566
+ } else if (tMod < b4) {
567
+ holdShape(f, 'monitor');
568
+ } else if (tMod < hEnvelope) {
569
+ blinkSwitchFrame(f, tMod - b4, 'monitor', 'envelope');
570
+ } else if (tMod < b5) {
571
+ holdShape(f, 'envelope');
572
+ } else if (tMod < hSmile) {
573
+ blinkSwitchFrame(f, tMod - b5, 'envelope', 'cloud', { smile: true });
574
+ } else if (tMod < b6) {
575
+ holdShape(f, 'cloud', 'smile');
576
+ } else if (tMod < hCloud) {
577
+ blinkSwitchFrame(f, tMod - b6, 'cloud', 'cloud');
578
+ } else {
579
+ holdShape(f, 'cloud');
580
+ }
581
+
582
+ const breathPhase = (Math.cos(2 * Math.PI * tMod / params.T_CYCLE) + 1) / 2;
583
+ f.breathScale = lerp(params.breathLow, params.breathHigh, breathPhase);
584
+ return f;
585
+ }
586
+
587
+ /* =========================================================================
588
+ * 渲染
589
+ * ========================================================================= */
590
+ const stage = document.getElementById('stage');
591
+
592
+ function renderFrame(t) {
593
+ const f = computeFrame(t);
594
+ const p = 'bld';
595
+
596
+ const pointSets = makeShapePointSets();
597
+ let bodyPoints = mixPointSets(pointSets[f.fromShape], pointSets[f.toShape], f.shapeU);
598
+ bodyPoints = squashPoints(bodyPoints, f.squashY);
599
+ const nonCloudness = 1 - (f.fromShape === 'cloud' && f.toShape === 'cloud' ? 1 : 0);
600
+ const sharpness = Math.max(f.opacities.terminal, f.opacities.module, f.opacities.monitor, f.opacities.envelope, nonCloudness * f.shapeU);
601
+ const cloudD = makePathFromPoints(bodyPoints, sharpness);
602
+ const cloudlingBody = makeCloudlingBody(p, f);
603
+ const eyes = makeEyes(p, f);
604
+
605
+ stage.innerHTML = `
606
+ <style>
607
+ .edge-rim-tight { stroke-opacity: .65; }
608
+ .edge-rim-soft { stroke-opacity: .50; }
609
+ .edge-contact { stroke-opacity: .65; }
610
+ @media (prefers-color-scheme: light) {
611
+ .edge-rim-tight { stroke-opacity: .75; }
612
+ .edge-rim-soft { stroke-opacity: .60; }
613
+ .edge-contact { stroke-opacity: .75; }
614
+ .cloud-drop-shadow { flood-opacity: .13; }
615
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
616
+ .food-cloud > path {
617
+ stroke: #5870E8 !important;
618
+ stroke-width: 1.2 !important;
619
+ stroke-opacity: .9 !important;
620
+ vector-effect: non-scaling-stroke !important;
621
+ }
622
+ }
623
+ </style>
624
+ <defs>
625
+ ${commonDefs(p, cloudD)}
626
+ <path id="${p}-cloud-shape" d="${cloudD}"/>
627
+ </defs>
628
+ <g class="scene">
629
+ <g class="spring" transform="translate(12 12) scale(${f.springScale.toFixed(4)}) translate(-12 -12)">
630
+ <g class="breath" transform="translate(12 12) scale(${f.breathScale.toFixed(4)}) translate(-12 -12)">
631
+ ${cloudlingBody}
632
+ ${eyes}
633
+ </g>
634
+ </g>
635
+ </g>
636
+ `;
637
+ }
638
+
639
+ /* =========================================================================
640
+ * 主循环 + 控件 (复用 carrying-v5 模板)
641
+ * ========================================================================= */
642
+ let startT = performance.now();
643
+ let elapsedOffset = 0;
644
+ let pausedAt = 0;
645
+
646
+ const scrubberEl = document.getElementById('scrubber');
647
+ const tDisplayEl = document.getElementById('t-display');
648
+
649
+ function getCurrentT() {
650
+ if (scrubbing) return Number(scrubberEl.value);
651
+ if (paused) return elapsedOffset + (pausedAt - startT) / 1000;
652
+ return elapsedOffset + (performance.now() - startT) / 1000;
653
+ }
654
+
655
+ function loop() {
656
+ const t = getCurrentT();
657
+ scrubberEl.max = params.T_CYCLE.toFixed(2);
658
+ const tMod = ((t % params.T_CYCLE) + params.T_CYCLE) % params.T_CYCLE;
659
+ if (!scrubbing) scrubberEl.value = tMod.toFixed(3);
660
+ tDisplayEl.textContent = `${tMod.toFixed(2)} s`;
661
+ renderFrame(t);
662
+ requestAnimationFrame(loop);
663
+ }
664
+ renderFrame(0);
665
+ requestAnimationFrame(loop);
666
+ ]]></script>
667
+ </svg>