@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,354 +1,354 @@
1
- <svg id="cloudling" xmlns="http://www.w3.org/2000/svg" viewBox="-6 -6 36 36" preserveAspectRatio="xMidYMid meet">
2
- <style><![CDATA[
3
- .edge-rim-tight { stroke-opacity: .9; }
4
- .edge-rim-soft { stroke-opacity: .75; }
5
- .edge-contact { stroke-opacity: .9; }
6
- @media (prefers-color-scheme: light) {
7
- .edge-rim-tight { stroke-opacity: .95; }
8
- .edge-rim-soft { stroke-opacity: .85; }
9
- .edge-contact { stroke-opacity: .95; }
10
- .cloud-drop-shadow { flood-opacity: .13; }
11
- .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
12
- .food-cloud > path {
13
- stroke: #5870E8 !important;
14
- stroke-width: 1.2 !important;
15
- stroke-opacity: .9 !important;
16
- vector-effect: non-scaling-stroke !important;
17
- }
18
- }
19
- ]]></style>
20
- <defs>
21
- <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
22
- <stop offset="0" stop-color="#93c5fd"/>
23
- <stop offset="0.5" stop-color="#60a5fa"/>
24
- <stop offset="1" stop-color="#3b82f6"/>
25
- </linearGradient>
26
- <linearGradient id="eye-grad" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
27
- <stop offset="0" stop-color="#B1A7FF"/>
28
- <stop offset="0.5" stop-color="#7A9DFF"/>
29
- <stop offset="1" stop-color="#3941FF"/>
30
- </linearGradient>
31
- <radialGradient id="hi-grad" cx="0.45" cy="0.25" r="0.55">
32
- <stop offset="0" stop-color="#93c5fd" stop-opacity="0.9"/>
33
- <stop offset="1" stop-color="#93c5fd" stop-opacity="0"/>
34
- </radialGradient>
35
- <filter id="drop" x="-40%" y="-40%" width="180%" height="180%">
36
- <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.22"/>
37
- </filter>
38
- <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
39
- <feGaussianBlur stdDeviation="0.16"/>
40
- </filter>
41
- <path id="cloud-shape" 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"/>
42
- <clipPath id="cloud-clip">
43
- <use href="#cloud-shape"/>
44
- </clipPath>
45
- </defs>
46
-
47
- <!-- 云朵 layer (rotate + scale, 配置不变 — drop shadow + radial highlight + body-grad) -->
48
- <g id="cloud-group">
49
- <g id="edge-system">
50
- <use class="edge-contact" href="#cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.4" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#edge-blur)"/>
51
- <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="1.2" stroke-linejoin="round" filter="url(#edge-blur)"/>
52
- <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.8" stroke-linejoin="round"/>
53
- </g>
54
- <g filter="url(#drop)">
55
- <use href="#cloud-shape" fill="url(#body-grad)"/>
56
- <rect x="1" y="2" width="22" height="11" fill="url(#hi-grad)" clip-path="url(#cloud-clip)"/>
57
- </g>
58
- </g>
59
-
60
- <!-- 眼睛 layer (只 scale 不 rotate, 字符直立; d/fill/stroke 由 JS 实时填充) -->
61
- <g id="eye-group">
62
- <path id="eye-l-path" stroke-linecap="round" stroke-linejoin="round"/>
63
- <path id="eye-r-path" stroke-linecap="round" stroke-linejoin="round"/>
64
- </g>
65
-
66
- <script type="application/ecmascript"><![CDATA[
67
- const cloudGroup = document.getElementById('cloud-group');
68
- const eyeGroup = document.getElementById('eye-group');
69
- const eyeL = document.getElementById('eye-l-path');
70
- const eyeR = document.getElementById('eye-r-path');
71
- const eyeGrad = document.getElementById('eye-grad');
72
-
73
- /* token 库保留原版 8 个; 组合池过滤纯随机撞出的丑 pair. */
74
- const TOKENS = ['gt', 'lt', 'und', 'colon', 'eq', 'plus', 'slash', 'bslash'];
75
- const A_PAIRS = [
76
- ['lt', 'gt'], ['lt', 'eq'], ['gt', 'eq'], ['eq', 'gt'], ['eq', 'lt'],
77
- ['gt', 'und'], ['lt', 'und'], ['slash', 'und'], ['und', 'bslash'],
78
- ['eq', 'slash'], ['eq', 'bslash'],
79
- ];
80
- const B_PAIRS = [
81
- ['colon', 'gt'], ['colon', 'slash'], ['colon', 'bslash'], ['colon', 'und'],
82
- ['plus', 'und'], ['slash', 'plus'], ['bslash', 'plus'],
83
- ];
84
- const ACTIVE_PAIRS = [...A_PAIRS, ...A_PAIRS, ...A_PAIRS, ...B_PAIRS];
85
-
86
- /* token builder: 返回 { d, mode, sw }. mode='stroke' 用描边, mode='fill' 用填充 (圆点类). */
87
- function tok(d, mode, sw) { return { d, mode, sw }; }
88
-
89
- const TOKEN_BUILDERS = {
90
- /* > chevron (描边折角) */
91
- gt: (cx, cy) => tok(
92
- `M ${cx-0.78} ${cy-1.58} L ${cx+0.74} ${cy} L ${cx-0.78} ${cy+1.58}`,
93
- 'stroke', 1.16
94
- ),
95
- /* < chevron (反向) */
96
- lt: (cx, cy) => tok(
97
- `M ${cx+0.78} ${cy-1.58} L ${cx-0.74} ${cy} L ${cx+0.78} ${cy+1.58}`,
98
- 'stroke', 1.16
99
- ),
100
- /* _ underscore (下划线) */
101
- und: (cx, cy) => tok(
102
- `M ${cx-1.18} ${cy+1.42} L ${cx+1.18} ${cy+1.42}`,
103
- 'stroke', 1.10
104
- ),
105
- /* : colon (上下两个圆点, fill 实心) */
106
- colon: (cx, cy) => {
107
- const r = 0.64;
108
- return tok(
109
- `M ${cx-r} ${cy-0.94} A ${r} ${r} 0 1 0 ${cx+r} ${cy-0.94} A ${r} ${r} 0 1 0 ${cx-r} ${cy-0.94} Z ` +
110
- `M ${cx-r} ${cy+0.94} A ${r} ${r} 0 1 0 ${cx+r} ${cy+0.94} A ${r} ${r} 0 1 0 ${cx-r} ${cy+0.94} Z`,
111
- 'fill', 0
112
- );
113
- },
114
- /* = 两道横线 (多线交叉降到 0.9 防糊) */
115
- eq: (cx, cy) => tok(
116
- `M ${cx-1.08} ${cy-0.72} L ${cx+1.08} ${cy-0.72} ` +
117
- `M ${cx-1.08} ${cy+0.72} L ${cx+1.08} ${cy+0.72}`,
118
- 'stroke', 0.84
119
- ),
120
- /* + 十字 (多线交叉 0.9) */
121
- plus: (cx, cy) => tok(
122
- `M ${cx-0.96} ${cy} L ${cx+0.96} ${cy} ` +
123
- `M ${cx} ${cy-0.96} L ${cx} ${cy+0.96}`,
124
- 'stroke', 0.82
125
- ),
126
- /* / slash 斜杠 (左下到右上) */
127
- slash: (cx, cy) => tok(
128
- `M ${cx-0.92} ${cy+1.48} L ${cx+0.92} ${cy-1.48}`,
129
- 'stroke', 1.16
130
- ),
131
- /* \ bslash 反斜杠 (左上到右下) */
132
- bslash: (cx, cy) => tok(
133
- `M ${cx-0.92} ${cy-1.48} L ${cx+0.92} ${cy+1.48}`,
134
- 'stroke', 1.16
135
- )
136
- };
137
-
138
- /* 把 token 应用到 path 元素 (每次切换调一次, 切 fill/stroke 模式 + d + stroke-width + 字符尺寸缩放) */
139
- function applyToken(el, cx, cy, tokenId, sx = 1, sy = 1, opacity = 1) {
140
- const t = TOKEN_BUILDERS[tokenId](cx, cy);
141
- el.setAttribute('d', t.d);
142
- if (t.mode === 'fill') {
143
- el.setAttribute('fill', 'url(#eye-grad)');
144
- el.setAttribute('stroke', 'none');
145
- el.removeAttribute('stroke-width');
146
- } else {
147
- el.setAttribute('fill', 'none');
148
- el.setAttribute('stroke', 'url(#eye-grad)');
149
- el.setAttribute('stroke-width', t.sw);
150
- }
151
- el.setAttribute('opacity', opacity.toFixed(3));
152
- /* 以字符中心为锚整体缩放 (stroke 跟随放大, 视觉一致) */
153
- el.setAttribute(
154
- 'transform',
155
- `translate(${cx} ${cy}) scale(${(P.EYE_SIZE * sx).toFixed(3)} ${(P.EYE_SIZE * sy).toFixed(3)}) translate(${-cx} ${-cy})`
156
- );
157
- }
158
-
159
- const P = {
160
- /* 字符尺寸 (鹿鹿 2026-04-26 锁定 1.40×) */
161
- EYE_SIZE: 1.40,
162
- /* 卡顿旋转 (鹿鹿 2026-04-26 锁定, 总周期 4.45s) */
163
- ROT_AMOUNT: 190, ROT_DIR: -1,
164
- T_SPIN: 1.40, OVERSHOOT: 8,
165
- T_HOLD_FAR: 0.85, T_BACK: 1.00, T_HOLD_HOME: 1.20,
166
- /* 大呼吸 */
167
- SCALE_LOW: 0.94, SCALE_HIGH: 1.05, BREATH_PERIOD: 1.6,
168
- /* 字符切换 (角速度阈值, 大角度 — 旋转一周才换 1-2 次) */
169
- SWITCH_DEG_L: 96, SWITCH_DEG_R: 96, SWITCH_JITTER: 0.25,
170
- SWITCH_SOFT_MS: 90,
171
- /* 流动渐变 */
172
- FLOW_PERIOD: 3.0, FLOW_AMP: 4
173
- };
174
-
175
- /* 字符切换状态 */
176
- let lastLeft = 'gt', lastRight = 'und';
177
- let switchAccumL = 0, switchAccumR = 0;
178
- let nextThresholdL = P.SWITCH_DEG_L;
179
- let nextThresholdR = P.SWITCH_DEG_R;
180
- let switchStartL = -Infinity;
181
- let switchStartR = -Infinity;
182
- let rngState = 0xC10D1E;
183
-
184
- function rand() {
185
- rngState = (rngState * 1664525 + 1013904223) >>> 0;
186
- return rngState / 4294967296;
187
- }
188
- function choose(list) { return list[Math.floor(rand() * list.length) % list.length]; }
189
- function jitterFactor(j) { return 1 + (rand() * 2 - 1) * j; }
190
- function pairKey(l, r) { return `${l}|${r}`; }
191
- const A_PAIR_KEYS = new Set(A_PAIRS.map(([l, r]) => pairKey(l, r)));
192
-
193
- function currentPairIsA() {
194
- return A_PAIR_KEYS.has(pairKey(lastLeft, lastRight));
195
- }
196
-
197
- function getPhaseInfo(t) {
198
- const cycle = P.T_SPIN + P.T_HOLD_FAR + P.T_BACK + P.T_HOLD_HOME;
199
- const phase = t % Math.max(cycle, 0.01);
200
- if (phase < P.T_SPIN) {
201
- return { name: 'spin', u: phase / Math.max(P.T_SPIN, 0.01), aOnly: phase / Math.max(P.T_SPIN, 0.01) > .80 };
202
- }
203
- if (phase < P.T_SPIN + P.T_HOLD_FAR) {
204
- return { name: 'holdFar', u: 1, aOnly: true };
205
- }
206
- if (phase < P.T_SPIN + P.T_HOLD_FAR + P.T_BACK) {
207
- const u = (phase - P.T_SPIN - P.T_HOLD_FAR) / Math.max(P.T_BACK, 0.01);
208
- return { name: 'back', u, aOnly: u > .75 };
209
- }
210
- return { name: 'holdHome', u: 1, aOnly: true };
211
- }
212
-
213
- function settleToA(now) {
214
- if (currentPairIsA()) return;
215
- const sameRight = A_PAIRS.filter(([l, r]) => r === lastRight && l !== lastLeft);
216
- const sameLeft = A_PAIRS.filter(([l, r]) => l === lastLeft && r !== lastRight);
217
- if (sameRight.length) {
218
- lastLeft = choose(sameRight)[0];
219
- switchStartL = now;
220
- } else if (sameLeft.length) {
221
- lastRight = choose(sameLeft)[1];
222
- switchStartR = now;
223
- } else {
224
- const [l, r] = choose(A_PAIRS);
225
- lastLeft = l;
226
- lastRight = r;
227
- switchStartL = now;
228
- switchStartR = now;
229
- }
230
- }
231
-
232
- /* 选 token: 另一只眼固定, 从 A/B 组合池中过滤候选; A-only 区间只用 A 档. */
233
- function pickToken(side, prev, otherEye, aOnly) {
234
- const pool = aOnly ? A_PAIRS : ACTIVE_PAIRS;
235
- const candidates = pool
236
- .filter(([l, r]) => side === 'left' ? r === otherEye : l === otherEye)
237
- .map(([l, r]) => side === 'left' ? l : r)
238
- .filter(token => token !== prev && token !== otherEye);
239
- if (candidates.length) return choose(candidates);
240
- return prev;
241
- }
242
-
243
- function switchSoft(startMs, nowMs) {
244
- const p = Math.min(1, Math.max(0, (nowMs - startMs) / P.SWITCH_SOFT_MS));
245
- const e = easeOutCubic(p);
246
- return {
247
- sx: 1.025 - 0.025 * e,
248
- sy: 0.56 + 0.44 * e,
249
- opacity: 0.76 + 0.24 * e
250
- };
251
- }
252
-
253
- /* easing */
254
- function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
255
- function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
256
- function spinOutWithOvershoot(k, overshootRatio) {
257
- if (k <= 0.78) {
258
- const k2 = k / 0.78;
259
- return (1 + overshootRatio) * easeOutCubic(k2);
260
- } else {
261
- const k2 = (k - 0.78) / 0.22;
262
- return (1 + overshootRatio) - overshootRatio * easeOutCubic(k2);
263
- }
264
- }
265
-
266
- /* 主旋转: 周期内分四段 */
267
- function getRotation(t) {
268
- const cycle = P.T_SPIN + P.T_HOLD_FAR + P.T_BACK + P.T_HOLD_HOME;
269
- const phase = t % Math.max(cycle, 0.01);
270
- const amount = P.ROT_AMOUNT * P.ROT_DIR;
271
- const overshootRatio = P.OVERSHOOT / Math.max(P.ROT_AMOUNT, 0.01);
272
-
273
- if (phase < P.T_SPIN) {
274
- return amount * spinOutWithOvershoot(phase / P.T_SPIN, overshootRatio);
275
- } else if (phase < P.T_SPIN + P.T_HOLD_FAR) {
276
- return amount;
277
- } else if (phase < P.T_SPIN + P.T_HOLD_FAR + P.T_BACK) {
278
- const k = (phase - P.T_SPIN - P.T_HOLD_FAR) / P.T_BACK;
279
- return amount * (1 - easeInOutCubic(k));
280
- } else {
281
- return 0;
282
- }
283
- }
284
-
285
- /* 初始字符 */
286
- applyToken(eyeL, 9, 12, lastLeft);
287
- applyToken(eyeR, 15, 12, lastRight);
288
-
289
- let prevAngle = 0;
290
- let lastT = performance.now();
291
- const startT = performance.now();
292
-
293
- function loop(now) {
294
- const dt = (now - lastT) / 1000;
295
- lastT = now;
296
- const t = (now - startT) / 1000;
297
- const phaseInfo = getPhaseInfo(t);
298
-
299
- /* 卡顿旋转 */
300
- const angleDeg = getRotation(t);
301
- /* 角速度 (用于字符切换累积) */
302
- const angularVelDeg = Math.abs(angleDeg - prevAngle) / Math.max(dt, 0.001);
303
- prevAngle = angleDeg;
304
-
305
- /* 大幅呼吸 */
306
- const phase = (Math.cos(2 * Math.PI * t / Math.max(P.BREATH_PERIOD, 0.01)) + 1) / 2;
307
- const scale = P.SCALE_LOW + (P.SCALE_HIGH - P.SCALE_LOW) * phase;
308
-
309
- /* 应用 cloud transform */
310
- cloudGroup.setAttribute(
311
- 'transform',
312
- `translate(12 12) rotate(${angleDeg.toFixed(3)}) scale(${scale.toFixed(4)}) translate(-12 -12)`
313
- );
314
-
315
- /* 眼睛 group: 只 scale, 字符直立 */
316
- eyeGroup.setAttribute(
317
- 'transform',
318
- `translate(12 12) scale(${scale.toFixed(4)}) translate(-12 -12)`
319
- );
320
-
321
- /* ⭐ 字符联动切换: 累积转过的角度, 到阈值才切换 */
322
- if (phaseInfo.aOnly) settleToA(now);
323
- const angleStep = angularVelDeg * dt;
324
- switchAccumL += angleStep;
325
- switchAccumR += angleStep;
326
- if (switchAccumL >= nextThresholdL) {
327
- lastLeft = pickToken('left', lastLeft, lastRight, phaseInfo.aOnly);
328
- switchStartL = now;
329
- switchAccumL = 0;
330
- nextThresholdL = P.SWITCH_DEG_L * jitterFactor(P.SWITCH_JITTER);
331
- }
332
- if (switchAccumR >= nextThresholdR) {
333
- lastRight = pickToken('right', lastRight, lastLeft, phaseInfo.aOnly);
334
- switchStartR = now;
335
- switchAccumR = 0;
336
- nextThresholdR = P.SWITCH_DEG_R * jitterFactor(P.SWITCH_JITTER);
337
- }
338
-
339
- const softL = switchSoft(switchStartL, now);
340
- const softR = switchSoft(switchStartR, now);
341
- applyToken(eyeL, 9, 12, lastLeft, softL.sx, softL.sy, softL.opacity);
342
- applyToken(eyeR, 15, 12, lastRight, softR.sx, softR.sy, softR.opacity);
343
-
344
- /* 流动渐变 (stroke 用 url(#eye-grad), 流动让描边色微妙变化) */
345
- const flowOffset = Math.sin(2 * Math.PI * t / Math.max(P.FLOW_PERIOD, 0.01)) * P.FLOW_AMP;
346
- eyeGrad.setAttribute('y1', (3 + flowOffset).toFixed(2));
347
- eyeGrad.setAttribute('y2', (21 + flowOffset).toFixed(2));
348
-
349
- requestAnimationFrame(loop);
350
- }
351
-
352
- requestAnimationFrame(loop);
353
- ]]></script>
354
- </svg>
1
+ <svg id="cloudling" xmlns="http://www.w3.org/2000/svg" viewBox="-6 -6 36 36" preserveAspectRatio="xMidYMid meet">
2
+ <style><![CDATA[
3
+ .edge-rim-tight { stroke-opacity: .9; }
4
+ .edge-rim-soft { stroke-opacity: .75; }
5
+ .edge-contact { stroke-opacity: .9; }
6
+ @media (prefers-color-scheme: light) {
7
+ .edge-rim-tight { stroke-opacity: .95; }
8
+ .edge-rim-soft { stroke-opacity: .85; }
9
+ .edge-contact { stroke-opacity: .95; }
10
+ .cloud-drop-shadow { flood-opacity: .13; }
11
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
12
+ .food-cloud > path {
13
+ stroke: #5870E8 !important;
14
+ stroke-width: 1.2 !important;
15
+ stroke-opacity: .9 !important;
16
+ vector-effect: non-scaling-stroke !important;
17
+ }
18
+ }
19
+ ]]></style>
20
+ <defs>
21
+ <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
22
+ <stop offset="0" stop-color="#93c5fd"/>
23
+ <stop offset="0.5" stop-color="#60a5fa"/>
24
+ <stop offset="1" stop-color="#3b82f6"/>
25
+ </linearGradient>
26
+ <linearGradient id="eye-grad" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
27
+ <stop offset="0" stop-color="#B1A7FF"/>
28
+ <stop offset="0.5" stop-color="#7A9DFF"/>
29
+ <stop offset="1" stop-color="#3941FF"/>
30
+ </linearGradient>
31
+ <radialGradient id="hi-grad" cx="0.45" cy="0.25" r="0.55">
32
+ <stop offset="0" stop-color="#93c5fd" stop-opacity="0.9"/>
33
+ <stop offset="1" stop-color="#93c5fd" stop-opacity="0"/>
34
+ </radialGradient>
35
+ <filter id="drop" x="-40%" y="-40%" width="180%" height="180%">
36
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.22"/>
37
+ </filter>
38
+ <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
39
+ <feGaussianBlur stdDeviation="0.16"/>
40
+ </filter>
41
+ <path id="cloud-shape" 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"/>
42
+ <clipPath id="cloud-clip">
43
+ <use href="#cloud-shape"/>
44
+ </clipPath>
45
+ </defs>
46
+
47
+ <!-- 云朵 layer (rotate + scale, 配置不变 — drop shadow + radial highlight + body-grad) -->
48
+ <g id="cloud-group">
49
+ <g id="edge-system">
50
+ <use class="edge-contact" href="#cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.4" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#edge-blur)"/>
51
+ <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="1.2" stroke-linejoin="round" filter="url(#edge-blur)"/>
52
+ <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.8" stroke-linejoin="round"/>
53
+ </g>
54
+ <g filter="url(#drop)">
55
+ <use href="#cloud-shape" fill="url(#body-grad)"/>
56
+ <rect x="1" y="2" width="22" height="11" fill="url(#hi-grad)" clip-path="url(#cloud-clip)"/>
57
+ </g>
58
+ </g>
59
+
60
+ <!-- 眼睛 layer (只 scale 不 rotate, 字符直立; d/fill/stroke 由 JS 实时填充) -->
61
+ <g id="eye-group">
62
+ <path id="eye-l-path" stroke-linecap="round" stroke-linejoin="round"/>
63
+ <path id="eye-r-path" stroke-linecap="round" stroke-linejoin="round"/>
64
+ </g>
65
+
66
+ <script type="application/ecmascript"><![CDATA[
67
+ const cloudGroup = document.getElementById('cloud-group');
68
+ const eyeGroup = document.getElementById('eye-group');
69
+ const eyeL = document.getElementById('eye-l-path');
70
+ const eyeR = document.getElementById('eye-r-path');
71
+ const eyeGrad = document.getElementById('eye-grad');
72
+
73
+ /* token 库保留原版 8 个; 组合池过滤纯随机撞出的丑 pair. */
74
+ const TOKENS = ['gt', 'lt', 'und', 'colon', 'eq', 'plus', 'slash', 'bslash'];
75
+ const A_PAIRS = [
76
+ ['lt', 'gt'], ['lt', 'eq'], ['gt', 'eq'], ['eq', 'gt'], ['eq', 'lt'],
77
+ ['gt', 'und'], ['lt', 'und'], ['slash', 'und'], ['und', 'bslash'],
78
+ ['eq', 'slash'], ['eq', 'bslash'],
79
+ ];
80
+ const B_PAIRS = [
81
+ ['colon', 'gt'], ['colon', 'slash'], ['colon', 'bslash'], ['colon', 'und'],
82
+ ['plus', 'und'], ['slash', 'plus'], ['bslash', 'plus'],
83
+ ];
84
+ const ACTIVE_PAIRS = [...A_PAIRS, ...A_PAIRS, ...A_PAIRS, ...B_PAIRS];
85
+
86
+ /* token builder: 返回 { d, mode, sw }. mode='stroke' 用描边, mode='fill' 用填充 (圆点类). */
87
+ function tok(d, mode, sw) { return { d, mode, sw }; }
88
+
89
+ const TOKEN_BUILDERS = {
90
+ /* > chevron (描边折角) */
91
+ gt: (cx, cy) => tok(
92
+ `M ${cx-0.78} ${cy-1.58} L ${cx+0.74} ${cy} L ${cx-0.78} ${cy+1.58}`,
93
+ 'stroke', 1.16
94
+ ),
95
+ /* < chevron (反向) */
96
+ lt: (cx, cy) => tok(
97
+ `M ${cx+0.78} ${cy-1.58} L ${cx-0.74} ${cy} L ${cx+0.78} ${cy+1.58}`,
98
+ 'stroke', 1.16
99
+ ),
100
+ /* _ underscore (下划线) */
101
+ und: (cx, cy) => tok(
102
+ `M ${cx-1.18} ${cy+1.42} L ${cx+1.18} ${cy+1.42}`,
103
+ 'stroke', 1.10
104
+ ),
105
+ /* : colon (上下两个圆点, fill 实心) */
106
+ colon: (cx, cy) => {
107
+ const r = 0.64;
108
+ return tok(
109
+ `M ${cx-r} ${cy-0.94} A ${r} ${r} 0 1 0 ${cx+r} ${cy-0.94} A ${r} ${r} 0 1 0 ${cx-r} ${cy-0.94} Z ` +
110
+ `M ${cx-r} ${cy+0.94} A ${r} ${r} 0 1 0 ${cx+r} ${cy+0.94} A ${r} ${r} 0 1 0 ${cx-r} ${cy+0.94} Z`,
111
+ 'fill', 0
112
+ );
113
+ },
114
+ /* = 两道横线 (多线交叉降到 0.9 防糊) */
115
+ eq: (cx, cy) => tok(
116
+ `M ${cx-1.08} ${cy-0.72} L ${cx+1.08} ${cy-0.72} ` +
117
+ `M ${cx-1.08} ${cy+0.72} L ${cx+1.08} ${cy+0.72}`,
118
+ 'stroke', 0.84
119
+ ),
120
+ /* + 十字 (多线交叉 0.9) */
121
+ plus: (cx, cy) => tok(
122
+ `M ${cx-0.96} ${cy} L ${cx+0.96} ${cy} ` +
123
+ `M ${cx} ${cy-0.96} L ${cx} ${cy+0.96}`,
124
+ 'stroke', 0.82
125
+ ),
126
+ /* / slash 斜杠 (左下到右上) */
127
+ slash: (cx, cy) => tok(
128
+ `M ${cx-0.92} ${cy+1.48} L ${cx+0.92} ${cy-1.48}`,
129
+ 'stroke', 1.16
130
+ ),
131
+ /* \ bslash 反斜杠 (左上到右下) */
132
+ bslash: (cx, cy) => tok(
133
+ `M ${cx-0.92} ${cy-1.48} L ${cx+0.92} ${cy+1.48}`,
134
+ 'stroke', 1.16
135
+ )
136
+ };
137
+
138
+ /* 把 token 应用到 path 元素 (每次切换调一次, 切 fill/stroke 模式 + d + stroke-width + 字符尺寸缩放) */
139
+ function applyToken(el, cx, cy, tokenId, sx = 1, sy = 1, opacity = 1) {
140
+ const t = TOKEN_BUILDERS[tokenId](cx, cy);
141
+ el.setAttribute('d', t.d);
142
+ if (t.mode === 'fill') {
143
+ el.setAttribute('fill', 'url(#eye-grad)');
144
+ el.setAttribute('stroke', 'none');
145
+ el.removeAttribute('stroke-width');
146
+ } else {
147
+ el.setAttribute('fill', 'none');
148
+ el.setAttribute('stroke', 'url(#eye-grad)');
149
+ el.setAttribute('stroke-width', t.sw);
150
+ }
151
+ el.setAttribute('opacity', opacity.toFixed(3));
152
+ /* 以字符中心为锚整体缩放 (stroke 跟随放大, 视觉一致) */
153
+ el.setAttribute(
154
+ 'transform',
155
+ `translate(${cx} ${cy}) scale(${(P.EYE_SIZE * sx).toFixed(3)} ${(P.EYE_SIZE * sy).toFixed(3)}) translate(${-cx} ${-cy})`
156
+ );
157
+ }
158
+
159
+ const P = {
160
+ /* 字符尺寸 (鹿鹿 2026-04-26 锁定 1.40×) */
161
+ EYE_SIZE: 1.40,
162
+ /* 卡顿旋转 (鹿鹿 2026-04-26 锁定, 总周期 4.45s) */
163
+ ROT_AMOUNT: 190, ROT_DIR: -1,
164
+ T_SPIN: 1.40, OVERSHOOT: 8,
165
+ T_HOLD_FAR: 0.85, T_BACK: 1.00, T_HOLD_HOME: 1.20,
166
+ /* 大呼吸 */
167
+ SCALE_LOW: 0.94, SCALE_HIGH: 1.05, BREATH_PERIOD: 1.6,
168
+ /* 字符切换 (角速度阈值, 大角度 — 旋转一周才换 1-2 次) */
169
+ SWITCH_DEG_L: 96, SWITCH_DEG_R: 96, SWITCH_JITTER: 0.25,
170
+ SWITCH_SOFT_MS: 90,
171
+ /* 流动渐变 */
172
+ FLOW_PERIOD: 3.0, FLOW_AMP: 4
173
+ };
174
+
175
+ /* 字符切换状态 */
176
+ let lastLeft = 'gt', lastRight = 'und';
177
+ let switchAccumL = 0, switchAccumR = 0;
178
+ let nextThresholdL = P.SWITCH_DEG_L;
179
+ let nextThresholdR = P.SWITCH_DEG_R;
180
+ let switchStartL = -Infinity;
181
+ let switchStartR = -Infinity;
182
+ let rngState = 0xC10D1E;
183
+
184
+ function rand() {
185
+ rngState = (rngState * 1664525 + 1013904223) >>> 0;
186
+ return rngState / 4294967296;
187
+ }
188
+ function choose(list) { return list[Math.floor(rand() * list.length) % list.length]; }
189
+ function jitterFactor(j) { return 1 + (rand() * 2 - 1) * j; }
190
+ function pairKey(l, r) { return `${l}|${r}`; }
191
+ const A_PAIR_KEYS = new Set(A_PAIRS.map(([l, r]) => pairKey(l, r)));
192
+
193
+ function currentPairIsA() {
194
+ return A_PAIR_KEYS.has(pairKey(lastLeft, lastRight));
195
+ }
196
+
197
+ function getPhaseInfo(t) {
198
+ const cycle = P.T_SPIN + P.T_HOLD_FAR + P.T_BACK + P.T_HOLD_HOME;
199
+ const phase = t % Math.max(cycle, 0.01);
200
+ if (phase < P.T_SPIN) {
201
+ return { name: 'spin', u: phase / Math.max(P.T_SPIN, 0.01), aOnly: phase / Math.max(P.T_SPIN, 0.01) > .80 };
202
+ }
203
+ if (phase < P.T_SPIN + P.T_HOLD_FAR) {
204
+ return { name: 'holdFar', u: 1, aOnly: true };
205
+ }
206
+ if (phase < P.T_SPIN + P.T_HOLD_FAR + P.T_BACK) {
207
+ const u = (phase - P.T_SPIN - P.T_HOLD_FAR) / Math.max(P.T_BACK, 0.01);
208
+ return { name: 'back', u, aOnly: u > .75 };
209
+ }
210
+ return { name: 'holdHome', u: 1, aOnly: true };
211
+ }
212
+
213
+ function settleToA(now) {
214
+ if (currentPairIsA()) return;
215
+ const sameRight = A_PAIRS.filter(([l, r]) => r === lastRight && l !== lastLeft);
216
+ const sameLeft = A_PAIRS.filter(([l, r]) => l === lastLeft && r !== lastRight);
217
+ if (sameRight.length) {
218
+ lastLeft = choose(sameRight)[0];
219
+ switchStartL = now;
220
+ } else if (sameLeft.length) {
221
+ lastRight = choose(sameLeft)[1];
222
+ switchStartR = now;
223
+ } else {
224
+ const [l, r] = choose(A_PAIRS);
225
+ lastLeft = l;
226
+ lastRight = r;
227
+ switchStartL = now;
228
+ switchStartR = now;
229
+ }
230
+ }
231
+
232
+ /* 选 token: 另一只眼固定, 从 A/B 组合池中过滤候选; A-only 区间只用 A 档. */
233
+ function pickToken(side, prev, otherEye, aOnly) {
234
+ const pool = aOnly ? A_PAIRS : ACTIVE_PAIRS;
235
+ const candidates = pool
236
+ .filter(([l, r]) => side === 'left' ? r === otherEye : l === otherEye)
237
+ .map(([l, r]) => side === 'left' ? l : r)
238
+ .filter(token => token !== prev && token !== otherEye);
239
+ if (candidates.length) return choose(candidates);
240
+ return prev;
241
+ }
242
+
243
+ function switchSoft(startMs, nowMs) {
244
+ const p = Math.min(1, Math.max(0, (nowMs - startMs) / P.SWITCH_SOFT_MS));
245
+ const e = easeOutCubic(p);
246
+ return {
247
+ sx: 1.025 - 0.025 * e,
248
+ sy: 0.56 + 0.44 * e,
249
+ opacity: 0.76 + 0.24 * e
250
+ };
251
+ }
252
+
253
+ /* easing */
254
+ function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
255
+ function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
256
+ function spinOutWithOvershoot(k, overshootRatio) {
257
+ if (k <= 0.78) {
258
+ const k2 = k / 0.78;
259
+ return (1 + overshootRatio) * easeOutCubic(k2);
260
+ } else {
261
+ const k2 = (k - 0.78) / 0.22;
262
+ return (1 + overshootRatio) - overshootRatio * easeOutCubic(k2);
263
+ }
264
+ }
265
+
266
+ /* 主旋转: 周期内分四段 */
267
+ function getRotation(t) {
268
+ const cycle = P.T_SPIN + P.T_HOLD_FAR + P.T_BACK + P.T_HOLD_HOME;
269
+ const phase = t % Math.max(cycle, 0.01);
270
+ const amount = P.ROT_AMOUNT * P.ROT_DIR;
271
+ const overshootRatio = P.OVERSHOOT / Math.max(P.ROT_AMOUNT, 0.01);
272
+
273
+ if (phase < P.T_SPIN) {
274
+ return amount * spinOutWithOvershoot(phase / P.T_SPIN, overshootRatio);
275
+ } else if (phase < P.T_SPIN + P.T_HOLD_FAR) {
276
+ return amount;
277
+ } else if (phase < P.T_SPIN + P.T_HOLD_FAR + P.T_BACK) {
278
+ const k = (phase - P.T_SPIN - P.T_HOLD_FAR) / P.T_BACK;
279
+ return amount * (1 - easeInOutCubic(k));
280
+ } else {
281
+ return 0;
282
+ }
283
+ }
284
+
285
+ /* 初始字符 */
286
+ applyToken(eyeL, 9, 12, lastLeft);
287
+ applyToken(eyeR, 15, 12, lastRight);
288
+
289
+ let prevAngle = 0;
290
+ let lastT = performance.now();
291
+ const startT = performance.now();
292
+
293
+ function loop(now) {
294
+ const dt = (now - lastT) / 1000;
295
+ lastT = now;
296
+ const t = (now - startT) / 1000;
297
+ const phaseInfo = getPhaseInfo(t);
298
+
299
+ /* 卡顿旋转 */
300
+ const angleDeg = getRotation(t);
301
+ /* 角速度 (用于字符切换累积) */
302
+ const angularVelDeg = Math.abs(angleDeg - prevAngle) / Math.max(dt, 0.001);
303
+ prevAngle = angleDeg;
304
+
305
+ /* 大幅呼吸 */
306
+ const phase = (Math.cos(2 * Math.PI * t / Math.max(P.BREATH_PERIOD, 0.01)) + 1) / 2;
307
+ const scale = P.SCALE_LOW + (P.SCALE_HIGH - P.SCALE_LOW) * phase;
308
+
309
+ /* 应用 cloud transform */
310
+ cloudGroup.setAttribute(
311
+ 'transform',
312
+ `translate(12 12) rotate(${angleDeg.toFixed(3)}) scale(${scale.toFixed(4)}) translate(-12 -12)`
313
+ );
314
+
315
+ /* 眼睛 group: 只 scale, 字符直立 */
316
+ eyeGroup.setAttribute(
317
+ 'transform',
318
+ `translate(12 12) scale(${scale.toFixed(4)}) translate(-12 -12)`
319
+ );
320
+
321
+ /* ⭐ 字符联动切换: 累积转过的角度, 到阈值才切换 */
322
+ if (phaseInfo.aOnly) settleToA(now);
323
+ const angleStep = angularVelDeg * dt;
324
+ switchAccumL += angleStep;
325
+ switchAccumR += angleStep;
326
+ if (switchAccumL >= nextThresholdL) {
327
+ lastLeft = pickToken('left', lastLeft, lastRight, phaseInfo.aOnly);
328
+ switchStartL = now;
329
+ switchAccumL = 0;
330
+ nextThresholdL = P.SWITCH_DEG_L * jitterFactor(P.SWITCH_JITTER);
331
+ }
332
+ if (switchAccumR >= nextThresholdR) {
333
+ lastRight = pickToken('right', lastRight, lastLeft, phaseInfo.aOnly);
334
+ switchStartR = now;
335
+ switchAccumR = 0;
336
+ nextThresholdR = P.SWITCH_DEG_R * jitterFactor(P.SWITCH_JITTER);
337
+ }
338
+
339
+ const softL = switchSoft(switchStartL, now);
340
+ const softR = switchSoft(switchStartR, now);
341
+ applyToken(eyeL, 9, 12, lastLeft, softL.sx, softL.sy, softL.opacity);
342
+ applyToken(eyeR, 15, 12, lastRight, softR.sx, softR.sy, softR.opacity);
343
+
344
+ /* 流动渐变 (stroke 用 url(#eye-grad), 流动让描边色微妙变化) */
345
+ const flowOffset = Math.sin(2 * Math.PI * t / Math.max(P.FLOW_PERIOD, 0.01)) * P.FLOW_AMP;
346
+ eyeGrad.setAttribute('y1', (3 + flowOffset).toFixed(2));
347
+ eyeGrad.setAttribute('y2', (21 + flowOffset).toFixed(2));
348
+
349
+ requestAnimationFrame(loop);
350
+ }
351
+
352
+ requestAnimationFrame(loop);
353
+ ]]></script>
354
+ </svg>