@mumulinya167/ccm-pet-assets 2.0.0

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