@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,811 @@
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
+ 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";
9
+
10
+ const SOFT_CLOUD_POINTS = [
11
+ {x:9.064,y:3.344},{x:9.825,y:3.106},{x:10.616,y:3.004},{x:11.413,y:3.040},
12
+ {x:12.192,y:3.210},{x:12.928,y:3.518},{x:13.599,y:3.948},{x:14.255,y:4.291},
13
+ {x:15.046,y:4.188},{x:15.843,y:4.224},{x:16.621,y:4.398},{x:17.357,y:4.706},
14
+ {x:18.029,y:5.134},{x:18.617,y:5.673},{x:19.103,y:6.306},{x:19.471,y:7.013},
15
+ {x:19.715,y:7.772},{x:19.812,y:8.564},{x:19.784,y:9.361},{x:19.797,y:10.105},
16
+ {x:20.287,y:10.735},{x:20.656,y:11.442},{x:20.897,y:12.202},{x:20.995,y:12.994},
17
+ {x:20.964,y:13.791},{x:20.791,y:14.569},{x:20.480,y:15.304},{x:20.052,y:15.977},
18
+ {x:19.516,y:16.567},{x:18.882,y:17.052},{x:18.174,y:17.418},{x:17.539,y:17.808},
19
+ {x:17.231,y:18.545},{x:16.805,y:19.218},{x:16.264,y:19.805},{x:15.633,y:20.292},
20
+ {x:14.925,y:20.660},{x:14.163,y:20.895},{x:13.372,y:20.996},{x:12.575,y:20.964},
21
+ {x:11.796,y:20.793},{x:11.061,y:20.483},{x:10.392,y:20.049},{x:9.736,y:19.714},
22
+ {x:8.946,y:19.811},{x:8.148,y:19.777},{x:7.370,y:19.606},{x:6.634,y:19.299},
23
+ {x:5.962,y:18.868},{x:5.377,y:18.326},{x:4.885,y:17.698},{x:4.522,y:16.989},
24
+ {x:4.283,y:16.228},{x:4.182,y:15.437},{x:4.220,y:14.640},{x:4.179,y:13.907},
25
+ {x:3.696,y:13.273},{x:3.332,y:12.563},{x:3.092,y:11.803},{x:3.003,y:11.010},
26
+ {x:3.038,y:10.213},{x:3.202,y:9.433},{x:3.514,y:8.699},{x:3.944,y:8.027},
27
+ {x:4.484,y:7.440},{x:5.114,y:6.951},{x:5.822,y:6.586},{x:6.445,y:6.191},
28
+ {x:6.752,y:5.455},{x:7.185,y:4.785},{x:7.723,y:4.197},{x:8.356,y:3.711},
29
+ ];
30
+
31
+ function catmullRomClosedPath(points, tension = 1) {
32
+ const q = (n) => n.toFixed(3);
33
+ let d = `M ${q(points[0].x)} ${q(points[0].y)}`;
34
+ const n = points.length;
35
+ for (let i = 0; i < n; i++) {
36
+ const p0 = points[(i - 1 + n) % n];
37
+ const p1 = points[i];
38
+ const p2 = points[(i + 1) % n];
39
+ const p3 = points[(i + 2) % n];
40
+ const c1x = p1.x + (p2.x - p0.x) * tension / 6;
41
+ const c1y = p1.y + (p2.y - p0.y) * tension / 6;
42
+ const c2x = p2.x - (p3.x - p1.x) * tension / 6;
43
+ const c2y = p2.y - (p3.y - p1.y) * tension / 6;
44
+ d += ` C ${q(c1x)} ${q(c1y)} ${q(c2x)} ${q(c2y)} ${q(p2.x)} ${q(p2.y)}`;
45
+ }
46
+ return `${d} Z`;
47
+ }
48
+
49
+ function makeSoftCloudPath(pressure) {
50
+ const a = clamp01(pressure);
51
+ if (a <= 0.0001) return CLOUD_D;
52
+ const volume = Math.sin(a * Math.PI) * 0.18;
53
+ const points = SOFT_CLOUD_POINTS.map((pt) => {
54
+ const distX = Math.abs(pt.x - 12);
55
+ const topWeight = Math.pow(clamp01((7.2 - pt.y) / 4.2), 1.15);
56
+ const contactWeight = clamp01(1 - distX / 8.6);
57
+ const shoulderWeight = clamp01((distX - 3.2) / 6.2) * clamp01((16.5 - pt.y) / 12.5);
58
+ const lowerWeight = clamp01((pt.y - 16.2) / 4.8);
59
+ const targetTopY = 4.58 + Math.pow(distX / 8.6, 2) * 0.34;
60
+ const topDown = Math.max(0, targetTopY - pt.y) * topWeight * (0.35 + contactWeight * 0.65) * a;
61
+ const sideOut = Math.sign(pt.x - 12 || 1) * (topWeight * 0.95 + shoulderWeight * 0.48) * a;
62
+ const lowerSettle = lowerWeight * volume * 0.18;
63
+ return {
64
+ x: pt.x + sideOut,
65
+ y: pt.y + topDown + lowerSettle
66
+ };
67
+ });
68
+ return catmullRomClosedPath(points, lerp(0.96, 0.72, a));
69
+ }
70
+
71
+ const DEFAULTS = {
72
+ // 时间线 (秒)
73
+ T_LAMP_APPEAR: 0.55,
74
+ T_NOTICE_BEGIN: 0.35, // notice 跟 lamp_appear 重叠 0.2s
75
+ T_NOTICE_END: 0.95,
76
+ T_CROUCH: 0.35,
77
+ T_JUMP_UP: 0.34,
78
+ T_CONTACT_LEAD: 0.28, // 压扁起点略早于视觉接触, 让 1.39s 已经有形变
79
+ T_IMPACT_HOLD: 0.16, // 给撞击和压扁一个可读停顿
80
+ T_FALL_BACK: 0.34,
81
+ T_SATISFIED: 0.72,
82
+ T_HAT_DROP: 0.72,
83
+ T_MASK_SLIDE: 0.72,
84
+ T_SETTLE: 0.55,
85
+ T_LAMP_FADE_OUT: 0.35, // 灯渐隐时长, 起点 = satisfied 结束
86
+
87
+ // notice 段眼睛朝灯方向偏移
88
+ noticeEyeOffsetX: -1.6,
89
+ noticeEyeOffsetY: -1.4,
90
+ noticeBodyTilt: -6,
91
+
92
+ // 跳跃路径 (云宝从中心跳到 capsule 下方)
93
+ jumpTx: -8.7,
94
+ jumpTy: -4.0, // 头顶进入灯底, 由接触形变表现压扁
95
+ crouchScaleY: 0.88,
96
+ jumpUpScaleY: 1.08,
97
+ impactScaleY: 0.88,
98
+
99
+ // 灯位置
100
+ lampCx: 3,
101
+ lampCy: -3,
102
+ lampImpactDip: 0.90, // 撞击瞬间灯 dip 峰值
103
+
104
+ // toggle knob X 偏移 (相对 capsule 中心, 跟原版 sleeping toggle 锁定值)
105
+ toggleDayKnobOffsetX: -3,
106
+ toggleNightKnobOffsetX: -0.2,
107
+
108
+ // 满意眼 (happy 微笑弧 锁定值)
109
+ smileH: 1.0,
110
+ smileW: 3.5,
111
+
112
+ // 帽子起点
113
+ hatStartTx: 3,
114
+ hatStartTy: -15,
115
+ hatStartRot: -30,
116
+ hatStartScale: 0.7,
117
+
118
+ // 眼罩起点
119
+ maskStartTx: 8,
120
+ maskStartTy: -4.5,
121
+ maskStartRot: 14,
122
+
123
+ // sleeping 锁定终点
124
+ sleepingBodyTilt: -6,
125
+ sleepingHatTx: -2,
126
+ sleepingHatTy: -0.4,
127
+ sleepingHatRot: -10,
128
+ sleepingHatScale: 0.98,
129
+ sleepingMaskTx: 0,
130
+ sleepingMaskTy: -0.6,
131
+ sleepingMaskRot: -9,
132
+ sleepingMaskScale: 1.0,
133
+ };
134
+
135
+ let params = { ...DEFAULTS };
136
+ let paused = false;
137
+ let scrubbing = false;
138
+
139
+ /* =========================================================================
140
+ * defs (复用 v1)
141
+ * ========================================================================= */
142
+ function commonDefs(p, cloudD = makeSoftCloudPath(0)) {
143
+ return `
144
+ <linearGradient id="${p}-body" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
145
+ <stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="#e0f2fe"/><stop offset="1" stop-color="#bae6fd"/>
146
+ </linearGradient>
147
+ <linearGradient id="${p}-eye" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
148
+ <stop offset="0" stop-color="#B1A7FF"/><stop offset="0.5" stop-color="#7A9DFF"/><stop offset="1" stop-color="#3941FF"/>
149
+ </linearGradient>
150
+ <radialGradient id="${p}-hi" cx="0.45" cy="0.25" r="0.55">
151
+ <stop offset="0" stop-color="#fff" stop-opacity="0.9"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
152
+ </radialGradient>
153
+ <linearGradient id="${p}-hat" x1="11" y1="-3" x2="14" y2="6" gradientUnits="userSpaceOnUse">
154
+ <stop offset="0" stop-color="#7AA0FF"/><stop offset="0.45" stop-color="#3941FF"/><stop offset="1" stop-color="#1F2BC2"/>
155
+ </linearGradient>
156
+ <linearGradient id="${p}-cuff" x1="12" y1="3" x2="12" y2="8" gradientUnits="userSpaceOnUse">
157
+ <stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="#fafbff"/><stop offset="1" stop-color="#e8ecf6"/>
158
+ </linearGradient>
159
+ <linearGradient id="${p}-star-yellow" x1="0" y1="-2" x2="0" y2="2" gradientUnits="userSpaceOnUse">
160
+ <stop offset="0" stop-color="#FFE9A8"/><stop offset="0.5" stop-color="#FFD360"/><stop offset="1" stop-color="#F5A623"/>
161
+ </linearGradient>
162
+ <filter id="${p}-star-glow" x="-80%" y="-80%" width="260%" height="260%">
163
+ <feDropShadow dx="0" dy="0.4" stdDeviation="0.5" flood-color="#F5A623" flood-opacity="0.32"/>
164
+ </filter>
165
+ <linearGradient id="${p}-z-grad" x1="0" y1="0" x2="0" y2="1">
166
+ <stop offset="0" stop-color="#B1A7FF"/><stop offset="0.5" stop-color="#7A9DFF"/><stop offset="1" stop-color="#3941FF"/>
167
+ </linearGradient>
168
+ <filter id="${p}-z-glow" x="-50%" y="-50%" width="200%" height="200%">
169
+ <feDropShadow dx="0" dy="0.3" stdDeviation="0.35" flood-color="#7A9DFF" flood-opacity="0.45"/>
170
+ </filter>
171
+ <filter id="${p}-shadow" x="-50%" y="-50%" width="200%" height="200%">
172
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
173
+ </filter>
174
+ <filter id="${p}-edge-blur" x="-40%" y="-40%" width="180%" height="180%">
175
+ <feGaussianBlur stdDeviation="0.16"/>
176
+ </filter>
177
+ <filter id="${p}-soft" x="-30%" y="-30%" width="160%" height="160%">
178
+ <feGaussianBlur stdDeviation="0.18"/>
179
+ </filter>
180
+ <linearGradient id="${p}-toggle-day" x1="0" y1="0" x2="0" y2="1">
181
+ <stop offset="0" stop-color="#E5EDFF"/><stop offset="1" stop-color="#BFD6FF"/>
182
+ </linearGradient>
183
+ <linearGradient id="${p}-toggle-night" x1="0" y1="0" x2="0" y2="1">
184
+ <stop offset="0" stop-color="#2A3554"/><stop offset="1" stop-color="#0F1428"/>
185
+ </linearGradient>
186
+ <clipPath id="${p}-clip"><path d="${cloudD}"/></clipPath>
187
+ `;
188
+ }
189
+
190
+ function makeFluffyCuff(p, cx, cy, rx, ry, asym, bumps) {
191
+ let parts = [];
192
+ const circles = [];
193
+ for (let i = 0; i < bumps; i++) {
194
+ const ang = (i * 2 * Math.PI) / bumps - Math.PI / 2;
195
+ const x = cx + Math.cos(ang) * rx;
196
+ const y = cy + Math.sin(ang) * ry;
197
+ const sizeY = -Math.sin(ang);
198
+ const sizeBoost = 0.18 * (sizeY + 1) / 2 + 0.10;
199
+ const jitter = ((i * 13) % 7) / 35;
200
+ let r = 0.62 + sizeBoost + jitter;
201
+ if (asym !== 0) r *= 1 + asym * Math.cos(ang) * 0.28;
202
+ circles.push({ x, y, r });
203
+ }
204
+ const innerRy = ry + 0.40;
205
+ parts.push(`<g filter="url(#${p}-soft)">`);
206
+ parts.push(`<ellipse cx="${cx}" cy="${cy}" rx="${(rx+0.30).toFixed(2)}" ry="${(innerRy+0.30).toFixed(2)}" fill="#7A9DFF" opacity="0.50"/>`);
207
+ circles.forEach(c => parts.push(`<circle cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${(c.r+0.30).toFixed(2)}" fill="#7A9DFF" opacity="0.50"/>`));
208
+ parts.push(`</g>`);
209
+ parts.push(`<ellipse cx="${cx}" cy="${cy}" rx="${(rx+0.15).toFixed(2)}" ry="${(innerRy+0.15).toFixed(2)}" fill="#aab3c6" opacity="0.65"/>`);
210
+ circles.forEach(c => parts.push(`<circle cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${(c.r+0.15).toFixed(2)}" fill="#aab3c6" opacity="0.65"/>`));
211
+ parts.push(`<ellipse cx="${cx}" cy="${cy}" rx="${rx.toFixed(2)}" ry="${innerRy.toFixed(2)}" fill="url(#${p}-cuff)"/>`);
212
+ circles.forEach(c => parts.push(`<circle cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${c.r.toFixed(2)}" fill="url(#${p}-cuff)"/>`));
213
+ parts.push(`<ellipse cx="${cx}" cy="${(cy-ry*0.45).toFixed(2)}" rx="${(rx*0.78).toFixed(2)}" ry="${(ry*0.32).toFixed(2)}" fill="none" stroke="#fff" stroke-opacity="0.55" stroke-width="0.14"/>`);
214
+ return parts.join('');
215
+ }
216
+
217
+ function makeBentHat(bend) {
218
+ const elbowX = 12 + bend * 2.2;
219
+ const elbowY = -0.5 + bend * 2.3;
220
+ const tipX = 11 - bend * 5.5;
221
+ const tipY = 0.0 + bend * 0.5;
222
+ const tipBottomX = tipX + 1.2 + bend * 0.4;
223
+ const tipBottomY = tipY + 1.4 + bend * 0.3;
224
+ return {
225
+ path: `M 8.5 5.0 Q 12 5.55 15.5 5.0 C 16.0 3.5 ${(elbowX+0.8).toFixed(2)} ${(elbowY+1.0).toFixed(2)} ${elbowX.toFixed(2)} ${elbowY.toFixed(2)} C ${(elbowX-0.5).toFixed(2)} ${(elbowY-0.8).toFixed(2)} ${(tipX+1.5).toFixed(2)} ${(tipY-0.5).toFixed(2)} ${tipX.toFixed(2)} ${tipY.toFixed(2)} C ${(tipX-0.3).toFixed(2)} ${(tipY+0.6).toFixed(2)} ${(tipBottomX-1.0).toFixed(2)} ${tipBottomY.toFixed(2)} ${tipBottomX.toFixed(2)} ${tipBottomY.toFixed(2)} C 8.5 4.0 8.0 4.5 8.5 5.0 Z`
226
+ };
227
+ }
228
+
229
+ function makeStar(cx, cy, r, rotationDeg, cornerRound, innerRatio) {
230
+ const innerR = r * innerRatio;
231
+ const rotRad = (rotationDeg * Math.PI) / 180;
232
+ const verts = [];
233
+ for (let i = 0; i < 10; i++) {
234
+ const ang = (i * Math.PI / 5) + rotRad - Math.PI / 2;
235
+ const radius = i % 2 === 0 ? r : innerR;
236
+ verts.push({ x: cx + Math.cos(ang) * radius, y: cy + Math.sin(ang) * radius });
237
+ }
238
+ const inP = [], outP = [];
239
+ for (let i = 0; i < 10; i++) {
240
+ const prev = verts[(i-1+10)%10], cur = verts[i], next = verts[(i+1)%10];
241
+ const v1x = prev.x - cur.x, v1y = prev.y - cur.y;
242
+ const v1Len = Math.hypot(v1x, v1y);
243
+ const v2x = next.x - cur.x, v2y = next.y - cur.y;
244
+ const v2Len = Math.hypot(v2x, v2y);
245
+ const cr = Math.min(cornerRound, v1Len*0.48, v2Len*0.48);
246
+ inP[i] = { x: cur.x + (v1x/v1Len)*cr, y: cur.y + (v1y/v1Len)*cr };
247
+ outP[i] = { x: cur.x + (v2x/v2Len)*cr, y: cur.y + (v2y/v2Len)*cr };
248
+ }
249
+ let path = `M ${inP[0].x.toFixed(2)} ${inP[0].y.toFixed(2)} `;
250
+ for (let i = 0; i < 10; i++) {
251
+ path += `Q ${verts[i].x.toFixed(2)} ${verts[i].y.toFixed(2)} ${outP[i].x.toFixed(2)} ${outP[i].y.toFixed(2)} `;
252
+ path += `L ${inP[(i+1)%10].x.toFixed(2)} ${inP[(i+1)%10].y.toFixed(2)} `;
253
+ }
254
+ return path + 'Z';
255
+ }
256
+
257
+ const SLEEP_MASK_PATH = {
258
+ d: "M488.728,341.587l-7.846-2.599c-0.405-16.992-7.605-47.34-48.625-70.941c-38.301-22.036-96.652-33.215-173.431-33.224c-76.822,0-135.146,11.165-173.438,33.183c-42.335,24.344-48.659,55.883-48.661,72.516c0,2.608,0.135,5.183,0.352,7.735h-2.649c-0.001,0-0.001,0-0.002,0c-9.635,0-17.446,7.811-17.448,17.445c-0.001,9.636,7.81,17.449,17.445,17.45l13,0.001c15.257,28.43,45.267,47.814,79.728,47.818c20.016-0.001,36.647-11.511,54.256-23.695c20.65-14.29,44.048-30.481,77.388-30.481c33.359,0.003,56.757,16.204,77.402,30.497c17.61,12.192,34.24,23.707,54.251,23.709h0.012c24.153,0,46.861-9.404,63.941-26.481c8.937-8.936,15.751-19.422,20.239-30.839l3.111,1.031c1.82,0.604,3.67,0.89,5.489,0.89c7.318,0,14.133-4.64,16.56-11.966C502.835,354.489,497.876,344.617,488.728,341.587Z",
259
+ bboxX: 15, bboxY: 232, bboxW: 487, bboxH: 200
260
+ };
261
+
262
+ function makeSleepMask(p, tx, ty, rot, scale) {
263
+ const cx = 12 + tx, cy = 12 + ty;
264
+ const W = 16.61 * scale, H = 7.55 * scale;
265
+ const sx = W / SLEEP_MASK_PATH.bboxW, sy = H / SLEEP_MASK_PATH.bboxH;
266
+ const cxOrig = SLEEP_MASK_PATH.bboxX + SLEEP_MASK_PATH.bboxW/2;
267
+ const cyOrig = SLEEP_MASK_PATH.bboxY + SLEEP_MASK_PATH.bboxH/2;
268
+ const body = `<g transform="translate(${cx} ${cy}) scale(${sx.toFixed(5)} ${sy.toFixed(5)}) translate(${(-cxOrig).toFixed(2)} ${(-cyOrig).toFixed(2)})">
269
+ <path d="${SLEEP_MASK_PATH.d}" fill="#22305A" stroke="#000" stroke-width="0.22" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/>
270
+ </g>`;
271
+ const lashW = 2.45, lashH = 0.70;
272
+ const arc = `<path d="M ${(-lashW/2).toFixed(2)} 0 Q 0 ${(lashH*2).toFixed(2)} ${(lashW/2).toFixed(2)} 0" fill="none" stroke="#fff" stroke-width="0.45" stroke-linecap="round"/>`;
273
+ const hairs = [];
274
+ for (let i = 0; i < 3; i++) {
275
+ const t = 0.18 + (i/2) * 0.64;
276
+ const sxh = (1-t)*(1-t)*(-lashW/2) + t*t*(lashW/2);
277
+ const syh = 2*(1-t)*t*(2*lashH);
278
+ const tangX = 2*(1-t)*(lashW/2) + 2*t*(lashW/2);
279
+ const tangY = 2*(1-t)*(2*lashH) - 2*t*(2*lashH);
280
+ const tLen = Math.hypot(tangX, tangY);
281
+ const dirX = tangY/tLen, dirY = -tangX/tLen;
282
+ const useDown = dirY > 0 ? 1 : -1;
283
+ const ex = sxh + dirX * 0.66 * useDown, ey = syh + dirY * 0.66 * useDown;
284
+ hairs.push(`<line x1="${sxh.toFixed(2)}" y1="${syh.toFixed(2)}" x2="${ex.toFixed(2)}" y2="${ey.toFixed(2)}" stroke="#fff" stroke-width="0.38" stroke-linecap="round" opacity="0.92"/>`);
285
+ }
286
+ const lashGap = 6.0 * scale;
287
+ const wrapEye = (ecx, sign) => `<g transform="translate(${ecx.toFixed(2)} ${(cy-0.25).toFixed(2)}) rotate(${(sign*5).toFixed(2)}) scale(1.18)">${arc}${hairs.join('')}</g>`;
288
+ const lashes = wrapEye(cx - lashGap/2, -1) + wrapEye(cx + lashGap/2, +1);
289
+ return `<g class="sleep-mask" transform="rotate(${rot} ${cx} ${cy})" filter="url(#${p}-shadow)">${body}${lashes}</g>`;
290
+ }
291
+
292
+ /* =========================================================================
293
+ * 眼睛 (多模式: capsule / arrows / smile / closed)
294
+ * ========================================================================= */
295
+ function makeEyes(p, mode, openness, offX, offY) {
296
+ const lcx = 9, rcx = 15, cy = 12; // 双眼默认位置
297
+ let lEye, rEye;
298
+
299
+ if (mode === 'arrows') {
300
+ // > < 用力表情 (左眼 > 朝右开, 右眼 < 朝左开)
301
+ // 用 typing spec stroke 1.25 + linecap=round, 字符尺寸 1.40
302
+ const sz = 1.4;
303
+ // > path (左眼): 左上 → 中右 → 左下
304
+ const gtPath = `M ${(-sz*0.7).toFixed(2)} ${(-sz*0.85).toFixed(2)} L ${(sz*0.7).toFixed(2)} 0 L ${(-sz*0.7).toFixed(2)} ${(sz*0.85).toFixed(2)}`;
305
+ // < path (右眼): 右上 → 中左 → 右下
306
+ const ltPath = `M ${(sz*0.7).toFixed(2)} ${(-sz*0.85).toFixed(2)} L ${(-sz*0.7).toFixed(2)} 0 L ${(sz*0.7).toFixed(2)} ${(sz*0.85).toFixed(2)}`;
307
+ lEye = `<g transform="translate(${lcx} ${cy})"><path d="${gtPath}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></g>`;
308
+ rEye = `<g transform="translate(${rcx} ${cy})"><path d="${ltPath}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></g>`;
309
+ } else if (mode === 'smile') {
310
+ // happy 微笑弧 h=1.0 w=3.5 stroke=1.25 cy=12.7
311
+ const w = params.smileW, h = params.smileH;
312
+ const smileCY = 12.7;
313
+ const smilePath = (cx) => `M ${(cx-w/2).toFixed(2)} ${smileCY} Q ${cx} ${(smileCY-2*h).toFixed(2)} ${(cx+w/2).toFixed(2)} ${smileCY}`;
314
+ lEye = `<path d="${smilePath(lcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>`;
315
+ rEye = `<path d="${smilePath(rcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>`;
316
+ } else {
317
+ // capsule (default) 或 closed (h 极小)
318
+ const baseH = 5;
319
+ const minH = 0.3;
320
+ const h = Math.max(minH, baseH * openness);
321
+ const y = 9.5 + (baseH - h) / 2;
322
+ const rx = Math.min(1.3, h / 2);
323
+ lEye = `<rect x="7.7" y="${y.toFixed(2)}" width="2.6" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="url(#${p}-eye)" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>`;
324
+ rEye = `<rect x="13.7" y="${y.toFixed(2)}" width="2.6" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="url(#${p}-eye)" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>`;
325
+ }
326
+
327
+ return `<g class="eyes" transform="translate(${offX.toFixed(2)} ${offY.toFixed(2)})">${lEye}${rEye}</g>`;
328
+ }
329
+
330
+ function makeCloudling(p, eyeMode, eyeOpenness, eyeOffX, eyeOffY) {
331
+ return `
332
+ <g class="cloudling-base">
333
+ <g class="edge-system">
334
+ <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)"/>
335
+ <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)"/>
336
+ <use class="edge-rim-tight" href="#${p}-cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
337
+ </g>
338
+ <g filter="url(#${p}-shadow)">
339
+ <use href="#${p}-cloud-shape" fill="url(#${p}-body)"/>
340
+ <rect x="1" y="2" width="22" height="11" fill="url(#${p}-hi)" clip-path="url(#${p}-clip)"/>
341
+ </g>
342
+ ${makeEyes(p, eyeMode, eyeOpenness, eyeOffX, eyeOffY)}
343
+ </g>
344
+ `;
345
+ }
346
+
347
+ const STAR_LOCKED = { cx: 8.55, cy: 0.35, size: 1.00, rotation: 30, cornerRound: 0.27, innerRatio: 0.56 };
348
+
349
+ function makeHat(p, tx, ty, rot, scale) {
350
+ const hatData = makeBentHat(0.4);
351
+ const cuffSVG = makeFluffyCuff(p, 12, 5.4, 3.7, 0.85, 0, 18);
352
+ const s = STAR_LOCKED;
353
+ const starPath = makeStar(s.cx, s.cy, s.size, s.rotation, s.cornerRound, s.innerRatio);
354
+ return `
355
+ <g class="hat-piece" transform="translate(${tx} ${ty}) rotate(${rot} 12 5.4) scale(${scale})">
356
+ <g filter="url(#${p}-shadow)"><path d="${hatData.path}" fill="url(#${p}-hat)"/></g>
357
+ <g class="cuff-fluffy">${cuffSVG}</g>
358
+ <g class="star-piece" filter="url(#${p}-star-glow)">
359
+ <path d="${starPath}" fill="url(#${p}-star-yellow)" stroke="#fff" stroke-width="0.06" stroke-opacity="0.7" stroke-linejoin="round"/>
360
+ </g>
361
+ </g>
362
+ `;
363
+ }
364
+
365
+ function makeZPath() {
366
+ return "M -1.0 -1.0 Q 0.0 -1.0 1.0 -1.0 Q 1.05 -0.85 0.6 -0.4 Q 0 0.1 -0.6 0.6 Q -1.05 0.95 -1.0 1.0 Q 0.0 1.0 1.0 1.0";
367
+ }
368
+
369
+ function makeLamp(p, cx, cy, dipY, toggleMix, dayOff, nightOff, opacity, scale, knockRot) {
370
+ const ccy = cy + dipY;
371
+ const mix = clamp01(toggleMix);
372
+ const dayOpacity = 1 - mix;
373
+ const nightOpacity = mix;
374
+ const dayKnobOpacity = clamp01(1 - mix * 2.2);
375
+ const nightKnobOpacity = clamp01((mix - 0.18) / 0.82);
376
+ const knobX = cx + lerp(dayOff, nightOff, mix);
377
+ const knobY = ccy;
378
+
379
+ const dayKnob = `
380
+ <g opacity="${dayKnobOpacity.toFixed(2)}">
381
+ <circle cx="${knobX.toFixed(2)}" cy="${knobY.toFixed(2)}" r="2.4" fill="#fff" filter="url(#${p}-shadow)"/>
382
+ <circle cx="${(knobX+0.85).toFixed(2)}" cy="${(knobY-0.95).toFixed(2)}" r="0.50" fill="#D4DFFF" opacity="0.78"/>
383
+ <circle cx="${(knobX+1.15).toFixed(2)}" cy="${(knobY+0.55).toFixed(2)}" r="0.28" fill="#D4DFFF" opacity="0.72"/>
384
+ </g>
385
+ `;
386
+
387
+ const outerR = 2.35, innerR = 1.70, innerDx = 0.80;
388
+ const ix = (outerR*outerR - innerR*innerR + innerDx*innerDx) / (2*innerDx);
389
+ const iy = Math.sqrt(outerR*outerR - ix*ix);
390
+ const nightKnob = `
391
+ <g class="crescent-knob" filter="url(#${p}-shadow)" opacity="${nightKnobOpacity.toFixed(2)}">
392
+ <path d="
393
+ M ${(knobX+ix).toFixed(3)} ${(knobY-iy).toFixed(3)}
394
+ A ${outerR} ${outerR} 0 1 1 ${(knobX+ix).toFixed(3)} ${(knobY+iy).toFixed(3)}
395
+ A ${innerR} ${innerR} 0 1 0 ${(knobX+ix).toFixed(3)} ${(knobY-iy).toFixed(3)}
396
+ Z
397
+ " fill="#fff"/>
398
+ </g>
399
+ `;
400
+
401
+ const stars = `
402
+ <g class="stars" opacity="${nightOpacity.toFixed(2)}">
403
+ <circle cx="${(cx-1.5).toFixed(2)}" cy="${(ccy-0.9).toFixed(2)}" r="0.18" fill="#fff" opacity="0.95"/>
404
+ <circle cx="${(cx-0.5).toFixed(2)}" cy="${(ccy+1.3).toFixed(2)}" r="0.14" fill="#fff" opacity="0.85"/>
405
+ <circle cx="${(cx+0.6).toFixed(2)}" cy="${(ccy-0.4).toFixed(2)}" r="0.10" fill="#fff" opacity="0.75"/>
406
+ <circle cx="${(cx-1.2).toFixed(2)}" cy="${(ccy+1.6).toFixed(2)}" r="0.10" fill="#fff" opacity="0.78"/>
407
+ <circle cx="${(cx).toFixed(2)}" cy="${(ccy-1.3).toFixed(2)}" r="0.13" fill="#fff" opacity="0.88"/>
408
+ </g>
409
+ `;
410
+
411
+ return `
412
+ <g class="lamp-wrapper" opacity="${opacity.toFixed(2)}" transform="translate(${cx} ${ccy}) rotate(${knockRot.toFixed(3)}) scale(${scale.toFixed(3)}) translate(${-cx} ${-ccy})">
413
+ <g class="lamp-group" filter="url(#${p}-shadow)">
414
+ <rect class="toggle-capsule-stroke" x="${(cx-7).toFixed(2)}" y="${(ccy-3).toFixed(2)}" width="14" height="6" rx="3" ry="3" fill="url(#${p}-toggle-day)" opacity="${dayOpacity.toFixed(2)}"/>
415
+ <rect class="toggle-capsule-stroke" x="${(cx-7).toFixed(2)}" y="${(ccy-3).toFixed(2)}" width="14" height="6" rx="3" ry="3" fill="url(#${p}-toggle-night)" opacity="${nightOpacity.toFixed(2)}"/>
416
+ <rect x="${(cx-7).toFixed(2)}" y="${(ccy-3).toFixed(2)}" width="14" height="6" rx="3" ry="3" fill="none" stroke="#fff" stroke-opacity="0.18" stroke-width="0.12"/>
417
+ </g>
418
+ ${stars}
419
+ ${dayKnob}
420
+ ${nightKnob}
421
+ </g>
422
+ `;
423
+ }
424
+
425
+ /* =========================================================================
426
+ * 时间线 + 插值
427
+ * ========================================================================= */
428
+ function lerp(a, b, t) { return a + (b - a) * t; }
429
+ function easeOut(t) { return 1 - Math.pow(1 - t, 2); }
430
+ function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
431
+ function easeOutBack(t) {
432
+ const c1 = 1.45;
433
+ const c3 = c1 + 1;
434
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
435
+ }
436
+ function easeIn(t) { return t * t; }
437
+ function easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2; }
438
+ function clamp01(t) { return Math.max(0, Math.min(1, t)); }
439
+ function progress(t, begin, end) { return clamp01((t - begin) / (end - begin)); }
440
+
441
+ function getTimeline() {
442
+ const tLampEnd = params.T_LAMP_APPEAR;
443
+ const tNoticeEnd = params.T_NOTICE_END;
444
+ const tCrouchStart = tNoticeEnd;
445
+ const tCrouchEnd = tCrouchStart + params.T_CROUCH;
446
+ const tJumpUpEnd = tCrouchEnd + params.T_JUMP_UP;
447
+ const tImpactStart = tJumpUpEnd;
448
+ const tContactStart = Math.max(tCrouchEnd, tImpactStart - params.T_CONTACT_LEAD);
449
+ const tImpactEnd = tImpactStart + params.T_IMPACT_HOLD;
450
+ const tFallEnd = tImpactEnd + params.T_FALL_BACK;
451
+ const tSatisfiedEnd = tFallEnd + params.T_SATISFIED;
452
+ const tHatDropEnd = tSatisfiedEnd + params.T_HAT_DROP;
453
+ const tMaskSlideEnd = tHatDropEnd + params.T_MASK_SLIDE;
454
+ const tSettleEnd = tMaskSlideEnd + params.T_SETTLE;
455
+ return {
456
+ tLampEnd, tNoticeBegin: params.T_NOTICE_BEGIN, tNoticeEnd,
457
+ tCrouchStart, tCrouchEnd,
458
+ tJumpUpEnd, tContactStart, tImpactStart, tImpactEnd,
459
+ tFallEnd, tSatisfiedEnd,
460
+ tHatDropEnd, tMaskSlideEnd, tSettleEnd
461
+ };
462
+ }
463
+
464
+ function computeFrame(t) {
465
+ const T = getTimeline();
466
+ const f = {};
467
+
468
+ // ---------- 灯渐显 → 满意后渐隐 ----------
469
+ const tFadeStart = T.tSatisfiedEnd;
470
+ const tFadeEnd = tFadeStart + params.T_LAMP_FADE_OUT;
471
+ if (t < params.T_LAMP_APPEAR) {
472
+ // 渐显 (lamp_appear 段)
473
+ const p = easeOut(progress(t, 0, params.T_LAMP_APPEAR));
474
+ f.lampOpacity = p;
475
+ f.lampScale = lerp(0.85, 1.0, p);
476
+ } else if (t < tFadeStart) {
477
+ // 全显 (lamp_appear 完成 → satisfied 结束)
478
+ f.lampOpacity = 1;
479
+ f.lampScale = 1;
480
+ } else if (t < tFadeEnd) {
481
+ // 渐隐 (满意后 → 完全消失)
482
+ const p = easeIn(progress(t, tFadeStart, tFadeEnd));
483
+ f.lampOpacity = lerp(1, 0, p);
484
+ f.lampScale = 1;
485
+ } else {
486
+ // 完全消失
487
+ f.lampOpacity = 0;
488
+ f.lampScale = 1;
489
+ }
490
+ f.lampScalePulse = 1;
491
+ f.lampRot = 0;
492
+ let contactAmount = 0;
493
+ if (t >= T.tContactStart && t < T.tImpactStart) {
494
+ contactAmount = lerp(0, 0.82, easeInOut(progress(t, T.tContactStart, T.tImpactStart)));
495
+ } else if (t >= T.tImpactStart && t < T.tImpactEnd) {
496
+ contactAmount = lerp(0.82, 1, easeOut(progress(t, T.tImpactStart, T.tImpactEnd)));
497
+ } else if (t >= T.tImpactEnd && t < T.tFallEnd) {
498
+ contactAmount = 1 - easeInOut(progress(t, T.tImpactEnd, T.tFallEnd));
499
+ }
500
+ f.contactAmount = contactAmount;
501
+ f.pressAmount = t < T.tImpactStart && contactAmount > 0
502
+ ? Math.sqrt(contactAmount / 0.82) * 0.82
503
+ : contactAmount;
504
+
505
+ // ---------- cloudling 整体 transform ----------
506
+ let cloudTx = 0, cloudTy = 0, cloudRot = 0, cloudScaleX = 1, cloudScaleY = 1;
507
+
508
+ if (t < T.tNoticeBegin) {
509
+ // idle 默认
510
+ } else if (t < T.tNoticeEnd) {
511
+ // notice: 微歪头朝灯方向
512
+ const p = easeOut(progress(t, T.tNoticeBegin, T.tNoticeEnd));
513
+ cloudRot = lerp(0, params.noticeBodyTilt, p);
514
+ } else if (t < T.tCrouchEnd) {
515
+ // crouch: scaleY 压扁
516
+ const p = easeIn(progress(t, T.tCrouchStart, T.tCrouchEnd));
517
+ cloudTy = lerp(0, 0.45, p);
518
+ cloudScaleY = lerp(1, params.crouchScaleY, p);
519
+ cloudRot = params.noticeBodyTilt;
520
+ } else if (t < T.tJumpUpEnd) {
521
+ // jump up: 斜向上弧线顶到 capsule, 不是直线飘过去
522
+ const p = easeOutCubic(progress(t, T.tCrouchEnd, T.tJumpUpEnd));
523
+ cloudTx = lerp(0, params.jumpTx, p);
524
+ cloudTy = lerp(0.45, params.jumpTy, p) - Math.sin(p * Math.PI) * 0.85;
525
+ const jumpScaleY = lerp(params.crouchScaleY, params.jumpUpScaleY, p);
526
+ cloudScaleX = lerp(1, 1.13, f.pressAmount);
527
+ cloudScaleY = lerp(jumpScaleY, params.impactScaleY, f.pressAmount);
528
+ cloudRot = lerp(params.noticeBodyTilt, 0, p); // 起跳过程歪头回正
529
+ } else if (t < T.tImpactEnd) {
530
+ // impact: 撞 capsule, 继续加深压扁, 不硬切
531
+ cloudTx = params.jumpTx;
532
+ cloudTy = params.jumpTy;
533
+ cloudScaleX = lerp(1, 1.13, f.pressAmount);
534
+ cloudScaleY = lerp(params.jumpUpScaleY, params.impactScaleY, f.pressAmount);
535
+ cloudRot = 0;
536
+ } else if (t < T.tFallEnd) {
537
+ // fall back: 从顶位回到中心, 压扁量逐渐释放
538
+ const p = easeInOut(progress(t, T.tImpactEnd, T.tFallEnd));
539
+ cloudTx = lerp(params.jumpTx, 0, p);
540
+ cloudTy = lerp(params.jumpTy, 0, p) + Math.sin(p * Math.PI) * 0.55;
541
+ cloudScaleX = lerp(1, 1.13, f.pressAmount);
542
+ cloudScaleY = lerp(1, params.impactScaleY, f.pressAmount);
543
+ } else if (t < T.tSatisfiedEnd) {
544
+ // satisfied: 落地后的满足小弹跳, 给表情留时间
545
+ const p = progress(t, T.tFallEnd, T.tSatisfiedEnd);
546
+ const settleBounce = Math.sin(p * Math.PI * 2) * Math.exp(-p * 3.2);
547
+ cloudTy = settleBounce * 0.22;
548
+ cloudRot = Math.sin(p * Math.PI) * 1.4;
549
+ } else {
550
+ // hat/mask/settle: bodyTilt 渐进到 sleeping -6°
551
+ const p = easeInOut(progress(t, T.tSatisfiedEnd, T.tSettleEnd));
552
+ cloudRot = lerp(0, params.sleepingBodyTilt, p);
553
+ }
554
+
555
+ f.cloudTx = cloudTx;
556
+ f.cloudTy = cloudTy;
557
+ f.cloudRot = cloudRot;
558
+ f.cloudScaleX = cloudScaleX;
559
+ f.cloudScaleY = cloudScaleY;
560
+
561
+ // ---------- 灯反馈: 跟 contactAmount 同步渐进下压, 离开后轻微余震 ----------
562
+ f.lampDipY = params.lampImpactDip * f.pressAmount;
563
+ if (t >= T.tContactStart && t < T.tFallEnd + 0.35) {
564
+ const contactShake = t < T.tImpactStart
565
+ ? -1.0 * contactAmount
566
+ : Math.sin((t - T.tImpactStart) * Math.PI * 12) * 3.8 * Math.exp(-(t - T.tImpactStart) * 4.6);
567
+ f.lampRot = contactShake;
568
+ if (t >= T.tImpactStart) {
569
+ const dt = t - T.tImpactStart;
570
+ f.lampScalePulse = 1 + Math.max(0, Math.sin(dt * Math.PI * 4.5)) * 0.035 * Math.exp(-dt * 5);
571
+ }
572
+ if (t >= T.tFallEnd) {
573
+ const rt = t - T.tFallEnd;
574
+ f.lampDipY += params.lampImpactDip * 0.08 * Math.exp(-rt * 8) * Math.sin(rt * Math.PI * 10);
575
+ f.lampRot = Math.sin(rt * Math.PI * 10) * 1.4 * Math.exp(-rt * 8);
576
+ }
577
+ }
578
+
579
+ // ---------- toggle state: 撞击后滑过去, 咔哒感比硬切清楚 ----------
580
+ f.toggleMix = easeOutCubic(progress(t, T.tImpactStart + 0.03, T.tImpactStart + 0.24));
581
+ f.toggleState = f.toggleMix >= 0.5 ? 'night' : 'day';
582
+
583
+ // ---------- 眼睛 mode ----------
584
+ // v7: 接触加深时进入 ><, 落地后给清楚的满意弧线.
585
+ let eyeMode, eyeOpenness = 1, eyeOffX = 0, eyeOffY = 0;
586
+ if (t < T.tNoticeBegin) {
587
+ eyeMode = 'capsule';
588
+ } else if (t < T.tNoticeEnd) {
589
+ // notice: capsule 朝灯偏移
590
+ const p = easeOut(progress(t, T.tNoticeBegin, T.tNoticeEnd));
591
+ eyeMode = 'capsule';
592
+ eyeOffX = lerp(0, params.noticeEyeOffsetX, p);
593
+ eyeOffY = lerp(0, params.noticeEyeOffsetY, p);
594
+ } else if (t < T.tImpactStart && f.contactAmount < 0.42) {
595
+ // crouch + jump up: 保持朝灯偏移, 微微眯眼像在瞄准
596
+ eyeMode = 'capsule';
597
+ eyeOpenness = 0.82;
598
+ eyeOffX = params.noticeEyeOffsetX;
599
+ eyeOffY = params.noticeEyeOffsetY;
600
+ } else if (t < T.tImpactEnd + 0.08) {
601
+ // 接触加深后进入用力表情
602
+ eyeMode = 'arrows';
603
+ } else if (t < T.tFallEnd) {
604
+ // fall_back: 解除用力表情, 避免像被打疼
605
+ eyeMode = 'capsule';
606
+ eyeOpenness = 0.72;
607
+ } else if (t < T.tSatisfiedEnd) {
608
+ // 落地满意 beat
609
+ eyeMode = 'smile';
610
+ } else if (t < T.tMaskSlideEnd) {
611
+ // 戴帽时还保持一点满意, 眼罩靠近时渐困
612
+ const p = progress(t, T.tSatisfiedEnd, T.tMaskSlideEnd);
613
+ if (p < 0.45) {
614
+ eyeMode = 'smile';
615
+ } else {
616
+ eyeMode = 'capsule';
617
+ eyeOpenness = lerp(0.72, 0.18, progress(p, 0.45, 1));
618
+ }
619
+ } else {
620
+ // settle + sleeping loop: 几乎闭眼 (被 mask 遮住)
621
+ eyeMode = 'capsule';
622
+ eyeOpenness = 0.05;
623
+ }
624
+ f.eyeMode = eyeMode;
625
+ f.eyeOpenness = eyeOpenness;
626
+ f.eyeOffX = eyeOffX;
627
+ f.eyeOffY = eyeOffY;
628
+
629
+ // ---------- 帽子 ----------
630
+ if (t < T.tSatisfiedEnd) {
631
+ f.hatVisible = false;
632
+ } else if (t < T.tHatDropEnd) {
633
+ f.hatVisible = true;
634
+ const u = progress(t, T.tSatisfiedEnd, T.tHatDropEnd);
635
+ const p = easeOutBack(u);
636
+ f.hatTx = lerp(params.hatStartTx, params.sleepingHatTx, p);
637
+ f.hatTy = lerp(params.hatStartTy, params.sleepingHatTy, p) - Math.sin(u * Math.PI) * 0.35;
638
+ f.hatRot = lerp(params.hatStartRot, params.sleepingHatRot, p) + Math.sin(u * Math.PI * 2) * 3.5 * (1 - u);
639
+ f.hatScale = lerp(params.hatStartScale, params.sleepingHatScale, p);
640
+ } else {
641
+ f.hatVisible = true;
642
+ f.hatTx = params.sleepingHatTx;
643
+ f.hatTy = params.sleepingHatTy;
644
+ f.hatRot = params.sleepingHatRot;
645
+ f.hatScale = params.sleepingHatScale;
646
+ }
647
+
648
+ // ---------- 眼罩 ----------
649
+ if (t < T.tHatDropEnd) {
650
+ f.maskVisible = false;
651
+ } else if (t < T.tMaskSlideEnd) {
652
+ f.maskVisible = true;
653
+ const u = progress(t, T.tHatDropEnd, T.tMaskSlideEnd);
654
+ const p = easeOutBack(u);
655
+ f.maskTx = lerp(params.maskStartTx, params.sleepingMaskTx, p);
656
+ f.maskTy = lerp(params.maskStartTy, params.sleepingMaskTy, p) - Math.sin(u * Math.PI) * 0.25;
657
+ f.maskRot = lerp(params.maskStartRot, params.sleepingMaskRot, p);
658
+ f.maskScale = params.sleepingMaskScale;
659
+ } else {
660
+ f.maskVisible = true;
661
+ f.maskTx = params.sleepingMaskTx;
662
+ f.maskTy = params.sleepingMaskTy;
663
+ f.maskRot = params.sleepingMaskRot;
664
+ f.maskScale = params.sleepingMaskScale;
665
+ }
666
+
667
+ // ---------- sleeping loop ----------
668
+ f.inSleepingLoop = t >= T.tSettleEnd;
669
+ if (f.inSleepingLoop) {
670
+ const lt = t - T.tSettleEnd;
671
+ const breathPhase = (Math.cos(2 * Math.PI * lt / 4) + 1) / 2;
672
+ f.breathScale = lerp(0.96, 1.02, breathPhase);
673
+ f.hatSwayDeg = Math.sin(2 * Math.PI * lt / 6) * 2.0;
674
+ } else {
675
+ f.breathScale = 1.0;
676
+ f.hatSwayDeg = 0;
677
+ }
678
+
679
+ return f;
680
+ }
681
+
682
+ /* =========================================================================
683
+ * 渲染
684
+ * ========================================================================= */
685
+ const stage = document.getElementById('stage');
686
+
687
+ function renderFrame(t) {
688
+ const f = computeFrame(t);
689
+ const p = 'idl';
690
+
691
+ const lamp = makeLamp(
692
+ p,
693
+ params.lampCx,
694
+ params.lampCy,
695
+ f.lampDipY,
696
+ f.toggleMix,
697
+ params.toggleDayKnobOffsetX,
698
+ params.toggleNightKnobOffsetX,
699
+ f.lampOpacity,
700
+ f.lampScale * f.lampScalePulse,
701
+ f.lampRot
702
+ );
703
+
704
+ const cloudD = makeSoftCloudPath(f.pressAmount);
705
+ const cloudling = makeCloudling(p, f.eyeMode, f.eyeOpenness, f.eyeOffX, f.eyeOffY);
706
+
707
+ const hat = f.hatVisible ? `<g transform="rotate(${f.hatSwayDeg.toFixed(3)} 12 5.4)">${makeHat(p, f.hatTx, f.hatTy, f.hatRot, f.hatScale)}</g>` : '';
708
+ const mask = f.maskVisible ? makeSleepMask(p, f.maskTx, f.maskTy, f.maskRot, f.maskScale) : '';
709
+
710
+ // Z 字 (sleeping loop)
711
+ let zHTML = '';
712
+ if (f.inSleepingLoop) {
713
+ const T = getTimeline();
714
+ const lt = t - T.tSettleEnd;
715
+ const lastSpawnTime = Math.floor(lt / 1.8) * 1.8;
716
+ for (let bornAt = lastSpawnTime; bornAt > lt - 2.9 && bornAt >= 0; bornAt -= 1.8) {
717
+ const age = lt - bornAt;
718
+ if (age < 0 || age >= 2.9) continue;
719
+ const u = age / 2.9;
720
+ const x = 19.4 + 4 * u;
721
+ const y = 3.6 - 8 * u;
722
+ const size = lerp(1.6, 1.3, u);
723
+ let opacity;
724
+ if (u < 0.18) opacity = (u/0.18) * 0.45;
725
+ else if (u < 0.7) opacity = 0.45;
726
+ else opacity = 0.45 * (1 - (u-0.7)/0.3);
727
+ const sway = Math.sin(age * 2.0) * 0.3;
728
+ zHTML += `<g transform="translate(${(x+sway).toFixed(2)} ${y.toFixed(2)}) scale(${size.toFixed(2)})" opacity="${opacity.toFixed(3)}">
729
+ <g filter="url(#${p}-z-glow)">
730
+ <path d="${makeZPath()}" fill="none" stroke="url(#${p}-z-grad)" stroke-width="0.55" stroke-linecap="round" stroke-linejoin="round"/>
731
+ </g>
732
+ </g>`;
733
+ }
734
+ }
735
+
736
+ // squash origin: impact + fall_back 段用云宝头顶 (12, 4.45) 模拟"被灯顶部压扁"
737
+ // 其他段用中心 (12, 12) 正常 squash & stretch
738
+ const T2 = getTimeline();
739
+ const squashOY = (t >= T2.tImpactStart && t < T2.tFallEnd) ? 4.45 : 12;
740
+
741
+ stage.innerHTML = `
742
+ <style>
743
+ .edge-rim-tight { stroke-opacity: .65; }
744
+ .edge-rim-soft { stroke-opacity: .50; }
745
+ .edge-contact { stroke-opacity: .65; }
746
+ .toggle-capsule-stroke { stroke-opacity: 0; }
747
+ @media (prefers-color-scheme: light) {
748
+ .edge-rim-tight { stroke-opacity: .75; }
749
+ .edge-rim-soft { stroke-opacity: .60; }
750
+ .edge-contact { stroke-opacity: .75; }
751
+ .cloud-drop-shadow { flood-opacity: .13; }
752
+ .toggle-capsule-stroke { stroke: #5C7BD9; stroke-width: 0.20; stroke-opacity: 0.55; }
753
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
754
+ .food-cloud > path {
755
+ stroke: #5870E8 !important;
756
+ stroke-width: 1.2 !important;
757
+ stroke-opacity: .9 !important;
758
+ vector-effect: non-scaling-stroke !important;
759
+ }
760
+ }
761
+ </style>
762
+ <defs>
763
+ ${commonDefs(p, cloudD)}
764
+ <path id="${p}-cloud-shape" d="${cloudD}"/>
765
+ </defs>
766
+ <!-- cloudling 在灯下面 (先渲染, 灯后渲染压在上面) -->
767
+ <g class="scene" transform="translate(${f.cloudTx.toFixed(3)} ${f.cloudTy.toFixed(3)}) rotate(${f.cloudRot.toFixed(3)} 12 12)">
768
+ <g class="squash" transform="translate(12 ${squashOY}) scale(${f.cloudScaleX.toFixed(4)} ${f.cloudScaleY.toFixed(4)}) translate(-12 ${(-squashOY).toFixed(3)})">
769
+ <g class="breath" transform="translate(12 12) scale(${f.breathScale.toFixed(4)}) translate(-12 -12)">
770
+ ${cloudling}
771
+ ${hat}
772
+ ${mask}
773
+ </g>
774
+ </g>
775
+ </g>
776
+ <!-- 灯在云宝上面 (云宝顶到灯底, 灯压云宝). v6 去掉拉绳三角坠. -->
777
+ ${lamp}
778
+ ${zHTML}
779
+ `;
780
+ }
781
+
782
+ /* =========================================================================
783
+ * 主循环 + 控件
784
+ * ========================================================================= */
785
+ let startT = performance.now();
786
+ let elapsedOffset = 0;
787
+ let pausedAt = 0;
788
+
789
+ const scrubberEl = document.getElementById('scrubber');
790
+ const tDisplayEl = document.getElementById('t-display');
791
+
792
+ function getCurrentT() {
793
+ if (scrubbing) return Number(scrubberEl.value);
794
+ if (paused) return elapsedOffset + (pausedAt - startT) / 1000;
795
+ return elapsedOffset + (performance.now() - startT) / 1000;
796
+ }
797
+
798
+ function loop() {
799
+ const t = getCurrentT();
800
+ const T = getTimeline();
801
+ const totalLen = T.tSettleEnd + 5;
802
+ scrubberEl.max = totalLen.toFixed(2);
803
+ if (!scrubbing) scrubberEl.value = Math.min(t, totalLen).toFixed(3);
804
+ tDisplayEl.textContent = `${t.toFixed(2)} s`;
805
+ renderFrame(t);
806
+ requestAnimationFrame(loop);
807
+ }
808
+ renderFrame(0);
809
+ requestAnimationFrame(loop);
810
+ ]]></script>
811
+ </svg>