@mumulinya167/ccm-pet-assets 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +4 -0
  2. package/assets/calico/calico-collapsing.apng +0 -0
  3. package/assets/calico/calico-dozing.apng +0 -0
  4. package/assets/calico/calico-error.apng +0 -0
  5. package/assets/calico/calico-happy.apng +0 -0
  6. package/assets/calico/calico-idle-follow.svg +324 -0
  7. package/assets/calico/calico-idle.apng +0 -0
  8. package/assets/calico/calico-mini-alert.apng +0 -0
  9. package/assets/calico/calico-mini-crabwalk.apng +0 -0
  10. package/assets/calico/calico-mini-enter.apng +0 -0
  11. package/assets/calico/calico-mini-happy.apng +0 -0
  12. package/assets/calico/calico-mini-idle.apng +0 -0
  13. package/assets/calico/calico-mini-peek.apng +0 -0
  14. package/assets/calico/calico-mini-sleep.apng +0 -0
  15. package/assets/calico/calico-notification.apng +0 -0
  16. package/assets/calico/calico-react-drag.apng +0 -0
  17. package/assets/calico/calico-react-left.apng +0 -0
  18. package/assets/calico/calico-react-poke.apng +0 -0
  19. package/assets/calico/calico-sleeping.apng +0 -0
  20. package/assets/calico/calico-thinking.apng +0 -0
  21. package/assets/calico/calico-waking.apng +0 -0
  22. package/assets/calico/calico-working-building.apng +0 -0
  23. package/assets/calico/calico-working-carrying.apng +0 -0
  24. package/assets/calico/calico-working-conducting.apng +0 -0
  25. package/assets/calico/calico-working-juggling.apng +0 -0
  26. package/assets/calico/calico-working-sweeping.apng +0 -0
  27. package/assets/calico/calico-working-typing.apng +0 -0
  28. package/assets/calico/calico-yawning.apng +0 -0
  29. package/assets/calico.svg +324 -0
  30. package/assets/clawd/LICENSE +22 -0
  31. package/assets/clawd/clawd-about-hero.svg +202 -0
  32. package/assets/clawd/clawd-collapse-sleep.svg +247 -0
  33. package/assets/clawd/clawd-error.svg +94 -0
  34. package/assets/clawd/clawd-happy.svg +161 -0
  35. package/assets/clawd/clawd-headphones-groove.svg +194 -0
  36. package/assets/clawd/clawd-idle-bubble.svg +180 -0
  37. package/assets/clawd/clawd-idle-collapse.svg +101 -0
  38. package/assets/clawd/clawd-idle-doze.svg +72 -0
  39. package/assets/clawd/clawd-idle-follow.svg +64 -0
  40. package/assets/clawd/clawd-idle-living.svg +196 -0
  41. package/assets/clawd/clawd-idle-look.svg +115 -0
  42. package/assets/clawd/clawd-idle-reading.svg +252 -0
  43. package/assets/clawd/clawd-idle-yawn.svg +158 -0
  44. package/assets/clawd/clawd-mini-alert.svg +129 -0
  45. package/assets/clawd/clawd-mini-crabwalk.svg +76 -0
  46. package/assets/clawd/clawd-mini-enter-sleep.svg +64 -0
  47. package/assets/clawd/clawd-mini-enter.svg +114 -0
  48. package/assets/clawd/clawd-mini-happy.svg +124 -0
  49. package/assets/clawd/clawd-mini-idle.svg +83 -0
  50. package/assets/clawd/clawd-mini-peek.svg +82 -0
  51. package/assets/clawd/clawd-mini-sleep.svg +112 -0
  52. package/assets/clawd/clawd-mini-typing.svg +153 -0
  53. package/assets/clawd/clawd-notification.svg +438 -0
  54. package/assets/clawd/clawd-react-annoyed.svg +167 -0
  55. package/assets/clawd/clawd-react-double-jump.svg +229 -0
  56. package/assets/clawd/clawd-react-double.svg +108 -0
  57. package/assets/clawd/clawd-react-drag.svg +102 -0
  58. package/assets/clawd/clawd-react-left.svg +102 -0
  59. package/assets/clawd/clawd-react-right.svg +102 -0
  60. package/assets/clawd/clawd-sleeping.svg +118 -0
  61. package/assets/clawd/clawd-static-base.svg +21 -0
  62. package/assets/clawd/clawd-wake.svg +277 -0
  63. package/assets/clawd/clawd-working-building.svg +329 -0
  64. package/assets/clawd/clawd-working-carrying.svg +178 -0
  65. package/assets/clawd/clawd-working-debugger.svg +245 -0
  66. package/assets/clawd/clawd-working-juggling.svg +183 -0
  67. package/assets/clawd/clawd-working-sweeping.svg +248 -0
  68. package/assets/clawd/clawd-working-thinking.svg +501 -0
  69. package/assets/clawd/clawd-working-typing-boss.svg +307 -0
  70. package/assets/clawd/clawd-working-typing.svg +273 -0
  71. package/assets/clawd/clawd-working-ultrathink.svg +166 -0
  72. package/assets/clawd/clawd-working-wizard.svg +356 -0
  73. package/assets/clawd.svg +64 -0
  74. package/assets/cloudling/cloudling/cloudling-attention.svg +380 -0
  75. package/assets/cloudling/cloudling/cloudling-building.svg +667 -0
  76. package/assets/cloudling/cloudling/cloudling-carrying.svg +604 -0
  77. package/assets/cloudling/cloudling/cloudling-conducting.svg +616 -0
  78. package/assets/cloudling/cloudling/cloudling-dozing-to-sleeping.svg +578 -0
  79. package/assets/cloudling/cloudling/cloudling-dozing.svg +358 -0
  80. package/assets/cloudling/cloudling/cloudling-error.svg +428 -0
  81. package/assets/cloudling/cloudling/cloudling-idle-reading.svg +658 -0
  82. package/assets/cloudling/cloudling/cloudling-idle-to-dozing.svg +568 -0
  83. package/assets/cloudling/cloudling/cloudling-idle-to-sleeping.svg +811 -0
  84. package/assets/cloudling/cloudling/cloudling-idle.svg +255 -0
  85. package/assets/cloudling/cloudling/cloudling-juggling.svg +648 -0
  86. package/assets/cloudling/cloudling/cloudling-mini-alert.svg +390 -0
  87. package/assets/cloudling/cloudling/cloudling-mini-crabwalk.svg +335 -0
  88. package/assets/cloudling/cloudling/cloudling-mini-enter-roll-in.svg +226 -0
  89. package/assets/cloudling/cloudling/cloudling-mini-enter-sleep.svg +232 -0
  90. package/assets/cloudling/cloudling/cloudling-mini-happy.svg +371 -0
  91. package/assets/cloudling/cloudling/cloudling-mini-idle.svg +279 -0
  92. package/assets/cloudling/cloudling/cloudling-mini-peek.svg +328 -0
  93. package/assets/cloudling/cloudling/cloudling-mini-sleep.svg +260 -0
  94. package/assets/cloudling/cloudling/cloudling-mini-typing.svg +692 -0
  95. package/assets/cloudling/cloudling/cloudling-notification.svg +494 -0
  96. package/assets/cloudling/cloudling/cloudling-react-drag.svg +83 -0
  97. package/assets/cloudling/cloudling/cloudling-sleeping-to-idle.svg +555 -0
  98. package/assets/cloudling/cloudling/cloudling-sleeping.svg +500 -0
  99. package/assets/cloudling/cloudling/cloudling-sweeping.svg +292 -0
  100. package/assets/cloudling/cloudling/cloudling-thinking.svg +720 -0
  101. package/assets/cloudling/cloudling/cloudling-typing.svg +354 -0
  102. package/assets/cloudling/cloudling-attention.svg +380 -0
  103. package/assets/cloudling/cloudling-building.svg +667 -0
  104. package/assets/cloudling/cloudling-carrying.svg +604 -0
  105. package/assets/cloudling/cloudling-conducting.svg +616 -0
  106. package/assets/cloudling/cloudling-dozing-to-sleeping.svg +578 -0
  107. package/assets/cloudling/cloudling-dozing.svg +358 -0
  108. package/assets/cloudling/cloudling-error.svg +428 -0
  109. package/assets/cloudling/cloudling-idle-reading.svg +658 -0
  110. package/assets/cloudling/cloudling-idle-to-dozing.svg +568 -0
  111. package/assets/cloudling/cloudling-idle-to-sleeping.svg +811 -0
  112. package/assets/cloudling/cloudling-idle.svg +255 -0
  113. package/assets/cloudling/cloudling-juggling.svg +648 -0
  114. package/assets/cloudling/cloudling-mini-alert.svg +390 -0
  115. package/assets/cloudling/cloudling-mini-crabwalk.svg +335 -0
  116. package/assets/cloudling/cloudling-mini-enter-roll-in.svg +226 -0
  117. package/assets/cloudling/cloudling-mini-enter-sleep.svg +232 -0
  118. package/assets/cloudling/cloudling-mini-happy.svg +371 -0
  119. package/assets/cloudling/cloudling-mini-idle.svg +279 -0
  120. package/assets/cloudling/cloudling-mini-peek.svg +328 -0
  121. package/assets/cloudling/cloudling-mini-sleep.svg +260 -0
  122. package/assets/cloudling/cloudling-mini-typing.svg +692 -0
  123. package/assets/cloudling/cloudling-notification.svg +494 -0
  124. package/assets/cloudling/cloudling-react-drag.svg +83 -0
  125. package/assets/cloudling/cloudling-sleeping-to-idle.svg +555 -0
  126. package/assets/cloudling/cloudling-sleeping.svg +500 -0
  127. package/assets/cloudling/cloudling-sweeping.svg +292 -0
  128. package/assets/cloudling/cloudling-thinking.svg +720 -0
  129. package/assets/cloudling/cloudling-typing.svg +354 -0
  130. package/assets/cloudling.svg +255 -0
  131. package/assets/ghost-attention.svg +19 -0
  132. package/assets/ghost-carrying.svg +19 -0
  133. package/assets/ghost-collapsing.svg +19 -0
  134. package/assets/ghost-dozing.svg +23 -0
  135. package/assets/ghost-error.svg +19 -0
  136. package/assets/ghost-happy.svg +23 -0
  137. package/assets/ghost-idle-action1.svg +19 -0
  138. package/assets/ghost-idle-action2.svg +19 -0
  139. package/assets/ghost-idle-action3.svg +19 -0
  140. package/assets/ghost-idle.svg +11 -0
  141. package/assets/ghost-juggling.svg +19 -0
  142. package/assets/ghost-notification.svg +19 -0
  143. package/assets/ghost-react-double.svg +23 -0
  144. package/assets/ghost-react-drag.svg +23 -0
  145. package/assets/ghost-react-left.svg +19 -0
  146. package/assets/ghost-react-right.svg +19 -0
  147. package/assets/ghost-sleeping.svg +19 -0
  148. package/assets/ghost-sweeping.svg +19 -0
  149. package/assets/ghost-thinking.svg +19 -0
  150. package/assets/ghost-waking.svg +19 -0
  151. package/assets/ghost-working.svg +19 -0
  152. package/assets/ghost-yawning.svg +23 -0
  153. package/assets/ghost.svg +11 -0
  154. package/assets/robot-attention.svg +19 -0
  155. package/assets/robot-carrying.svg +19 -0
  156. package/assets/robot-collapsing.svg +19 -0
  157. package/assets/robot-dozing.svg +23 -0
  158. package/assets/robot-error.svg +19 -0
  159. package/assets/robot-happy.svg +23 -0
  160. package/assets/robot-idle-action1.svg +19 -0
  161. package/assets/robot-idle-action2.svg +19 -0
  162. package/assets/robot-idle-action3.svg +19 -0
  163. package/assets/robot-idle.svg +11 -0
  164. package/assets/robot-juggling.svg +19 -0
  165. package/assets/robot-notification.svg +19 -0
  166. package/assets/robot-react-double.svg +23 -0
  167. package/assets/robot-react-drag.svg +23 -0
  168. package/assets/robot-react-left.svg +19 -0
  169. package/assets/robot-react-right.svg +19 -0
  170. package/assets/robot-sleeping.svg +19 -0
  171. package/assets/robot-sweeping.svg +19 -0
  172. package/assets/robot-thinking.svg +19 -0
  173. package/assets/robot-waking.svg +19 -0
  174. package/assets/robot-working.svg +19 -0
  175. package/assets/robot-yawning.svg +23 -0
  176. package/assets/robot.svg +11 -0
  177. package/manifest.json +889 -0
  178. package/package.json +18 -0
@@ -0,0 +1,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>