@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,494 +1,494 @@
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
- .statusbar {
4
- font-size: 10px;
5
- font-family: ui-monospace, monospace;
6
- color: #7A9DFF;
7
- background: #1f1f26;
8
- border-radius: 8px;
9
- padding: 6px 14px;
10
- min-width: 760px;
11
- text-align: center;
12
- letter-spacing: 0.05em;
13
- }
14
- .statusbar .phase-A { color: #7A9DFF; }
15
- .statusbar .phase-B { color: #FFA56B; font-weight: 700; }
16
- .statusbar .phase-C { color: #B1A7FF; }
17
- .edge-rim-tight { stroke-opacity: .65; }
18
- .edge-rim-soft { stroke-opacity: .50; }
19
- .edge-contact { stroke-opacity: .65; }
20
- @media (prefers-color-scheme: light) {
21
- .statusbar { background: #fff; color: #3941FF; }
22
- .edge-rim-tight { stroke-opacity: .75; }
23
- .edge-rim-soft { stroke-opacity: .60; }
24
- .edge-contact { stroke-opacity: .75; }
25
- .cloud-drop-shadow { flood-opacity: .13; }
26
- .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
27
- .food-cloud > path {
28
- stroke: #5870E8 !important;
29
- stroke-width: 1.2 !important;
30
- stroke-opacity: .9 !important;
31
- vector-effect: non-scaling-stroke !important;
32
- }
33
- }
34
- ]]></style>
35
- <defs>
36
- <linearGradient id="bg-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
37
- <stop offset="0" stop-color="#f0f9ff"/>
38
- <stop offset="0.5" stop-color="#e0f2fe"/>
39
- <stop offset="1" stop-color="#bae6fd"/>
40
- </linearGradient>
41
- <radialGradient id="hg-grad" cx="0.45" cy="0.25" r="0.55">
42
- <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.9"/>
43
- <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
44
- </radialGradient>
45
- <linearGradient id="eye-grad" x1="0" y1="0" x2="0" y2="24" gradientUnits="userSpaceOnUse">
46
- <stop offset="0" stop-color="#B1A7FF"/>
47
- <stop offset="0.5" stop-color="#7A9DFF"/>
48
- <stop offset="1" stop-color="#3941FF"/>
49
- </linearGradient>
50
- <linearGradient id="signal-grad" x1="-2" y1="12" x2="26" y2="12" gradientUnits="userSpaceOnUse">
51
- <stop offset="0" stop-color="#B1A7FF" stop-opacity="0.10"/>
52
- <stop offset="0.22" stop-color="#8EA7FF" stop-opacity="0.92"/>
53
- <stop offset="0.78" stop-color="#7A9DFF" stop-opacity="0.92"/>
54
- <stop offset="1" stop-color="#3941FF" stop-opacity="0.10"/>
55
- </linearGradient>
56
- <linearGradient id="alert-grad" x1="19" y1="4" x2="5" y2="22" gradientUnits="userSpaceOnUse">
57
- <stop offset="0" stop-color="#FFD36E" stop-opacity="0.98"/>
58
- <stop offset="0.36" stop-color="#FFA56B" stop-opacity="0.9"/>
59
- <stop offset="0.72" stop-color="#8EA7FF" stop-opacity="0.74"/>
60
- <stop offset="1" stop-color="#3941FF" stop-opacity="0.18"/>
61
- </linearGradient>
62
- <filter id="cloud-shadow" x="-40%" y="-40%" width="180%" height="180%">
63
- <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
64
- </filter>
65
- <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
66
- <feGaussianBlur stdDeviation="0.16"/>
67
- </filter>
68
- <filter id="pulse-glow" x="-50%" y="-50%" width="200%" height="200%">
69
- <feGaussianBlur stdDeviation="0.22" result="b"/>
70
- <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
71
- </filter>
72
- <filter id="alert-glow" x="-80%" y="-80%" width="260%" height="260%">
73
- <feGaussianBlur stdDeviation="0.38" result="b"/>
74
- <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
75
- </filter>
76
- <clipPath id="cloud-clip">
77
- <path 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"/>
78
- </clipPath>
79
- </defs>
80
-
81
- <!-- 脉冲圈层 (在 cloudling 后面) -->
82
- <g id="pulses-layer"></g>
83
-
84
- <!-- cloudling 主体 -->
85
- <g id="g-translate">
86
- <g id="g-rotate">
87
- <g id="g-squash">
88
- <g id="edge-system">
89
- <path class="edge-contact" 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" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#edge-blur)"/>
90
- <path class="edge-rim-soft" 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" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#edge-blur)"/>
91
- <path class="edge-rim-tight" 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" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
92
- </g>
93
- <g id="cloud-body" filter="url(#cloud-shadow)">
94
- <path 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" fill="url(#bg-grad)"/>
95
- <rect x="1" y="2" width="22" height="11" fill="url(#hg-grad)" clip-path="url(#cloud-clip)"/>
96
- </g>
97
- <g id="rim-flash-layer"></g>
98
- <!-- 眼睛层 (counter-rotate 容器) -->
99
- <g id="eyes-counter">
100
- <g id="eye-left" transform="translate(9 12)"></g>
101
- <g id="eye-right" transform="translate(15 12)"></g>
102
- </g>
103
- </g>
104
- </g>
105
- </g>
106
- <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
107
- <g id="params"></g>
108
- <g id="statusbar"></g>
109
- </g>
110
-
111
- <script type="application/ecmascript"><![CDATA[
112
- /* ============================================================
113
- Cloudling notification — ABC 三段式 + 老式闹铃
114
- ============================================================ */
115
-
116
- const COLOR = '#7A9DFF';
117
- const EYE_GRAD = 'url(#eye-grad)';
118
-
119
- // ===== 默认参数 (推荐值) =====
120
- const DEFAULTS = {
121
- totalPeriod: 2600, // ms 总周期
122
- ratioA: 0.22, // A 段占比
123
- ratioB: 0.50, // B 段占比 (C = 1 - A - B)
124
- shakeAngle: 14, // ° 摇晃幅度
125
- shakeFreqHz: 7, // Hz 摇晃频率
126
- shakeJumpY: 0.46, // px 跟随微跳幅度
127
- squashStrength: 0.040, // 摇晃顶点 squash (1 - x)
128
- scaleMicro: 0.034, // micro 顶点撑大幅度
129
- scaleMacro: 0.060, // macro B 段整气幅度
130
- idleBreathAmp: 0.02, // idle 微呼吸幅度 (0.96 ↔ 1.00 → ±0.02)
131
- idleBreathPeriod: 5000, // ms idle 呼吸周期 (跟 idle 锁定一致)
132
- pulseLifetimeA: 760, // ms A 段预亮生命
133
- pulseLifetimeB: 520, // ms B 段冲击弧生命
134
- pulseScaleEnd: 1.54, // 终值 scale
135
- pulseStrokeWidth: 0.76, // 通知弧描边宽度
136
- pulseGlow: 0.36, // glow stdDeviation
137
- pulseStartRadius: 8.9, // 弧出生时半径
138
- pulseFadeIn: 35, // ms 弧入场 fade in (opacity 0→max)
139
- eyeWidenAmount: 0.22, // capsule rx +22%
140
- eyeWidenDuration: 100, // ms 瞪大持续时间
141
- counterRotate: true, // 眼睛是否抵消 rotate
142
- };
143
- let P = { ...DEFAULTS };
144
-
145
- // ===== params UI =====
146
- const PARAM_DEFS = [
147
- { group: '节奏 / RHYTHM' },
148
- { key: 'totalPeriod', label: '总周期 (ms)', min: 1500, max: 4000, step: 100 },
149
- { key: 'ratioA', label: 'A 段占比 (蓄势)', min: 0.10, max: 0.50, step: 0.02 },
150
- { key: 'ratioB', label: 'B 段占比 (发作)', min: 0.20, max: 0.70, step: 0.02 },
151
- { group: '摇晃 / SHAKE' },
152
- { key: 'shakeAngle', label: '摇晃幅度 (±°)', min: 4, max: 25, step: 1 },
153
- { key: 'shakeFreqHz', label: '摇晃频率 (Hz)', min: 3, max: 10, step: 0.5 },
154
- { key: 'shakeJumpY', label: '微跳幅度 (px)', min: 0, max: 2, step: 0.1 },
155
- { key: 'squashStrength', label: 'squash 强度 (顶点压)', min: 0, max: 0.12, step: 0.01 },
156
- { group: '呼吸 / BREATH (整体 scale)' },
157
- { key: 'scaleMicro', label: 'micro 顶点撑大 (B)', min: 0, max: 0.10, step: 0.005 },
158
- { key: 'scaleMacro', label: 'macro 整气 (B 段)', min: 0, max: 0.15, step: 0.005 },
159
- { key: 'idleBreathAmp', label: 'idle 呼吸幅度 (A/C)', min: 0, max: 0.05, step: 0.005 },
160
- { key: 'idleBreathPeriod',label: 'idle 呼吸周期 (ms)', min: 2000, max: 8000, step: 200 },
161
- { group: '通知弧 / PULSE' },
162
- { key: 'pulseLifetimeA', label: 'A 段缓弧生命 (ms)', min: 500, max: 1500, step: 50 },
163
- { key: 'pulseLifetimeB', label: 'B 段密集弧生命 (ms)', min: 250, max: 1000, step: 50 },
164
- { key: 'pulseScaleEnd', label: '弧终值 scale', min: 1.15, max: 1.9, step: 0.05 },
165
- { key: 'pulseStrokeWidth',label: '弧描边粗细', min: 0.35, max: 1.6, step: 0.05 },
166
- { key: 'pulseGlow', label: 'glow stdDeviation', min: 0.1, max: 1.5, step: 0.05 },
167
- { key: 'pulseStartRadius',label: '弧出生半径', min: 8.8, max: 12, step: 0.2 },
168
- { key: 'pulseFadeIn', label: '弧入场 fade in (ms)', min: 0, max: 250, step: 10 },
169
- { group: '眼睛 / EYES' },
170
- { key: 'eyeWidenAmount', label: '瞪大幅度 (+rx %)', min: 0, max: 0.5, step: 0.02 },
171
- { key: 'eyeWidenDuration',label: '瞪大持续 (ms)', min: 50, max: 300, step: 10 },
172
- { key: 'counterRotate', label: '眼睛 counter-rotate', type: 'cb' },
173
- ];
174
-
175
- const paramsEl = document.getElementById('params');
176
- function buildParams() {
177
- paramsEl.innerHTML = '';
178
- for (const def of PARAM_DEFS) {
179
- if (def.group) {
180
- const h = document.createElement('h3');
181
- h.textContent = def.group;
182
- paramsEl.appendChild(h);
183
- continue;
184
- }
185
- const lab = document.createElement('label');
186
- if (def.type === 'cb') {
187
- lab.className = 'cb';
188
- lab.innerHTML = `
189
- <input type="checkbox" data-key="${def.key}" ${P[def.key] ? 'checked' : ''}/>
190
- <span>${def.label}</span>
191
- `;
192
- lab.querySelector('input').onchange = e => { P[def.key] = e.target.checked; };
193
- } else {
194
- lab.innerHTML = `
195
- <span>${def.label}</span>
196
- <input type="range" data-key="${def.key}" min="${def.min}" max="${def.max}" step="${def.step}" value="${P[def.key]}"/>
197
- <span class="val" data-val="${def.key}">${P[def.key]}</span>
198
- `;
199
- const range = lab.querySelector('input');
200
- const val = lab.querySelector('[data-val]');
201
- range.oninput = e => {
202
- P[def.key] = parseFloat(e.target.value);
203
- val.textContent = (def.step < 1) ? P[def.key].toFixed(2) : P[def.key];
204
- };
205
- }
206
- paramsEl.appendChild(lab);
207
- }
208
- }
209
-
210
- // ===== 眼睛绘制 =====
211
- function setEyeCapsule(g, widenFactor = 1, heightFactor = 1, tilt = 0) {
212
- const w = 2.55 * widenFactor;
213
- const h = 4.85 * heightFactor;
214
- const rx = Math.min(w / 2, 1.25);
215
- g.innerHTML = `<rect x="${(-w/2).toFixed(3)}" y="${(-h/2).toFixed(3)}" width="${w.toFixed(3)}" height="${h.toFixed(3)}" rx="${rx.toFixed(3)}" fill="${EYE_GRAD}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.55" transform="rotate(${tilt.toFixed(3)})"/>`;
216
- }
217
- function setEyeGT(g) {
218
- // > chevron 向右
219
- const w = 1.6, h = 1.7;
220
- g.innerHTML = `<path d="M ${-w/2} ${-h} L ${w/2} 0 L ${-w/2} ${h}" fill="none" stroke="${EYE_GRAD}" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>`;
221
- }
222
- function setEyeLT(g) {
223
- // < chevron 向左
224
- const w = 1.6, h = 1.7;
225
- g.innerHTML = `<path d="M ${w/2} ${-h} L ${-w/2} 0 L ${w/2} ${h}" fill="none" stroke="${EYE_GRAD}" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>`;
226
- }
227
-
228
- const eyeL = document.getElementById('eye-left');
229
- const eyeR = document.getElementById('eye-right');
230
- const eyesCounter = document.getElementById('eyes-counter');
231
- const gTrans = document.getElementById('g-translate');
232
- const gRotate = document.getElementById('g-rotate');
233
- const gSquash = document.getElementById('g-squash');
234
- const pulsesLayer = document.getElementById('pulses-layer');
235
- const rimFlashLayer = document.getElementById('rim-flash-layer');
236
- const cloudPathD = document.querySelector('#cloud-clip path').getAttribute('d');
237
-
238
- // ===== 脉冲池 =====
239
- let pulses = []; // { bornT (perf ms), lifetime, scaleEnd, type }
240
- let pulseIdCounter = 0;
241
-
242
- function emitPulse(type, now) {
243
- pulses.push({
244
- id: ++pulseIdCounter,
245
- bornT: now,
246
- lifetime: type === 'A' ? P.pulseLifetimeA : P.pulseLifetimeB,
247
- scaleEnd: P.pulseScaleEnd,
248
- type
249
- });
250
- }
251
-
252
- // 动态 glow filter
253
- const glowBlur = document.querySelector('#pulse-glow feGaussianBlur');
254
- function updateGlowStdDev() {
255
- glowBlur.setAttribute('stdDeviation', P.pulseGlow);
256
- }
257
-
258
- function polarPoint(cx, cy, r, deg) {
259
- const a = deg * Math.PI / 180;
260
- return [cx + Math.cos(a) * r, cy + Math.sin(a) * r];
261
- }
262
-
263
- function arcPath(cx, cy, r, startDeg, endDeg) {
264
- const [x0, y0] = polarPoint(cx, cy, r, startDeg);
265
- const [x1, y1] = polarPoint(cx, cy, r, endDeg);
266
- const largeArc = Math.abs(endDeg - startDeg) > 180 ? 1 : 0;
267
- const sweep = endDeg >= startDeg ? 1 : 0;
268
- return `M${x0.toFixed(3)} ${y0.toFixed(3)} A${r.toFixed(3)} ${r.toFixed(3)} 0 ${largeArc} ${sweep} ${x1.toFixed(3)} ${y1.toFixed(3)}`;
269
- }
270
-
271
- function renderPulses(now) {
272
- pulses = pulses.filter(p => now - p.bornT < p.lifetime);
273
- let html = '';
274
- for (const p of pulses) {
275
- const ageMs = now - p.bornT;
276
- const t = ageMs / p.lifetime;
277
- const scale = 1 + (p.scaleEnd - 1) * easeOutCubic(t);
278
- const r = P.pulseStartRadius * scale;
279
- const baseOp = p.type === 'A' ? 0.34 : 0.82;
280
- const fadeIn = P.pulseFadeIn > 0 ? Math.min(1, ageMs / P.pulseFadeIn) : 1;
281
- const fadeOut = Math.pow(1 - t, p.type === 'A' ? 1.22 : 1.55);
282
- const opacity = baseOp * fadeIn * fadeOut;
283
- const wobble = p.type === 'B' ? ((p.id % 3) - 1) * 7 : 0;
284
- const strokeW = P.pulseStrokeWidth * (p.type === 'A' ? 0.62 : 1);
285
- const segments = p.type === 'A'
286
- ? [[-70, -34], [-12, 18], [34, 56]]
287
- : [[-122, -86], [-48, 14], [42, 78], [130, 166], [190, 216]];
288
-
289
- for (let i = 0; i < segments.length; i++) {
290
- const [a0, a1] = segments[i];
291
- const d = arcPath(12, 12, r + (i % 2) * 0.26, a0 + wobble, a1 + wobble);
292
- const segOp = opacity * (1 - i * 0.085);
293
- const stroke = (p.type === 'B' && i < 2) ? 'url(#alert-grad)' : 'url(#signal-grad)';
294
- html += `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeW.toFixed(3)}" stroke-linecap="round" stroke-opacity="${segOp.toFixed(3)}" filter="url(#pulse-glow)"/>`;
295
- }
296
- }
297
- pulsesLayer.innerHTML = html;
298
- }
299
-
300
- function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
301
- function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
302
- function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
303
- function expPulse(x, center, width) {
304
- const d = (x - center) / width;
305
- return Math.exp(-d * d);
306
- }
307
-
308
- function renderAlertLayer(phase, phaseTms, tA, tB, tC, shakePeak, bEnvelope) {
309
- let prep = 0, ping = 0, fade = 0;
310
- if (phase === 'A') {
311
- prep = easeInOutCubic(clamp(phaseTms / tA, 0, 1));
312
- fade = 0.48 + prep * 0.36;
313
- } else if (phase === 'B') {
314
- const b = clamp(phaseTms / tB, 0, 1);
315
- const ping1 = expPulse(b, 0.10, 0.055);
316
- const ping2 = expPulse(b, 0.54, 0.070);
317
- ping = Math.max(ping1, ping2, shakePeak * bEnvelope * 0.34);
318
- fade = 0.92;
319
- } else {
320
- const c = clamp(phaseTms / tC, 0, 1);
321
- fade = (1 - easeOutCubic(c)) * 0.56;
322
- }
323
-
324
- const rimOp = clamp(ping * 0.58 + prep * 0.12, 0, 0.68);
325
- const rimW = 0.28 + ping * 0.42;
326
- rimFlashLayer.innerHTML = rimOp > 0.015
327
- ? `<path d="${cloudPathD}" fill="none" stroke="url(#alert-grad)" stroke-width="${rimW.toFixed(3)}" stroke-linejoin="round" stroke-opacity="${rimOp.toFixed(3)}" filter="url(#alert-glow)"/>`
328
- : '';
329
- }
330
-
331
- // ===== 主循环 =====
332
- let t0 = performance.now();
333
- let lastCycleT = -1;
334
- let emittedA = false;
335
- let emittedBPeaks = 0;
336
- let cycleCount = 0;
337
- let paused = false;
338
-
339
- let pauseAccumT = 0;
340
- let pauseStart = 0;
341
-
342
- const sb = document.getElementById('statusbar');
343
-
344
- function tick() {
345
- if (paused) {
346
- requestAnimationFrame(tick);
347
- return;
348
- }
349
- const now = performance.now();
350
- const tInCycle = ((now - t0) % P.totalPeriod);
351
-
352
- // cycle wrap detection
353
- if (tInCycle < lastCycleT) {
354
- cycleCount++;
355
- emittedA = false;
356
- emittedBPeaks = 0;
357
- }
358
- lastCycleT = tInCycle;
359
-
360
- // 段时长 (动态从当前 P 算)
361
- const tA = P.totalPeriod * P.ratioA;
362
- const tB = P.totalPeriod * P.ratioB;
363
- const tC = P.totalPeriod - tA - tB;
364
-
365
- let phase, phaseTms;
366
- if (tInCycle < tA) {
367
- phase = 'A'; phaseTms = tInCycle;
368
- } else if (tInCycle < tA + tB) {
369
- phase = 'B'; phaseTms = tInCycle - tA;
370
- } else {
371
- phase = 'C'; phaseTms = tInCycle - tA - tB;
372
- }
373
-
374
- // ===== 脉冲发射 =====
375
- // A 段: 在段开头发 1 圈 (缓)
376
- if (phase === 'A' && !emittedA) {
377
- emitPulse('A', now);
378
- emittedA = true;
379
- }
380
- // B 段: 摇晃顶点 = 圈出生 (每半周期一个顶点)
381
- if (phase === 'B') {
382
- const tBSec = phaseTms / 1000;
383
- const peakInterval = 1 / (2 * P.shakeFreqHz); // 半周期 = 一个顶点间隔
384
- // 顶点位于 (k + 0.25) * (1/freq) = (2k+0.5) * peakInterval, k=0,1,2,...
385
- // 简化: 等间隔发射, 第一发在 peakInterval / 2 (即 1/(4*freq))
386
- const expectedPeaks = Math.floor((tBSec - peakInterval / 2) / peakInterval) + 1;
387
- while (emittedBPeaks < expectedPeaks && emittedBPeaks * peakInterval + peakInterval / 2 < (tB / 1000)) {
388
- if (emittedBPeaks % 3 === 0) emitPulse('B', now);
389
- emittedBPeaks++;
390
- }
391
- }
392
-
393
- // ===== 摇晃 / 形变 / 呼吸 =====
394
- let rotate = 0, jumpY = 0;
395
- let burstX = 1, burstY = 1; // B 段额外 scale (在 idle 呼吸基础上叠加)
396
- let shakePeak = 0;
397
- let bEnvelope = 0;
398
-
399
- if (phase === 'B') {
400
- const tBSec = phaseTms / 1000;
401
- const angularFreq = 2 * Math.PI * P.shakeFreqHz;
402
- // 进场 envelope: 前 0.15s 内从 0 -> 1, ease in
403
- const envIn = Math.min(1, tBSec / 0.15);
404
- // 出场 envelope: 最后 0.2s ease out
405
- const tBTotal = tB / 1000;
406
- const envOut = Math.min(1, (tBTotal - tBSec) / 0.2);
407
- const env = Math.min(envIn, envOut);
408
- const sinV = Math.sin(angularFreq * tBSec);
409
- const absSin = Math.abs(sinV);
410
- shakePeak = absSin;
411
- bEnvelope = env;
412
- rotate = P.shakeAngle * sinV * env;
413
- // 微跳: abs(sin) 的 2 倍频, 每次顶点跳一下
414
- jumpY = -P.shakeJumpY * Math.abs(Math.sin(angularFreq * tBSec / 2 + Math.PI / 4)) * env;
415
- // micro 撑大: 跟摇晃顶点同步, abs(sin) 在顶点为 1
416
- const microFactor = 1 + P.scaleMicro * absSin * env;
417
- // macro 整气: B 段一次大吸气-呼气, sin(π * t/T), 中点峰值
418
- const macroFactor = 1 + P.scaleMacro * Math.sin(Math.PI * Math.min(1, Math.max(0, tBSec / tBTotal)));
419
- // squash 顶点压 (scaleY 减一点)
420
- const squashFactor = 1 - P.squashStrength * absSin * env;
421
- burstX = microFactor * macroFactor;
422
- burstY = microFactor * macroFactor * squashFactor;
423
- } else if (phase === 'C') {
424
- // C 段: 摇晃用衰减振荡 ease out
425
- const tCSec = phaseTms / 1000;
426
- const tCTotal = tC / 1000;
427
- const decay = Math.max(0, 1 - tCSec / tCTotal);
428
- const decayEased = decay * decay;
429
- const angularFreq = 2 * Math.PI * P.shakeFreqHz * 0.7;
430
- rotate = P.shakeAngle * decayEased * 0.5 * Math.sin(angularFreq * (tB / 1000 + tCSec));
431
- jumpY = 0;
432
- }
433
-
434
- // ===== idle 呼吸 base (跨 cycle 连续, 不重置) =====
435
- // 中心值 1 - amp, 上下 ±amp, 范围 [1-2*amp, 1] (跟 idle 锁定 0.96-1.00 一致)
436
- const breathPhase = (now / P.idleBreathPeriod) * 2 * Math.PI;
437
- const baseBreath = (1 - P.idleBreathAmp) + P.idleBreathAmp * Math.sin(breathPhase);
438
- // 注意: 上面公式范围 [1-2*amp, 1], idle 锁定 0.96-1.00 即 amp=0.02
439
-
440
- const finalScaleX = baseBreath * burstX;
441
- const finalScaleY = baseBreath * burstY;
442
-
443
- // ===== 应用 transform =====
444
- gTrans.setAttribute('transform', `translate(0 ${jumpY.toFixed(3)})`);
445
- gRotate.setAttribute('transform', `rotate(${rotate.toFixed(3)} 12 12)`);
446
- gSquash.setAttribute('transform', `translate(12 12) scale(${finalScaleX.toFixed(4)} ${finalScaleY.toFixed(4)}) translate(-12 -12)`);
447
-
448
- // ===== 眼睛 counter-rotate =====
449
- if (P.counterRotate) {
450
- eyesCounter.setAttribute('transform', `rotate(${(-rotate).toFixed(3)} 12 12)`);
451
- } else {
452
- eyesCounter.removeAttribute('transform');
453
- }
454
-
455
- // ===== 眼睛 swap =====
456
- let widenFactor = 1;
457
- let eyeHeight = 1;
458
- let eyeTiltL = 0;
459
- let eyeTiltR = 0;
460
- if (phase === 'A') {
461
- const remainA = tA - phaseTms;
462
- if (remainA < P.eyeWidenDuration) {
463
- // 瞪大
464
- const widen01 = 1 - remainA / P.eyeWidenDuration;
465
- widenFactor = 1 + P.eyeWidenAmount * widen01;
466
- }
467
- } else if (phase === 'B') {
468
- widenFactor = 1 + P.eyeWidenAmount * 0.92 + shakePeak * bEnvelope * 0.070;
469
- eyeHeight = 1.04 + shakePeak * bEnvelope * 0.040;
470
- eyeTiltL = -3.0 * bEnvelope;
471
- eyeTiltR = 3.0 * bEnvelope;
472
- } else if (phase === 'C') {
473
- // C 段开头 capsule 重新渐入 (微小入场)
474
- const fadeIn = Math.min(1, phaseTms / 120);
475
- widenFactor = 1 + P.eyeWidenAmount * (1 - fadeIn) * 0.5; // 收回时残留
476
- }
477
-
478
- setEyeCapsule(eyeL, widenFactor, eyeHeight, eyeTiltL);
479
- setEyeCapsule(eyeR, widenFactor, eyeHeight, eyeTiltR);
480
-
481
- // ===== 渲染脉冲 =====
482
- updateGlowStdDev();
483
- renderPulses(now);
484
- renderAlertLayer(phase, phaseTms, tA, tB, tC, shakePeak, bEnvelope);
485
-
486
- // ===== 状态栏 =====
487
- sb.innerHTML = `phase: <span class="phase-${phase}">${phase}</span> · cycle: ${cycleCount} · pulses: ${pulses.length} · t: ${tInCycle.toFixed(0)}ms / ${P.totalPeriod}`;
488
-
489
- requestAnimationFrame(tick);
490
- }
491
-
492
- requestAnimationFrame(tick);
493
- ]]></script>
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
+ .statusbar {
4
+ font-size: 10px;
5
+ font-family: ui-monospace, monospace;
6
+ color: #7A9DFF;
7
+ background: #1f1f26;
8
+ border-radius: 8px;
9
+ padding: 6px 14px;
10
+ min-width: 760px;
11
+ text-align: center;
12
+ letter-spacing: 0.05em;
13
+ }
14
+ .statusbar .phase-A { color: #7A9DFF; }
15
+ .statusbar .phase-B { color: #FFA56B; font-weight: 700; }
16
+ .statusbar .phase-C { color: #B1A7FF; }
17
+ .edge-rim-tight { stroke-opacity: .65; }
18
+ .edge-rim-soft { stroke-opacity: .50; }
19
+ .edge-contact { stroke-opacity: .65; }
20
+ @media (prefers-color-scheme: light) {
21
+ .statusbar { background: #fff; color: #3941FF; }
22
+ .edge-rim-tight { stroke-opacity: .75; }
23
+ .edge-rim-soft { stroke-opacity: .60; }
24
+ .edge-contact { stroke-opacity: .75; }
25
+ .cloud-drop-shadow { flood-opacity: .13; }
26
+ .food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
27
+ .food-cloud > path {
28
+ stroke: #5870E8 !important;
29
+ stroke-width: 1.2 !important;
30
+ stroke-opacity: .9 !important;
31
+ vector-effect: non-scaling-stroke !important;
32
+ }
33
+ }
34
+ ]]></style>
35
+ <defs>
36
+ <linearGradient id="bg-grad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
37
+ <stop offset="0" stop-color="#f0f9ff"/>
38
+ <stop offset="0.5" stop-color="#e0f2fe"/>
39
+ <stop offset="1" stop-color="#bae6fd"/>
40
+ </linearGradient>
41
+ <radialGradient id="hg-grad" cx="0.45" cy="0.25" r="0.55">
42
+ <stop offset="0" stop-color="#f0f9ff" stop-opacity="0.9"/>
43
+ <stop offset="1" stop-color="#f0f9ff" stop-opacity="0"/>
44
+ </radialGradient>
45
+ <linearGradient id="eye-grad" x1="0" y1="0" x2="0" y2="24" gradientUnits="userSpaceOnUse">
46
+ <stop offset="0" stop-color="#B1A7FF"/>
47
+ <stop offset="0.5" stop-color="#7A9DFF"/>
48
+ <stop offset="1" stop-color="#3941FF"/>
49
+ </linearGradient>
50
+ <linearGradient id="signal-grad" x1="-2" y1="12" x2="26" y2="12" gradientUnits="userSpaceOnUse">
51
+ <stop offset="0" stop-color="#B1A7FF" stop-opacity="0.10"/>
52
+ <stop offset="0.22" stop-color="#8EA7FF" stop-opacity="0.92"/>
53
+ <stop offset="0.78" stop-color="#7A9DFF" stop-opacity="0.92"/>
54
+ <stop offset="1" stop-color="#3941FF" stop-opacity="0.10"/>
55
+ </linearGradient>
56
+ <linearGradient id="alert-grad" x1="19" y1="4" x2="5" y2="22" gradientUnits="userSpaceOnUse">
57
+ <stop offset="0" stop-color="#FFD36E" stop-opacity="0.98"/>
58
+ <stop offset="0.36" stop-color="#FFA56B" stop-opacity="0.9"/>
59
+ <stop offset="0.72" stop-color="#8EA7FF" stop-opacity="0.74"/>
60
+ <stop offset="1" stop-color="#3941FF" stop-opacity="0.18"/>
61
+ </linearGradient>
62
+ <filter id="cloud-shadow" x="-40%" y="-40%" width="180%" height="180%">
63
+ <feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="0.18"/>
64
+ </filter>
65
+ <filter id="edge-blur" x="-40%" y="-40%" width="180%" height="180%">
66
+ <feGaussianBlur stdDeviation="0.16"/>
67
+ </filter>
68
+ <filter id="pulse-glow" x="-50%" y="-50%" width="200%" height="200%">
69
+ <feGaussianBlur stdDeviation="0.22" result="b"/>
70
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
71
+ </filter>
72
+ <filter id="alert-glow" x="-80%" y="-80%" width="260%" height="260%">
73
+ <feGaussianBlur stdDeviation="0.38" result="b"/>
74
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
75
+ </filter>
76
+ <clipPath id="cloud-clip">
77
+ <path 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"/>
78
+ </clipPath>
79
+ </defs>
80
+
81
+ <!-- 脉冲圈层 (在 cloudling 后面) -->
82
+ <g id="pulses-layer"></g>
83
+
84
+ <!-- cloudling 主体 -->
85
+ <g id="g-translate">
86
+ <g id="g-rotate">
87
+ <g id="g-squash">
88
+ <g id="edge-system">
89
+ <path class="edge-contact" 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" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#edge-blur)"/>
90
+ <path class="edge-rim-soft" 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" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#edge-blur)"/>
91
+ <path class="edge-rim-tight" 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" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
92
+ </g>
93
+ <g id="cloud-body" filter="url(#cloud-shadow)">
94
+ <path 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" fill="url(#bg-grad)"/>
95
+ <rect x="1" y="2" width="22" height="11" fill="url(#hg-grad)" clip-path="url(#cloud-clip)"/>
96
+ </g>
97
+ <g id="rim-flash-layer"></g>
98
+ <!-- 眼睛层 (counter-rotate 容器) -->
99
+ <g id="eyes-counter">
100
+ <g id="eye-left" transform="translate(9 12)"></g>
101
+ <g id="eye-right" transform="translate(15 12)"></g>
102
+ </g>
103
+ </g>
104
+ </g>
105
+ </g>
106
+ <g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
107
+ <g id="params"></g>
108
+ <g id="statusbar"></g>
109
+ </g>
110
+
111
+ <script type="application/ecmascript"><![CDATA[
112
+ /* ============================================================
113
+ Cloudling notification — ABC 三段式 + 老式闹铃
114
+ ============================================================ */
115
+
116
+ const COLOR = '#7A9DFF';
117
+ const EYE_GRAD = 'url(#eye-grad)';
118
+
119
+ // ===== 默认参数 (推荐值) =====
120
+ const DEFAULTS = {
121
+ totalPeriod: 2600, // ms 总周期
122
+ ratioA: 0.22, // A 段占比
123
+ ratioB: 0.50, // B 段占比 (C = 1 - A - B)
124
+ shakeAngle: 14, // ° 摇晃幅度
125
+ shakeFreqHz: 7, // Hz 摇晃频率
126
+ shakeJumpY: 0.46, // px 跟随微跳幅度
127
+ squashStrength: 0.040, // 摇晃顶点 squash (1 - x)
128
+ scaleMicro: 0.034, // micro 顶点撑大幅度
129
+ scaleMacro: 0.060, // macro B 段整气幅度
130
+ idleBreathAmp: 0.02, // idle 微呼吸幅度 (0.96 ↔ 1.00 → ±0.02)
131
+ idleBreathPeriod: 5000, // ms idle 呼吸周期 (跟 idle 锁定一致)
132
+ pulseLifetimeA: 760, // ms A 段预亮生命
133
+ pulseLifetimeB: 520, // ms B 段冲击弧生命
134
+ pulseScaleEnd: 1.54, // 终值 scale
135
+ pulseStrokeWidth: 0.76, // 通知弧描边宽度
136
+ pulseGlow: 0.36, // glow stdDeviation
137
+ pulseStartRadius: 8.9, // 弧出生时半径
138
+ pulseFadeIn: 35, // ms 弧入场 fade in (opacity 0→max)
139
+ eyeWidenAmount: 0.22, // capsule rx +22%
140
+ eyeWidenDuration: 100, // ms 瞪大持续时间
141
+ counterRotate: true, // 眼睛是否抵消 rotate
142
+ };
143
+ let P = { ...DEFAULTS };
144
+
145
+ // ===== params UI =====
146
+ const PARAM_DEFS = [
147
+ { group: '节奏 / RHYTHM' },
148
+ { key: 'totalPeriod', label: '总周期 (ms)', min: 1500, max: 4000, step: 100 },
149
+ { key: 'ratioA', label: 'A 段占比 (蓄势)', min: 0.10, max: 0.50, step: 0.02 },
150
+ { key: 'ratioB', label: 'B 段占比 (发作)', min: 0.20, max: 0.70, step: 0.02 },
151
+ { group: '摇晃 / SHAKE' },
152
+ { key: 'shakeAngle', label: '摇晃幅度 (±°)', min: 4, max: 25, step: 1 },
153
+ { key: 'shakeFreqHz', label: '摇晃频率 (Hz)', min: 3, max: 10, step: 0.5 },
154
+ { key: 'shakeJumpY', label: '微跳幅度 (px)', min: 0, max: 2, step: 0.1 },
155
+ { key: 'squashStrength', label: 'squash 强度 (顶点压)', min: 0, max: 0.12, step: 0.01 },
156
+ { group: '呼吸 / BREATH (整体 scale)' },
157
+ { key: 'scaleMicro', label: 'micro 顶点撑大 (B)', min: 0, max: 0.10, step: 0.005 },
158
+ { key: 'scaleMacro', label: 'macro 整气 (B 段)', min: 0, max: 0.15, step: 0.005 },
159
+ { key: 'idleBreathAmp', label: 'idle 呼吸幅度 (A/C)', min: 0, max: 0.05, step: 0.005 },
160
+ { key: 'idleBreathPeriod',label: 'idle 呼吸周期 (ms)', min: 2000, max: 8000, step: 200 },
161
+ { group: '通知弧 / PULSE' },
162
+ { key: 'pulseLifetimeA', label: 'A 段缓弧生命 (ms)', min: 500, max: 1500, step: 50 },
163
+ { key: 'pulseLifetimeB', label: 'B 段密集弧生命 (ms)', min: 250, max: 1000, step: 50 },
164
+ { key: 'pulseScaleEnd', label: '弧终值 scale', min: 1.15, max: 1.9, step: 0.05 },
165
+ { key: 'pulseStrokeWidth',label: '弧描边粗细', min: 0.35, max: 1.6, step: 0.05 },
166
+ { key: 'pulseGlow', label: 'glow stdDeviation', min: 0.1, max: 1.5, step: 0.05 },
167
+ { key: 'pulseStartRadius',label: '弧出生半径', min: 8.8, max: 12, step: 0.2 },
168
+ { key: 'pulseFadeIn', label: '弧入场 fade in (ms)', min: 0, max: 250, step: 10 },
169
+ { group: '眼睛 / EYES' },
170
+ { key: 'eyeWidenAmount', label: '瞪大幅度 (+rx %)', min: 0, max: 0.5, step: 0.02 },
171
+ { key: 'eyeWidenDuration',label: '瞪大持续 (ms)', min: 50, max: 300, step: 10 },
172
+ { key: 'counterRotate', label: '眼睛 counter-rotate', type: 'cb' },
173
+ ];
174
+
175
+ const paramsEl = document.getElementById('params');
176
+ function buildParams() {
177
+ paramsEl.innerHTML = '';
178
+ for (const def of PARAM_DEFS) {
179
+ if (def.group) {
180
+ const h = document.createElement('h3');
181
+ h.textContent = def.group;
182
+ paramsEl.appendChild(h);
183
+ continue;
184
+ }
185
+ const lab = document.createElement('label');
186
+ if (def.type === 'cb') {
187
+ lab.className = 'cb';
188
+ lab.innerHTML = `
189
+ <input type="checkbox" data-key="${def.key}" ${P[def.key] ? 'checked' : ''}/>
190
+ <span>${def.label}</span>
191
+ `;
192
+ lab.querySelector('input').onchange = e => { P[def.key] = e.target.checked; };
193
+ } else {
194
+ lab.innerHTML = `
195
+ <span>${def.label}</span>
196
+ <input type="range" data-key="${def.key}" min="${def.min}" max="${def.max}" step="${def.step}" value="${P[def.key]}"/>
197
+ <span class="val" data-val="${def.key}">${P[def.key]}</span>
198
+ `;
199
+ const range = lab.querySelector('input');
200
+ const val = lab.querySelector('[data-val]');
201
+ range.oninput = e => {
202
+ P[def.key] = parseFloat(e.target.value);
203
+ val.textContent = (def.step < 1) ? P[def.key].toFixed(2) : P[def.key];
204
+ };
205
+ }
206
+ paramsEl.appendChild(lab);
207
+ }
208
+ }
209
+
210
+ // ===== 眼睛绘制 =====
211
+ function setEyeCapsule(g, widenFactor = 1, heightFactor = 1, tilt = 0) {
212
+ const w = 2.55 * widenFactor;
213
+ const h = 4.85 * heightFactor;
214
+ const rx = Math.min(w / 2, 1.25);
215
+ g.innerHTML = `<rect x="${(-w/2).toFixed(3)}" y="${(-h/2).toFixed(3)}" width="${w.toFixed(3)}" height="${h.toFixed(3)}" rx="${rx.toFixed(3)}" fill="${EYE_GRAD}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.55" transform="rotate(${tilt.toFixed(3)})"/>`;
216
+ }
217
+ function setEyeGT(g) {
218
+ // > chevron 向右
219
+ const w = 1.6, h = 1.7;
220
+ g.innerHTML = `<path d="M ${-w/2} ${-h} L ${w/2} 0 L ${-w/2} ${h}" fill="none" stroke="${EYE_GRAD}" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>`;
221
+ }
222
+ function setEyeLT(g) {
223
+ // < chevron 向左
224
+ const w = 1.6, h = 1.7;
225
+ g.innerHTML = `<path d="M ${w/2} ${-h} L ${-w/2} 0 L ${w/2} ${h}" fill="none" stroke="${EYE_GRAD}" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>`;
226
+ }
227
+
228
+ const eyeL = document.getElementById('eye-left');
229
+ const eyeR = document.getElementById('eye-right');
230
+ const eyesCounter = document.getElementById('eyes-counter');
231
+ const gTrans = document.getElementById('g-translate');
232
+ const gRotate = document.getElementById('g-rotate');
233
+ const gSquash = document.getElementById('g-squash');
234
+ const pulsesLayer = document.getElementById('pulses-layer');
235
+ const rimFlashLayer = document.getElementById('rim-flash-layer');
236
+ const cloudPathD = document.querySelector('#cloud-clip path').getAttribute('d');
237
+
238
+ // ===== 脉冲池 =====
239
+ let pulses = []; // { bornT (perf ms), lifetime, scaleEnd, type }
240
+ let pulseIdCounter = 0;
241
+
242
+ function emitPulse(type, now) {
243
+ pulses.push({
244
+ id: ++pulseIdCounter,
245
+ bornT: now,
246
+ lifetime: type === 'A' ? P.pulseLifetimeA : P.pulseLifetimeB,
247
+ scaleEnd: P.pulseScaleEnd,
248
+ type
249
+ });
250
+ }
251
+
252
+ // 动态 glow filter
253
+ const glowBlur = document.querySelector('#pulse-glow feGaussianBlur');
254
+ function updateGlowStdDev() {
255
+ glowBlur.setAttribute('stdDeviation', P.pulseGlow);
256
+ }
257
+
258
+ function polarPoint(cx, cy, r, deg) {
259
+ const a = deg * Math.PI / 180;
260
+ return [cx + Math.cos(a) * r, cy + Math.sin(a) * r];
261
+ }
262
+
263
+ function arcPath(cx, cy, r, startDeg, endDeg) {
264
+ const [x0, y0] = polarPoint(cx, cy, r, startDeg);
265
+ const [x1, y1] = polarPoint(cx, cy, r, endDeg);
266
+ const largeArc = Math.abs(endDeg - startDeg) > 180 ? 1 : 0;
267
+ const sweep = endDeg >= startDeg ? 1 : 0;
268
+ return `M${x0.toFixed(3)} ${y0.toFixed(3)} A${r.toFixed(3)} ${r.toFixed(3)} 0 ${largeArc} ${sweep} ${x1.toFixed(3)} ${y1.toFixed(3)}`;
269
+ }
270
+
271
+ function renderPulses(now) {
272
+ pulses = pulses.filter(p => now - p.bornT < p.lifetime);
273
+ let html = '';
274
+ for (const p of pulses) {
275
+ const ageMs = now - p.bornT;
276
+ const t = ageMs / p.lifetime;
277
+ const scale = 1 + (p.scaleEnd - 1) * easeOutCubic(t);
278
+ const r = P.pulseStartRadius * scale;
279
+ const baseOp = p.type === 'A' ? 0.34 : 0.82;
280
+ const fadeIn = P.pulseFadeIn > 0 ? Math.min(1, ageMs / P.pulseFadeIn) : 1;
281
+ const fadeOut = Math.pow(1 - t, p.type === 'A' ? 1.22 : 1.55);
282
+ const opacity = baseOp * fadeIn * fadeOut;
283
+ const wobble = p.type === 'B' ? ((p.id % 3) - 1) * 7 : 0;
284
+ const strokeW = P.pulseStrokeWidth * (p.type === 'A' ? 0.62 : 1);
285
+ const segments = p.type === 'A'
286
+ ? [[-70, -34], [-12, 18], [34, 56]]
287
+ : [[-122, -86], [-48, 14], [42, 78], [130, 166], [190, 216]];
288
+
289
+ for (let i = 0; i < segments.length; i++) {
290
+ const [a0, a1] = segments[i];
291
+ const d = arcPath(12, 12, r + (i % 2) * 0.26, a0 + wobble, a1 + wobble);
292
+ const segOp = opacity * (1 - i * 0.085);
293
+ const stroke = (p.type === 'B' && i < 2) ? 'url(#alert-grad)' : 'url(#signal-grad)';
294
+ html += `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeW.toFixed(3)}" stroke-linecap="round" stroke-opacity="${segOp.toFixed(3)}" filter="url(#pulse-glow)"/>`;
295
+ }
296
+ }
297
+ pulsesLayer.innerHTML = html;
298
+ }
299
+
300
+ function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
301
+ function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
302
+ function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
303
+ function expPulse(x, center, width) {
304
+ const d = (x - center) / width;
305
+ return Math.exp(-d * d);
306
+ }
307
+
308
+ function renderAlertLayer(phase, phaseTms, tA, tB, tC, shakePeak, bEnvelope) {
309
+ let prep = 0, ping = 0, fade = 0;
310
+ if (phase === 'A') {
311
+ prep = easeInOutCubic(clamp(phaseTms / tA, 0, 1));
312
+ fade = 0.48 + prep * 0.36;
313
+ } else if (phase === 'B') {
314
+ const b = clamp(phaseTms / tB, 0, 1);
315
+ const ping1 = expPulse(b, 0.10, 0.055);
316
+ const ping2 = expPulse(b, 0.54, 0.070);
317
+ ping = Math.max(ping1, ping2, shakePeak * bEnvelope * 0.34);
318
+ fade = 0.92;
319
+ } else {
320
+ const c = clamp(phaseTms / tC, 0, 1);
321
+ fade = (1 - easeOutCubic(c)) * 0.56;
322
+ }
323
+
324
+ const rimOp = clamp(ping * 0.58 + prep * 0.12, 0, 0.68);
325
+ const rimW = 0.28 + ping * 0.42;
326
+ rimFlashLayer.innerHTML = rimOp > 0.015
327
+ ? `<path d="${cloudPathD}" fill="none" stroke="url(#alert-grad)" stroke-width="${rimW.toFixed(3)}" stroke-linejoin="round" stroke-opacity="${rimOp.toFixed(3)}" filter="url(#alert-glow)"/>`
328
+ : '';
329
+ }
330
+
331
+ // ===== 主循环 =====
332
+ let t0 = performance.now();
333
+ let lastCycleT = -1;
334
+ let emittedA = false;
335
+ let emittedBPeaks = 0;
336
+ let cycleCount = 0;
337
+ let paused = false;
338
+
339
+ let pauseAccumT = 0;
340
+ let pauseStart = 0;
341
+
342
+ const sb = document.getElementById('statusbar');
343
+
344
+ function tick() {
345
+ if (paused) {
346
+ requestAnimationFrame(tick);
347
+ return;
348
+ }
349
+ const now = performance.now();
350
+ const tInCycle = ((now - t0) % P.totalPeriod);
351
+
352
+ // cycle wrap detection
353
+ if (tInCycle < lastCycleT) {
354
+ cycleCount++;
355
+ emittedA = false;
356
+ emittedBPeaks = 0;
357
+ }
358
+ lastCycleT = tInCycle;
359
+
360
+ // 段时长 (动态从当前 P 算)
361
+ const tA = P.totalPeriod * P.ratioA;
362
+ const tB = P.totalPeriod * P.ratioB;
363
+ const tC = P.totalPeriod - tA - tB;
364
+
365
+ let phase, phaseTms;
366
+ if (tInCycle < tA) {
367
+ phase = 'A'; phaseTms = tInCycle;
368
+ } else if (tInCycle < tA + tB) {
369
+ phase = 'B'; phaseTms = tInCycle - tA;
370
+ } else {
371
+ phase = 'C'; phaseTms = tInCycle - tA - tB;
372
+ }
373
+
374
+ // ===== 脉冲发射 =====
375
+ // A 段: 在段开头发 1 圈 (缓)
376
+ if (phase === 'A' && !emittedA) {
377
+ emitPulse('A', now);
378
+ emittedA = true;
379
+ }
380
+ // B 段: 摇晃顶点 = 圈出生 (每半周期一个顶点)
381
+ if (phase === 'B') {
382
+ const tBSec = phaseTms / 1000;
383
+ const peakInterval = 1 / (2 * P.shakeFreqHz); // 半周期 = 一个顶点间隔
384
+ // 顶点位于 (k + 0.25) * (1/freq) = (2k+0.5) * peakInterval, k=0,1,2,...
385
+ // 简化: 等间隔发射, 第一发在 peakInterval / 2 (即 1/(4*freq))
386
+ const expectedPeaks = Math.floor((tBSec - peakInterval / 2) / peakInterval) + 1;
387
+ while (emittedBPeaks < expectedPeaks && emittedBPeaks * peakInterval + peakInterval / 2 < (tB / 1000)) {
388
+ if (emittedBPeaks % 3 === 0) emitPulse('B', now);
389
+ emittedBPeaks++;
390
+ }
391
+ }
392
+
393
+ // ===== 摇晃 / 形变 / 呼吸 =====
394
+ let rotate = 0, jumpY = 0;
395
+ let burstX = 1, burstY = 1; // B 段额外 scale (在 idle 呼吸基础上叠加)
396
+ let shakePeak = 0;
397
+ let bEnvelope = 0;
398
+
399
+ if (phase === 'B') {
400
+ const tBSec = phaseTms / 1000;
401
+ const angularFreq = 2 * Math.PI * P.shakeFreqHz;
402
+ // 进场 envelope: 前 0.15s 内从 0 -> 1, ease in
403
+ const envIn = Math.min(1, tBSec / 0.15);
404
+ // 出场 envelope: 最后 0.2s ease out
405
+ const tBTotal = tB / 1000;
406
+ const envOut = Math.min(1, (tBTotal - tBSec) / 0.2);
407
+ const env = Math.min(envIn, envOut);
408
+ const sinV = Math.sin(angularFreq * tBSec);
409
+ const absSin = Math.abs(sinV);
410
+ shakePeak = absSin;
411
+ bEnvelope = env;
412
+ rotate = P.shakeAngle * sinV * env;
413
+ // 微跳: abs(sin) 的 2 倍频, 每次顶点跳一下
414
+ jumpY = -P.shakeJumpY * Math.abs(Math.sin(angularFreq * tBSec / 2 + Math.PI / 4)) * env;
415
+ // micro 撑大: 跟摇晃顶点同步, abs(sin) 在顶点为 1
416
+ const microFactor = 1 + P.scaleMicro * absSin * env;
417
+ // macro 整气: B 段一次大吸气-呼气, sin(π * t/T), 中点峰值
418
+ const macroFactor = 1 + P.scaleMacro * Math.sin(Math.PI * Math.min(1, Math.max(0, tBSec / tBTotal)));
419
+ // squash 顶点压 (scaleY 减一点)
420
+ const squashFactor = 1 - P.squashStrength * absSin * env;
421
+ burstX = microFactor * macroFactor;
422
+ burstY = microFactor * macroFactor * squashFactor;
423
+ } else if (phase === 'C') {
424
+ // C 段: 摇晃用衰减振荡 ease out
425
+ const tCSec = phaseTms / 1000;
426
+ const tCTotal = tC / 1000;
427
+ const decay = Math.max(0, 1 - tCSec / tCTotal);
428
+ const decayEased = decay * decay;
429
+ const angularFreq = 2 * Math.PI * P.shakeFreqHz * 0.7;
430
+ rotate = P.shakeAngle * decayEased * 0.5 * Math.sin(angularFreq * (tB / 1000 + tCSec));
431
+ jumpY = 0;
432
+ }
433
+
434
+ // ===== idle 呼吸 base (跨 cycle 连续, 不重置) =====
435
+ // 中心值 1 - amp, 上下 ±amp, 范围 [1-2*amp, 1] (跟 idle 锁定 0.96-1.00 一致)
436
+ const breathPhase = (now / P.idleBreathPeriod) * 2 * Math.PI;
437
+ const baseBreath = (1 - P.idleBreathAmp) + P.idleBreathAmp * Math.sin(breathPhase);
438
+ // 注意: 上面公式范围 [1-2*amp, 1], idle 锁定 0.96-1.00 即 amp=0.02
439
+
440
+ const finalScaleX = baseBreath * burstX;
441
+ const finalScaleY = baseBreath * burstY;
442
+
443
+ // ===== 应用 transform =====
444
+ gTrans.setAttribute('transform', `translate(0 ${jumpY.toFixed(3)})`);
445
+ gRotate.setAttribute('transform', `rotate(${rotate.toFixed(3)} 12 12)`);
446
+ gSquash.setAttribute('transform', `translate(12 12) scale(${finalScaleX.toFixed(4)} ${finalScaleY.toFixed(4)}) translate(-12 -12)`);
447
+
448
+ // ===== 眼睛 counter-rotate =====
449
+ if (P.counterRotate) {
450
+ eyesCounter.setAttribute('transform', `rotate(${(-rotate).toFixed(3)} 12 12)`);
451
+ } else {
452
+ eyesCounter.removeAttribute('transform');
453
+ }
454
+
455
+ // ===== 眼睛 swap =====
456
+ let widenFactor = 1;
457
+ let eyeHeight = 1;
458
+ let eyeTiltL = 0;
459
+ let eyeTiltR = 0;
460
+ if (phase === 'A') {
461
+ const remainA = tA - phaseTms;
462
+ if (remainA < P.eyeWidenDuration) {
463
+ // 瞪大
464
+ const widen01 = 1 - remainA / P.eyeWidenDuration;
465
+ widenFactor = 1 + P.eyeWidenAmount * widen01;
466
+ }
467
+ } else if (phase === 'B') {
468
+ widenFactor = 1 + P.eyeWidenAmount * 0.92 + shakePeak * bEnvelope * 0.070;
469
+ eyeHeight = 1.04 + shakePeak * bEnvelope * 0.040;
470
+ eyeTiltL = -3.0 * bEnvelope;
471
+ eyeTiltR = 3.0 * bEnvelope;
472
+ } else if (phase === 'C') {
473
+ // C 段开头 capsule 重新渐入 (微小入场)
474
+ const fadeIn = Math.min(1, phaseTms / 120);
475
+ widenFactor = 1 + P.eyeWidenAmount * (1 - fadeIn) * 0.5; // 收回时残留
476
+ }
477
+
478
+ setEyeCapsule(eyeL, widenFactor, eyeHeight, eyeTiltL);
479
+ setEyeCapsule(eyeR, widenFactor, eyeHeight, eyeTiltR);
480
+
481
+ // ===== 渲染脉冲 =====
482
+ updateGlowStdDev();
483
+ renderPulses(now);
484
+ renderAlertLayer(phase, phaseTms, tA, tB, tC, shakePeak, bEnvelope);
485
+
486
+ // ===== 状态栏 =====
487
+ sb.innerHTML = `phase: <span class="phase-${phase}">${phase}</span> · cycle: ${cycleCount} · pulses: ${pulses.length} · t: ${tInCycle.toFixed(0)}ms / ${P.totalPeriod}`;
488
+
489
+ requestAnimationFrame(tick);
490
+ }
491
+
492
+ requestAnimationFrame(tick);
493
+ ]]></script>
494
494
  </svg>