@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,658 +1,658 @@
1
- <svg id="svg" xmlns="http://www.w3.org/2000/svg" width="880" height="720" viewBox="-32 -24 88 72" preserveAspectRatio="xMidYMid meet">
2
- <style><![CDATA[
3
- .phase-tag {
4
- position: absolute; top: 14px; left: 50%;
5
- transform: translateX(-50%);
6
- padding: 4px 12px; border-radius: 6px;
7
- background: rgba(255,165,107,.16); color: #FFD3B3;
8
- font: 700 10px ui-monospace, monospace; letter-spacing: .05em;
9
- pointer-events: none;
10
- }
11
- .timeline {
12
- position: absolute; left: 16px; right: 16px; bottom: 12px;
13
- height: 4px; border-radius: 2px; overflow: hidden;
14
- background: rgba(255,255,255,.06);
15
- }
16
- .timeline-fill {
17
- height: 100%; width: 0; border-radius: 2px;
18
- background: linear-gradient(to right, #B1A7FF, #FFD3B3, #FCE3B0, #FCC9DD);
19
- }
20
- .edge-rim-tight { stroke-opacity: .65; }
21
- .edge-rim-soft { stroke-opacity: .50; }
22
- .edge-contact { stroke-opacity: .65; }
23
- .statusbar {
24
- min-width: 760px; background: #1f1f26; color: #FFA56B;
25
- border-radius: 8px; padding: 6px 14px; text-align: center;
26
- font: 10px ui-monospace, monospace; letter-spacing: .05em;
27
- }
28
- @media (prefers-color-scheme: light) {
29
- .phase-tag { background: rgba(201,122,74,.10); color: #C97A4A; }
30
- .timeline { background: rgba(57,65,255,.08); }
31
- .edge-rim-tight { stroke-opacity: .75; }
32
- .edge-rim-soft { stroke-opacity: .60; }
33
- .edge-contact { stroke-opacity: .75; }
34
- .cloud-drop-shadow { flood-opacity: .13; }
35
- .statusbar { background: #fff; color: #C97A4A; }
36
- }
37
- ]]></style>
38
- <defs>
39
- <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
40
- <stop offset="0" stop-color="#f0f9ff"/>
41
- <stop offset="0.5" stop-color="#e0f2fe"/>
42
- <stop offset="1" stop-color="#bae6fd"/>
43
- </linearGradient>
44
- <linearGradient id="eye-grad" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
45
- <stop offset="0" stop-color="#B1A7FF"/>
46
- <stop offset="0.5" stop-color="#7A9DFF"/>
47
- <stop offset="1" stop-color="#3941FF"/>
48
- </linearGradient>
49
- <radialGradient id="hi-grad" cx="0.45" cy="0.25" r="0.55">
50
- <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.9"/>
51
- <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
52
- </radialGradient>
53
-
54
- <linearGradient id="warm-back-grad" x1="0" y1="-9" x2="0" y2="9" gradientUnits="userSpaceOnUse">
55
- <stop offset="0" stop-color="#FFE3CE"/>
56
- <stop offset="0.5" stop-color="#FFD3B3"/>
57
- <stop offset="1" stop-color="#FFB89A"/>
58
- </linearGradient>
59
- <linearGradient id="warm-front-grad" x1="0" y1="-9" x2="0" y2="9" gradientUnits="userSpaceOnUse">
60
- <stop offset="0" stop-color="#FFF1CE"/>
61
- <stop offset="0.5" stop-color="#FCE3B0"/>
62
- <stop offset="1" stop-color="#F5C374"/>
63
- </linearGradient>
64
- <radialGradient id="warm-hi" cx="32%" cy="24%" r="60%">
65
- <stop offset="0" stop-color="#f0f9ff" stop-opacity=".62"/>
66
- <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
67
- </radialGradient>
68
-
69
- <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"/>
70
- <clipPath id="cloud-clip"><use href="#cloud-shape"/></clipPath>
71
- <path id="warm-cloud-path" d="M-15.6 5.4c-2.4 0-4.4-1.8-4.4-4.0 0-1.9 1.5-3.6 3.5-4.0 .4-2.7 3.0-4.8 6.2-4.8 1.2 0 2.3 .3 3.3 .9 1.4-2.1 3.9-3.5 6.8-3.5 3.6 0 6.6 2.1 7.4 5.0 3.4 0 6.0 2.4 6.0 5.3 0 2.8-2.5 5.0-5.6 5.0H-15.6z"/>
72
-
73
- <filter id="cloud-shadow" x="-50%" y="-50%" width="200%" height="200%">
74
- <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
75
- </filter>
76
- <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
77
- <feGaussianBlur stdDeviation="0.16"/>
78
- </filter>
79
- <filter id="warm-shadow" x="-80%" y="-120%" width="260%" height="340%">
80
- <feDropShadow dx="0" dy="0.9" stdDeviation="1.05" flood-color="#A2603C" flood-opacity=".18"/>
81
- </filter>
82
- <filter id="soft-glow" x="-120%" y="-120%" width="340%" height="340%">
83
- <feGaussianBlur stdDeviation="0.7" result="blur"/>
84
- <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
85
- </filter>
86
- <filter id="q-glow" x="-80%" y="-80%" width="260%" height="260%">
87
- <feGaussianBlur stdDeviation="0.28" result="b"/>
88
- <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
89
- </filter>
90
-
91
- <symbol id="warm-back-cloud" viewBox="-22 -14 44 28">
92
- <g filter="url(#warm-shadow)">
93
- <use href="#warm-cloud-path" fill="url(#warm-back-grad)" stroke="#E69E78" stroke-width="0.55" stroke-opacity="0.32" stroke-linejoin="round"/>
94
- <ellipse cx="-3.5" cy="-4.5" rx="6" ry="2.8" fill="url(#warm-hi)" opacity=".48"/>
95
- </g>
96
- </symbol>
97
- <symbol id="warm-front-cloud" viewBox="-22 -14 44 28">
98
- <g filter="url(#warm-shadow)">
99
- <use href="#warm-cloud-path" fill="url(#warm-front-grad)" stroke="#D9A04F" stroke-width="0.55" stroke-opacity="0.36" stroke-linejoin="round"/>
100
- <ellipse cx="-3" cy="-4" rx="5.2" ry="2.4" fill="url(#warm-hi)" opacity=".55"/>
101
- </g>
102
- </symbol>
103
- </defs>
104
-
105
- <!-- 后层暖云 (在 cloudling 后面) -->
106
- <g id="warmBackLayer" filter="url(#soft-glow)" opacity="0">
107
- <g id="warmBackRoot">
108
- <use href="#warm-back-cloud" x="-22" y="-14" width="44" height="28"/>
109
- </g>
110
- </g>
111
-
112
- <!-- cloudling 主体 -->
113
- <g id="cloudRoot">
114
- <g id="bodyWarp">
115
- <g id="cloudShape">
116
- <g id="edge-system">
117
- <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)"/>
118
- <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#edge-blur)"/>
119
- <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
120
- </g>
121
- <g filter="url(#cloud-shadow)">
122
- <use href="#cloud-shape" fill="url(#body-grad)"/>
123
- <rect x="1" y="2" width="22" height="11" fill="url(#hi-grad)" clip-path="url(#cloud-clip)"/>
124
- </g>
125
- </g>
126
- </g>
127
- <g id="eyeLayer">
128
- <g id="capsuleEyes">
129
- <rect id="capL" x="7.7" y="9.5" width="2.6" height="5" rx="1.3" ry="1.3" fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
130
- <rect id="capR" x="13.7" y="9.5" width="2.6" height="5" rx="1.3" ry="1.3" fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
131
- </g>
132
- </g>
133
- </g>
134
-
135
- <!-- 前层暖云 (在 cloudling 前面) -->
136
- <g id="warmFrontLayer" filter="url(#soft-glow)" opacity="0">
137
- <g id="warmFrontRoot">
138
- <use href="#warm-front-cloud" x="-22" y="-14" width="44" height="28"/>
139
- </g>
140
- </g>
141
-
142
- <!-- 问号 (跟着 cloudling 头顶, 默认隐藏) -->
143
- <g id="questionMark" opacity="0" filter="url(#q-glow)">
144
- <g id="qScale" transform="translate(12 0)">
145
- <path d="M -1.4 -1.6 Q -1.4 -3.0 0 -3.0 Q 1.4 -3.0 1.4 -1.6 Q 1.4 -0.6 0.4 0.0 Q -0.1 0.35 -0.1 1.3"
146
- fill="none" stroke="url(#eye-grad)" stroke-width="0.72" stroke-linecap="round" stroke-linejoin="round"/>
147
- <circle cx="-0.1" cy="2.7" r="0.44" fill="url(#eye-grad)"/>
148
- </g>
149
- </g>
150
- <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
151
- <g id="btn-pause"></g>
152
- <g id="btn-reset"></g>
153
- <g id="btn-restart"></g>
154
- <g id="btn-theme"></g>
155
- <g id="phaseTag"></g>
156
- <g id="statusbar"></g>
157
- <g id="timelineFill"></g>
158
- </g>
159
-
160
- <script type="application/ecmascript"><![CDATA[
161
- const CENTER = { x: 12, y: 12 };
162
- const DEFAULTS = {
163
- duration: 6.2,
164
- gap: 1.0,
165
- bodyBreath: 0.030,
166
- rimRotate: 1.5,
167
- warmSpeed: 1.00,
168
- warmOp: 0.95,
169
- barrierGap: 3.0,
170
- barrierY: 16.5,
171
- hideTy: 1.0,
172
- peekTy: -4.5,
173
- peekTx: 8.5,
174
- peekLean: 12.5,
175
- peekScaleDip: 0.075,
176
- eyeWiden: 0.19,
177
- rightEyeOffsetX: 1.40,
178
- rightEyeLift: 0.30,
179
- qSize: 1.00,
180
- qOffsetX: -6.0,
181
- qOffsetY: -10.0,
182
- };
183
-
184
- const controls = Object.fromEntries(Object.entries(DEFAULTS).map(([key, value]) => [key, { value: String(value) }]));
185
- const cloudRoot = document.getElementById('cloudRoot');
186
- const bodyWarp = document.getElementById('bodyWarp');
187
- const cloudShape = document.getElementById('cloudShape');
188
- const eyeLayer = document.getElementById('eyeLayer');
189
- const capL = document.getElementById('capL');
190
- const capR = document.getElementById('capR');
191
- const warmBackLayer = document.getElementById('warmBackLayer');
192
- const warmBackRoot = document.getElementById('warmBackRoot');
193
- const warmFrontLayer = document.getElementById('warmFrontLayer');
194
- const warmFrontRoot = document.getElementById('warmFrontRoot');
195
- const questionMark = document.getElementById('questionMark');
196
- const qScale = document.getElementById('qScale');
197
- const phaseTag = document.getElementById('phaseTag');
198
- const timelineFill = document.getElementById('timelineFill');
199
- const statusbar = document.getElementById('statusbar');
200
-
201
- let start = performance.now();
202
- let paused = false;
203
- let pauseAt = 0;
204
-
205
- function clamp(v, a = 0, b = 1) { return Math.max(a, Math.min(b, v)); }
206
- function lerp(a, b, t) { return a + (b - a) * t; }
207
- function easeIn(t) { return t * t; }
208
- function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
209
- function easeInOut(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
210
- function smoother(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
211
- function wave(u) { return Math.sin(clamp(u) * Math.PI); }
212
- function remap(t, a, b) { return clamp((t - a) / (b - a)); }
213
- function enterHold(t, inEnd = 0.18) {
214
- if (t < inEnd) return easeOut(t / inEnd);
215
- return 1;
216
- }
217
-
218
- // ===== 9 段时间线: 6.2s 叙事版 =====
219
- // 1 暖云入场+合拢 0.00 - 1.10
220
- // 2 左探前压扁蓄力 1.10 - 1.45
221
- // 3 左探+问号 1.45 - 2.35
222
- // 4 缩回 1 2.35 - 2.75
223
- // 5 右探前压扁蓄力 2.75 - 3.10
224
- // 6 右探+左上右中眨眼 3.10 - 4.65
225
- // 7 缩回 2 4.65 - 5.05
226
- // 8 暖云散开中心露出 5.05 - 5.85
227
- // 9 回到 idle 5.85 - 6.20
228
- function phaseAt(p) {
229
- if (p < 1.10 / 6.20) return { name: 'warm-enter', local: remap(p, 0, 1.10 / 6.20) };
230
- if (p < 1.45 / 6.20) return { name: 'crouch-left', local: remap(p, 1.10 / 6.20, 1.45 / 6.20) };
231
- if (p < 2.35 / 6.20) return { name: 'peek-left', local: remap(p, 1.45 / 6.20, 2.35 / 6.20) };
232
- if (p < 2.75 / 6.20) return { name: 'retract-1', local: remap(p, 2.35 / 6.20, 2.75 / 6.20) };
233
- if (p < 3.10 / 6.20) return { name: 'crouch-right', local: remap(p, 2.75 / 6.20, 3.10 / 6.20) };
234
- if (p < 4.65 / 6.20) return { name: 'peek-right', local: remap(p, 3.10 / 6.20, 4.65 / 6.20) };
235
- if (p < 5.05 / 6.20) return { name: 'retract-2', local: remap(p, 4.65 / 6.20, 5.05 / 6.20) };
236
- if (p < 5.85 / 6.20) return { name: 'cloud-fade', local: remap(p, 5.05 / 6.20, 5.85 / 6.20) };
237
- return { name: 'recover', local: remap(p, 5.85 / 6.20, 1) };
238
- }
239
-
240
- function setEyeRect(node, cx, cy, w, h) {
241
- node.setAttribute('width', w.toFixed(3));
242
- node.setAttribute('height', h.toFixed(3));
243
- const rx = Math.min(w / 2, h / 2, 1.35);
244
- node.setAttribute('rx', rx.toFixed(3));
245
- node.setAttribute('ry', rx.toFixed(3));
246
- node.setAttribute('x', (cx - w / 2).toFixed(3));
247
- node.setAttribute('y', (cy - h / 2).toFixed(3));
248
- }
249
-
250
- // ===== 暖云更新 =====
251
- // 屏障位置: 偏向可视中心, 后层 x=warmCenterX-barrierGap, 前层 x=warmCenterX+barrierGap
252
- // 入场起点: 从画面内侧进入, 避免 Electron 裁切框露出硬边
253
- // 消散方向: 先淡出再外飘, 避免退出时被 viewBox 切出直边
254
- function updateWarmClouds(t, p, phase, v) {
255
- const warmCenterX = 15.4;
256
- const restBackX = warmCenterX - v.barrierGap;
257
- const restFrontX = warmCenterX + v.barrierGap;
258
- const enterBackX = 12.4;
259
- const enterFrontX = 13.4;
260
- const restY = v.barrierY;
261
- const microFloat = Math.sin(t * 1.6) * 0.12;
262
-
263
- let backX = enterBackX, backY = restY;
264
- let frontX = enterFrontX, frontY = restY;
265
- let backOp = 0, frontOp = 0;
266
- let backRot = 0, frontRot = 0;
267
- let backScaleX = 1.0, backScaleY = 1.0;
268
- let frontScaleX = 1.0, frontScaleY = 1.0;
269
-
270
- if (phase.name === 'idle') {
271
- backOp = 0; frontOp = 0;
272
- } else if (phase.name === 'warm-enter') {
273
- const qBack = easeOut(clamp(phase.local * 1.10 * v.warmSpeed));
274
- const qFront = easeOut(clamp((phase.local - 0.18) * 1.25 * v.warmSpeed));
275
- const bump = wave(remap(phase.local, 0.68, 1.0));
276
- backX = lerp(enterBackX, restBackX, qBack);
277
- frontX = lerp(enterFrontX, restFrontX, qFront);
278
- backY = restY + microFloat * 0.5 + bump * 0.24;
279
- frontY = restY + microFloat + bump * 0.30;
280
- backScaleX = 1 + bump * 0.035;
281
- backScaleY = 1 - bump * 0.045;
282
- frontScaleX = 1 + bump * 0.045;
283
- frontScaleY = 1 - bump * 0.055;
284
- backOp = v.warmOp * qBack;
285
- frontOp = v.warmOp * qFront;
286
- } else if (phase.name === 'crouch-left' || phase.name === 'peek-left' || phase.name === 'retract-1' ||
287
- phase.name === 'crouch-right' || phase.name === 'peek-right' || phase.name === 'retract-2') {
288
- const contactBump = (phase.name === 'crouch-left' || phase.name === 'crouch-right') ? wave(phase.local) : 0;
289
- const leftPress = phase.name === 'peek-left'
290
- ? enterHold(phase.local, 0.18)
291
- : (phase.name === 'retract-1' ? 1 - smoother(phase.local) : 0);
292
- const rightPress = phase.name === 'peek-right'
293
- ? enterHold(phase.local, 0.16)
294
- : (phase.name === 'retract-2' ? 1 - smoother(phase.local) : 0);
295
- backX = restBackX + microFloat * 0.3;
296
- backY = restY + microFloat * 0.5 + contactBump * 0.14 + leftPress * 0.46;
297
- frontX = restFrontX + microFloat * 0.3;
298
- frontY = restY + microFloat + contactBump * 0.18 + rightPress * 0.46;
299
- backRot = -leftPress * 2.6;
300
- frontRot = rightPress * 2.6;
301
- backScaleX = 1 + contactBump * 0.018 + leftPress * 0.035;
302
- backScaleY = 1 - contactBump * 0.025 - leftPress * 0.050;
303
- frontScaleX = 1 + contactBump * 0.022 + rightPress * 0.035;
304
- frontScaleY = 1 - contactBump * 0.030 - rightPress * 0.050;
305
- backOp = v.warmOp;
306
- frontOp = v.warmOp;
307
- } else if (phase.name === 'cloud-fade') {
308
- const q = phase.local;
309
- const driftQ = smoother(remap(q, 0.18, 1));
310
- const driftDistance = 42;
311
- const fade = 1 - smoother(remap(q, 0.08, 0.48));
312
- backX = restBackX - driftDistance * driftQ;
313
- backY = restY + microFloat * 0.5 - 1 * driftQ;
314
- frontX = restFrontX + driftDistance * driftQ;
315
- frontY = restY + microFloat - 1 * driftQ;
316
- backRot = -3 * driftQ;
317
- frontRot = 3 * driftQ;
318
- backScaleX = 1.0 - driftQ * 0.10;
319
- backScaleY = 1.0 - driftQ * 0.06;
320
- frontScaleX = 1.0 - driftQ * 0.10;
321
- frontScaleY = 1.0 - driftQ * 0.06;
322
- backOp = v.warmOp * fade;
323
- frontOp = v.warmOp * fade;
324
- } else {
325
- // recover, idle: 已散
326
- backOp = 0; frontOp = 0;
327
- }
328
-
329
- if (backOp > 0 || frontOp > 0) {
330
- const warmBreath = Math.sin(t * 1.35) * 0.008;
331
- backScaleX += warmBreath;
332
- backScaleY -= warmBreath * 0.45;
333
- frontScaleX -= warmBreath * 0.55;
334
- frontScaleY += warmBreath * 0.35;
335
- }
336
-
337
- warmBackRoot.setAttribute('transform',
338
- `translate(${backX.toFixed(3)} ${backY.toFixed(3)}) rotate(${backRot.toFixed(2)}) scale(${backScaleX.toFixed(3)} ${backScaleY.toFixed(3)})`);
339
- warmBackLayer.setAttribute('opacity', backOp.toFixed(3));
340
- warmFrontRoot.setAttribute('transform',
341
- `translate(${frontX.toFixed(3)} ${frontY.toFixed(3)}) rotate(${frontRot.toFixed(2)}) scale(${frontScaleX.toFixed(3)} ${frontScaleY.toFixed(3)})`);
342
- warmFrontLayer.setAttribute('opacity', frontOp.toFixed(3));
343
- }
344
-
345
- // ===== 问号更新 =====
346
- function updateQuestionMark(phase, cloudTx, cloudTy, v) {
347
- // 只在 peek-left 段显示
348
- let qOp = 0;
349
- let qScaleVal = 0;
350
- let qFloatY = 0;
351
- if (phase.name === 'peek-left') {
352
- const q = phase.local;
353
- // 探头出来后慢慢从头外空白处冒出, 避免贴纸式弹跳.
354
- const inT = smoother(remap(q, 0.22, 0.68));
355
- const outT = smoother(remap(q, 0.78, 0.98));
356
- qOp = inT * (1 - outT);
357
- qScaleVal = lerp(0.78, 1.0, inT) * (1 - 0.12 * outT);
358
- qFloatY = lerp(1.05, -0.55, inT) - 0.25 * outT;
359
- }
360
- // 问号位置: 跟着 cloudling 头顶 (cloudling 中心 + headOffset)
361
- const qX = CENTER.x + cloudTx + v.qOffsetX;
362
- const qY = CENTER.y + cloudTy + v.qOffsetY + qFloatY;
363
- questionMark.setAttribute('opacity', qOp.toFixed(3));
364
- qScale.setAttribute('transform',
365
- `translate(${qX.toFixed(2)} ${qY.toFixed(2)}) scale(${(qScaleVal * v.qSize).toFixed(3)})`);
366
- }
367
-
368
- // ===== cloudling 更新 =====
369
- function updateCloud(t, p, phase, v) {
370
- let tx = 0, ty = 0;
371
- let sx = 1, sy = 1;
372
- let rootScale = 1;
373
- let rot = 0;
374
- let eyeShiftX = 0, eyeShiftY = 0;
375
- let widenFactor = 1;
376
- let heightFactor = 1;
377
- let cloudOpacity = 1;
378
-
379
- if (phase.name === 'idle') {
380
- const breath = Math.sin(t * Math.PI * 2 / 3.6) * 0.012;
381
- sx = 1 - breath * 0.4;
382
- sy = 1 + breath;
383
- } else if (phase.name === 'warm-enter') {
384
- // cloudling 站在原地, 眼睛追着暖云从左飘到中央
385
- const q = phase.local;
386
- const hideSink = smoother(remap(q, 0.58, 1.0));
387
- ty = v.hideTy * hideSink;
388
- eyeShiftX = lerp(-0.40, -0.05, smoother(q)); // 看左 → 几乎归正 (云在中下方了)
389
- eyeShiftY = lerp(0.0, 0.20, smoother(q)); // 由平视 → 略向下看 (云沉到中下)
390
- const breath = Math.sin(t * Math.PI * 2 / 3.6) * 0.010;
391
- sx = 1 - breath * 0.4;
392
- sy = 1 + breath;
393
- } else if (phase.name === 'crouch-left') {
394
- // 左探前明确压扁蓄力, 让起跳有准备动作.
395
- const q = phase.local;
396
- const crouch = wave(q);
397
- tx = 0;
398
- ty = v.hideTy;
399
- rootScale = 1 - crouch * 0.020;
400
- sx = 1 + crouch * 0.085;
401
- sy = 1 - crouch * 0.115;
402
- rot = -crouch * 1.5;
403
- eyeShiftY = lerp(0.18, 0.32, crouch);
404
- cloudOpacity = 1;
405
- } else if (phase.name === 'peek-left') {
406
- // 从蓄力释放到左上, 然后停住给问号和小眼神.
407
- const q = phase.local;
408
- const outT = enterHold(q, 0.18);
409
- tx = lerp(0, -v.peekTx, outT);
410
- ty = lerp(v.hideTy, v.peekTy, outT);
411
- const peekPulse = Math.sin(clamp(q) * Math.PI) * v.peekScaleDip * 0.16;
412
- rootScale = 1 - outT * v.peekScaleDip + peekPulse;
413
- sx = 1 + outT * v.peekScaleDip * 0.34;
414
- sy = 1 - outT * v.peekScaleDip * 0.38;
415
- // 眼睛: 露出来后好奇瞪大 + 左右扫
416
- if (q > 0.24) {
417
- widenFactor = 1 + v.eyeWiden;
418
- heightFactor = 1 + 0.04;
419
- const scanQ = remap(q, 0.28, 0.76);
420
- eyeShiftX = Math.sin(scanQ * Math.PI * 1.25) * 0.28;
421
- eyeShiftY = -0.05 + Math.sin(scanQ * Math.PI) * 0.06;
422
- }
423
- rot = -outT * v.peekLean + Math.sin(clamp(q) * Math.PI * 2) * 0.45;
424
- cloudOpacity = 1;
425
- } else if (phase.name === 'retract-1') {
426
- // "哧溜" 缩回中央被遮 (easeIn 后段加速)
427
- const q = phase.local;
428
- // 0-0.65: 快速缩 (easeIn), 0.65-1.0: 完全藏好
429
- const inT = q < 0.65 ? easeIn(q / 0.65) : 1.0;
430
- tx = lerp(-v.peekTx, 0, inT);
431
- ty = lerp(v.peekTy, v.hideTy, inT);
432
- rootScale = lerp(1 - v.peekScaleDip, 0.985, inT);
433
- sx = lerp(1 + v.peekScaleDip * 0.34, 1, inT);
434
- sy = lerp(1 - v.peekScaleDip * 0.38, 1, inT);
435
- rot = lerp(-v.peekLean, 0, inT);
436
- // 缩回时眼睛快速归正
437
- eyeShiftX = Math.sin(Math.PI * 1.25) * 0.28 * (1 - easeOut(remap(q, 0, 0.38)));
438
- eyeShiftY = -0.05 * (1 - easeOut(remap(q, 0, 0.38)));
439
- widenFactor = 1 + v.eyeWiden * (1 - inT);
440
- cloudOpacity = 1;
441
- } else if (phase.name === 'crouch-right') {
442
- // 第二次蓄力稍短, 为右探观察做铺垫.
443
- const q = phase.local;
444
- const crouch = wave(q);
445
- tx = 0;
446
- ty = v.hideTy;
447
- rootScale = 1 - crouch * 0.018;
448
- sx = 1 + crouch * 0.075;
449
- sy = 1 - crouch * 0.105;
450
- rot = crouch * 1.3;
451
- eyeShiftY = lerp(0.16, 0.30, crouch);
452
- cloudOpacity = 1;
453
- } else if (phase.name === 'peek-right') {
454
- // 第二次多停一拍, 像确认状况: 左、上、右、下看一圈再准备缩回.
455
- const q = phase.local;
456
- const outT = enterHold(q, 0.16);
457
- const bodyCheck = Math.sin(remap(q, 0.28, 0.78) * Math.PI * 2) * wave(remap(q, 0.22, 0.92)) * 0.16;
458
- tx = lerp(0, v.peekTx, outT);
459
- tx += bodyCheck;
460
- ty = lerp(v.hideTy, v.peekTy, outT);
461
- const peekPulse = Math.sin(clamp(q) * Math.PI) * v.peekScaleDip * 0.16;
462
- rootScale = 1 - outT * v.peekScaleDip + peekPulse;
463
- sx = 1 + outT * v.peekScaleDip * 0.34;
464
- sy = 1 - outT * v.peekScaleDip * 0.38;
465
- const rightEyeBiasT = smoother(remap(q, 0.08, 0.36));
466
- if (q > 0.18) {
467
- widenFactor = 1 + v.eyeWiden;
468
- const scanAmp = 0.28;
469
- const blink = Math.max(wave(remap(q, 0.78, 0.84)), wave(remap(q, 0.88, 0.94)));
470
- heightFactor = (1 + 0.04) * (1 - blink * 0.86);
471
- widenFactor *= 1 + blink * 0.08;
472
- if (blink > 0.05) {
473
- eyeShiftX = 0;
474
- eyeShiftY = 0.03;
475
- } else if (q < 0.30) {
476
- eyeShiftX = 0;
477
- eyeShiftY = 0.00;
478
- } else if (q < 0.42) {
479
- eyeShiftX = lerp(0, -0.06, easeInOut(remap(q, 0.30, 0.42)));
480
- eyeShiftY = lerp(0.00, -0.30, easeOut(remap(q, 0.30, 0.42)));
481
- } else if (q < 0.50) {
482
- eyeShiftX = -0.06;
483
- eyeShiftY = -0.30;
484
- } else if (q < 0.58) {
485
- eyeShiftX = lerp(-0.06, scanAmp, easeInOut(remap(q, 0.50, 0.58)));
486
- eyeShiftY = lerp(-0.30, 0.00, easeInOut(remap(q, 0.50, 0.58)));
487
- } else if (q < 0.66) {
488
- eyeShiftX = scanAmp;
489
- eyeShiftY = 0.00;
490
- } else if (q < 0.74) {
491
- eyeShiftX = lerp(scanAmp, 0, easeOut(remap(q, 0.66, 0.74)));
492
- eyeShiftY = 0.00;
493
- } else {
494
- eyeShiftX = 0;
495
- eyeShiftY = 0.00;
496
- }
497
- }
498
- eyeShiftX += v.rightEyeOffsetX * rightEyeBiasT;
499
- eyeShiftY -= v.rightEyeLift * rightEyeBiasT;
500
- rot = outT * v.peekLean - Math.sin(clamp(q) * Math.PI * 2) * 0.45 + bodyCheck * 0.65;
501
- cloudOpacity = 1;
502
- } else if (phase.name === 'retract-2') {
503
- const q = phase.local;
504
- const inT = q < 0.65 ? easeIn(q / 0.65) : 1.0;
505
- tx = lerp(v.peekTx, 0, inT);
506
- ty = lerp(v.peekTy, v.hideTy, inT);
507
- rootScale = lerp(1 - v.peekScaleDip, 0.985, inT);
508
- sx = lerp(1 + v.peekScaleDip * 0.34, 1, inT);
509
- sy = lerp(1 - v.peekScaleDip * 0.38, 1, inT);
510
- rot = lerp(v.peekLean, 0, inT);
511
- const eyeReturn = 1 - easeOut(remap(q, 0, 0.35));
512
- eyeShiftX = v.rightEyeOffsetX * eyeReturn;
513
- eyeShiftY = -v.rightEyeLift * eyeReturn;
514
- widenFactor = 1 + v.eyeWiden * (1 - inT);
515
- cloudOpacity = 1;
516
- } else if (phase.name === 'cloud-fade') {
517
- // 云先左右散开, 再露出完整 Cloudling.
518
- const q = phase.local;
519
- const reveal = smoother(remap(q, 0.12, 0.42));
520
- tx = 0;
521
- ty = lerp(v.hideTy, 0, smoother(remap(q, 0.20, 0.66)));
522
- rootScale = 0.985 + reveal * 0.015;
523
- sx = 1 + Math.sin(reveal * Math.PI) * 0.012;
524
- sy = 1 - Math.sin(reveal * Math.PI) * 0.008;
525
- cloudOpacity = 1;
526
- } else if (phase.name === 'recover') {
527
- // 完整露出后, 眼睛轻扫一下回 idle.
528
- const q = phase.local;
529
- if (q < 0.35) {
530
- tx = 0; ty = 0;
531
- const local = q / 0.35;
532
- eyeShiftX = -0.30 * easeOut(local);
533
- eyeShiftY = 0.04;
534
- } else if (q < 0.70) {
535
- tx = 0; ty = 0;
536
- const local = (q - 0.35) / 0.35;
537
- eyeShiftX = lerp(-0.30, 0.30, easeInOut(local));
538
- eyeShiftY = 0.04;
539
- } else {
540
- tx = 0; ty = 0;
541
- const local = (q - 0.70) / 0.30;
542
- eyeShiftX = lerp(0.30, 0, easeOut(local));
543
- eyeShiftY = lerp(0.04, 0, local);
544
- const breath = Math.sin(t * Math.PI * 2 / 3.6) * 0.010;
545
- sx = 1 - breath * 0.4;
546
- sy = 1 + breath;
547
- }
548
- }
549
-
550
- let rimRot = 0;
551
- if (cloudOpacity > 0.05) {
552
- const breath = Math.sin(t * Math.PI * 2 / 3.2);
553
- const liveBreath = breath * v.bodyBreath;
554
- const rimWave = Math.sin(t * Math.PI * 2 / 4.8 + 0.7);
555
- rootScale *= 1 + liveBreath;
556
- sx -= liveBreath * 0.18;
557
- sy += liveBreath * 0.42;
558
- rimRot = rimWave * v.rimRotate;
559
- }
560
-
561
- // 应用 transform
562
- cloudRoot.setAttribute('transform',
563
- `translate(${tx.toFixed(3)} ${ty.toFixed(3)}) rotate(${rot.toFixed(3)} ${CENTER.x} ${CENTER.y}) translate(${CENTER.x} ${CENTER.y}) scale(${rootScale.toFixed(4)}) translate(${-CENTER.x} ${-CENTER.y})`);
564
- cloudRoot.setAttribute('opacity', cloudOpacity.toFixed(3));
565
- bodyWarp.setAttribute('transform',
566
- `translate(${CENTER.x} ${CENTER.y}) scale(${sx.toFixed(4)} ${sy.toFixed(4)}) translate(${-CENTER.x} ${-CENTER.y})`);
567
- cloudShape.setAttribute('transform', `rotate(${rimRot.toFixed(3)} ${CENTER.x} ${CENTER.y})`);
568
-
569
- // 眼睛 layer 整体偏移 (eye-track)
570
- eyeLayer.setAttribute('transform', `translate(${eyeShiftX.toFixed(3)} ${eyeShiftY.toFixed(3)})`);
571
-
572
- // 眼睛 capsule 大小
573
- const eyeW = 2.6 * widenFactor;
574
- const eyeH = 5.0 * heightFactor;
575
- setEyeRect(capL, 9, 12, eyeW, eyeH);
576
- setEyeRect(capR, 15, 12, eyeW, eyeH);
577
-
578
- return { tx, ty, rot };
579
- }
580
-
581
- function values() {
582
- const out = {};
583
- Object.entries(controls).forEach(([key, node]) => {
584
- out[key] = Number(node.value);
585
- const label = document.getElementById(`${key}V`);
586
- if (!label) return;
587
- if (key === 'duration' || key === 'gap') label.textContent = `${out[key].toFixed(2)} s`;
588
- else if (key === 'peekLean' || key === 'rimRotate') label.textContent = `${out[key].toFixed(1)}°`;
589
- else if (key === 'peekScaleDip' || key === 'bodyBreath') label.textContent = out[key].toFixed(3);
590
- else if (key === 'barrierY' || key === 'hideTy' || key === 'peekTy' || key === 'peekTx' || key === 'qOffsetX' || key === 'qOffsetY' || key === 'barrierGap') label.textContent = out[key].toFixed(1);
591
- else label.textContent = out[key].toFixed(2);
592
- });
593
- return out;
594
- }
595
-
596
- function frame(now) {
597
- if (!paused) {
598
- const v = values();
599
- const cycle = v.duration + v.gap;
600
- const elapsed = (now - start) / 1000;
601
- const t = elapsed % cycle;
602
- const activeT = Math.min(t, v.duration);
603
- const p = activeT / v.duration;
604
- const inGap = t > v.duration;
605
- const phase = inGap ? { name: 'idle', local: 1 } : phaseAt(p);
606
- updateWarmClouds(elapsed, p, phase, v);
607
- const state = updateCloud(elapsed, p, phase, v);
608
- updateQuestionMark(phase, state.tx, state.ty, v);
609
- phaseTag.textContent = phase.name;
610
- timelineFill.style.width = `${(p * 100).toFixed(2)}%`;
611
- statusbar.textContent = `phase: ${phase.name} · loop ${(p * 100).toFixed(1)}% · cloud (${state.tx.toFixed(2)}, ${state.ty.toFixed(2)})`;
612
- }
613
- requestAnimationFrame(frame);
614
- }
615
-
616
- document.getElementById('btn-theme').addEventListener('click', () => {
617
- document.body.classList.toggle('light');
618
- });
619
- document.getElementById('btn-pause').addEventListener('click', () => {
620
- paused = !paused;
621
- if (paused) pauseAt = performance.now();
622
- else start += performance.now() - pauseAt;
623
- });
624
- document.getElementById('btn-restart').addEventListener('click', () => {
625
- start = performance.now();
626
- });
627
- document.getElementById('btn-reset').addEventListener('click', () => {
628
- Object.entries(DEFAULTS).forEach(([key, value]) => {
629
- controls[key].value = String(value);
630
- });
631
- });
632
- Object.entries(DEFAULTS).forEach(([key, value]) => {
633
- controls[key].value = String(value);
634
- });
635
- values();
636
-
637
- // debug seek hook
638
- window.__seekTo = (p) => {
639
- const v = values();
640
- paused = true;
641
- const elapsed = p * v.duration;
642
- const phase = phaseAt(p);
643
- updateWarmClouds(elapsed, p, phase, v);
644
- const state = updateCloud(elapsed, p, phase, v);
645
- updateQuestionMark(phase, state.tx, state.ty, v);
646
- phaseTag.textContent = phase.name;
647
- timelineFill.style.width = `${(p * 100).toFixed(2)}%`;
648
- statusbar.textContent = `phase: ${phase.name} · loop ${(p * 100).toFixed(1)}% · cloud (${state.tx.toFixed(2)}, ${state.ty.toFixed(2)})`;
649
- };
650
-
651
- const urlP = new URLSearchParams(location.search).get('p');
652
- if (urlP !== null) {
653
- setTimeout(() => window.__seekTo(parseFloat(urlP)), 100);
654
- }
655
-
656
- requestAnimationFrame(frame);
657
- ]]></script>
658
- </svg>
1
+ <svg id="svg" xmlns="http://www.w3.org/2000/svg" width="880" height="720" viewBox="-32 -24 88 72" preserveAspectRatio="xMidYMid meet">
2
+ <style><![CDATA[
3
+ .phase-tag {
4
+ position: absolute; top: 14px; left: 50%;
5
+ transform: translateX(-50%);
6
+ padding: 4px 12px; border-radius: 6px;
7
+ background: rgba(255,165,107,.16); color: #FFD3B3;
8
+ font: 700 10px ui-monospace, monospace; letter-spacing: .05em;
9
+ pointer-events: none;
10
+ }
11
+ .timeline {
12
+ position: absolute; left: 16px; right: 16px; bottom: 12px;
13
+ height: 4px; border-radius: 2px; overflow: hidden;
14
+ background: rgba(255,255,255,.06);
15
+ }
16
+ .timeline-fill {
17
+ height: 100%; width: 0; border-radius: 2px;
18
+ background: linear-gradient(to right, #B1A7FF, #FFD3B3, #FCE3B0, #FCC9DD);
19
+ }
20
+ .edge-rim-tight { stroke-opacity: .65; }
21
+ .edge-rim-soft { stroke-opacity: .50; }
22
+ .edge-contact { stroke-opacity: .65; }
23
+ .statusbar {
24
+ min-width: 760px; background: #1f1f26; color: #FFA56B;
25
+ border-radius: 8px; padding: 6px 14px; text-align: center;
26
+ font: 10px ui-monospace, monospace; letter-spacing: .05em;
27
+ }
28
+ @media (prefers-color-scheme: light) {
29
+ .phase-tag { background: rgba(201,122,74,.10); color: #C97A4A; }
30
+ .timeline { background: rgba(57,65,255,.08); }
31
+ .edge-rim-tight { stroke-opacity: .75; }
32
+ .edge-rim-soft { stroke-opacity: .60; }
33
+ .edge-contact { stroke-opacity: .75; }
34
+ .cloud-drop-shadow { flood-opacity: .13; }
35
+ .statusbar { background: #fff; color: #C97A4A; }
36
+ }
37
+ ]]></style>
38
+ <defs>
39
+ <linearGradient id="body-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
40
+ <stop offset="0" stop-color="#f0f9ff"/>
41
+ <stop offset="0.5" stop-color="#e0f2fe"/>
42
+ <stop offset="1" stop-color="#bae6fd"/>
43
+ </linearGradient>
44
+ <linearGradient id="eye-grad" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
45
+ <stop offset="0" stop-color="#B1A7FF"/>
46
+ <stop offset="0.5" stop-color="#7A9DFF"/>
47
+ <stop offset="1" stop-color="#3941FF"/>
48
+ </linearGradient>
49
+ <radialGradient id="hi-grad" cx="0.45" cy="0.25" r="0.55">
50
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.9"/>
51
+ <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
52
+ </radialGradient>
53
+
54
+ <linearGradient id="warm-back-grad" x1="0" y1="-9" x2="0" y2="9" gradientUnits="userSpaceOnUse">
55
+ <stop offset="0" stop-color="#FFE3CE"/>
56
+ <stop offset="0.5" stop-color="#FFD3B3"/>
57
+ <stop offset="1" stop-color="#FFB89A"/>
58
+ </linearGradient>
59
+ <linearGradient id="warm-front-grad" x1="0" y1="-9" x2="0" y2="9" gradientUnits="userSpaceOnUse">
60
+ <stop offset="0" stop-color="#FFF1CE"/>
61
+ <stop offset="0.5" stop-color="#FCE3B0"/>
62
+ <stop offset="1" stop-color="#F5C374"/>
63
+ </linearGradient>
64
+ <radialGradient id="warm-hi" cx="32%" cy="24%" r="60%">
65
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity=".62"/>
66
+ <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
67
+ </radialGradient>
68
+
69
+ <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"/>
70
+ <clipPath id="cloud-clip"><use href="#cloud-shape"/></clipPath>
71
+ <path id="warm-cloud-path" d="M-15.6 5.4c-2.4 0-4.4-1.8-4.4-4.0 0-1.9 1.5-3.6 3.5-4.0 .4-2.7 3.0-4.8 6.2-4.8 1.2 0 2.3 .3 3.3 .9 1.4-2.1 3.9-3.5 6.8-3.5 3.6 0 6.6 2.1 7.4 5.0 3.4 0 6.0 2.4 6.0 5.3 0 2.8-2.5 5.0-5.6 5.0H-15.6z"/>
72
+
73
+ <filter id="cloud-shadow" x="-50%" y="-50%" width="200%" height="200%">
74
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
75
+ </filter>
76
+ <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
77
+ <feGaussianBlur stdDeviation="0.16"/>
78
+ </filter>
79
+ <filter id="warm-shadow" x="-80%" y="-120%" width="260%" height="340%">
80
+ <feDropShadow dx="0" dy="0.9" stdDeviation="1.05" flood-color="#A2603C" flood-opacity=".18"/>
81
+ </filter>
82
+ <filter id="soft-glow" x="-120%" y="-120%" width="340%" height="340%">
83
+ <feGaussianBlur stdDeviation="0.7" result="blur"/>
84
+ <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
85
+ </filter>
86
+ <filter id="q-glow" x="-80%" y="-80%" width="260%" height="260%">
87
+ <feGaussianBlur stdDeviation="0.28" result="b"/>
88
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
89
+ </filter>
90
+
91
+ <symbol id="warm-back-cloud" viewBox="-22 -14 44 28">
92
+ <g filter="url(#warm-shadow)">
93
+ <use href="#warm-cloud-path" fill="url(#warm-back-grad)" stroke="#E69E78" stroke-width="0.55" stroke-opacity="0.32" stroke-linejoin="round"/>
94
+ <ellipse cx="-3.5" cy="-4.5" rx="6" ry="2.8" fill="url(#warm-hi)" opacity=".48"/>
95
+ </g>
96
+ </symbol>
97
+ <symbol id="warm-front-cloud" viewBox="-22 -14 44 28">
98
+ <g filter="url(#warm-shadow)">
99
+ <use href="#warm-cloud-path" fill="url(#warm-front-grad)" stroke="#D9A04F" stroke-width="0.55" stroke-opacity="0.36" stroke-linejoin="round"/>
100
+ <ellipse cx="-3" cy="-4" rx="5.2" ry="2.4" fill="url(#warm-hi)" opacity=".55"/>
101
+ </g>
102
+ </symbol>
103
+ </defs>
104
+
105
+ <!-- 后层暖云 (在 cloudling 后面) -->
106
+ <g id="warmBackLayer" filter="url(#soft-glow)" opacity="0">
107
+ <g id="warmBackRoot">
108
+ <use href="#warm-back-cloud" x="-22" y="-14" width="44" height="28"/>
109
+ </g>
110
+ </g>
111
+
112
+ <!-- cloudling 主体 -->
113
+ <g id="cloudRoot">
114
+ <g id="bodyWarp">
115
+ <g id="cloudShape">
116
+ <g id="edge-system">
117
+ <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)"/>
118
+ <use class="edge-rim-soft" href="#cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#edge-blur)"/>
119
+ <use class="edge-rim-tight" href="#cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
120
+ </g>
121
+ <g filter="url(#cloud-shadow)">
122
+ <use href="#cloud-shape" fill="url(#body-grad)"/>
123
+ <rect x="1" y="2" width="22" height="11" fill="url(#hi-grad)" clip-path="url(#cloud-clip)"/>
124
+ </g>
125
+ </g>
126
+ </g>
127
+ <g id="eyeLayer">
128
+ <g id="capsuleEyes">
129
+ <rect id="capL" x="7.7" y="9.5" width="2.6" height="5" rx="1.3" ry="1.3" fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
130
+ <rect id="capR" x="13.7" y="9.5" width="2.6" height="5" rx="1.3" ry="1.3" fill="url(#eye-grad)" stroke="#ffffff" stroke-width="0.08" stroke-opacity="0.7"/>
131
+ </g>
132
+ </g>
133
+ </g>
134
+
135
+ <!-- 前层暖云 (在 cloudling 前面) -->
136
+ <g id="warmFrontLayer" filter="url(#soft-glow)" opacity="0">
137
+ <g id="warmFrontRoot">
138
+ <use href="#warm-front-cloud" x="-22" y="-14" width="44" height="28"/>
139
+ </g>
140
+ </g>
141
+
142
+ <!-- 问号 (跟着 cloudling 头顶, 默认隐藏) -->
143
+ <g id="questionMark" opacity="0" filter="url(#q-glow)">
144
+ <g id="qScale" transform="translate(12 0)">
145
+ <path d="M -1.4 -1.6 Q -1.4 -3.0 0 -3.0 Q 1.4 -3.0 1.4 -1.6 Q 1.4 -0.6 0.4 0.0 Q -0.1 0.35 -0.1 1.3"
146
+ fill="none" stroke="url(#eye-grad)" stroke-width="0.72" stroke-linecap="round" stroke-linejoin="round"/>
147
+ <circle cx="-0.1" cy="2.7" r="0.44" fill="url(#eye-grad)"/>
148
+ </g>
149
+ </g>
150
+ <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
151
+ <g id="btn-pause"></g>
152
+ <g id="btn-reset"></g>
153
+ <g id="btn-restart"></g>
154
+ <g id="btn-theme"></g>
155
+ <g id="phaseTag"></g>
156
+ <g id="statusbar"></g>
157
+ <g id="timelineFill"></g>
158
+ </g>
159
+
160
+ <script type="application/ecmascript"><![CDATA[
161
+ const CENTER = { x: 12, y: 12 };
162
+ const DEFAULTS = {
163
+ duration: 6.2,
164
+ gap: 1.0,
165
+ bodyBreath: 0.030,
166
+ rimRotate: 1.5,
167
+ warmSpeed: 1.00,
168
+ warmOp: 0.95,
169
+ barrierGap: 3.0,
170
+ barrierY: 16.5,
171
+ hideTy: 1.0,
172
+ peekTy: -4.5,
173
+ peekTx: 8.5,
174
+ peekLean: 12.5,
175
+ peekScaleDip: 0.075,
176
+ eyeWiden: 0.19,
177
+ rightEyeOffsetX: 1.40,
178
+ rightEyeLift: 0.30,
179
+ qSize: 1.00,
180
+ qOffsetX: -6.0,
181
+ qOffsetY: -10.0,
182
+ };
183
+
184
+ const controls = Object.fromEntries(Object.entries(DEFAULTS).map(([key, value]) => [key, { value: String(value) }]));
185
+ const cloudRoot = document.getElementById('cloudRoot');
186
+ const bodyWarp = document.getElementById('bodyWarp');
187
+ const cloudShape = document.getElementById('cloudShape');
188
+ const eyeLayer = document.getElementById('eyeLayer');
189
+ const capL = document.getElementById('capL');
190
+ const capR = document.getElementById('capR');
191
+ const warmBackLayer = document.getElementById('warmBackLayer');
192
+ const warmBackRoot = document.getElementById('warmBackRoot');
193
+ const warmFrontLayer = document.getElementById('warmFrontLayer');
194
+ const warmFrontRoot = document.getElementById('warmFrontRoot');
195
+ const questionMark = document.getElementById('questionMark');
196
+ const qScale = document.getElementById('qScale');
197
+ const phaseTag = document.getElementById('phaseTag');
198
+ const timelineFill = document.getElementById('timelineFill');
199
+ const statusbar = document.getElementById('statusbar');
200
+
201
+ let start = performance.now();
202
+ let paused = false;
203
+ let pauseAt = 0;
204
+
205
+ function clamp(v, a = 0, b = 1) { return Math.max(a, Math.min(b, v)); }
206
+ function lerp(a, b, t) { return a + (b - a) * t; }
207
+ function easeIn(t) { return t * t; }
208
+ function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
209
+ function easeInOut(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
210
+ function smoother(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
211
+ function wave(u) { return Math.sin(clamp(u) * Math.PI); }
212
+ function remap(t, a, b) { return clamp((t - a) / (b - a)); }
213
+ function enterHold(t, inEnd = 0.18) {
214
+ if (t < inEnd) return easeOut(t / inEnd);
215
+ return 1;
216
+ }
217
+
218
+ // ===== 9 段时间线: 6.2s 叙事版 =====
219
+ // 1 暖云入场+合拢 0.00 - 1.10
220
+ // 2 左探前压扁蓄力 1.10 - 1.45
221
+ // 3 左探+问号 1.45 - 2.35
222
+ // 4 缩回 1 2.35 - 2.75
223
+ // 5 右探前压扁蓄力 2.75 - 3.10
224
+ // 6 右探+左上右中眨眼 3.10 - 4.65
225
+ // 7 缩回 2 4.65 - 5.05
226
+ // 8 暖云散开中心露出 5.05 - 5.85
227
+ // 9 回到 idle 5.85 - 6.20
228
+ function phaseAt(p) {
229
+ if (p < 1.10 / 6.20) return { name: 'warm-enter', local: remap(p, 0, 1.10 / 6.20) };
230
+ if (p < 1.45 / 6.20) return { name: 'crouch-left', local: remap(p, 1.10 / 6.20, 1.45 / 6.20) };
231
+ if (p < 2.35 / 6.20) return { name: 'peek-left', local: remap(p, 1.45 / 6.20, 2.35 / 6.20) };
232
+ if (p < 2.75 / 6.20) return { name: 'retract-1', local: remap(p, 2.35 / 6.20, 2.75 / 6.20) };
233
+ if (p < 3.10 / 6.20) return { name: 'crouch-right', local: remap(p, 2.75 / 6.20, 3.10 / 6.20) };
234
+ if (p < 4.65 / 6.20) return { name: 'peek-right', local: remap(p, 3.10 / 6.20, 4.65 / 6.20) };
235
+ if (p < 5.05 / 6.20) return { name: 'retract-2', local: remap(p, 4.65 / 6.20, 5.05 / 6.20) };
236
+ if (p < 5.85 / 6.20) return { name: 'cloud-fade', local: remap(p, 5.05 / 6.20, 5.85 / 6.20) };
237
+ return { name: 'recover', local: remap(p, 5.85 / 6.20, 1) };
238
+ }
239
+
240
+ function setEyeRect(node, cx, cy, w, h) {
241
+ node.setAttribute('width', w.toFixed(3));
242
+ node.setAttribute('height', h.toFixed(3));
243
+ const rx = Math.min(w / 2, h / 2, 1.35);
244
+ node.setAttribute('rx', rx.toFixed(3));
245
+ node.setAttribute('ry', rx.toFixed(3));
246
+ node.setAttribute('x', (cx - w / 2).toFixed(3));
247
+ node.setAttribute('y', (cy - h / 2).toFixed(3));
248
+ }
249
+
250
+ // ===== 暖云更新 =====
251
+ // 屏障位置: 偏向可视中心, 后层 x=warmCenterX-barrierGap, 前层 x=warmCenterX+barrierGap
252
+ // 入场起点: 从画面内侧进入, 避免 Electron 裁切框露出硬边
253
+ // 消散方向: 先淡出再外飘, 避免退出时被 viewBox 切出直边
254
+ function updateWarmClouds(t, p, phase, v) {
255
+ const warmCenterX = 15.4;
256
+ const restBackX = warmCenterX - v.barrierGap;
257
+ const restFrontX = warmCenterX + v.barrierGap;
258
+ const enterBackX = 12.4;
259
+ const enterFrontX = 13.4;
260
+ const restY = v.barrierY;
261
+ const microFloat = Math.sin(t * 1.6) * 0.12;
262
+
263
+ let backX = enterBackX, backY = restY;
264
+ let frontX = enterFrontX, frontY = restY;
265
+ let backOp = 0, frontOp = 0;
266
+ let backRot = 0, frontRot = 0;
267
+ let backScaleX = 1.0, backScaleY = 1.0;
268
+ let frontScaleX = 1.0, frontScaleY = 1.0;
269
+
270
+ if (phase.name === 'idle') {
271
+ backOp = 0; frontOp = 0;
272
+ } else if (phase.name === 'warm-enter') {
273
+ const qBack = easeOut(clamp(phase.local * 1.10 * v.warmSpeed));
274
+ const qFront = easeOut(clamp((phase.local - 0.18) * 1.25 * v.warmSpeed));
275
+ const bump = wave(remap(phase.local, 0.68, 1.0));
276
+ backX = lerp(enterBackX, restBackX, qBack);
277
+ frontX = lerp(enterFrontX, restFrontX, qFront);
278
+ backY = restY + microFloat * 0.5 + bump * 0.24;
279
+ frontY = restY + microFloat + bump * 0.30;
280
+ backScaleX = 1 + bump * 0.035;
281
+ backScaleY = 1 - bump * 0.045;
282
+ frontScaleX = 1 + bump * 0.045;
283
+ frontScaleY = 1 - bump * 0.055;
284
+ backOp = v.warmOp * qBack;
285
+ frontOp = v.warmOp * qFront;
286
+ } else if (phase.name === 'crouch-left' || phase.name === 'peek-left' || phase.name === 'retract-1' ||
287
+ phase.name === 'crouch-right' || phase.name === 'peek-right' || phase.name === 'retract-2') {
288
+ const contactBump = (phase.name === 'crouch-left' || phase.name === 'crouch-right') ? wave(phase.local) : 0;
289
+ const leftPress = phase.name === 'peek-left'
290
+ ? enterHold(phase.local, 0.18)
291
+ : (phase.name === 'retract-1' ? 1 - smoother(phase.local) : 0);
292
+ const rightPress = phase.name === 'peek-right'
293
+ ? enterHold(phase.local, 0.16)
294
+ : (phase.name === 'retract-2' ? 1 - smoother(phase.local) : 0);
295
+ backX = restBackX + microFloat * 0.3;
296
+ backY = restY + microFloat * 0.5 + contactBump * 0.14 + leftPress * 0.46;
297
+ frontX = restFrontX + microFloat * 0.3;
298
+ frontY = restY + microFloat + contactBump * 0.18 + rightPress * 0.46;
299
+ backRot = -leftPress * 2.6;
300
+ frontRot = rightPress * 2.6;
301
+ backScaleX = 1 + contactBump * 0.018 + leftPress * 0.035;
302
+ backScaleY = 1 - contactBump * 0.025 - leftPress * 0.050;
303
+ frontScaleX = 1 + contactBump * 0.022 + rightPress * 0.035;
304
+ frontScaleY = 1 - contactBump * 0.030 - rightPress * 0.050;
305
+ backOp = v.warmOp;
306
+ frontOp = v.warmOp;
307
+ } else if (phase.name === 'cloud-fade') {
308
+ const q = phase.local;
309
+ const driftQ = smoother(remap(q, 0.18, 1));
310
+ const driftDistance = 42;
311
+ const fade = 1 - smoother(remap(q, 0.08, 0.48));
312
+ backX = restBackX - driftDistance * driftQ;
313
+ backY = restY + microFloat * 0.5 - 1 * driftQ;
314
+ frontX = restFrontX + driftDistance * driftQ;
315
+ frontY = restY + microFloat - 1 * driftQ;
316
+ backRot = -3 * driftQ;
317
+ frontRot = 3 * driftQ;
318
+ backScaleX = 1.0 - driftQ * 0.10;
319
+ backScaleY = 1.0 - driftQ * 0.06;
320
+ frontScaleX = 1.0 - driftQ * 0.10;
321
+ frontScaleY = 1.0 - driftQ * 0.06;
322
+ backOp = v.warmOp * fade;
323
+ frontOp = v.warmOp * fade;
324
+ } else {
325
+ // recover, idle: 已散
326
+ backOp = 0; frontOp = 0;
327
+ }
328
+
329
+ if (backOp > 0 || frontOp > 0) {
330
+ const warmBreath = Math.sin(t * 1.35) * 0.008;
331
+ backScaleX += warmBreath;
332
+ backScaleY -= warmBreath * 0.45;
333
+ frontScaleX -= warmBreath * 0.55;
334
+ frontScaleY += warmBreath * 0.35;
335
+ }
336
+
337
+ warmBackRoot.setAttribute('transform',
338
+ `translate(${backX.toFixed(3)} ${backY.toFixed(3)}) rotate(${backRot.toFixed(2)}) scale(${backScaleX.toFixed(3)} ${backScaleY.toFixed(3)})`);
339
+ warmBackLayer.setAttribute('opacity', backOp.toFixed(3));
340
+ warmFrontRoot.setAttribute('transform',
341
+ `translate(${frontX.toFixed(3)} ${frontY.toFixed(3)}) rotate(${frontRot.toFixed(2)}) scale(${frontScaleX.toFixed(3)} ${frontScaleY.toFixed(3)})`);
342
+ warmFrontLayer.setAttribute('opacity', frontOp.toFixed(3));
343
+ }
344
+
345
+ // ===== 问号更新 =====
346
+ function updateQuestionMark(phase, cloudTx, cloudTy, v) {
347
+ // 只在 peek-left 段显示
348
+ let qOp = 0;
349
+ let qScaleVal = 0;
350
+ let qFloatY = 0;
351
+ if (phase.name === 'peek-left') {
352
+ const q = phase.local;
353
+ // 探头出来后慢慢从头外空白处冒出, 避免贴纸式弹跳.
354
+ const inT = smoother(remap(q, 0.22, 0.68));
355
+ const outT = smoother(remap(q, 0.78, 0.98));
356
+ qOp = inT * (1 - outT);
357
+ qScaleVal = lerp(0.78, 1.0, inT) * (1 - 0.12 * outT);
358
+ qFloatY = lerp(1.05, -0.55, inT) - 0.25 * outT;
359
+ }
360
+ // 问号位置: 跟着 cloudling 头顶 (cloudling 中心 + headOffset)
361
+ const qX = CENTER.x + cloudTx + v.qOffsetX;
362
+ const qY = CENTER.y + cloudTy + v.qOffsetY + qFloatY;
363
+ questionMark.setAttribute('opacity', qOp.toFixed(3));
364
+ qScale.setAttribute('transform',
365
+ `translate(${qX.toFixed(2)} ${qY.toFixed(2)}) scale(${(qScaleVal * v.qSize).toFixed(3)})`);
366
+ }
367
+
368
+ // ===== cloudling 更新 =====
369
+ function updateCloud(t, p, phase, v) {
370
+ let tx = 0, ty = 0;
371
+ let sx = 1, sy = 1;
372
+ let rootScale = 1;
373
+ let rot = 0;
374
+ let eyeShiftX = 0, eyeShiftY = 0;
375
+ let widenFactor = 1;
376
+ let heightFactor = 1;
377
+ let cloudOpacity = 1;
378
+
379
+ if (phase.name === 'idle') {
380
+ const breath = Math.sin(t * Math.PI * 2 / 3.6) * 0.012;
381
+ sx = 1 - breath * 0.4;
382
+ sy = 1 + breath;
383
+ } else if (phase.name === 'warm-enter') {
384
+ // cloudling 站在原地, 眼睛追着暖云从左飘到中央
385
+ const q = phase.local;
386
+ const hideSink = smoother(remap(q, 0.58, 1.0));
387
+ ty = v.hideTy * hideSink;
388
+ eyeShiftX = lerp(-0.40, -0.05, smoother(q)); // 看左 → 几乎归正 (云在中下方了)
389
+ eyeShiftY = lerp(0.0, 0.20, smoother(q)); // 由平视 → 略向下看 (云沉到中下)
390
+ const breath = Math.sin(t * Math.PI * 2 / 3.6) * 0.010;
391
+ sx = 1 - breath * 0.4;
392
+ sy = 1 + breath;
393
+ } else if (phase.name === 'crouch-left') {
394
+ // 左探前明确压扁蓄力, 让起跳有准备动作.
395
+ const q = phase.local;
396
+ const crouch = wave(q);
397
+ tx = 0;
398
+ ty = v.hideTy;
399
+ rootScale = 1 - crouch * 0.020;
400
+ sx = 1 + crouch * 0.085;
401
+ sy = 1 - crouch * 0.115;
402
+ rot = -crouch * 1.5;
403
+ eyeShiftY = lerp(0.18, 0.32, crouch);
404
+ cloudOpacity = 1;
405
+ } else if (phase.name === 'peek-left') {
406
+ // 从蓄力释放到左上, 然后停住给问号和小眼神.
407
+ const q = phase.local;
408
+ const outT = enterHold(q, 0.18);
409
+ tx = lerp(0, -v.peekTx, outT);
410
+ ty = lerp(v.hideTy, v.peekTy, outT);
411
+ const peekPulse = Math.sin(clamp(q) * Math.PI) * v.peekScaleDip * 0.16;
412
+ rootScale = 1 - outT * v.peekScaleDip + peekPulse;
413
+ sx = 1 + outT * v.peekScaleDip * 0.34;
414
+ sy = 1 - outT * v.peekScaleDip * 0.38;
415
+ // 眼睛: 露出来后好奇瞪大 + 左右扫
416
+ if (q > 0.24) {
417
+ widenFactor = 1 + v.eyeWiden;
418
+ heightFactor = 1 + 0.04;
419
+ const scanQ = remap(q, 0.28, 0.76);
420
+ eyeShiftX = Math.sin(scanQ * Math.PI * 1.25) * 0.28;
421
+ eyeShiftY = -0.05 + Math.sin(scanQ * Math.PI) * 0.06;
422
+ }
423
+ rot = -outT * v.peekLean + Math.sin(clamp(q) * Math.PI * 2) * 0.45;
424
+ cloudOpacity = 1;
425
+ } else if (phase.name === 'retract-1') {
426
+ // "哧溜" 缩回中央被遮 (easeIn 后段加速)
427
+ const q = phase.local;
428
+ // 0-0.65: 快速缩 (easeIn), 0.65-1.0: 完全藏好
429
+ const inT = q < 0.65 ? easeIn(q / 0.65) : 1.0;
430
+ tx = lerp(-v.peekTx, 0, inT);
431
+ ty = lerp(v.peekTy, v.hideTy, inT);
432
+ rootScale = lerp(1 - v.peekScaleDip, 0.985, inT);
433
+ sx = lerp(1 + v.peekScaleDip * 0.34, 1, inT);
434
+ sy = lerp(1 - v.peekScaleDip * 0.38, 1, inT);
435
+ rot = lerp(-v.peekLean, 0, inT);
436
+ // 缩回时眼睛快速归正
437
+ eyeShiftX = Math.sin(Math.PI * 1.25) * 0.28 * (1 - easeOut(remap(q, 0, 0.38)));
438
+ eyeShiftY = -0.05 * (1 - easeOut(remap(q, 0, 0.38)));
439
+ widenFactor = 1 + v.eyeWiden * (1 - inT);
440
+ cloudOpacity = 1;
441
+ } else if (phase.name === 'crouch-right') {
442
+ // 第二次蓄力稍短, 为右探观察做铺垫.
443
+ const q = phase.local;
444
+ const crouch = wave(q);
445
+ tx = 0;
446
+ ty = v.hideTy;
447
+ rootScale = 1 - crouch * 0.018;
448
+ sx = 1 + crouch * 0.075;
449
+ sy = 1 - crouch * 0.105;
450
+ rot = crouch * 1.3;
451
+ eyeShiftY = lerp(0.16, 0.30, crouch);
452
+ cloudOpacity = 1;
453
+ } else if (phase.name === 'peek-right') {
454
+ // 第二次多停一拍, 像确认状况: 左、上、右、下看一圈再准备缩回.
455
+ const q = phase.local;
456
+ const outT = enterHold(q, 0.16);
457
+ const bodyCheck = Math.sin(remap(q, 0.28, 0.78) * Math.PI * 2) * wave(remap(q, 0.22, 0.92)) * 0.16;
458
+ tx = lerp(0, v.peekTx, outT);
459
+ tx += bodyCheck;
460
+ ty = lerp(v.hideTy, v.peekTy, outT);
461
+ const peekPulse = Math.sin(clamp(q) * Math.PI) * v.peekScaleDip * 0.16;
462
+ rootScale = 1 - outT * v.peekScaleDip + peekPulse;
463
+ sx = 1 + outT * v.peekScaleDip * 0.34;
464
+ sy = 1 - outT * v.peekScaleDip * 0.38;
465
+ const rightEyeBiasT = smoother(remap(q, 0.08, 0.36));
466
+ if (q > 0.18) {
467
+ widenFactor = 1 + v.eyeWiden;
468
+ const scanAmp = 0.28;
469
+ const blink = Math.max(wave(remap(q, 0.78, 0.84)), wave(remap(q, 0.88, 0.94)));
470
+ heightFactor = (1 + 0.04) * (1 - blink * 0.86);
471
+ widenFactor *= 1 + blink * 0.08;
472
+ if (blink > 0.05) {
473
+ eyeShiftX = 0;
474
+ eyeShiftY = 0.03;
475
+ } else if (q < 0.30) {
476
+ eyeShiftX = 0;
477
+ eyeShiftY = 0.00;
478
+ } else if (q < 0.42) {
479
+ eyeShiftX = lerp(0, -0.06, easeInOut(remap(q, 0.30, 0.42)));
480
+ eyeShiftY = lerp(0.00, -0.30, easeOut(remap(q, 0.30, 0.42)));
481
+ } else if (q < 0.50) {
482
+ eyeShiftX = -0.06;
483
+ eyeShiftY = -0.30;
484
+ } else if (q < 0.58) {
485
+ eyeShiftX = lerp(-0.06, scanAmp, easeInOut(remap(q, 0.50, 0.58)));
486
+ eyeShiftY = lerp(-0.30, 0.00, easeInOut(remap(q, 0.50, 0.58)));
487
+ } else if (q < 0.66) {
488
+ eyeShiftX = scanAmp;
489
+ eyeShiftY = 0.00;
490
+ } else if (q < 0.74) {
491
+ eyeShiftX = lerp(scanAmp, 0, easeOut(remap(q, 0.66, 0.74)));
492
+ eyeShiftY = 0.00;
493
+ } else {
494
+ eyeShiftX = 0;
495
+ eyeShiftY = 0.00;
496
+ }
497
+ }
498
+ eyeShiftX += v.rightEyeOffsetX * rightEyeBiasT;
499
+ eyeShiftY -= v.rightEyeLift * rightEyeBiasT;
500
+ rot = outT * v.peekLean - Math.sin(clamp(q) * Math.PI * 2) * 0.45 + bodyCheck * 0.65;
501
+ cloudOpacity = 1;
502
+ } else if (phase.name === 'retract-2') {
503
+ const q = phase.local;
504
+ const inT = q < 0.65 ? easeIn(q / 0.65) : 1.0;
505
+ tx = lerp(v.peekTx, 0, inT);
506
+ ty = lerp(v.peekTy, v.hideTy, inT);
507
+ rootScale = lerp(1 - v.peekScaleDip, 0.985, inT);
508
+ sx = lerp(1 + v.peekScaleDip * 0.34, 1, inT);
509
+ sy = lerp(1 - v.peekScaleDip * 0.38, 1, inT);
510
+ rot = lerp(v.peekLean, 0, inT);
511
+ const eyeReturn = 1 - easeOut(remap(q, 0, 0.35));
512
+ eyeShiftX = v.rightEyeOffsetX * eyeReturn;
513
+ eyeShiftY = -v.rightEyeLift * eyeReturn;
514
+ widenFactor = 1 + v.eyeWiden * (1 - inT);
515
+ cloudOpacity = 1;
516
+ } else if (phase.name === 'cloud-fade') {
517
+ // 云先左右散开, 再露出完整 Cloudling.
518
+ const q = phase.local;
519
+ const reveal = smoother(remap(q, 0.12, 0.42));
520
+ tx = 0;
521
+ ty = lerp(v.hideTy, 0, smoother(remap(q, 0.20, 0.66)));
522
+ rootScale = 0.985 + reveal * 0.015;
523
+ sx = 1 + Math.sin(reveal * Math.PI) * 0.012;
524
+ sy = 1 - Math.sin(reveal * Math.PI) * 0.008;
525
+ cloudOpacity = 1;
526
+ } else if (phase.name === 'recover') {
527
+ // 完整露出后, 眼睛轻扫一下回 idle.
528
+ const q = phase.local;
529
+ if (q < 0.35) {
530
+ tx = 0; ty = 0;
531
+ const local = q / 0.35;
532
+ eyeShiftX = -0.30 * easeOut(local);
533
+ eyeShiftY = 0.04;
534
+ } else if (q < 0.70) {
535
+ tx = 0; ty = 0;
536
+ const local = (q - 0.35) / 0.35;
537
+ eyeShiftX = lerp(-0.30, 0.30, easeInOut(local));
538
+ eyeShiftY = 0.04;
539
+ } else {
540
+ tx = 0; ty = 0;
541
+ const local = (q - 0.70) / 0.30;
542
+ eyeShiftX = lerp(0.30, 0, easeOut(local));
543
+ eyeShiftY = lerp(0.04, 0, local);
544
+ const breath = Math.sin(t * Math.PI * 2 / 3.6) * 0.010;
545
+ sx = 1 - breath * 0.4;
546
+ sy = 1 + breath;
547
+ }
548
+ }
549
+
550
+ let rimRot = 0;
551
+ if (cloudOpacity > 0.05) {
552
+ const breath = Math.sin(t * Math.PI * 2 / 3.2);
553
+ const liveBreath = breath * v.bodyBreath;
554
+ const rimWave = Math.sin(t * Math.PI * 2 / 4.8 + 0.7);
555
+ rootScale *= 1 + liveBreath;
556
+ sx -= liveBreath * 0.18;
557
+ sy += liveBreath * 0.42;
558
+ rimRot = rimWave * v.rimRotate;
559
+ }
560
+
561
+ // 应用 transform
562
+ cloudRoot.setAttribute('transform',
563
+ `translate(${tx.toFixed(3)} ${ty.toFixed(3)}) rotate(${rot.toFixed(3)} ${CENTER.x} ${CENTER.y}) translate(${CENTER.x} ${CENTER.y}) scale(${rootScale.toFixed(4)}) translate(${-CENTER.x} ${-CENTER.y})`);
564
+ cloudRoot.setAttribute('opacity', cloudOpacity.toFixed(3));
565
+ bodyWarp.setAttribute('transform',
566
+ `translate(${CENTER.x} ${CENTER.y}) scale(${sx.toFixed(4)} ${sy.toFixed(4)}) translate(${-CENTER.x} ${-CENTER.y})`);
567
+ cloudShape.setAttribute('transform', `rotate(${rimRot.toFixed(3)} ${CENTER.x} ${CENTER.y})`);
568
+
569
+ // 眼睛 layer 整体偏移 (eye-track)
570
+ eyeLayer.setAttribute('transform', `translate(${eyeShiftX.toFixed(3)} ${eyeShiftY.toFixed(3)})`);
571
+
572
+ // 眼睛 capsule 大小
573
+ const eyeW = 2.6 * widenFactor;
574
+ const eyeH = 5.0 * heightFactor;
575
+ setEyeRect(capL, 9, 12, eyeW, eyeH);
576
+ setEyeRect(capR, 15, 12, eyeW, eyeH);
577
+
578
+ return { tx, ty, rot };
579
+ }
580
+
581
+ function values() {
582
+ const out = {};
583
+ Object.entries(controls).forEach(([key, node]) => {
584
+ out[key] = Number(node.value);
585
+ const label = document.getElementById(`${key}V`);
586
+ if (!label) return;
587
+ if (key === 'duration' || key === 'gap') label.textContent = `${out[key].toFixed(2)} s`;
588
+ else if (key === 'peekLean' || key === 'rimRotate') label.textContent = `${out[key].toFixed(1)}°`;
589
+ else if (key === 'peekScaleDip' || key === 'bodyBreath') label.textContent = out[key].toFixed(3);
590
+ else if (key === 'barrierY' || key === 'hideTy' || key === 'peekTy' || key === 'peekTx' || key === 'qOffsetX' || key === 'qOffsetY' || key === 'barrierGap') label.textContent = out[key].toFixed(1);
591
+ else label.textContent = out[key].toFixed(2);
592
+ });
593
+ return out;
594
+ }
595
+
596
+ function frame(now) {
597
+ if (!paused) {
598
+ const v = values();
599
+ const cycle = v.duration + v.gap;
600
+ const elapsed = (now - start) / 1000;
601
+ const t = elapsed % cycle;
602
+ const activeT = Math.min(t, v.duration);
603
+ const p = activeT / v.duration;
604
+ const inGap = t > v.duration;
605
+ const phase = inGap ? { name: 'idle', local: 1 } : phaseAt(p);
606
+ updateWarmClouds(elapsed, p, phase, v);
607
+ const state = updateCloud(elapsed, p, phase, v);
608
+ updateQuestionMark(phase, state.tx, state.ty, v);
609
+ phaseTag.textContent = phase.name;
610
+ timelineFill.style.width = `${(p * 100).toFixed(2)}%`;
611
+ statusbar.textContent = `phase: ${phase.name} · loop ${(p * 100).toFixed(1)}% · cloud (${state.tx.toFixed(2)}, ${state.ty.toFixed(2)})`;
612
+ }
613
+ requestAnimationFrame(frame);
614
+ }
615
+
616
+ document.getElementById('btn-theme').addEventListener('click', () => {
617
+ document.body.classList.toggle('light');
618
+ });
619
+ document.getElementById('btn-pause').addEventListener('click', () => {
620
+ paused = !paused;
621
+ if (paused) pauseAt = performance.now();
622
+ else start += performance.now() - pauseAt;
623
+ });
624
+ document.getElementById('btn-restart').addEventListener('click', () => {
625
+ start = performance.now();
626
+ });
627
+ document.getElementById('btn-reset').addEventListener('click', () => {
628
+ Object.entries(DEFAULTS).forEach(([key, value]) => {
629
+ controls[key].value = String(value);
630
+ });
631
+ });
632
+ Object.entries(DEFAULTS).forEach(([key, value]) => {
633
+ controls[key].value = String(value);
634
+ });
635
+ values();
636
+
637
+ // debug seek hook
638
+ window.__seekTo = (p) => {
639
+ const v = values();
640
+ paused = true;
641
+ const elapsed = p * v.duration;
642
+ const phase = phaseAt(p);
643
+ updateWarmClouds(elapsed, p, phase, v);
644
+ const state = updateCloud(elapsed, p, phase, v);
645
+ updateQuestionMark(phase, state.tx, state.ty, v);
646
+ phaseTag.textContent = phase.name;
647
+ timelineFill.style.width = `${(p * 100).toFixed(2)}%`;
648
+ statusbar.textContent = `phase: ${phase.name} · loop ${(p * 100).toFixed(1)}% · cloud (${state.tx.toFixed(2)}, ${state.ty.toFixed(2)})`;
649
+ };
650
+
651
+ const urlP = new URLSearchParams(location.search).get('p');
652
+ if (urlP !== null) {
653
+ setTimeout(() => window.__seekTo(parseFloat(urlP)), 100);
654
+ }
655
+
656
+ requestAnimationFrame(frame);
657
+ ]]></script>
658
+ </svg>