@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,692 +1,692 @@
1
- <svg id="cloudling" xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="-12 -12 48 48" preserveAspectRatio="xMidYMid meet">
2
- <style><![CDATA[
3
- .phase {
4
- font-family: ui-monospace, monospace;
5
- font-size: 11px;
6
- color: #888;
7
- text-align: center;
8
- }
9
- .phase span { color: #7A9DFF; font-weight: 600; }
10
- .edge-rim-tight { stroke-opacity: .65; }
11
- .edge-rim-soft { stroke-opacity: .50; }
12
- .edge-contact { stroke-opacity: .65; }
13
- @media (prefers-color-scheme: light) {
14
- .phase span { color: #3941FF; }
15
- .edge-rim-tight { stroke-opacity: .75; }
16
- .edge-rim-soft { stroke-opacity: .60; }
17
- .edge-contact { stroke-opacity: .75; }
18
- .cloud-drop-shadow { flood-opacity: .13; }
19
- .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
20
- .food-cloud > path {
21
- stroke: #5870E8 !important;
22
- stroke-width: 1.2 !important;
23
- stroke-opacity: .9 !important;
24
- vector-effect: non-scaling-stroke !important;
25
- }
26
- }
27
- ]]></style>
28
- <defs>
29
- <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
30
- <stop offset="0" stop-color="#f0f9ff"/>
31
- <stop offset="0.5" stop-color="#e0f2fe"/>
32
- <stop offset="1" stop-color="#bae6fd"/>
33
- </linearGradient>
34
- <linearGradient id="eye-grad" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
35
- <stop offset="0" stop-color="#B1A7FF"/>
36
- <stop offset="0.5" stop-color="#7A9DFF"/>
37
- <stop offset="1" stop-color="#3941FF"/>
38
- </linearGradient>
39
- <radialGradient id="hi-grad" cx="0.45" cy="0.25" r="0.55">
40
- <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.9"/>
41
- <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
42
- </radialGradient>
43
- <filter id="drop" x="-40%" y="-40%" width="180%" height="180%">
44
- <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
45
- </filter>
46
- <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
47
- <feGaussianBlur stdDeviation="0.16"/>
48
- </filter>
49
- <radialGradient id="lens-glass-grad" cx="0.34" cy="0.22" r="0.86">
50
- <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.72"/>
51
- <stop offset="0.48" stop-color="#DDE7FF" stop-opacity="0.35"/>
52
- <stop offset="1" stop-color="#89A7FF" stop-opacity="0.12"/>
53
- </radialGradient>
54
- <linearGradient id="lens-rim-grad" x1="-5" y1="-5" x2="5" y2="5" gradientUnits="userSpaceOnUse">
55
- <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.82"/>
56
- <stop offset="0.44" stop-color="#B9C6DC" stop-opacity="0.54"/>
57
- <stop offset="1" stop-color="#6F83B7" stop-opacity="0.42"/>
58
- </linearGradient>
59
- <linearGradient id="scan-grad" x1="-4" y1="0" x2="4" y2="0" gradientUnits="userSpaceOnUse">
60
- <stop offset="0" stop-color="#89F2FF" stop-opacity="0"/>
61
- <stop offset="0.5" stop-color="#EAFBFF" stop-opacity="0.72"/>
62
- <stop offset="1" stop-color="#89F2FF" stop-opacity="0"/>
63
- </linearGradient>
64
- <filter id="lens-soft-glow" x="-60%" y="-60%" width="220%" height="220%">
65
- <feGaussianBlur stdDeviation="0.45" result="glow"/>
66
- <feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>
67
- </filter>
68
- <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"/>
69
- <clipPath id="cloud-clip"><use href="#cloud-shape"/></clipPath>
70
-
71
- <!-- 镜片 clip 中心化到 (0,0), lens-group 通过 transform 平移 -->
72
- <clipPath id="lens-clip"><circle cx="0" cy="0" r="4.75"/></clipPath>
73
-
74
- <!-- 轻玻璃镜框 (中心化到 0,0) -->
75
- <g id="lens-frame">
76
- <circle cx="0" cy="0" r="5.05" stroke="url(#lens-rim-grad)" stroke-width="0.34" fill="none" opacity="0.9"/>
77
- <circle cx="0" cy="0" r="4.55" stroke="#ffffff" stroke-width="0.10" stroke-opacity="0.34" fill="none"/>
78
- <path id="lens-frame-highlight" d="M -1.728 -4.745 A 5.05 5.05 0 0 1 1.728 -4.745"
79
- stroke="#ffffff" stroke-width="0.22" stroke-opacity="0.78"
80
- stroke-linecap="round" fill="none"/>
81
- </g>
82
- </defs>
83
-
84
- <g id="runtime-layer">
85
- <!-- 云朵 layer (rotate + scale 围绕 12,12) -->
86
- <g id="cloud-group">
87
- <g id="edge-system">
88
- <use class="edge-contact" href="#cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#edge-blur)"/>
89
- <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#edge-blur)"/>
90
- <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
91
- </g>
92
- <g filter="url(#drop)">
93
- <use href="#cloud-shape" fill="url(#body-grad)"/>
94
- <rect x="1" y="2" width="22" height="11" fill="url(#hi-grad)" clip-path="url(#cloud-clip)"/>
95
- </g>
96
- </g>
97
-
98
- <!-- 眼睛 layer (只 scale, 不 rotate, 眼睛保持竖直 — typing 同款分层)
99
- 每只眼 = capsule + dot 两个元素, opacity 互斥 fade (不再 morph 形状, 避免横扁椭圆中间帧鬼畜) -->
100
- <g id="eye-group">
101
- <g id="eye-l-wrap" transform="translate(9 12)">
102
- <rect class="cap" x="-1.3" y="-2.5" width="2.6" height="5" rx="1.3" ry="1.3"
103
- fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
104
- <circle class="dot" cx="0" cy="0" r="1.35" fill="url(#eye-grad)"/>
105
- </g>
106
- <g id="eye-r-wrap" transform="translate(14.85 12)">
107
- <rect class="cap" x="-1.3" y="-2.5" width="2.6" height="5" rx="1.3" ry="1.3"
108
- fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
109
- <circle class="dot" cx="0" cy="0" r="1.35" fill="url(#eye-grad)"/>
110
- </g>
111
- </g>
112
-
113
- <!-- 镜片 layer (独立, transform 平移到 lensX/lensY) -->
114
- <g id="lens-group" transform="translate(15 12)">
115
- <circle id="lens-glow" cx="0" cy="0" r="5.05" fill="#7A9DFF" opacity="0.08" filter="url(#lens-soft-glow)"/>
116
- <circle id="lens-glass" cx="0" cy="0" r="4.75" fill="url(#lens-glass-grad)"/>
117
- <g clip-path="url(#lens-clip)">
118
- <g id="lens-content"></g>
119
- </g>
120
- <use href="#lens-frame"/>
121
- </g>
122
- </g>
123
-
124
- <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
125
- <g id="phaseLabel"></g>
126
- </g>
127
-
128
- <script type="application/ecmascript"><![CDATA[
129
- /* ───────────── 元素引用 ───────────── */
130
- const cloudGroup = document.getElementById('cloud-group');
131
- const eyeGroup = document.getElementById('eye-group');
132
- const eyeLWrap = document.getElementById('eye-l-wrap');
133
- const eyeRWrap = document.getElementById('eye-r-wrap');
134
- const eyeLCap = eyeLWrap.querySelector('.cap');
135
- const eyeLDot = eyeLWrap.querySelector('.dot');
136
- const eyeRCap = eyeRWrap.querySelector('.cap');
137
- const eyeRDot = eyeRWrap.querySelector('.dot');
138
- const lensGroup = document.getElementById('lens-group');
139
- const lensFrameHighlight = document.getElementById('lens-frame-highlight');
140
- const lensContent = document.getElementById('lens-content');
141
- const eyeGrad = document.getElementById('eye-grad');
142
- const phaseLabel = document.getElementById('phaseLabel');
143
-
144
- /* ───────────── 拉杆参数 ───────────── */
145
- const params = {
146
- total: 6.0,
147
- ratio: 3.00, // 定睛/滑动 = 2.25/0.75 = 3.00
148
- rot: 7.0,
149
- sh: 1.10,
150
- sl: 1.00,
151
- ba: 0.024,
152
- bper: 5.0,
153
- arc: 1.15,
154
- fa: 0.12,
155
- fp: 3.1,
156
- fh: 1.10,
157
- lensScale: 1.14,
158
- rightEyeX: 14.85,
159
- sweep: 1.72, // 单轮代码扫描总时长
160
- accel: 1.65, // 加速强度: 1=匀速, 高=末尾更快
161
- codeSteps: 9,
162
- codeDrift: 0.62,
163
- underBlinkPeriod: 3.6,
164
- underBlinkDur: 0.16,
165
- ph: 0.04,
166
- bd: 0.16,
167
- bm: 0.06, // 眨眼最低 sy (BLINK_MIN)
168
- hd: 1.25,
169
- dd: 6.0,
170
- gp: 5.0,
171
- ga: 9,
172
- pause: false
173
- };
174
-
175
- /* 段时长 (从 total + ratio 推) */
176
- function segments() {
177
- // 定睛 2 段 + 滑动 2 段, 满足: 2*focus + 2*transit = total, focus/transit = ratio
178
- const transit = params.total / (2 * (params.ratio + 1));
179
- const focus = transit * params.ratio;
180
- return { focus, transit };
181
- }
182
-
183
- /* ───────────── 工具函数 ───────────── */
184
- const lerp = (a, b, t) => a + (b - a) * t;
185
- const easeInOut = t => t * t * (3 - 2 * t);
186
- const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
187
-
188
- function getBodyBreath(t) {
189
- const period = Math.max(params.bper, 0.1);
190
- const p = ((t % period) + period) % period / period;
191
- // 稍快吸气、稍慢呼气,比正弦更像自然呼吸。
192
- const inhale = p < 0.42
193
- ? easeInOut(p / 0.42)
194
- : 1 - easeInOut((p - 0.42) / 0.58);
195
- const centered = inhale * 2 - 1;
196
- return {
197
- sx: 1 + centered * params.ba * 0.35,
198
- sy: 1 + centered * params.ba
199
- };
200
- }
201
-
202
- /* ───────────── token 库 (中心化到 0,0) ───────────── */
203
- const TOKENS = ['gt', 'lt', 'und', 'colon', 'eq', 'plus', 'slash', 'bslash'];
204
-
205
- /* token stroke 跟着 transform scale 放大, v2 降低线重, 给镜片内代码流留精细感。 */
206
- function tokenSVG(name) {
207
- switch (name) {
208
- case 'gt': return `<path d="M -0.8 -1.7 L 0.8 0 L -0.8 1.7" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round" fill="none"/>`;
209
- case 'lt': return `<path d="M 0.8 -1.7 L -0.8 0 L 0.8 1.7" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round" fill="none"/>`;
210
- case 'und': return `<line x1="-1.0" y1="1.0" x2="1.0" y2="1.0" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round"/>`;
211
- case 'colon': return `<circle cx="0" cy="-1.05" r="0.78" fill="url(#eye-grad)"/><circle cx="0" cy="1.05" r="0.78" fill="url(#eye-grad)"/>`;
212
- case 'eq': return `<line x1="-1.0" y1="-0.85" x2="1.0" y2="-0.85" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/><line x1="-1.0" y1="0.85" x2="1.0" y2="0.85" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/>`;
213
- case 'plus': return `<line x1="-1.0" y1="0" x2="1.0" y2="0" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/><line x1="0" y1="-1.0" x2="0" y2="1.0" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/>`;
214
- case 'slash': return `<path d="M 0.8 -1.5 L -0.8 1.5" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" fill="none"/>`;
215
- case 'bslash': return `<path d="M -0.8 -1.5 L 0.8 1.5" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" fill="none"/>`;
216
- }
217
- return '';
218
- }
219
-
220
- const LENS_MAG = 1.35;
221
-
222
- function contentOffsetForEye(eyeX, eyeY, lensX, lensY) {
223
- return {
224
- x: (eyeX - lensX) * LENS_MAG,
225
- y: (eyeY - lensY) * LENS_MAG
226
- };
227
- }
228
-
229
- /* capsule (默认中心化到 0,0), scale 只用于镜片内鱼眼放大 */
230
- function capsuleSVG(scale = 1, offset = { x: 0, y: 0 }) {
231
- const w = 2.6 * scale;
232
- const h = 5 * scale;
233
- const r = 1.3 * scale;
234
- return `<rect x="${offset.x - w/2}" y="${offset.y - h/2}" width="${w}" height="${h}" rx="${r}" ry="${r}" fill="url(#eye-grad)"/>`;
235
- }
236
-
237
- /* 每轮重新 pick token;入口固定 slash, 出口固定 bslash,中间允许重复以增加代码变化量。 */
238
- const MIDDLE_POOL = ['gt', 'lt', 'und', 'colon', 'eq', 'plus'];
239
- function pickFocusTokens() {
240
- const count = Math.max(5, Math.round(params.codeSteps || 9));
241
- const middleCount = count - 2;
242
- const middle = [];
243
- let prev = '';
244
- for (let i = 0; i < middleCount; i++) {
245
- let next = MIDDLE_POOL[Math.floor(Math.random() * MIDDLE_POOL.length)];
246
- if (next === prev) {
247
- next = MIDDLE_POOL[(MIDDLE_POOL.indexOf(next) + 1 + i) % MIDDLE_POOL.length];
248
- }
249
- middle.push(next);
250
- prev = next;
251
- }
252
- return ['slash', ...middle, 'bslash'];
253
- }
254
-
255
- let rightFocusTokens = pickFocusTokens();
256
- let leftFocusTokens = pickFocusTokens();
257
- let lastLoopId = -1;
258
-
259
- /* ───────────── 镜片下那只眼的内部时序 (纯代码流) ─────────────
260
- 单轮内部只切换 code token,不再回到 capsule,也不再眨眼。
261
- accel=1.0 → 等分; accel 越高, 前几个 token 停留越久, 后几个更快切完。 */
262
-
263
- const MORPH_IN_DUR = 0.13;
264
- const MORPH_OUT_DUR = 0.13;
265
- const INTER_BLINK_HOLD = 0.03;
266
-
267
- function getFocusCycleDuration() {
268
- return Math.max(params.sweep, 0.1);
269
- }
270
-
271
- function getTokenDurations(sweepDur, accel, count) {
272
- const steps = Math.max(1, count);
273
- if (steps === 1) return [sweepDur];
274
- const firstWeight = Math.max(accel, 1);
275
- const weights = Array.from({ length: steps }, (_, i) => {
276
- const p = i / (steps - 1);
277
- return firstWeight + (1 - firstWeight) * p;
278
- });
279
- const total = weights.reduce((sum, v) => sum + v, 0);
280
- return weights.map(v => sweepDur * v / total);
281
- }
282
-
283
- function blinkLerp(p, blinkMin) { // p 0..1, 闭+开: 1 → MIN → 1
284
- p = clamp(p, 0, 1);
285
- return p < 0.5
286
- ? lerp(1, blinkMin, easeInOut(p / 0.5))
287
- : lerp(blinkMin, 1, easeInOut((p - 0.5) / 0.5));
288
- }
289
-
290
- function getFocusEyeState(localT) {
291
- const sweepDur = params.sweep;
292
- const count = Math.max(5, Math.round(params.codeSteps || 9));
293
- const tokenDurs = getTokenDurations(sweepDur, params.accel, count);
294
- let end = 0;
295
- for (let i = 0; i < tokenDurs.length; i++) {
296
- end += tokenDurs[i];
297
- if (localT < end) return { stage: 'token', tokenIdx: i };
298
- }
299
- return { stage: 'token', tokenIdx: tokenDurs.length - 1 };
300
- }
301
-
302
- /* ───────────── 主状态 ───────────── */
303
- function getState(t) {
304
- const { transit } = segments();
305
- const focusCycle = Math.max(getFocusCycleDuration(), 0.1);
306
- const localT = ((t % focusCycle) + focusCycle) % focusCycle;
307
- let phase = 'left-focus';
308
- let lensX = 9;
309
- let lensY = 12;
310
- let cloudRot = -params.rot;
311
- let cloudScale = params.sh;
312
- const transitP = 0;
313
- const transitDir = 0;
314
- const motion = 0;
315
-
316
- // 镜片微浮动 (异步 sin)
317
- const nowSec = (lastWallTime || performance.now()) / 1000;
318
- lensY += params.fa * Math.sin(2 * Math.PI * nowSec / params.fp);
319
-
320
- return { phase, localT, focusDur: focusCycle, transit, lensX, lensY, cloudRot, cloudScale, transitP, transitDir, motion };
321
- }
322
-
323
- /* ───────────── 眼睛 capsule/dot opacity swap ─────────────
324
- 不再 morph 形状 (避免横扁椭圆中间帧 = 视觉旋转鬼畜)
325
- distance < hd: hidden (镜片覆盖)
326
- hd ≤ distance < SWITCH: capsule (远到看清, 镜片刚走 / 远端)
327
- distance ≥ SWITCH: dot (圆点眯眼, 离镜片远 = 被对方审视配合眯起)
328
- 切换瞬间 dot/capsule opacity 互换 (短窗口 fade) */
329
- const SWITCH_DIST = 4.0;
330
- const FADE_HALF = 0.25;
331
- function getEyeOpacities(eyeCx, lensX) {
332
- const d = Math.abs(lensX - eyeCx);
333
- const visibility = clamp((d - params.hd) / 0.5, 0, 1); // hd..(hd+0.5) 渐显
334
- // dot 占比: distance 越远越显, SWITCH 附近 fade swap
335
- const dotRatio = clamp((d - (SWITCH_DIST - FADE_HALF)) / (2 * FADE_HALF), 0, 1);
336
- return {
337
- capsuleOpacity: (1 - dotRatio) * visibility,
338
- dotOpacity: dotRatio * visibility
339
- };
340
- }
341
-
342
- /* ───────────── 渲染 ───────────── */
343
- let lastWallTime = 0;
344
-
345
- function applyEyeFollow(s) {
346
- const ldx = clamp((s.lensX - 9) / 6, -1, 1) * 0.22;
347
- const rdx = clamp((s.lensX - params.rightEyeX) / 6, -1, 1) * 0.22;
348
- const dy = clamp((s.lensY - 12) / 3, -1, 1) * 0.12;
349
- eyeLWrap.setAttribute('transform', `translate(9 12) translate(${ldx.toFixed(3)} ${dy.toFixed(3)})`);
350
- eyeRWrap.setAttribute('transform', `translate(${params.rightEyeX.toFixed(2)} 12) translate(${rdx.toFixed(3)} ${dy.toFixed(3)})`);
351
- }
352
-
353
- function render(t) {
354
- const s = getState(t);
355
- const breath = getBodyBreath(t);
356
-
357
- // 1. 云朵 transform (rotate + scale + body-only breathing around 12,12)
358
- cloudGroup.setAttribute('transform',
359
- `translate(12 12) scale(${s.cloudScale}) rotate(${s.cloudRot}) scale(${breath.sx.toFixed(4)} ${breath.sy.toFixed(4)}) translate(-12 -12)`);
360
-
361
- // 2. 眼睛 layer 跟着 scale 但不 rotate (typing 同款)
362
- eyeGroup.setAttribute('transform',
363
- `translate(12 12) scale(${s.cloudScale}) translate(-12 -12)`);
364
- applyEyeFollow(s);
365
-
366
- // 3. 双眼 capsule/dot opacity swap (不 morph 形状)
367
- const lO = getEyeOpacities(9, s.lensX);
368
- const rO = getEyeOpacities(params.rightEyeX, s.lensX);
369
- eyeLCap.setAttribute('opacity', lO.capsuleOpacity);
370
- eyeLDot.setAttribute('opacity', lO.dotOpacity);
371
- eyeRCap.setAttribute('opacity', rO.capsuleOpacity);
372
- eyeRDot.setAttribute('opacity', rO.dotOpacity);
373
-
374
- // 4. 镜片位置
375
- lensGroup.setAttribute('transform', `translate(${s.lensX} ${s.lensY}) scale(${params.lensScale.toFixed(3)})`);
376
- applyLensFrameHighlight(s);
377
-
378
- // 5. 镜片下内容
379
- let stageLabel = '';
380
- if (s.phase === 'right-focus' || s.phase === 'left-focus') {
381
- const focusState = getFocusEyeState(s.localT);
382
- const tokens = (s.phase === 'right-focus') ? rightFocusTokens : leftFocusTokens;
383
- const sourceX = s.phase === 'right-focus' ? params.rightEyeX : 9;
384
- renderLensContent(focusState, tokens, contentOffsetForEye(sourceX, 12, s.lensX, s.lensY));
385
- stageLabel = focusState.stage === 'token'
386
- ? `scan[${focusState.tokenIdx}]=${tokens[focusState.tokenIdx]}`
387
- : focusState.stage;
388
- } else {
389
- // transit: 按 lens 到最近眼的距离 fade capsule, 避免 focus 切 transit 瞬间内容突然清空
390
- const dL = Math.hypot(s.lensX - 9, s.lensY - 12);
391
- const dR = Math.hypot(s.lensX - params.rightEyeX, s.lensY - 12);
392
- const dMin = Math.min(dL, dR);
393
- // 距离 0-1: fade=1 (lens 在眼附近, 保持 capsule 满显)
394
- // 距离 1-2.5: fade 从 1 → 0 渐隐
395
- // 距离 > 2.5: 保留微弱 glint, 避免镜片滑动时完全空掉
396
- const fade = 1 - clamp((dMin - 1) / 1.5, 0, 1);
397
- if (fade < 0.02) {
398
- lensContent.innerHTML = renderLensTransitGlints(0.28 + s.motion * 0.12);
399
- stageLabel = `transit-glints (d=${dMin.toFixed(1)})`;
400
- } else {
401
- const sourceX = dL <= dR ? 9 : params.rightEyeX;
402
- const offset = contentOffsetForEye(sourceX, 12, s.lensX, s.lensY);
403
- lensContent.innerHTML = `
404
- <g opacity="${(fade * 0.56).toFixed(3)}">${renderLensCapsule(1, offset)}</g>
405
- ${renderLensTransitGlints(0.22 + (1 - fade) * 0.18)}
406
- `;
407
- stageLabel = `transit-fade=${fade.toFixed(2)} (d=${dMin.toFixed(1)})`;
408
- }
409
- }
410
-
411
- // 6. 流动渐变 (跟 idle/typing 同语言, 周期 + 幅度)
412
- const gradPhase = (lastWallTime / 1000) % params.gp / params.gp;
413
- const gShift = Math.sin(2 * Math.PI * gradPhase) * params.ga;
414
- eyeGrad.setAttribute('y1', 3 + gShift);
415
- eyeGrad.setAttribute('y2', 21 + gShift);
416
-
417
- // 7. 每 loop 重 pick 两组 focus token (各取中间 3 个随机)
418
- const focusCycle = Math.max(getFocusCycleDuration(), 0.1);
419
- const loopId = Math.floor(t / focusCycle);
420
- if (loopId !== lastLoopId) {
421
- leftFocusTokens = pickFocusTokens();
422
- lastLoopId = loopId;
423
- }
424
-
425
- // phase label
426
- phaseLabel.innerHTML = `<span>${s.phase}</span> · lens locked (${s.lensX.toFixed(2)}, ${s.lensY.toFixed(2)}) · focus cycle ${s.focusDur.toFixed(2)}s · cloud rot ${s.cloudRot.toFixed(1)}° scale ${s.cloudScale.toFixed(3)} · stage <b>${stageLabel}</b> · L=[${leftFocusTokens.join(',')}]`;
427
- }
428
-
429
- function applyLensFrameHighlight(s) {
430
- const strength = params.fh;
431
- const move = clamp(s.motion, 0, 1);
432
- const dir = s.transitDir || 0;
433
- const effective = Math.min(strength, 1.2);
434
- const centerDeg = dir * move * 5.0 * effective;
435
- const spanDeg = 34 + 14 * move * effective;
436
- const opacity = 0.64 + 0.12 * move * Math.min(strength, 1);
437
-
438
- lensFrameHighlight.setAttribute('d', frameHighlightArc(centerDeg, spanDeg));
439
- lensFrameHighlight.removeAttribute('transform');
440
- lensFrameHighlight.setAttribute('stroke-opacity', opacity.toFixed(3));
441
- }
442
-
443
- function frameHighlightArc(centerDeg, spanDeg) {
444
- const r = 5.05;
445
- const start = (-90 + centerDeg - spanDeg / 2) * Math.PI / 180;
446
- const end = (-90 + centerDeg + spanDeg / 2) * Math.PI / 180;
447
- const x1 = Math.cos(start) * r;
448
- const y1 = Math.sin(start) * r;
449
- const x2 = Math.cos(end) * r;
450
- const y2 = Math.sin(end) * r;
451
- return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A ${r} ${r} 0 0 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
452
- }
453
-
454
- /* ─── lens 内 capsule 渲染 (含 sy 闭眼幅度, 用于末段眨 2 下) ─── */
455
- function renderLensCapsule(sy, offset = { x: 0, y: 0 }) {
456
- const scale = LENS_MAG;
457
- const baseW = 2.6 * scale;
458
- const baseH = 5 * scale;
459
- const openness = clamp((sy - params.bm) / (1 - params.bm), 0, 1);
460
-
461
- const eyeMarkup = capsuleSVG(scale, offset);
462
-
463
- if (openness > 0.995) return eyeMarkup;
464
-
465
- // 玻璃镜片内不能用浅色眼皮遮盖, 否则眨眼时会露出矩形补丁。
466
- // 这里只裁剪 capsule 本体, 再叠一条闭眼线。
467
- const openEase = easeInOut(openness);
468
- const apertureW = lerp(baseW * 0.78, baseW + 0.42, Math.sqrt(openEase));
469
- const apertureH = lerp(0.16, baseH + 0.36, openEase);
470
- const hw = apertureW / 2;
471
- const hh = apertureH / 2;
472
- const c = hw * 0.74;
473
- const ox = offset.x;
474
- const oy = offset.y;
475
-
476
- const apertureD = `M ${ox - hw} ${oy} C ${ox - c} ${oy - hh} ${ox + c} ${oy - hh} ${ox + hw} ${oy} C ${ox + c} ${oy + hh} ${ox - c} ${oy + hh} ${ox - hw} ${oy} Z`;
477
- const upperEdgeD = `M ${ox - hw} ${oy} C ${ox - c} ${oy - hh} ${ox + c} ${oy - hh} ${ox + hw} ${oy}`;
478
- const lowerEdgeD = `M ${ox - hw} ${oy} C ${ox - c} ${oy + hh} ${ox + c} ${oy + hh} ${ox + hw} ${oy}`;
479
- const edgeOpacity = clamp((1 - openness) * 0.24, 0, 0.18);
480
- const lineOpacity = clamp((0.55 - openness) / 0.55, 0, 1);
481
- const lineW = baseW * 0.78;
482
-
483
- return `
484
- <defs>
485
- <clipPath id="lens-eye-aperture"><path d="${apertureD}"/></clipPath>
486
- </defs>
487
- <g clip-path="url(#lens-eye-aperture)">${eyeMarkup}</g>
488
- <g>
489
- <path d="${upperEdgeD}" fill="none" stroke="#aeb7c6" stroke-width="0.20" stroke-opacity="${edgeOpacity}" stroke-linecap="round"/>
490
- <path d="${lowerEdgeD}" fill="none" stroke="#aeb7c6" stroke-width="0.20" stroke-opacity="${edgeOpacity}" stroke-linecap="round"/>
491
- </g>
492
- <line x1="${ox - lineW/2}" y1="${oy}" x2="${ox + lineW/2}" y2="${oy}"
493
- stroke="url(#eye-grad)" stroke-width="0.42" stroke-linecap="round"
494
- opacity="${lineOpacity}" vector-effect="non-scaling-stroke"/>
495
- `;
496
- }
497
-
498
- /* ─── morphIn: capsule 收 + 渐隐, token 渐显 + 长出 (overlap 0.4-0.6 区间, 避免空白帧) ─── */
499
- function renderMorphIn(p, exitToken, offset = { x: 0, y: 0 }) {
500
- const scale = LENS_MAG;
501
- const e = easeInOut(clamp(p, 0, 1));
502
- let html = '';
503
- const ox = offset.x;
504
- const oy = offset.y;
505
-
506
- // capsule: e ∈ [0, 0.6] 收 + 渐隐
507
- if (e < 0.6) {
508
- const t = e / 0.6; // 0 → 1
509
- const sy = 1 - t * 0.7; // 1 → 0.3
510
- const sx = 1 - t * 0.7; // 1 → 0.3
511
- const op = 1 - t; // 1 → 0
512
- const w = 2.6 * scale * sx;
513
- const h = 5 * scale * sy;
514
- const r = Math.min(1.3 * scale * sx, h / 2);
515
- html += `<rect x="${ox - w/2}" y="${oy - h/2}" width="${w}" height="${h}" rx="${r}" ry="${r}" fill="url(#eye-grad)" opacity="${op}"/>`;
516
- }
517
-
518
- // token: e ∈ [0.4, 1.0] 从中心 burst
519
- if (e > 0.4) {
520
- const t = (e - 0.4) / 0.6; // 0 → 1
521
- const radius = 0.5 + t * 5; // 0.5 → 5.5
522
- html += `
523
- <defs><clipPath id="morph-clip"><circle cx="${ox}" cy="${oy}" r="${radius}"/></clipPath></defs>
524
- <g clip-path="url(#morph-clip)" transform="translate(${ox} ${oy}) scale(${scale})">${tokenSVG(exitToken)}</g>
525
- `;
526
- }
527
- return html;
528
- }
529
-
530
- /* ─── morphOut: token 收 + 渐隐, capsule 渐显 + 长出 (镜像 morphIn) ─── */
531
- function renderMorphOut(p, lastToken, offset = { x: 0, y: 0 }) {
532
- const scale = LENS_MAG;
533
- const e = easeInOut(clamp(p, 0, 1));
534
- let html = '';
535
- const ox = offset.x;
536
- const oy = offset.y;
537
-
538
- // token: e ∈ [0, 0.6] 收 + 渐隐
539
- if (e < 0.6) {
540
- const t = e / 0.6; // 0 → 1
541
- const radius = 5.5 - t * 5; // 5.5 → 0.5
542
- const op = 1 - t; // 1 → 0
543
- html += `
544
- <defs><clipPath id="morph-clip"><circle cx="${ox}" cy="${oy}" r="${radius}"/></clipPath></defs>
545
- <g clip-path="url(#morph-clip)" transform="translate(${ox} ${oy}) scale(${scale})" opacity="${op}">${tokenSVG(lastToken)}</g>
546
- `;
547
- }
548
-
549
- // capsule: e ∈ [0.4, 1.0] 从中心展开
550
- if (e > 0.4) {
551
- const t = (e - 0.4) / 0.6; // 0 → 1
552
- const sy = 0.3 + t * 0.7; // 0.3 → 1
553
- const sx = 0.3 + t * 0.7; // 0.3 → 1
554
- const op = t; // 0 → 1
555
- const w = 2.6 * scale * sx;
556
- const h = 5 * scale * sy;
557
- const r = Math.min(1.3 * scale * sx, h / 2);
558
- html += `<rect x="${ox - w/2}" y="${oy - h/2}" width="${w}" height="${h}" rx="${r}" ry="${r}" fill="url(#eye-grad)" opacity="${op}"/>`;
559
- }
560
- return html;
561
- }
562
-
563
- /* ─── 镜片内代码扫描流 ─── */
564
- function renderLensToken(name, offset = { x: 0, y: 0 }) {
565
- return `<g transform="translate(${offset.x} ${offset.y}) scale(${LENS_MAG})">${tokenSVG(name)}</g>`;
566
- }
567
-
568
- function renderMiniToken(name, x, y, scale, opacity, active = false) {
569
- const glow = active
570
- ? `<circle cx="0" cy="0" r="1.62" fill="#7A9DFF" opacity="0.14"/>`
571
- : '';
572
- return `
573
- <g transform="translate(${x.toFixed(3)} ${y.toFixed(3)}) scale(${scale.toFixed(3)})"
574
- opacity="${opacity.toFixed(3)}">
575
- ${glow}
576
- ${tokenSVG(name)}
577
- </g>
578
- `;
579
- }
580
-
581
- function renderLensScan(opacity = 0.45) {
582
- const t = (((lastWallTime || 0) / 1000) * params.codeDrift) % 1;
583
- const y = -3.7 + t * 7.4;
584
- return `
585
- <g opacity="${opacity.toFixed(3)}">
586
- <rect x="-4.1" y="${(y - 0.18).toFixed(3)}" width="8.2" height="0.36"
587
- fill="url(#scan-grad)" transform="rotate(-8)" opacity="0.9"/>
588
- <line x1="-3.2" y1="${(y + 0.38).toFixed(3)}" x2="3.2" y2="${(y - 0.52).toFixed(3)}"
589
- stroke="#ffffff" stroke-width="0.06" stroke-opacity="0.5"/>
590
- </g>
591
- `;
592
- }
593
-
594
- function renderLensCodeStream(focusTokens, tokenIdx) {
595
- const time = (lastWallTime || 0) / 1000;
596
- const count = focusTokens.length;
597
- const active = focusTokens[tokenIdx] || focusTokens[0];
598
- const rows = [
599
- [focusTokens[(tokenIdx + count - 2) % count], 'eq', focusTokens[(tokenIdx + 1) % count], 'plus'],
600
- ['lt', active, 'colon', focusTokens[(tokenIdx + 2) % count]],
601
- [focusTokens[(tokenIdx + 3) % count], 'und', 'gt', active]
602
- ];
603
-
604
- let html = `
605
- <g opacity="0.82">
606
- <path d="M -3.55 -2.9 H 3.35 M -3.25 0 H 3.55 M -3.45 2.9 H 3.25"
607
- stroke="#8FA2C6" stroke-width="0.08" stroke-opacity="0.34" stroke-linecap="round"/>
608
- `;
609
-
610
- rows.forEach((row, rowIdx) => {
611
- const y = -2.12 + rowIdx * 2.12;
612
- const drift = ((((time * params.codeDrift) + rowIdx * 0.27) % 1) - 0.5) * 0.62;
613
- const xs = [-2.55, -0.85, 0.85, 2.55].map(x => x + drift);
614
- row.forEach((token, colIdx) => {
615
- const isActive = token === active && (rowIdx === 1 || colIdx === 3);
616
- const scale = isActive ? 0.46 : 0.34;
617
- const opacity = isActive ? 0.96 : 0.42 + rowIdx * 0.05;
618
- html += renderMiniToken(token, xs[colIdx], y, scale, opacity, isActive);
619
- });
620
- });
621
-
622
- html += `
623
- ${renderLensScan(0.34)}
624
- </g>
625
- `;
626
- return html;
627
- }
628
-
629
- function getUnderEyeBlinkScale(t) {
630
- const period = Math.max(params.underBlinkPeriod, 0.5);
631
- const dur = Math.min(params.underBlinkDur, period * 0.45);
632
- const local = ((t + 0.42) % period + period) % period;
633
- if (local > dur) return 1;
634
- return blinkLerp(local / dur, params.bm);
635
- }
636
-
637
- function renderLensUnderEye(offset) {
638
- const sy = getUnderEyeBlinkScale(currentT);
639
- const op = sy < 0.98 ? 0.24 : 0.17;
640
- return `<g opacity="${op.toFixed(3)}">${renderLensCapsule(sy, offset)}</g>`;
641
- }
642
-
643
- function renderLensTransitGlints(opacity = 0.32) {
644
- const time = (lastWallTime || 0) / 1000;
645
- const drift = Math.sin(time * Math.PI * 2.1) * 0.28;
646
- const drift2 = Math.cos(time * Math.PI * 1.7) * 0.22;
647
- return `
648
- <g opacity="${opacity.toFixed(3)}">
649
- <circle cx="${(-1.42 + drift).toFixed(3)}" cy="${(-1.08 + drift2 * 0.35).toFixed(3)}" r="0.13" fill="#EAFBFF" opacity="0.42"/>
650
- <circle cx="${(0.18 - drift2).toFixed(3)}" cy="${(0.22 + drift * 0.3).toFixed(3)}" r="0.09" fill="#C8D4FF" opacity="0.30"/>
651
- <circle cx="${(1.36 - drift).toFixed(3)}" cy="${(1.02 - drift2 * 0.28).toFixed(3)}" r="0.11" fill="#B1A7FF" opacity="0.34"/>
652
- </g>
653
- `;
654
- }
655
-
656
- function renderLensContent(focusState, focusTokens, offset = { x: 0, y: 0 }) {
657
- if (focusState.stage === 'morphIn') {
658
- lensContent.innerHTML = renderMorphIn(focusState.progress, focusTokens[0], offset);
659
- return;
660
- }
661
- if (focusState.stage === 'token') {
662
- lensContent.innerHTML = `
663
- ${renderLensUnderEye(offset)}
664
- ${renderLensCodeStream(focusTokens, focusState.tokenIdx)}
665
- `;
666
- return;
667
- }
668
- if (focusState.stage === 'morphOut') {
669
- lensContent.innerHTML = renderMorphOut(focusState.progress, focusTokens[focusTokens.length - 1], offset);
670
- return;
671
- }
672
- // capsule (含眨眼)
673
- lensContent.innerHTML = renderLensCapsule(focusState.sy, offset);
674
- }
675
-
676
- /* ───────────── 主循环 ───────────── */
677
- let startTime = null, currentT = 0;
678
-
679
- function loop(ts) {
680
- if (startTime === null) startTime = ts;
681
- lastWallTime = ts;
682
- if (!params.pause) {
683
- currentT = (ts - startTime) / 1000;
684
- }
685
- render(currentT);
686
- requestAnimationFrame(loop);
687
- }
688
-
689
- /* ───────────── 拉杆绑定 ───────────── */
690
- requestAnimationFrame(loop);
691
- ]]></script>
1
+ <svg id="cloudling" xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="-12 -12 48 48" preserveAspectRatio="xMidYMid meet">
2
+ <style><![CDATA[
3
+ .phase {
4
+ font-family: ui-monospace, monospace;
5
+ font-size: 11px;
6
+ color: #888;
7
+ text-align: center;
8
+ }
9
+ .phase span { color: #7A9DFF; font-weight: 600; }
10
+ .edge-rim-tight { stroke-opacity: .65; }
11
+ .edge-rim-soft { stroke-opacity: .50; }
12
+ .edge-contact { stroke-opacity: .65; }
13
+ @media (prefers-color-scheme: light) {
14
+ .phase span { color: #3941FF; }
15
+ .edge-rim-tight { stroke-opacity: .75; }
16
+ .edge-rim-soft { stroke-opacity: .60; }
17
+ .edge-contact { stroke-opacity: .75; }
18
+ .cloud-drop-shadow { flood-opacity: .13; }
19
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
20
+ .food-cloud > path {
21
+ stroke: #5870E8 !important;
22
+ stroke-width: 1.2 !important;
23
+ stroke-opacity: .9 !important;
24
+ vector-effect: non-scaling-stroke !important;
25
+ }
26
+ }
27
+ ]]></style>
28
+ <defs>
29
+ <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
30
+ <stop offset="0" stop-color="#f0f9ff"/>
31
+ <stop offset="0.5" stop-color="#e0f2fe"/>
32
+ <stop offset="1" stop-color="#bae6fd"/>
33
+ </linearGradient>
34
+ <linearGradient id="eye-grad" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
35
+ <stop offset="0" stop-color="#B1A7FF"/>
36
+ <stop offset="0.5" stop-color="#7A9DFF"/>
37
+ <stop offset="1" stop-color="#3941FF"/>
38
+ </linearGradient>
39
+ <radialGradient id="hi-grad" cx="0.45" cy="0.25" r="0.55">
40
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.9"/>
41
+ <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
42
+ </radialGradient>
43
+ <filter id="drop" x="-40%" y="-40%" width="180%" height="180%">
44
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
45
+ </filter>
46
+ <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
47
+ <feGaussianBlur stdDeviation="0.16"/>
48
+ </filter>
49
+ <radialGradient id="lens-glass-grad" cx="0.34" cy="0.22" r="0.86">
50
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.72"/>
51
+ <stop offset="0.48" stop-color="#DDE7FF" stop-opacity="0.35"/>
52
+ <stop offset="1" stop-color="#89A7FF" stop-opacity="0.12"/>
53
+ </radialGradient>
54
+ <linearGradient id="lens-rim-grad" x1="-5" y1="-5" x2="5" y2="5" gradientUnits="userSpaceOnUse">
55
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.82"/>
56
+ <stop offset="0.44" stop-color="#B9C6DC" stop-opacity="0.54"/>
57
+ <stop offset="1" stop-color="#6F83B7" stop-opacity="0.42"/>
58
+ </linearGradient>
59
+ <linearGradient id="scan-grad" x1="-4" y1="0" x2="4" y2="0" gradientUnits="userSpaceOnUse">
60
+ <stop offset="0" stop-color="#89F2FF" stop-opacity="0"/>
61
+ <stop offset="0.5" stop-color="#EAFBFF" stop-opacity="0.72"/>
62
+ <stop offset="1" stop-color="#89F2FF" stop-opacity="0"/>
63
+ </linearGradient>
64
+ <filter id="lens-soft-glow" x="-60%" y="-60%" width="220%" height="220%">
65
+ <feGaussianBlur stdDeviation="0.45" result="glow"/>
66
+ <feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>
67
+ </filter>
68
+ <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"/>
69
+ <clipPath id="cloud-clip"><use href="#cloud-shape"/></clipPath>
70
+
71
+ <!-- 镜片 clip 中心化到 (0,0), lens-group 通过 transform 平移 -->
72
+ <clipPath id="lens-clip"><circle cx="0" cy="0" r="4.75"/></clipPath>
73
+
74
+ <!-- 轻玻璃镜框 (中心化到 0,0) -->
75
+ <g id="lens-frame">
76
+ <circle cx="0" cy="0" r="5.05" stroke="url(#lens-rim-grad)" stroke-width="0.34" fill="none" opacity="0.9"/>
77
+ <circle cx="0" cy="0" r="4.55" stroke="#ffffff" stroke-width="0.10" stroke-opacity="0.34" fill="none"/>
78
+ <path id="lens-frame-highlight" d="M -1.728 -4.745 A 5.05 5.05 0 0 1 1.728 -4.745"
79
+ stroke="#ffffff" stroke-width="0.22" stroke-opacity="0.78"
80
+ stroke-linecap="round" fill="none"/>
81
+ </g>
82
+ </defs>
83
+
84
+ <g id="runtime-layer">
85
+ <!-- 云朵 layer (rotate + scale 围绕 12,12) -->
86
+ <g id="cloud-group">
87
+ <g id="edge-system">
88
+ <use class="edge-contact" href="#cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#edge-blur)"/>
89
+ <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#edge-blur)"/>
90
+ <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
91
+ </g>
92
+ <g filter="url(#drop)">
93
+ <use href="#cloud-shape" fill="url(#body-grad)"/>
94
+ <rect x="1" y="2" width="22" height="11" fill="url(#hi-grad)" clip-path="url(#cloud-clip)"/>
95
+ </g>
96
+ </g>
97
+
98
+ <!-- 眼睛 layer (只 scale, 不 rotate, 眼睛保持竖直 — typing 同款分层)
99
+ 每只眼 = capsule + dot 两个元素, opacity 互斥 fade (不再 morph 形状, 避免横扁椭圆中间帧鬼畜) -->
100
+ <g id="eye-group">
101
+ <g id="eye-l-wrap" transform="translate(9 12)">
102
+ <rect class="cap" x="-1.3" y="-2.5" width="2.6" height="5" rx="1.3" ry="1.3"
103
+ fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
104
+ <circle class="dot" cx="0" cy="0" r="1.35" fill="url(#eye-grad)"/>
105
+ </g>
106
+ <g id="eye-r-wrap" transform="translate(14.85 12)">
107
+ <rect class="cap" x="-1.3" y="-2.5" width="2.6" height="5" rx="1.3" ry="1.3"
108
+ fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
109
+ <circle class="dot" cx="0" cy="0" r="1.35" fill="url(#eye-grad)"/>
110
+ </g>
111
+ </g>
112
+
113
+ <!-- 镜片 layer (独立, transform 平移到 lensX/lensY) -->
114
+ <g id="lens-group" transform="translate(15 12)">
115
+ <circle id="lens-glow" cx="0" cy="0" r="5.05" fill="#7A9DFF" opacity="0.08" filter="url(#lens-soft-glow)"/>
116
+ <circle id="lens-glass" cx="0" cy="0" r="4.75" fill="url(#lens-glass-grad)"/>
117
+ <g clip-path="url(#lens-clip)">
118
+ <g id="lens-content"></g>
119
+ </g>
120
+ <use href="#lens-frame"/>
121
+ </g>
122
+ </g>
123
+
124
+ <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
125
+ <g id="phaseLabel"></g>
126
+ </g>
127
+
128
+ <script type="application/ecmascript"><![CDATA[
129
+ /* ───────────── 元素引用 ───────────── */
130
+ const cloudGroup = document.getElementById('cloud-group');
131
+ const eyeGroup = document.getElementById('eye-group');
132
+ const eyeLWrap = document.getElementById('eye-l-wrap');
133
+ const eyeRWrap = document.getElementById('eye-r-wrap');
134
+ const eyeLCap = eyeLWrap.querySelector('.cap');
135
+ const eyeLDot = eyeLWrap.querySelector('.dot');
136
+ const eyeRCap = eyeRWrap.querySelector('.cap');
137
+ const eyeRDot = eyeRWrap.querySelector('.dot');
138
+ const lensGroup = document.getElementById('lens-group');
139
+ const lensFrameHighlight = document.getElementById('lens-frame-highlight');
140
+ const lensContent = document.getElementById('lens-content');
141
+ const eyeGrad = document.getElementById('eye-grad');
142
+ const phaseLabel = document.getElementById('phaseLabel');
143
+
144
+ /* ───────────── 拉杆参数 ───────────── */
145
+ const params = {
146
+ total: 6.0,
147
+ ratio: 3.00, // 定睛/滑动 = 2.25/0.75 = 3.00
148
+ rot: 7.0,
149
+ sh: 1.10,
150
+ sl: 1.00,
151
+ ba: 0.024,
152
+ bper: 5.0,
153
+ arc: 1.15,
154
+ fa: 0.12,
155
+ fp: 3.1,
156
+ fh: 1.10,
157
+ lensScale: 1.14,
158
+ rightEyeX: 14.85,
159
+ sweep: 1.72, // 单轮代码扫描总时长
160
+ accel: 1.65, // 加速强度: 1=匀速, 高=末尾更快
161
+ codeSteps: 9,
162
+ codeDrift: 0.62,
163
+ underBlinkPeriod: 3.6,
164
+ underBlinkDur: 0.16,
165
+ ph: 0.04,
166
+ bd: 0.16,
167
+ bm: 0.06, // 眨眼最低 sy (BLINK_MIN)
168
+ hd: 1.25,
169
+ dd: 6.0,
170
+ gp: 5.0,
171
+ ga: 9,
172
+ pause: false
173
+ };
174
+
175
+ /* 段时长 (从 total + ratio 推) */
176
+ function segments() {
177
+ // 定睛 2 段 + 滑动 2 段, 满足: 2*focus + 2*transit = total, focus/transit = ratio
178
+ const transit = params.total / (2 * (params.ratio + 1));
179
+ const focus = transit * params.ratio;
180
+ return { focus, transit };
181
+ }
182
+
183
+ /* ───────────── 工具函数 ───────────── */
184
+ const lerp = (a, b, t) => a + (b - a) * t;
185
+ const easeInOut = t => t * t * (3 - 2 * t);
186
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
187
+
188
+ function getBodyBreath(t) {
189
+ const period = Math.max(params.bper, 0.1);
190
+ const p = ((t % period) + period) % period / period;
191
+ // 稍快吸气、稍慢呼气,比正弦更像自然呼吸。
192
+ const inhale = p < 0.42
193
+ ? easeInOut(p / 0.42)
194
+ : 1 - easeInOut((p - 0.42) / 0.58);
195
+ const centered = inhale * 2 - 1;
196
+ return {
197
+ sx: 1 + centered * params.ba * 0.35,
198
+ sy: 1 + centered * params.ba
199
+ };
200
+ }
201
+
202
+ /* ───────────── token 库 (中心化到 0,0) ───────────── */
203
+ const TOKENS = ['gt', 'lt', 'und', 'colon', 'eq', 'plus', 'slash', 'bslash'];
204
+
205
+ /* token stroke 跟着 transform scale 放大, v2 降低线重, 给镜片内代码流留精细感。 */
206
+ function tokenSVG(name) {
207
+ switch (name) {
208
+ case 'gt': return `<path d="M -0.8 -1.7 L 0.8 0 L -0.8 1.7" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round" fill="none"/>`;
209
+ case 'lt': return `<path d="M 0.8 -1.7 L -0.8 0 L 0.8 1.7" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round" fill="none"/>`;
210
+ case 'und': return `<line x1="-1.0" y1="1.0" x2="1.0" y2="1.0" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round"/>`;
211
+ case 'colon': return `<circle cx="0" cy="-1.05" r="0.78" fill="url(#eye-grad)"/><circle cx="0" cy="1.05" r="0.78" fill="url(#eye-grad)"/>`;
212
+ case 'eq': return `<line x1="-1.0" y1="-0.85" x2="1.0" y2="-0.85" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/><line x1="-1.0" y1="0.85" x2="1.0" y2="0.85" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/>`;
213
+ case 'plus': return `<line x1="-1.0" y1="0" x2="1.0" y2="0" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/><line x1="0" y1="-1.0" x2="0" y2="1.0" stroke="url(#eye-grad)" stroke-width="0.76" stroke-linecap="round"/>`;
214
+ case 'slash': return `<path d="M 0.8 -1.5 L -0.8 1.5" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" fill="none"/>`;
215
+ case 'bslash': return `<path d="M -0.8 -1.5 L 0.8 1.5" stroke="url(#eye-grad)" stroke-width="1.05" stroke-linecap="round" fill="none"/>`;
216
+ }
217
+ return '';
218
+ }
219
+
220
+ const LENS_MAG = 1.35;
221
+
222
+ function contentOffsetForEye(eyeX, eyeY, lensX, lensY) {
223
+ return {
224
+ x: (eyeX - lensX) * LENS_MAG,
225
+ y: (eyeY - lensY) * LENS_MAG
226
+ };
227
+ }
228
+
229
+ /* capsule (默认中心化到 0,0), scale 只用于镜片内鱼眼放大 */
230
+ function capsuleSVG(scale = 1, offset = { x: 0, y: 0 }) {
231
+ const w = 2.6 * scale;
232
+ const h = 5 * scale;
233
+ const r = 1.3 * scale;
234
+ return `<rect x="${offset.x - w/2}" y="${offset.y - h/2}" width="${w}" height="${h}" rx="${r}" ry="${r}" fill="url(#eye-grad)"/>`;
235
+ }
236
+
237
+ /* 每轮重新 pick token;入口固定 slash, 出口固定 bslash,中间允许重复以增加代码变化量。 */
238
+ const MIDDLE_POOL = ['gt', 'lt', 'und', 'colon', 'eq', 'plus'];
239
+ function pickFocusTokens() {
240
+ const count = Math.max(5, Math.round(params.codeSteps || 9));
241
+ const middleCount = count - 2;
242
+ const middle = [];
243
+ let prev = '';
244
+ for (let i = 0; i < middleCount; i++) {
245
+ let next = MIDDLE_POOL[Math.floor(Math.random() * MIDDLE_POOL.length)];
246
+ if (next === prev) {
247
+ next = MIDDLE_POOL[(MIDDLE_POOL.indexOf(next) + 1 + i) % MIDDLE_POOL.length];
248
+ }
249
+ middle.push(next);
250
+ prev = next;
251
+ }
252
+ return ['slash', ...middle, 'bslash'];
253
+ }
254
+
255
+ let rightFocusTokens = pickFocusTokens();
256
+ let leftFocusTokens = pickFocusTokens();
257
+ let lastLoopId = -1;
258
+
259
+ /* ───────────── 镜片下那只眼的内部时序 (纯代码流) ─────────────
260
+ 单轮内部只切换 code token,不再回到 capsule,也不再眨眼。
261
+ accel=1.0 → 等分; accel 越高, 前几个 token 停留越久, 后几个更快切完。 */
262
+
263
+ const MORPH_IN_DUR = 0.13;
264
+ const MORPH_OUT_DUR = 0.13;
265
+ const INTER_BLINK_HOLD = 0.03;
266
+
267
+ function getFocusCycleDuration() {
268
+ return Math.max(params.sweep, 0.1);
269
+ }
270
+
271
+ function getTokenDurations(sweepDur, accel, count) {
272
+ const steps = Math.max(1, count);
273
+ if (steps === 1) return [sweepDur];
274
+ const firstWeight = Math.max(accel, 1);
275
+ const weights = Array.from({ length: steps }, (_, i) => {
276
+ const p = i / (steps - 1);
277
+ return firstWeight + (1 - firstWeight) * p;
278
+ });
279
+ const total = weights.reduce((sum, v) => sum + v, 0);
280
+ return weights.map(v => sweepDur * v / total);
281
+ }
282
+
283
+ function blinkLerp(p, blinkMin) { // p 0..1, 闭+开: 1 → MIN → 1
284
+ p = clamp(p, 0, 1);
285
+ return p < 0.5
286
+ ? lerp(1, blinkMin, easeInOut(p / 0.5))
287
+ : lerp(blinkMin, 1, easeInOut((p - 0.5) / 0.5));
288
+ }
289
+
290
+ function getFocusEyeState(localT) {
291
+ const sweepDur = params.sweep;
292
+ const count = Math.max(5, Math.round(params.codeSteps || 9));
293
+ const tokenDurs = getTokenDurations(sweepDur, params.accel, count);
294
+ let end = 0;
295
+ for (let i = 0; i < tokenDurs.length; i++) {
296
+ end += tokenDurs[i];
297
+ if (localT < end) return { stage: 'token', tokenIdx: i };
298
+ }
299
+ return { stage: 'token', tokenIdx: tokenDurs.length - 1 };
300
+ }
301
+
302
+ /* ───────────── 主状态 ───────────── */
303
+ function getState(t) {
304
+ const { transit } = segments();
305
+ const focusCycle = Math.max(getFocusCycleDuration(), 0.1);
306
+ const localT = ((t % focusCycle) + focusCycle) % focusCycle;
307
+ let phase = 'left-focus';
308
+ let lensX = 9;
309
+ let lensY = 12;
310
+ let cloudRot = -params.rot;
311
+ let cloudScale = params.sh;
312
+ const transitP = 0;
313
+ const transitDir = 0;
314
+ const motion = 0;
315
+
316
+ // 镜片微浮动 (异步 sin)
317
+ const nowSec = (lastWallTime || performance.now()) / 1000;
318
+ lensY += params.fa * Math.sin(2 * Math.PI * nowSec / params.fp);
319
+
320
+ return { phase, localT, focusDur: focusCycle, transit, lensX, lensY, cloudRot, cloudScale, transitP, transitDir, motion };
321
+ }
322
+
323
+ /* ───────────── 眼睛 capsule/dot opacity swap ─────────────
324
+ 不再 morph 形状 (避免横扁椭圆中间帧 = 视觉旋转鬼畜)
325
+ distance < hd: hidden (镜片覆盖)
326
+ hd ≤ distance < SWITCH: capsule (远到看清, 镜片刚走 / 远端)
327
+ distance ≥ SWITCH: dot (圆点眯眼, 离镜片远 = 被对方审视配合眯起)
328
+ 切换瞬间 dot/capsule opacity 互换 (短窗口 fade) */
329
+ const SWITCH_DIST = 4.0;
330
+ const FADE_HALF = 0.25;
331
+ function getEyeOpacities(eyeCx, lensX) {
332
+ const d = Math.abs(lensX - eyeCx);
333
+ const visibility = clamp((d - params.hd) / 0.5, 0, 1); // hd..(hd+0.5) 渐显
334
+ // dot 占比: distance 越远越显, SWITCH 附近 fade swap
335
+ const dotRatio = clamp((d - (SWITCH_DIST - FADE_HALF)) / (2 * FADE_HALF), 0, 1);
336
+ return {
337
+ capsuleOpacity: (1 - dotRatio) * visibility,
338
+ dotOpacity: dotRatio * visibility
339
+ };
340
+ }
341
+
342
+ /* ───────────── 渲染 ───────────── */
343
+ let lastWallTime = 0;
344
+
345
+ function applyEyeFollow(s) {
346
+ const ldx = clamp((s.lensX - 9) / 6, -1, 1) * 0.22;
347
+ const rdx = clamp((s.lensX - params.rightEyeX) / 6, -1, 1) * 0.22;
348
+ const dy = clamp((s.lensY - 12) / 3, -1, 1) * 0.12;
349
+ eyeLWrap.setAttribute('transform', `translate(9 12) translate(${ldx.toFixed(3)} ${dy.toFixed(3)})`);
350
+ eyeRWrap.setAttribute('transform', `translate(${params.rightEyeX.toFixed(2)} 12) translate(${rdx.toFixed(3)} ${dy.toFixed(3)})`);
351
+ }
352
+
353
+ function render(t) {
354
+ const s = getState(t);
355
+ const breath = getBodyBreath(t);
356
+
357
+ // 1. 云朵 transform (rotate + scale + body-only breathing around 12,12)
358
+ cloudGroup.setAttribute('transform',
359
+ `translate(12 12) scale(${s.cloudScale}) rotate(${s.cloudRot}) scale(${breath.sx.toFixed(4)} ${breath.sy.toFixed(4)}) translate(-12 -12)`);
360
+
361
+ // 2. 眼睛 layer 跟着 scale 但不 rotate (typing 同款)
362
+ eyeGroup.setAttribute('transform',
363
+ `translate(12 12) scale(${s.cloudScale}) translate(-12 -12)`);
364
+ applyEyeFollow(s);
365
+
366
+ // 3. 双眼 capsule/dot opacity swap (不 morph 形状)
367
+ const lO = getEyeOpacities(9, s.lensX);
368
+ const rO = getEyeOpacities(params.rightEyeX, s.lensX);
369
+ eyeLCap.setAttribute('opacity', lO.capsuleOpacity);
370
+ eyeLDot.setAttribute('opacity', lO.dotOpacity);
371
+ eyeRCap.setAttribute('opacity', rO.capsuleOpacity);
372
+ eyeRDot.setAttribute('opacity', rO.dotOpacity);
373
+
374
+ // 4. 镜片位置
375
+ lensGroup.setAttribute('transform', `translate(${s.lensX} ${s.lensY}) scale(${params.lensScale.toFixed(3)})`);
376
+ applyLensFrameHighlight(s);
377
+
378
+ // 5. 镜片下内容
379
+ let stageLabel = '';
380
+ if (s.phase === 'right-focus' || s.phase === 'left-focus') {
381
+ const focusState = getFocusEyeState(s.localT);
382
+ const tokens = (s.phase === 'right-focus') ? rightFocusTokens : leftFocusTokens;
383
+ const sourceX = s.phase === 'right-focus' ? params.rightEyeX : 9;
384
+ renderLensContent(focusState, tokens, contentOffsetForEye(sourceX, 12, s.lensX, s.lensY));
385
+ stageLabel = focusState.stage === 'token'
386
+ ? `scan[${focusState.tokenIdx}]=${tokens[focusState.tokenIdx]}`
387
+ : focusState.stage;
388
+ } else {
389
+ // transit: 按 lens 到最近眼的距离 fade capsule, 避免 focus 切 transit 瞬间内容突然清空
390
+ const dL = Math.hypot(s.lensX - 9, s.lensY - 12);
391
+ const dR = Math.hypot(s.lensX - params.rightEyeX, s.lensY - 12);
392
+ const dMin = Math.min(dL, dR);
393
+ // 距离 0-1: fade=1 (lens 在眼附近, 保持 capsule 满显)
394
+ // 距离 1-2.5: fade 从 1 → 0 渐隐
395
+ // 距离 > 2.5: 保留微弱 glint, 避免镜片滑动时完全空掉
396
+ const fade = 1 - clamp((dMin - 1) / 1.5, 0, 1);
397
+ if (fade < 0.02) {
398
+ lensContent.innerHTML = renderLensTransitGlints(0.28 + s.motion * 0.12);
399
+ stageLabel = `transit-glints (d=${dMin.toFixed(1)})`;
400
+ } else {
401
+ const sourceX = dL <= dR ? 9 : params.rightEyeX;
402
+ const offset = contentOffsetForEye(sourceX, 12, s.lensX, s.lensY);
403
+ lensContent.innerHTML = `
404
+ <g opacity="${(fade * 0.56).toFixed(3)}">${renderLensCapsule(1, offset)}</g>
405
+ ${renderLensTransitGlints(0.22 + (1 - fade) * 0.18)}
406
+ `;
407
+ stageLabel = `transit-fade=${fade.toFixed(2)} (d=${dMin.toFixed(1)})`;
408
+ }
409
+ }
410
+
411
+ // 6. 流动渐变 (跟 idle/typing 同语言, 周期 + 幅度)
412
+ const gradPhase = (lastWallTime / 1000) % params.gp / params.gp;
413
+ const gShift = Math.sin(2 * Math.PI * gradPhase) * params.ga;
414
+ eyeGrad.setAttribute('y1', 3 + gShift);
415
+ eyeGrad.setAttribute('y2', 21 + gShift);
416
+
417
+ // 7. 每 loop 重 pick 两组 focus token (各取中间 3 个随机)
418
+ const focusCycle = Math.max(getFocusCycleDuration(), 0.1);
419
+ const loopId = Math.floor(t / focusCycle);
420
+ if (loopId !== lastLoopId) {
421
+ leftFocusTokens = pickFocusTokens();
422
+ lastLoopId = loopId;
423
+ }
424
+
425
+ // phase label
426
+ phaseLabel.innerHTML = `<span>${s.phase}</span> · lens locked (${s.lensX.toFixed(2)}, ${s.lensY.toFixed(2)}) · focus cycle ${s.focusDur.toFixed(2)}s · cloud rot ${s.cloudRot.toFixed(1)}° scale ${s.cloudScale.toFixed(3)} · stage <b>${stageLabel}</b> · L=[${leftFocusTokens.join(',')}]`;
427
+ }
428
+
429
+ function applyLensFrameHighlight(s) {
430
+ const strength = params.fh;
431
+ const move = clamp(s.motion, 0, 1);
432
+ const dir = s.transitDir || 0;
433
+ const effective = Math.min(strength, 1.2);
434
+ const centerDeg = dir * move * 5.0 * effective;
435
+ const spanDeg = 34 + 14 * move * effective;
436
+ const opacity = 0.64 + 0.12 * move * Math.min(strength, 1);
437
+
438
+ lensFrameHighlight.setAttribute('d', frameHighlightArc(centerDeg, spanDeg));
439
+ lensFrameHighlight.removeAttribute('transform');
440
+ lensFrameHighlight.setAttribute('stroke-opacity', opacity.toFixed(3));
441
+ }
442
+
443
+ function frameHighlightArc(centerDeg, spanDeg) {
444
+ const r = 5.05;
445
+ const start = (-90 + centerDeg - spanDeg / 2) * Math.PI / 180;
446
+ const end = (-90 + centerDeg + spanDeg / 2) * Math.PI / 180;
447
+ const x1 = Math.cos(start) * r;
448
+ const y1 = Math.sin(start) * r;
449
+ const x2 = Math.cos(end) * r;
450
+ const y2 = Math.sin(end) * r;
451
+ return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A ${r} ${r} 0 0 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
452
+ }
453
+
454
+ /* ─── lens 内 capsule 渲染 (含 sy 闭眼幅度, 用于末段眨 2 下) ─── */
455
+ function renderLensCapsule(sy, offset = { x: 0, y: 0 }) {
456
+ const scale = LENS_MAG;
457
+ const baseW = 2.6 * scale;
458
+ const baseH = 5 * scale;
459
+ const openness = clamp((sy - params.bm) / (1 - params.bm), 0, 1);
460
+
461
+ const eyeMarkup = capsuleSVG(scale, offset);
462
+
463
+ if (openness > 0.995) return eyeMarkup;
464
+
465
+ // 玻璃镜片内不能用浅色眼皮遮盖, 否则眨眼时会露出矩形补丁。
466
+ // 这里只裁剪 capsule 本体, 再叠一条闭眼线。
467
+ const openEase = easeInOut(openness);
468
+ const apertureW = lerp(baseW * 0.78, baseW + 0.42, Math.sqrt(openEase));
469
+ const apertureH = lerp(0.16, baseH + 0.36, openEase);
470
+ const hw = apertureW / 2;
471
+ const hh = apertureH / 2;
472
+ const c = hw * 0.74;
473
+ const ox = offset.x;
474
+ const oy = offset.y;
475
+
476
+ const apertureD = `M ${ox - hw} ${oy} C ${ox - c} ${oy - hh} ${ox + c} ${oy - hh} ${ox + hw} ${oy} C ${ox + c} ${oy + hh} ${ox - c} ${oy + hh} ${ox - hw} ${oy} Z`;
477
+ const upperEdgeD = `M ${ox - hw} ${oy} C ${ox - c} ${oy - hh} ${ox + c} ${oy - hh} ${ox + hw} ${oy}`;
478
+ const lowerEdgeD = `M ${ox - hw} ${oy} C ${ox - c} ${oy + hh} ${ox + c} ${oy + hh} ${ox + hw} ${oy}`;
479
+ const edgeOpacity = clamp((1 - openness) * 0.24, 0, 0.18);
480
+ const lineOpacity = clamp((0.55 - openness) / 0.55, 0, 1);
481
+ const lineW = baseW * 0.78;
482
+
483
+ return `
484
+ <defs>
485
+ <clipPath id="lens-eye-aperture"><path d="${apertureD}"/></clipPath>
486
+ </defs>
487
+ <g clip-path="url(#lens-eye-aperture)">${eyeMarkup}</g>
488
+ <g>
489
+ <path d="${upperEdgeD}" fill="none" stroke="#aeb7c6" stroke-width="0.20" stroke-opacity="${edgeOpacity}" stroke-linecap="round"/>
490
+ <path d="${lowerEdgeD}" fill="none" stroke="#aeb7c6" stroke-width="0.20" stroke-opacity="${edgeOpacity}" stroke-linecap="round"/>
491
+ </g>
492
+ <line x1="${ox - lineW/2}" y1="${oy}" x2="${ox + lineW/2}" y2="${oy}"
493
+ stroke="url(#eye-grad)" stroke-width="0.42" stroke-linecap="round"
494
+ opacity="${lineOpacity}" vector-effect="non-scaling-stroke"/>
495
+ `;
496
+ }
497
+
498
+ /* ─── morphIn: capsule 收 + 渐隐, token 渐显 + 长出 (overlap 0.4-0.6 区间, 避免空白帧) ─── */
499
+ function renderMorphIn(p, exitToken, offset = { x: 0, y: 0 }) {
500
+ const scale = LENS_MAG;
501
+ const e = easeInOut(clamp(p, 0, 1));
502
+ let html = '';
503
+ const ox = offset.x;
504
+ const oy = offset.y;
505
+
506
+ // capsule: e ∈ [0, 0.6] 收 + 渐隐
507
+ if (e < 0.6) {
508
+ const t = e / 0.6; // 0 → 1
509
+ const sy = 1 - t * 0.7; // 1 → 0.3
510
+ const sx = 1 - t * 0.7; // 1 → 0.3
511
+ const op = 1 - t; // 1 → 0
512
+ const w = 2.6 * scale * sx;
513
+ const h = 5 * scale * sy;
514
+ const r = Math.min(1.3 * scale * sx, h / 2);
515
+ html += `<rect x="${ox - w/2}" y="${oy - h/2}" width="${w}" height="${h}" rx="${r}" ry="${r}" fill="url(#eye-grad)" opacity="${op}"/>`;
516
+ }
517
+
518
+ // token: e ∈ [0.4, 1.0] 从中心 burst
519
+ if (e > 0.4) {
520
+ const t = (e - 0.4) / 0.6; // 0 → 1
521
+ const radius = 0.5 + t * 5; // 0.5 → 5.5
522
+ html += `
523
+ <defs><clipPath id="morph-clip"><circle cx="${ox}" cy="${oy}" r="${radius}"/></clipPath></defs>
524
+ <g clip-path="url(#morph-clip)" transform="translate(${ox} ${oy}) scale(${scale})">${tokenSVG(exitToken)}</g>
525
+ `;
526
+ }
527
+ return html;
528
+ }
529
+
530
+ /* ─── morphOut: token 收 + 渐隐, capsule 渐显 + 长出 (镜像 morphIn) ─── */
531
+ function renderMorphOut(p, lastToken, offset = { x: 0, y: 0 }) {
532
+ const scale = LENS_MAG;
533
+ const e = easeInOut(clamp(p, 0, 1));
534
+ let html = '';
535
+ const ox = offset.x;
536
+ const oy = offset.y;
537
+
538
+ // token: e ∈ [0, 0.6] 收 + 渐隐
539
+ if (e < 0.6) {
540
+ const t = e / 0.6; // 0 → 1
541
+ const radius = 5.5 - t * 5; // 5.5 → 0.5
542
+ const op = 1 - t; // 1 → 0
543
+ html += `
544
+ <defs><clipPath id="morph-clip"><circle cx="${ox}" cy="${oy}" r="${radius}"/></clipPath></defs>
545
+ <g clip-path="url(#morph-clip)" transform="translate(${ox} ${oy}) scale(${scale})" opacity="${op}">${tokenSVG(lastToken)}</g>
546
+ `;
547
+ }
548
+
549
+ // capsule: e ∈ [0.4, 1.0] 从中心展开
550
+ if (e > 0.4) {
551
+ const t = (e - 0.4) / 0.6; // 0 → 1
552
+ const sy = 0.3 + t * 0.7; // 0.3 → 1
553
+ const sx = 0.3 + t * 0.7; // 0.3 → 1
554
+ const op = t; // 0 → 1
555
+ const w = 2.6 * scale * sx;
556
+ const h = 5 * scale * sy;
557
+ const r = Math.min(1.3 * scale * sx, h / 2);
558
+ html += `<rect x="${ox - w/2}" y="${oy - h/2}" width="${w}" height="${h}" rx="${r}" ry="${r}" fill="url(#eye-grad)" opacity="${op}"/>`;
559
+ }
560
+ return html;
561
+ }
562
+
563
+ /* ─── 镜片内代码扫描流 ─── */
564
+ function renderLensToken(name, offset = { x: 0, y: 0 }) {
565
+ return `<g transform="translate(${offset.x} ${offset.y}) scale(${LENS_MAG})">${tokenSVG(name)}</g>`;
566
+ }
567
+
568
+ function renderMiniToken(name, x, y, scale, opacity, active = false) {
569
+ const glow = active
570
+ ? `<circle cx="0" cy="0" r="1.62" fill="#7A9DFF" opacity="0.14"/>`
571
+ : '';
572
+ return `
573
+ <g transform="translate(${x.toFixed(3)} ${y.toFixed(3)}) scale(${scale.toFixed(3)})"
574
+ opacity="${opacity.toFixed(3)}">
575
+ ${glow}
576
+ ${tokenSVG(name)}
577
+ </g>
578
+ `;
579
+ }
580
+
581
+ function renderLensScan(opacity = 0.45) {
582
+ const t = (((lastWallTime || 0) / 1000) * params.codeDrift) % 1;
583
+ const y = -3.7 + t * 7.4;
584
+ return `
585
+ <g opacity="${opacity.toFixed(3)}">
586
+ <rect x="-4.1" y="${(y - 0.18).toFixed(3)}" width="8.2" height="0.36"
587
+ fill="url(#scan-grad)" transform="rotate(-8)" opacity="0.9"/>
588
+ <line x1="-3.2" y1="${(y + 0.38).toFixed(3)}" x2="3.2" y2="${(y - 0.52).toFixed(3)}"
589
+ stroke="#ffffff" stroke-width="0.06" stroke-opacity="0.5"/>
590
+ </g>
591
+ `;
592
+ }
593
+
594
+ function renderLensCodeStream(focusTokens, tokenIdx) {
595
+ const time = (lastWallTime || 0) / 1000;
596
+ const count = focusTokens.length;
597
+ const active = focusTokens[tokenIdx] || focusTokens[0];
598
+ const rows = [
599
+ [focusTokens[(tokenIdx + count - 2) % count], 'eq', focusTokens[(tokenIdx + 1) % count], 'plus'],
600
+ ['lt', active, 'colon', focusTokens[(tokenIdx + 2) % count]],
601
+ [focusTokens[(tokenIdx + 3) % count], 'und', 'gt', active]
602
+ ];
603
+
604
+ let html = `
605
+ <g opacity="0.82">
606
+ <path d="M -3.55 -2.9 H 3.35 M -3.25 0 H 3.55 M -3.45 2.9 H 3.25"
607
+ stroke="#8FA2C6" stroke-width="0.08" stroke-opacity="0.34" stroke-linecap="round"/>
608
+ `;
609
+
610
+ rows.forEach((row, rowIdx) => {
611
+ const y = -2.12 + rowIdx * 2.12;
612
+ const drift = ((((time * params.codeDrift) + rowIdx * 0.27) % 1) - 0.5) * 0.62;
613
+ const xs = [-2.55, -0.85, 0.85, 2.55].map(x => x + drift);
614
+ row.forEach((token, colIdx) => {
615
+ const isActive = token === active && (rowIdx === 1 || colIdx === 3);
616
+ const scale = isActive ? 0.46 : 0.34;
617
+ const opacity = isActive ? 0.96 : 0.42 + rowIdx * 0.05;
618
+ html += renderMiniToken(token, xs[colIdx], y, scale, opacity, isActive);
619
+ });
620
+ });
621
+
622
+ html += `
623
+ ${renderLensScan(0.34)}
624
+ </g>
625
+ `;
626
+ return html;
627
+ }
628
+
629
+ function getUnderEyeBlinkScale(t) {
630
+ const period = Math.max(params.underBlinkPeriod, 0.5);
631
+ const dur = Math.min(params.underBlinkDur, period * 0.45);
632
+ const local = ((t + 0.42) % period + period) % period;
633
+ if (local > dur) return 1;
634
+ return blinkLerp(local / dur, params.bm);
635
+ }
636
+
637
+ function renderLensUnderEye(offset) {
638
+ const sy = getUnderEyeBlinkScale(currentT);
639
+ const op = sy < 0.98 ? 0.24 : 0.17;
640
+ return `<g opacity="${op.toFixed(3)}">${renderLensCapsule(sy, offset)}</g>`;
641
+ }
642
+
643
+ function renderLensTransitGlints(opacity = 0.32) {
644
+ const time = (lastWallTime || 0) / 1000;
645
+ const drift = Math.sin(time * Math.PI * 2.1) * 0.28;
646
+ const drift2 = Math.cos(time * Math.PI * 1.7) * 0.22;
647
+ return `
648
+ <g opacity="${opacity.toFixed(3)}">
649
+ <circle cx="${(-1.42 + drift).toFixed(3)}" cy="${(-1.08 + drift2 * 0.35).toFixed(3)}" r="0.13" fill="#EAFBFF" opacity="0.42"/>
650
+ <circle cx="${(0.18 - drift2).toFixed(3)}" cy="${(0.22 + drift * 0.3).toFixed(3)}" r="0.09" fill="#C8D4FF" opacity="0.30"/>
651
+ <circle cx="${(1.36 - drift).toFixed(3)}" cy="${(1.02 - drift2 * 0.28).toFixed(3)}" r="0.11" fill="#B1A7FF" opacity="0.34"/>
652
+ </g>
653
+ `;
654
+ }
655
+
656
+ function renderLensContent(focusState, focusTokens, offset = { x: 0, y: 0 }) {
657
+ if (focusState.stage === 'morphIn') {
658
+ lensContent.innerHTML = renderMorphIn(focusState.progress, focusTokens[0], offset);
659
+ return;
660
+ }
661
+ if (focusState.stage === 'token') {
662
+ lensContent.innerHTML = `
663
+ ${renderLensUnderEye(offset)}
664
+ ${renderLensCodeStream(focusTokens, focusState.tokenIdx)}
665
+ `;
666
+ return;
667
+ }
668
+ if (focusState.stage === 'morphOut') {
669
+ lensContent.innerHTML = renderMorphOut(focusState.progress, focusTokens[focusTokens.length - 1], offset);
670
+ return;
671
+ }
672
+ // capsule (含眨眼)
673
+ lensContent.innerHTML = renderLensCapsule(focusState.sy, offset);
674
+ }
675
+
676
+ /* ───────────── 主循环 ───────────── */
677
+ let startTime = null, currentT = 0;
678
+
679
+ function loop(ts) {
680
+ if (startTime === null) startTime = ts;
681
+ lastWallTime = ts;
682
+ if (!params.pause) {
683
+ currentT = (ts - startTime) / 1000;
684
+ }
685
+ render(currentT);
686
+ requestAnimationFrame(loop);
687
+ }
688
+
689
+ /* ───────────── 拉杆绑定 ───────────── */
690
+ requestAnimationFrame(loop);
691
+ ]]></script>
692
692
  </svg>