@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.
- package/README.md +4 -0
- package/assets/calico/calico-collapsing.apng +0 -0
- package/assets/calico/calico-dozing.apng +0 -0
- package/assets/calico/calico-error.apng +0 -0
- package/assets/calico/calico-happy.apng +0 -0
- package/assets/calico/calico-idle-follow.svg +324 -0
- package/assets/calico/calico-idle.apng +0 -0
- package/assets/calico/calico-mini-alert.apng +0 -0
- package/assets/calico/calico-mini-crabwalk.apng +0 -0
- package/assets/calico/calico-mini-enter.apng +0 -0
- package/assets/calico/calico-mini-happy.apng +0 -0
- package/assets/calico/calico-mini-idle.apng +0 -0
- package/assets/calico/calico-mini-peek.apng +0 -0
- package/assets/calico/calico-mini-sleep.apng +0 -0
- package/assets/calico/calico-notification.apng +0 -0
- package/assets/calico/calico-react-drag.apng +0 -0
- package/assets/calico/calico-react-left.apng +0 -0
- package/assets/calico/calico-react-poke.apng +0 -0
- package/assets/calico/calico-sleeping.apng +0 -0
- package/assets/calico/calico-thinking.apng +0 -0
- package/assets/calico/calico-waking.apng +0 -0
- package/assets/calico/calico-working-building.apng +0 -0
- package/assets/calico/calico-working-carrying.apng +0 -0
- package/assets/calico/calico-working-conducting.apng +0 -0
- package/assets/calico/calico-working-juggling.apng +0 -0
- package/assets/calico/calico-working-sweeping.apng +0 -0
- package/assets/calico/calico-working-typing.apng +0 -0
- package/assets/calico/calico-yawning.apng +0 -0
- package/assets/calico.svg +324 -0
- package/assets/clawd/LICENSE +22 -0
- package/assets/clawd/clawd-about-hero.svg +202 -0
- package/assets/clawd/clawd-collapse-sleep.svg +247 -0
- package/assets/clawd/clawd-error.svg +94 -0
- package/assets/clawd/clawd-happy.svg +161 -0
- package/assets/clawd/clawd-headphones-groove.svg +194 -0
- package/assets/clawd/clawd-idle-bubble.svg +180 -0
- package/assets/clawd/clawd-idle-collapse.svg +101 -0
- package/assets/clawd/clawd-idle-doze.svg +72 -0
- package/assets/clawd/clawd-idle-follow.svg +64 -0
- package/assets/clawd/clawd-idle-living.svg +196 -0
- package/assets/clawd/clawd-idle-look.svg +115 -0
- package/assets/clawd/clawd-idle-reading.svg +252 -0
- package/assets/clawd/clawd-idle-yawn.svg +158 -0
- package/assets/clawd/clawd-mini-alert.svg +129 -0
- package/assets/clawd/clawd-mini-crabwalk.svg +76 -0
- package/assets/clawd/clawd-mini-enter-sleep.svg +64 -0
- package/assets/clawd/clawd-mini-enter.svg +114 -0
- package/assets/clawd/clawd-mini-happy.svg +124 -0
- package/assets/clawd/clawd-mini-idle.svg +83 -0
- package/assets/clawd/clawd-mini-peek.svg +82 -0
- package/assets/clawd/clawd-mini-sleep.svg +112 -0
- package/assets/clawd/clawd-mini-typing.svg +153 -0
- package/assets/clawd/clawd-notification.svg +438 -0
- package/assets/clawd/clawd-react-annoyed.svg +167 -0
- package/assets/clawd/clawd-react-double-jump.svg +229 -0
- package/assets/clawd/clawd-react-double.svg +108 -0
- package/assets/clawd/clawd-react-drag.svg +102 -0
- package/assets/clawd/clawd-react-left.svg +102 -0
- package/assets/clawd/clawd-react-right.svg +102 -0
- package/assets/clawd/clawd-sleeping.svg +118 -0
- package/assets/clawd/clawd-static-base.svg +21 -0
- package/assets/clawd/clawd-wake.svg +277 -0
- package/assets/clawd/clawd-working-building.svg +329 -0
- package/assets/clawd/clawd-working-carrying.svg +178 -0
- package/assets/clawd/clawd-working-debugger.svg +245 -0
- package/assets/clawd/clawd-working-juggling.svg +183 -0
- package/assets/clawd/clawd-working-sweeping.svg +248 -0
- package/assets/clawd/clawd-working-thinking.svg +501 -0
- package/assets/clawd/clawd-working-typing-boss.svg +307 -0
- package/assets/clawd/clawd-working-typing.svg +273 -0
- package/assets/clawd/clawd-working-ultrathink.svg +166 -0
- package/assets/clawd/clawd-working-wizard.svg +356 -0
- package/assets/clawd.svg +64 -0
- package/assets/cloudling/cloudling/cloudling-attention.svg +380 -0
- package/assets/cloudling/cloudling/cloudling-building.svg +667 -0
- package/assets/cloudling/cloudling/cloudling-carrying.svg +604 -0
- package/assets/cloudling/cloudling/cloudling-conducting.svg +616 -0
- package/assets/cloudling/cloudling/cloudling-dozing-to-sleeping.svg +578 -0
- package/assets/cloudling/cloudling/cloudling-dozing.svg +358 -0
- package/assets/cloudling/cloudling/cloudling-error.svg +428 -0
- package/assets/cloudling/cloudling/cloudling-idle-reading.svg +658 -0
- package/assets/cloudling/cloudling/cloudling-idle-to-dozing.svg +568 -0
- package/assets/cloudling/cloudling/cloudling-idle-to-sleeping.svg +811 -0
- package/assets/cloudling/cloudling/cloudling-idle.svg +255 -0
- package/assets/cloudling/cloudling/cloudling-juggling.svg +648 -0
- package/assets/cloudling/cloudling/cloudling-mini-alert.svg +390 -0
- package/assets/cloudling/cloudling/cloudling-mini-crabwalk.svg +335 -0
- package/assets/cloudling/cloudling/cloudling-mini-enter-roll-in.svg +226 -0
- package/assets/cloudling/cloudling/cloudling-mini-enter-sleep.svg +232 -0
- package/assets/cloudling/cloudling/cloudling-mini-happy.svg +371 -0
- package/assets/cloudling/cloudling/cloudling-mini-idle.svg +279 -0
- package/assets/cloudling/cloudling/cloudling-mini-peek.svg +328 -0
- package/assets/cloudling/cloudling/cloudling-mini-sleep.svg +260 -0
- package/assets/cloudling/cloudling/cloudling-mini-typing.svg +692 -0
- package/assets/cloudling/cloudling/cloudling-notification.svg +494 -0
- package/assets/cloudling/cloudling/cloudling-react-drag.svg +83 -0
- package/assets/cloudling/cloudling/cloudling-sleeping-to-idle.svg +555 -0
- package/assets/cloudling/cloudling/cloudling-sleeping.svg +500 -0
- package/assets/cloudling/cloudling/cloudling-sweeping.svg +292 -0
- package/assets/cloudling/cloudling/cloudling-thinking.svg +720 -0
- package/assets/cloudling/cloudling/cloudling-typing.svg +354 -0
- package/assets/cloudling/cloudling-attention.svg +380 -0
- package/assets/cloudling/cloudling-building.svg +667 -0
- package/assets/cloudling/cloudling-carrying.svg +604 -0
- package/assets/cloudling/cloudling-conducting.svg +616 -0
- package/assets/cloudling/cloudling-dozing-to-sleeping.svg +578 -0
- package/assets/cloudling/cloudling-dozing.svg +358 -0
- package/assets/cloudling/cloudling-error.svg +428 -0
- package/assets/cloudling/cloudling-idle-reading.svg +658 -0
- package/assets/cloudling/cloudling-idle-to-dozing.svg +568 -0
- package/assets/cloudling/cloudling-idle-to-sleeping.svg +811 -0
- package/assets/cloudling/cloudling-idle.svg +255 -0
- package/assets/cloudling/cloudling-juggling.svg +648 -0
- package/assets/cloudling/cloudling-mini-alert.svg +390 -0
- package/assets/cloudling/cloudling-mini-crabwalk.svg +335 -0
- package/assets/cloudling/cloudling-mini-enter-roll-in.svg +226 -0
- package/assets/cloudling/cloudling-mini-enter-sleep.svg +232 -0
- package/assets/cloudling/cloudling-mini-happy.svg +371 -0
- package/assets/cloudling/cloudling-mini-idle.svg +279 -0
- package/assets/cloudling/cloudling-mini-peek.svg +328 -0
- package/assets/cloudling/cloudling-mini-sleep.svg +260 -0
- package/assets/cloudling/cloudling-mini-typing.svg +692 -0
- package/assets/cloudling/cloudling-notification.svg +494 -0
- package/assets/cloudling/cloudling-react-drag.svg +83 -0
- package/assets/cloudling/cloudling-sleeping-to-idle.svg +555 -0
- package/assets/cloudling/cloudling-sleeping.svg +500 -0
- package/assets/cloudling/cloudling-sweeping.svg +292 -0
- package/assets/cloudling/cloudling-thinking.svg +720 -0
- package/assets/cloudling/cloudling-typing.svg +354 -0
- package/assets/cloudling.svg +255 -0
- package/assets/ghost-attention.svg +19 -0
- package/assets/ghost-carrying.svg +19 -0
- package/assets/ghost-collapsing.svg +19 -0
- package/assets/ghost-dozing.svg +23 -0
- package/assets/ghost-error.svg +19 -0
- package/assets/ghost-happy.svg +23 -0
- package/assets/ghost-idle-action1.svg +19 -0
- package/assets/ghost-idle-action2.svg +19 -0
- package/assets/ghost-idle-action3.svg +19 -0
- package/assets/ghost-idle.svg +11 -0
- package/assets/ghost-juggling.svg +19 -0
- package/assets/ghost-notification.svg +19 -0
- package/assets/ghost-react-double.svg +23 -0
- package/assets/ghost-react-drag.svg +23 -0
- package/assets/ghost-react-left.svg +19 -0
- package/assets/ghost-react-right.svg +19 -0
- package/assets/ghost-sleeping.svg +19 -0
- package/assets/ghost-sweeping.svg +19 -0
- package/assets/ghost-thinking.svg +19 -0
- package/assets/ghost-waking.svg +19 -0
- package/assets/ghost-working.svg +19 -0
- package/assets/ghost-yawning.svg +23 -0
- package/assets/ghost.svg +11 -0
- package/assets/robot-attention.svg +19 -0
- package/assets/robot-carrying.svg +19 -0
- package/assets/robot-collapsing.svg +19 -0
- package/assets/robot-dozing.svg +23 -0
- package/assets/robot-error.svg +19 -0
- package/assets/robot-happy.svg +23 -0
- package/assets/robot-idle-action1.svg +19 -0
- package/assets/robot-idle-action2.svg +19 -0
- package/assets/robot-idle-action3.svg +19 -0
- package/assets/robot-idle.svg +11 -0
- package/assets/robot-juggling.svg +19 -0
- package/assets/robot-notification.svg +19 -0
- package/assets/robot-react-double.svg +23 -0
- package/assets/robot-react-drag.svg +23 -0
- package/assets/robot-react-left.svg +19 -0
- package/assets/robot-react-right.svg +19 -0
- package/assets/robot-sleeping.svg +19 -0
- package/assets/robot-sweeping.svg +19 -0
- package/assets/robot-thinking.svg +19 -0
- package/assets/robot-waking.svg +19 -0
- package/assets/robot-working.svg +19 -0
- package/assets/robot-yawning.svg +23 -0
- package/assets/robot.svg +11 -0
- package/manifest.json +889 -0
- package/package.json +18 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="stage" xmlns="http://www.w3.org/2000/svg" width="880" height="720" viewBox="-32 -24 88 72" preserveAspectRatio="xMidYMid meet">
|
|
3
|
+
<g id="__cloudling-export-shims" opacity="0" pointer-events="none" style="display:none">
|
|
4
|
+
<g id="scrubber"></g>
|
|
5
|
+
<g id="t-display"></g>
|
|
6
|
+
</g>
|
|
7
|
+
<script type="application/ecmascript"><![CDATA[
|
|
8
|
+
/* =========================================================================
|
|
9
|
+
* Cloudling 主体几何 (沿用 V2 视觉定稿, 跟 carrying-v5 / idle-to-sleeping-v1 同源)
|
|
10
|
+
* ========================================================================= */
|
|
11
|
+
const CLOUD_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";
|
|
12
|
+
|
|
13
|
+
const SOFT_CLOUD_POINTS = [
|
|
14
|
+
{x:9.064,y:3.344},{x:9.825,y:3.106},{x:10.616,y:3.004},{x:11.413,y:3.040},
|
|
15
|
+
{x:12.192,y:3.210},{x:12.928,y:3.518},{x:13.599,y:3.948},{x:14.255,y:4.291},
|
|
16
|
+
{x:15.046,y:4.188},{x:15.843,y:4.224},{x:16.621,y:4.398},{x:17.357,y:4.706},
|
|
17
|
+
{x:18.029,y:5.134},{x:18.617,y:5.673},{x:19.103,y:6.306},{x:19.471,y:7.013},
|
|
18
|
+
{x:19.715,y:7.772},{x:19.812,y:8.564},{x:19.784,y:9.361},{x:19.797,y:10.105},
|
|
19
|
+
{x:20.287,y:10.735},{x:20.656,y:11.442},{x:20.897,y:12.202},{x:20.995,y:12.994},
|
|
20
|
+
{x:20.964,y:13.791},{x:20.791,y:14.569},{x:20.480,y:15.304},{x:20.052,y:15.977},
|
|
21
|
+
{x:19.516,y:16.567},{x:18.882,y:17.052},{x:18.174,y:17.418},{x:17.539,y:17.808},
|
|
22
|
+
{x:17.231,y:18.545},{x:16.805,y:19.218},{x:16.264,y:19.805},{x:15.633,y:20.292},
|
|
23
|
+
{x:14.925,y:20.660},{x:14.163,y:20.895},{x:13.372,y:20.996},{x:12.575,y:20.964},
|
|
24
|
+
{x:11.796,y:20.793},{x:11.061,y:20.483},{x:10.392,y:20.049},{x:9.736,y:19.714},
|
|
25
|
+
{x:8.946,y:19.811},{x:8.148,y:19.777},{x:7.370,y:19.606},{x:6.634,y:19.299},
|
|
26
|
+
{x:5.962,y:18.868},{x:5.377,y:18.326},{x:4.885,y:17.698},{x:4.522,y:16.989},
|
|
27
|
+
{x:4.283,y:16.228},{x:4.182,y:15.437},{x:4.220,y:14.640},{x:4.179,y:13.907},
|
|
28
|
+
{x:3.696,y:13.273},{x:3.332,y:12.563},{x:3.092,y:11.803},{x:3.003,y:11.010},
|
|
29
|
+
{x:3.038,y:10.213},{x:3.202,y:9.433},{x:3.514,y:8.699},{x:3.944,y:8.027},
|
|
30
|
+
{x:4.484,y:7.440},{x:5.114,y:6.951},{x:5.822,y:6.586},{x:6.445,y:6.191},
|
|
31
|
+
{x:6.752,y:5.455},{x:7.185,y:4.785},{x:7.723,y:4.197},{x:8.356,y:3.711},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function lerp(a, b, t) { return a + (b - a) * t; }
|
|
35
|
+
function clamp01(t) { return Math.max(0, Math.min(1, t)); }
|
|
36
|
+
function easeIn(t) { return t * t; }
|
|
37
|
+
function easeOut(t) { return 1 - Math.pow(1 - t, 2); }
|
|
38
|
+
function easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2; }
|
|
39
|
+
function easeOutBack(t) { const c=1.70158; return 1 + (c+1)*Math.pow(t-1,3) + c*Math.pow(t-1,2); }
|
|
40
|
+
function progress(t, begin, end) { return clamp01((t - begin) / (end - begin)); }
|
|
41
|
+
|
|
42
|
+
function catmullRomClosedPath(points, tension = 1) {
|
|
43
|
+
const q = (n) => n.toFixed(3);
|
|
44
|
+
let d = `M ${q(points[0].x)} ${q(points[0].y)}`;
|
|
45
|
+
const n = points.length;
|
|
46
|
+
for (let i = 0; i < n; i++) {
|
|
47
|
+
const p0 = points[(i - 1 + n) % n];
|
|
48
|
+
const p1 = points[i];
|
|
49
|
+
const p2 = points[(i + 1) % n];
|
|
50
|
+
const p3 = points[(i + 2) % n];
|
|
51
|
+
const c1x = p1.x + (p2.x - p0.x) * tension / 6;
|
|
52
|
+
const c1y = p1.y + (p2.y - p0.y) * tension / 6;
|
|
53
|
+
const c2x = p2.x - (p3.x - p1.x) * tension / 6;
|
|
54
|
+
const c2y = p2.y - (p3.y - p1.y) * tension / 6;
|
|
55
|
+
d += ` C ${q(c1x)} ${q(c1y)} ${q(c2x)} ${q(c2y)} ${q(p2.x)} ${q(p2.y)}`;
|
|
56
|
+
}
|
|
57
|
+
return `${d} Z`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* =========================================================================
|
|
61
|
+
* 径向射线投影: 从中心 (cx,cy) 沿角度 angle 射出, 与圆角矩形边界求交.
|
|
62
|
+
* 这是 building 通用 morph 工具 — 后续 7 个形状只要换边界几何就能复用.
|
|
63
|
+
* ========================================================================= */
|
|
64
|
+
function rayToRoundedRect(angle, cx, cy, halfW, halfH, r) {
|
|
65
|
+
const dx = Math.cos(angle), dy = Math.sin(angle);
|
|
66
|
+
// 1. 先求射线与无圆角矩形的最近交点
|
|
67
|
+
let t = Infinity;
|
|
68
|
+
if (Math.abs(dx) > 1e-9) t = Math.min(t, halfW / Math.abs(dx));
|
|
69
|
+
if (Math.abs(dy) > 1e-9) t = Math.min(t, halfH / Math.abs(dy));
|
|
70
|
+
let px = cx + dx * t;
|
|
71
|
+
let py = cy + dy * t;
|
|
72
|
+
// 2. 检查交点是否落在圆角象限内
|
|
73
|
+
const lx = Math.abs(px - cx);
|
|
74
|
+
const ly = Math.abs(py - cy);
|
|
75
|
+
if (lx > halfW - r - 1e-6 && ly > halfH - r - 1e-6) {
|
|
76
|
+
// 落在圆角象限 → 用射线与圆求交
|
|
77
|
+
const sx = px >= cx ? 1 : -1;
|
|
78
|
+
const sy = py >= cy ? 1 : -1;
|
|
79
|
+
const ccx = cx + sx * (halfW - r);
|
|
80
|
+
const ccy = cy + sy * (halfH - r);
|
|
81
|
+
const ex = cx - ccx;
|
|
82
|
+
const ey = cy - ccy;
|
|
83
|
+
const a = dx*dx + dy*dy;
|
|
84
|
+
const b = 2*(ex*dx + ey*dy);
|
|
85
|
+
const c = ex*ex + ey*ey - r*r;
|
|
86
|
+
const disc = b*b - 4*a*c;
|
|
87
|
+
if (disc >= 0) {
|
|
88
|
+
const s = (-b + Math.sqrt(disc)) / (2*a);
|
|
89
|
+
px = cx + dx * s;
|
|
90
|
+
py = cy + dy * s;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {x: px, y: py};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function makeRoundedRectTargetPoints(halfW, halfH, r) {
|
|
97
|
+
const cx = 12, cy = 12;
|
|
98
|
+
return SOFT_CLOUD_POINTS.map(cp => {
|
|
99
|
+
const angle = Math.atan2(cp.y - cy, cp.x - cx);
|
|
100
|
+
return rayToRoundedRect(angle, cx, cy, halfW, halfH, r);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* =========================================================================
|
|
105
|
+
* 多形态 body path: cloud / terminal / module / monitor / envelope
|
|
106
|
+
* ========================================================================= */
|
|
107
|
+
function softenTargetPoints(targetPoints, amount) {
|
|
108
|
+
return SOFT_CLOUD_POINTS.map((cp, i) => ({
|
|
109
|
+
x: lerp(cp.x, targetPoints[i].x, amount),
|
|
110
|
+
y: lerp(cp.y, targetPoints[i].y, amount),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mixPointSets(a, b, t) {
|
|
115
|
+
const u = clamp01(t);
|
|
116
|
+
return a.map((p, i) => ({
|
|
117
|
+
x: lerp(p.x, b[i].x, u),
|
|
118
|
+
y: lerp(p.y, b[i].y, u),
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function squashPoints(points, squashY) {
|
|
123
|
+
if (squashY === 1) return points;
|
|
124
|
+
const cy = 12;
|
|
125
|
+
return points.map(p => ({ x: p.x, y: cy + (p.y - cy) * squashY }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function makePathFromPoints(points, sharpness = 0) {
|
|
129
|
+
return catmullRomClosedPath(points, lerp(1.0, 0.76, clamp01(sharpness)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function makeShapePointSets() {
|
|
133
|
+
return {
|
|
134
|
+
cloud: SOFT_CLOUD_POINTS,
|
|
135
|
+
terminal: softenTargetPoints(makeRoundedRectTargetPoints(params.terminalHalfW, params.terminalHalfH, params.terminalCornerR), params.terminalMorphAmount),
|
|
136
|
+
module: softenTargetPoints(makeRoundedRectTargetPoints(params.squareHalfW, params.squareHalfH, params.squareCornerR), params.moduleMorphAmount),
|
|
137
|
+
monitor: softenTargetPoints(makeRoundedRectTargetPoints(params.monitorHalfW, params.monitorHalfH, params.monitorCornerR), params.monitorMorphAmount),
|
|
138
|
+
envelope: softenTargetPoints(makeRoundedRectTargetPoints(params.envHalfW, params.envHalfH, params.envCornerR), params.envelopeMorphAmount),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* =========================================================================
|
|
143
|
+
* 默认参数
|
|
144
|
+
* ========================================================================= */
|
|
145
|
+
const DEFAULTS = {
|
|
146
|
+
T_CYCLE: 5.4,
|
|
147
|
+
T_CLOUD_HOLD: 0.42,
|
|
148
|
+
T_TERMINAL_HOLD: 1.32,
|
|
149
|
+
T_MODULE_HOLD: 2.18,
|
|
150
|
+
T_MONITOR_HOLD: 3.04,
|
|
151
|
+
T_ENVELOPE_HOLD: 3.96,
|
|
152
|
+
T_SMILE_HOLD: 4.78,
|
|
153
|
+
blinkClose: 0.12,
|
|
154
|
+
blinkSwitch: 0.08,
|
|
155
|
+
blinkOpen: 0.18,
|
|
156
|
+
|
|
157
|
+
// morph
|
|
158
|
+
squashY: 0.86,
|
|
159
|
+
blinkSquashY: 0.84,
|
|
160
|
+
terminalMorphAmount: 0.93,
|
|
161
|
+
moduleMorphAmount: 0.94,
|
|
162
|
+
monitorMorphAmount: 0.93,
|
|
163
|
+
envelopeMorphAmount: 0.93,
|
|
164
|
+
|
|
165
|
+
// 终端窗口几何
|
|
166
|
+
terminalHalfW: 11.2,
|
|
167
|
+
terminalHalfH: 7.3,
|
|
168
|
+
terminalCornerR: 1.15,
|
|
169
|
+
|
|
170
|
+
// 终端装饰
|
|
171
|
+
titleBarH: 1.35, // 标题栏高度
|
|
172
|
+
screenInset: 1.20, // 屏幕距主体边距, 留出更厚白色边框
|
|
173
|
+
|
|
174
|
+
// 模块方块几何
|
|
175
|
+
squareHalfW: 9.4,
|
|
176
|
+
squareHalfH: 9.0,
|
|
177
|
+
squareCornerR: 2.35,
|
|
178
|
+
moduleDepth: 1.15,
|
|
179
|
+
|
|
180
|
+
// 显示器
|
|
181
|
+
monitorHalfW: 10.8,
|
|
182
|
+
monitorHalfH: 6.25,
|
|
183
|
+
monitorCornerR: 1.0,
|
|
184
|
+
monitorScreenInset: 1.0,
|
|
185
|
+
monitorScreenCornerR: 0.65,
|
|
186
|
+
standNeckW: 2.0,
|
|
187
|
+
standNeckH: 1.6,
|
|
188
|
+
standBaseW: 6.2,
|
|
189
|
+
standBaseH: 0.9,
|
|
190
|
+
standBaseR: 0.38,
|
|
191
|
+
|
|
192
|
+
// 信封
|
|
193
|
+
envHalfW: 11.0,
|
|
194
|
+
envHalfH: 6.6,
|
|
195
|
+
envCornerR: 1.05,
|
|
196
|
+
flapDepth: 3.4,
|
|
197
|
+
flapInset: 0.85,
|
|
198
|
+
flapFillOpacity: 0.08,
|
|
199
|
+
mStrokeW: 1.12,
|
|
200
|
+
mLegBoost: 1.28,
|
|
201
|
+
|
|
202
|
+
// duang spring
|
|
203
|
+
springAmp: 0.075,
|
|
204
|
+
springFreq: 4.0,
|
|
205
|
+
springDamping: 5.5,
|
|
206
|
+
springDuration: 0.65,
|
|
207
|
+
|
|
208
|
+
// 呼吸
|
|
209
|
+
breathLow: 0.97,
|
|
210
|
+
breathHigh: 1.02,
|
|
211
|
+
|
|
212
|
+
// 眼睛
|
|
213
|
+
eyeLeftX: 9,
|
|
214
|
+
eyeRightX: 15,
|
|
215
|
+
eyeWidth: 2.6,
|
|
216
|
+
eyeHeight: 5,
|
|
217
|
+
eyeY: 9.5,
|
|
218
|
+
eyeFlashStrength: 0.5, // 变身瞬间眼睛亮度增强 (0-1)
|
|
219
|
+
|
|
220
|
+
// 阴影
|
|
221
|
+
cloudShadowOpacity: 0.28,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
let params = { ...DEFAULTS };
|
|
225
|
+
let paused = false;
|
|
226
|
+
let scrubbing = false;
|
|
227
|
+
|
|
228
|
+
/* =========================================================================
|
|
229
|
+
* defs (复用 cloudling 标准件)
|
|
230
|
+
* ========================================================================= */
|
|
231
|
+
function commonDefs(p, cloudD) {
|
|
232
|
+
return `
|
|
233
|
+
<linearGradient id="${p}-body" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
|
|
234
|
+
<stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="#e0f2fe"/><stop offset="1" stop-color="#bae6fd"/>
|
|
235
|
+
</linearGradient>
|
|
236
|
+
<linearGradient id="${p}-eye" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
|
|
237
|
+
<stop offset="0" stop-color="#B1A7FF"/><stop offset="0.5" stop-color="#7A9DFF"/><stop offset="1" stop-color="#3941FF"/>
|
|
238
|
+
</linearGradient>
|
|
239
|
+
<linearGradient id="${p}-eye-flash" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
|
|
240
|
+
<stop offset="0" stop-color="#E8E2FF"/><stop offset="0.5" stop-color="#B6CBFF"/><stop offset="1" stop-color="#7A8AFF"/>
|
|
241
|
+
</linearGradient>
|
|
242
|
+
<radialGradient id="${p}-hi" cx="0.45" cy="0.25" r="0.55">
|
|
243
|
+
<stop offset="0" stop-color="#fff" stop-opacity="0.9"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
|
244
|
+
</radialGradient>
|
|
245
|
+
<filter id="${p}-shadow" x="-50%" y="-50%" width="200%" height="200%">
|
|
246
|
+
<feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="${params.cloudShadowOpacity}"/>
|
|
247
|
+
</filter>
|
|
248
|
+
<filter id="${p}-edge-blur" x="-40%" y="-40%" width="180%" height="180%">
|
|
249
|
+
<feGaussianBlur stdDeviation="0.16"/>
|
|
250
|
+
</filter>
|
|
251
|
+
<filter id="${p}-eye-glow" x="-80%" y="-80%" width="260%" height="260%">
|
|
252
|
+
<feGaussianBlur stdDeviation="0.45" result="b"/>
|
|
253
|
+
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
254
|
+
</filter>
|
|
255
|
+
<clipPath id="${p}-clip"><path d="${cloudD}"/></clipPath>
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* =========================================================================
|
|
260
|
+
* cloudling 主体 (edge-system + drop shadow + 高光) — 跟 carrying-v5 同款
|
|
261
|
+
* ========================================================================= */
|
|
262
|
+
function makeCloudlingBody(p, f) {
|
|
263
|
+
const terminalOpacity = f.opacities.terminal;
|
|
264
|
+
const moduleOpacity = f.opacities.module;
|
|
265
|
+
const monitorOpacity = f.opacities.monitor;
|
|
266
|
+
const envelopeOpacity = f.opacities.envelope;
|
|
267
|
+
|
|
268
|
+
const moduleDepth = params.moduleDepth * clamp01(moduleOpacity);
|
|
269
|
+
const moduleSide = moduleOpacity > 0.01 ? `
|
|
270
|
+
<use href="#${p}-cloud-shape"
|
|
271
|
+
transform="translate(${moduleDepth.toFixed(2)} ${moduleDepth.toFixed(2)})"
|
|
272
|
+
fill="#8EA4FF"
|
|
273
|
+
opacity="${(0.24 * moduleOpacity).toFixed(3)}"/>
|
|
274
|
+
` : '';
|
|
275
|
+
|
|
276
|
+
const tHalfW = params.terminalHalfW;
|
|
277
|
+
const tHalfH = params.terminalHalfH;
|
|
278
|
+
const titleH = params.titleBarH;
|
|
279
|
+
const inset = params.screenInset;
|
|
280
|
+
const titleX1 = 12 - tHalfW + 0.5;
|
|
281
|
+
const titleX2 = 12 + tHalfW - 0.5;
|
|
282
|
+
const titleY1 = 12 - tHalfH + 0.5;
|
|
283
|
+
const titleW = titleX2 - titleX1;
|
|
284
|
+
const screenX1 = titleX1 + inset;
|
|
285
|
+
const screenX2 = titleX2 - inset;
|
|
286
|
+
const screenY1 = titleY1 + titleH + 0.3;
|
|
287
|
+
const screenY2 = 12 + tHalfH - 0.5 - 0.5;
|
|
288
|
+
const dotY = titleY1 + titleH * 0.5;
|
|
289
|
+
const dotR = Math.min(titleH * 0.28, 0.48);
|
|
290
|
+
const dotX1 = titleX1 + 0.75;
|
|
291
|
+
const dotX2 = dotX1 + 1.20;
|
|
292
|
+
const dotX3 = dotX2 + 1.20;
|
|
293
|
+
const promptSize = Math.max(2.4, Math.min(3.6, (screenY2 - screenY1) * 0.34));
|
|
294
|
+
const promptX = screenX1 + 0.5;
|
|
295
|
+
const promptY = (screenY1 + screenY2) * 0.5 + promptSize * 0.32;
|
|
296
|
+
|
|
297
|
+
const terminalDecor = terminalOpacity > 0.01 ? `
|
|
298
|
+
<g class="terminal-decor" opacity="${terminalOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
|
|
299
|
+
<rect x="${titleX1.toFixed(2)}" y="${titleY1.toFixed(2)}" width="${titleW.toFixed(2)}" height="${titleH.toFixed(2)}" rx="0.45" fill="#EEF2FF" fill-opacity="0.72"/>
|
|
300
|
+
<circle cx="${dotX1.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#96A9E8"/>
|
|
301
|
+
<circle cx="${dotX2.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#B7C3F6"/>
|
|
302
|
+
<circle cx="${dotX3.toFixed(2)}" cy="${dotY.toFixed(2)}" r="${dotR.toFixed(2)}" fill="#D3DAFF"/>
|
|
303
|
+
<rect x="${screenX1.toFixed(2)}" y="${screenY1.toFixed(2)}" width="${(screenX2-screenX1).toFixed(2)}" height="${(screenY2-screenY1).toFixed(2)}" rx="0.65" fill="#171D2D"/>
|
|
304
|
+
<text x="${promptX.toFixed(2)}" y="${promptY.toFixed(2)}" font-family="ui-monospace, monospace" font-size="${promptSize.toFixed(2)}" font-weight="700" fill="#7AC8FF" opacity="0.95">></text>
|
|
305
|
+
</g>
|
|
306
|
+
` : '';
|
|
307
|
+
|
|
308
|
+
const mHalfW = params.monitorHalfW;
|
|
309
|
+
const mHalfH = params.monitorHalfH;
|
|
310
|
+
const mInset = params.monitorScreenInset;
|
|
311
|
+
const monX1 = 12 - mHalfW + 0.5 + mInset;
|
|
312
|
+
const monX2 = 12 + mHalfW - 0.5 - mInset;
|
|
313
|
+
const monY1 = 12 - mHalfH + 0.5 + mInset;
|
|
314
|
+
const monY2 = 12 + mHalfH - 0.5 - mInset;
|
|
315
|
+
const monBottom = 12 + mHalfH - 0.5;
|
|
316
|
+
const neckW = params.standNeckW;
|
|
317
|
+
const neckH = params.standNeckH;
|
|
318
|
+
const neckX = 12 - neckW * 0.5;
|
|
319
|
+
const neckY = monBottom - 0.05;
|
|
320
|
+
const baseW = params.standBaseW;
|
|
321
|
+
const baseH = params.standBaseH;
|
|
322
|
+
const baseX = 12 - baseW * 0.5;
|
|
323
|
+
const baseY = neckY + neckH;
|
|
324
|
+
const monitorStand = monitorOpacity > 0.01 ? `
|
|
325
|
+
<g class="stand-decor" opacity="${monitorOpacity.toFixed(3)}" filter="url(#${p}-shadow)">
|
|
326
|
+
<rect x="${neckX.toFixed(2)}" y="${neckY.toFixed(2)}" width="${neckW.toFixed(2)}" height="${neckH.toFixed(2)}" rx="0.18" fill="url(#${p}-body)"/>
|
|
327
|
+
<rect x="${baseX.toFixed(2)}" y="${baseY.toFixed(2)}" width="${baseW.toFixed(2)}" height="${baseH.toFixed(2)}" rx="${params.standBaseR}" fill="url(#${p}-body)"/>
|
|
328
|
+
</g>
|
|
329
|
+
<g class="stand-edge" opacity="${monitorOpacity.toFixed(3)}">
|
|
330
|
+
<rect x="${neckX.toFixed(2)}" y="${(neckY + 0.05).toFixed(2)}" width="${neckW.toFixed(2)}" height="${(neckH - 0.05).toFixed(2)}" rx="0.18" fill="none" stroke="#aab3c6" stroke-width="0.34" stroke-opacity="0.55"/>
|
|
331
|
+
<rect x="${baseX.toFixed(2)}" y="${baseY.toFixed(2)}" width="${baseW.toFixed(2)}" height="${baseH.toFixed(2)}" rx="${params.standBaseR}" fill="none" stroke="#aab3c6" stroke-width="0.34" stroke-opacity="0.55"/>
|
|
332
|
+
</g>
|
|
333
|
+
` : '';
|
|
334
|
+
const monitorScreen = monitorOpacity > 0.01 ? `
|
|
335
|
+
<g class="screen-decor" opacity="${monitorOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
|
|
336
|
+
<rect x="${monX1.toFixed(2)}" y="${monY1.toFixed(2)}" width="${(monX2-monX1).toFixed(2)}" height="${(monY2-monY1).toFixed(2)}" rx="${params.monitorScreenCornerR}" fill="#171D2D"/>
|
|
337
|
+
<rect x="${(monX1 + 0.20).toFixed(2)}" y="${(monY1 + 0.20).toFixed(2)}" width="${(monX2-monX1 - 0.40).toFixed(2)}" height="${(monY2-monY1 - 0.40).toFixed(2)}" rx="${Math.max(0.2, params.monitorScreenCornerR - 0.16).toFixed(2)}" fill="none" stroke="#7A9DFF" stroke-width="0.18" stroke-opacity="0.34"/>
|
|
338
|
+
</g>
|
|
339
|
+
` : '';
|
|
340
|
+
|
|
341
|
+
const eHalfW = params.envHalfW;
|
|
342
|
+
const eHalfH = params.envHalfH;
|
|
343
|
+
const eInset = params.flapInset;
|
|
344
|
+
const flapX1 = 12 - eHalfW + 0.5 + eInset;
|
|
345
|
+
const flapX2 = 12 + eHalfW - 0.5 - eInset;
|
|
346
|
+
const flapY = 12 - eHalfH + 0.5 + eInset;
|
|
347
|
+
const flapBottomY = flapY + params.flapDepth;
|
|
348
|
+
const mBottomY = 12 + eHalfH - 1.18;
|
|
349
|
+
const mStrokeW = params.mStrokeW;
|
|
350
|
+
const mLegStrokeW = mStrokeW * params.mLegBoost;
|
|
351
|
+
const envelopeDecor = envelopeOpacity > 0.01 ? `
|
|
352
|
+
<g class="envelope-decor" opacity="${envelopeOpacity.toFixed(3)}" clip-path="url(#${p}-clip)">
|
|
353
|
+
<path d="M ${flapX1.toFixed(2)} ${flapY.toFixed(2)} L 12 ${flapBottomY.toFixed(2)} L ${flapX2.toFixed(2)} ${flapY.toFixed(2)} Z"
|
|
354
|
+
fill="#B1A7FF" fill-opacity="${params.flapFillOpacity.toFixed(3)}"/>
|
|
355
|
+
<path d="M ${flapX1.toFixed(2)} ${mBottomY.toFixed(2)} L ${flapX1.toFixed(2)} ${flapY.toFixed(2)}"
|
|
356
|
+
fill="none" stroke="#6EA8FF" stroke-width="${mLegStrokeW.toFixed(2)}" stroke-linecap="round"/>
|
|
357
|
+
<path d="M ${flapX1.toFixed(2)} ${flapY.toFixed(2)} L 12 ${flapBottomY.toFixed(2)}"
|
|
358
|
+
fill="none" stroke="#FF6B7A" stroke-width="${mStrokeW.toFixed(2)}" stroke-linecap="round"/>
|
|
359
|
+
<path d="M 12 ${flapBottomY.toFixed(2)} L ${flapX2.toFixed(2)} ${flapY.toFixed(2)}"
|
|
360
|
+
fill="none" stroke="#FFD45C" stroke-width="${mStrokeW.toFixed(2)}" stroke-linecap="round"/>
|
|
361
|
+
<path d="M ${flapX2.toFixed(2)} ${flapY.toFixed(2)} L ${flapX2.toFixed(2)} ${mBottomY.toFixed(2)}"
|
|
362
|
+
fill="none" stroke="#55D08A" stroke-width="${mLegStrokeW.toFixed(2)}" stroke-linecap="round"/>
|
|
363
|
+
</g>
|
|
364
|
+
` : '';
|
|
365
|
+
|
|
366
|
+
return `
|
|
367
|
+
<g class="cloudling-body">
|
|
368
|
+
${moduleSide}
|
|
369
|
+
${monitorStand}
|
|
370
|
+
<g class="edge-system">
|
|
371
|
+
<use class="edge-contact" href="#${p}-cloud-shape" fill="none" stroke="#7d8aa7" stroke-width="1.05" stroke-linejoin="round" transform="translate(0 0.18)" filter="url(#${p}-edge-blur)"/>
|
|
372
|
+
<use class="edge-rim-soft" href="#${p}-cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.90" stroke-linejoin="round" filter="url(#${p}-edge-blur)"/>
|
|
373
|
+
<use class="edge-rim-tight" href="#${p}-cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
|
|
374
|
+
</g>
|
|
375
|
+
<g filter="url(#${p}-shadow)">
|
|
376
|
+
<use href="#${p}-cloud-shape" fill="url(#${p}-body)"/>
|
|
377
|
+
<rect x="1" y="2" width="22" height="11" fill="url(#${p}-hi)" clip-path="url(#${p}-clip)"/>
|
|
378
|
+
</g>
|
|
379
|
+
${terminalDecor}
|
|
380
|
+
${monitorScreen}
|
|
381
|
+
${envelopeDecor}
|
|
382
|
+
</g>
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* =========================================================================
|
|
387
|
+
* 眼睛 (capsule + 闭眼线 + smile, 锚点固定 9,12 / 15,12)
|
|
388
|
+
* ========================================================================= */
|
|
389
|
+
function makeEyes(p, f) {
|
|
390
|
+
const mode = f.eyeMode;
|
|
391
|
+
const openness = f.eyeOpenness;
|
|
392
|
+
const flashStrength = f.eyeFlash;
|
|
393
|
+
const lcx = f.eyeLeftX ?? params.eyeLeftX ?? 9;
|
|
394
|
+
const rcx = f.eyeRightX ?? params.eyeRightX ?? 15;
|
|
395
|
+
const useFlash = flashStrength > 0.05;
|
|
396
|
+
const eyeFill = useFlash ? `url(#${p}-eye-flash)` : `url(#${p}-eye)`;
|
|
397
|
+
const filter = useFlash ? `filter="url(#${p}-eye-glow)"` : '';
|
|
398
|
+
|
|
399
|
+
if (mode === 'closed') {
|
|
400
|
+
const eyeW = f.eyeWidth ?? params.eyeWidth ?? 2.6;
|
|
401
|
+
const cy = (f.eyeY ?? params.eyeY ?? 9.5) + ((f.eyeHeight ?? params.eyeHeight ?? 5) * 0.5);
|
|
402
|
+
const linePath = (cx) => `M ${(cx-eyeW*0.48).toFixed(2)} ${cy.toFixed(2)} Q ${cx.toFixed(2)} ${(cy+0.18).toFixed(2)} ${(cx+eyeW*0.48).toFixed(2)} ${cy.toFixed(2)}`;
|
|
403
|
+
return `
|
|
404
|
+
<g class="eyes" ${filter}>
|
|
405
|
+
<path d="${linePath(lcx)}" fill="none" stroke="${eyeFill}" stroke-width="0.62" stroke-linecap="round"/>
|
|
406
|
+
<path d="${linePath(rcx)}" fill="none" stroke="${eyeFill}" stroke-width="0.62" stroke-linecap="round"/>
|
|
407
|
+
</g>
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (mode === 'smile') {
|
|
412
|
+
const w = 3.5, h = 1.0, smileCY = 12.7;
|
|
413
|
+
const smilePath = (cx) => `M ${(cx-w/2).toFixed(2)} ${smileCY} Q ${cx} ${(smileCY-2*h).toFixed(2)} ${(cx+w/2).toFixed(2)} ${smileCY}`;
|
|
414
|
+
return `
|
|
415
|
+
<g class="eyes">
|
|
416
|
+
<path d="${smilePath(lcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>
|
|
417
|
+
<path d="${smilePath(rcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>
|
|
418
|
+
</g>
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const eyeW = f.eyeWidth ?? params.eyeWidth ?? 2.6;
|
|
423
|
+
const baseH = f.eyeHeight ?? params.eyeHeight ?? 5;
|
|
424
|
+
const h = Math.max(0.3, baseH * openness);
|
|
425
|
+
const y = (f.eyeY ?? params.eyeY ?? 9.5) + (baseH - h) / 2;
|
|
426
|
+
const rx = Math.min(1.3, h / 2);
|
|
427
|
+
const leftX = lcx - eyeW / 2;
|
|
428
|
+
const rightX = rcx - eyeW / 2;
|
|
429
|
+
return `
|
|
430
|
+
<g class="eyes" ${filter}>
|
|
431
|
+
<rect x="${leftX.toFixed(2)}" y="${y.toFixed(2)}" width="${eyeW.toFixed(2)}" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="${eyeFill}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>
|
|
432
|
+
<rect x="${rightX.toFixed(2)}" y="${y.toFixed(2)}" width="${eyeW.toFixed(2)}" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="${eyeFill}" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>
|
|
433
|
+
</g>
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* =========================================================================
|
|
438
|
+
* duang duang spring overshoot (跟 carrying-v5 同款)
|
|
439
|
+
* ========================================================================= */
|
|
440
|
+
function springOvershoot(tLocal, amp, freq, damping) {
|
|
441
|
+
if (tLocal < 0) return 0;
|
|
442
|
+
return amp * Math.cos(2 * Math.PI * freq * tLocal) * Math.exp(-damping * tLocal);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* =========================================================================
|
|
446
|
+
* 时间线计算
|
|
447
|
+
* ========================================================================= */
|
|
448
|
+
const EYE_STYLES = {
|
|
449
|
+
cloud: { left: 9.00, right: 15.00, width: 2.60, height: 5.00, y: 9.50 },
|
|
450
|
+
terminal: { left: 9.35, right: 14.85, width: 2.35, height: 4.20, y: 10.15 },
|
|
451
|
+
module: { left: 8.85, right: 15.15, width: 2.65, height: 4.85, y: 9.65 },
|
|
452
|
+
monitor: { left: 8.85, right: 15.15, width: 2.25, height: 4.05, y: 10.25 },
|
|
453
|
+
envelope: { left: 8.75, right: 15.25, width: 2.40, height: 4.20, y: 10.20 },
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
function mixEyeStyle(from, to, u) {
|
|
457
|
+
const a = EYE_STYLES[from];
|
|
458
|
+
const b = EYE_STYLES[to];
|
|
459
|
+
return {
|
|
460
|
+
left: lerp(a.left, b.left, u),
|
|
461
|
+
right: lerp(a.right, b.right, u),
|
|
462
|
+
width: lerp(a.width, b.width, u),
|
|
463
|
+
height: lerp(a.height, b.height, u),
|
|
464
|
+
y: lerp(a.y, b.y, u),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function setShape(f, from, to, uRaw) {
|
|
469
|
+
const u = clamp01(uRaw);
|
|
470
|
+
f.fromShape = from;
|
|
471
|
+
f.toShape = to;
|
|
472
|
+
f.shapeU = u;
|
|
473
|
+
f.opacities = { terminal: 0, module: 0, monitor: 0, envelope: 0 };
|
|
474
|
+
if (from !== 'cloud') f.opacities[from] += 1 - u;
|
|
475
|
+
if (to !== 'cloud') f.opacities[to] += u;
|
|
476
|
+
|
|
477
|
+
const eye = mixEyeStyle(from, to, u);
|
|
478
|
+
f.eyeLeftX = eye.left;
|
|
479
|
+
f.eyeRightX = eye.right;
|
|
480
|
+
f.eyeWidth = eye.width;
|
|
481
|
+
f.eyeHeight = eye.height;
|
|
482
|
+
f.eyeY = eye.y;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function holdShape(f, shape, eyeMode = 'capsule') {
|
|
486
|
+
setShape(f, shape, shape, 0);
|
|
487
|
+
f.eyeMode = eyeMode;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function blinkSwitchFrame(f, tLocal, from, to, options = {}) {
|
|
491
|
+
const closeDur = Math.max(0.04, params.blinkClose);
|
|
492
|
+
const switchDur = Math.max(0.04, params.blinkSwitch);
|
|
493
|
+
const openDur = Math.max(0.06, params.blinkOpen);
|
|
494
|
+
const total = closeDur + switchDur + openDur;
|
|
495
|
+
const targetEyeMode = options.smile ? 'smile' : 'capsule';
|
|
496
|
+
|
|
497
|
+
if (tLocal < closeDur) {
|
|
498
|
+
const u = easeIn(progress(tLocal, 0, closeDur));
|
|
499
|
+
setShape(f, from, from, 0);
|
|
500
|
+
f.eyeMode = 'closed';
|
|
501
|
+
f.eyeOpenness = lerp(1, 0.08, u);
|
|
502
|
+
f.squashY = lerp(1, params.blinkSquashY, u);
|
|
503
|
+
f.eyeFlash = u * params.eyeFlashStrength * 0.55;
|
|
504
|
+
} else if (tLocal < closeDur + switchDur) {
|
|
505
|
+
const u = easeInOut(progress(tLocal, closeDur, closeDur + switchDur));
|
|
506
|
+
setShape(f, from, to, u);
|
|
507
|
+
f.eyeMode = 'closed';
|
|
508
|
+
f.eyeOpenness = 0.08;
|
|
509
|
+
f.squashY = params.blinkSquashY;
|
|
510
|
+
f.eyeFlash = params.eyeFlashStrength * 0.35;
|
|
511
|
+
} else if (tLocal < total) {
|
|
512
|
+
const u = easeOutBack(progress(tLocal, closeDur + switchDur, total));
|
|
513
|
+
setShape(f, to, to, 0);
|
|
514
|
+
f.eyeOpenness = lerp(0.08, 1, clamp01(u));
|
|
515
|
+
f.squashY = lerp(params.blinkSquashY, 1, clamp01(u));
|
|
516
|
+
f.eyeFlash = lerp(params.eyeFlashStrength * 0.35, 0, clamp01(u));
|
|
517
|
+
f.springScale = 1 + Math.sin(Math.PI * clamp01(u)) * 0.018;
|
|
518
|
+
if (options.smile && u > 0.28) f.eyeMode = 'smile';
|
|
519
|
+
} else {
|
|
520
|
+
holdShape(f, to, targetEyeMode);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function computeFrame(t) {
|
|
525
|
+
const T = params.T_CYCLE;
|
|
526
|
+
const tMod = ((t % T) + T) % T;
|
|
527
|
+
const f = {
|
|
528
|
+
tMod,
|
|
529
|
+
fromShape: 'cloud',
|
|
530
|
+
toShape: 'cloud',
|
|
531
|
+
shapeU: 0,
|
|
532
|
+
squashY: 1,
|
|
533
|
+
springScale: 1,
|
|
534
|
+
eyeMode: 'capsule',
|
|
535
|
+
eyeOpenness: 1,
|
|
536
|
+
eyeFlash: 0,
|
|
537
|
+
opacities: { terminal: 0, module: 0, monitor: 0, envelope: 0 },
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const blinkTotal = params.blinkClose + params.blinkSwitch + params.blinkOpen;
|
|
541
|
+
const b1 = params.T_CLOUD_HOLD;
|
|
542
|
+
const hTerminal = b1 + blinkTotal;
|
|
543
|
+
const b2 = params.T_TERMINAL_HOLD;
|
|
544
|
+
const hModule = b2 + blinkTotal;
|
|
545
|
+
const b3 = params.T_MODULE_HOLD;
|
|
546
|
+
const hMonitor = b3 + blinkTotal;
|
|
547
|
+
const b4 = params.T_MONITOR_HOLD;
|
|
548
|
+
const hEnvelope = b4 + blinkTotal;
|
|
549
|
+
const b5 = params.T_ENVELOPE_HOLD;
|
|
550
|
+
const hSmile = b5 + blinkTotal;
|
|
551
|
+
const b6 = Math.max(params.T_SMILE_HOLD, hSmile + 0.05);
|
|
552
|
+
const hCloud = b6 + blinkTotal;
|
|
553
|
+
|
|
554
|
+
if (tMod < b1) {
|
|
555
|
+
holdShape(f, 'cloud');
|
|
556
|
+
} else if (tMod < hTerminal) {
|
|
557
|
+
blinkSwitchFrame(f, tMod - b1, 'cloud', 'terminal');
|
|
558
|
+
} else if (tMod < b2) {
|
|
559
|
+
holdShape(f, 'terminal');
|
|
560
|
+
} else if (tMod < hModule) {
|
|
561
|
+
blinkSwitchFrame(f, tMod - b2, 'terminal', 'module');
|
|
562
|
+
} else if (tMod < b3) {
|
|
563
|
+
holdShape(f, 'module');
|
|
564
|
+
} else if (tMod < hMonitor) {
|
|
565
|
+
blinkSwitchFrame(f, tMod - b3, 'module', 'monitor');
|
|
566
|
+
} else if (tMod < b4) {
|
|
567
|
+
holdShape(f, 'monitor');
|
|
568
|
+
} else if (tMod < hEnvelope) {
|
|
569
|
+
blinkSwitchFrame(f, tMod - b4, 'monitor', 'envelope');
|
|
570
|
+
} else if (tMod < b5) {
|
|
571
|
+
holdShape(f, 'envelope');
|
|
572
|
+
} else if (tMod < hSmile) {
|
|
573
|
+
blinkSwitchFrame(f, tMod - b5, 'envelope', 'cloud', { smile: true });
|
|
574
|
+
} else if (tMod < b6) {
|
|
575
|
+
holdShape(f, 'cloud', 'smile');
|
|
576
|
+
} else if (tMod < hCloud) {
|
|
577
|
+
blinkSwitchFrame(f, tMod - b6, 'cloud', 'cloud');
|
|
578
|
+
} else {
|
|
579
|
+
holdShape(f, 'cloud');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const breathPhase = (Math.cos(2 * Math.PI * tMod / params.T_CYCLE) + 1) / 2;
|
|
583
|
+
f.breathScale = lerp(params.breathLow, params.breathHigh, breathPhase);
|
|
584
|
+
return f;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* =========================================================================
|
|
588
|
+
* 渲染
|
|
589
|
+
* ========================================================================= */
|
|
590
|
+
const stage = document.getElementById('stage');
|
|
591
|
+
|
|
592
|
+
function renderFrame(t) {
|
|
593
|
+
const f = computeFrame(t);
|
|
594
|
+
const p = 'bld';
|
|
595
|
+
|
|
596
|
+
const pointSets = makeShapePointSets();
|
|
597
|
+
let bodyPoints = mixPointSets(pointSets[f.fromShape], pointSets[f.toShape], f.shapeU);
|
|
598
|
+
bodyPoints = squashPoints(bodyPoints, f.squashY);
|
|
599
|
+
const nonCloudness = 1 - (f.fromShape === 'cloud' && f.toShape === 'cloud' ? 1 : 0);
|
|
600
|
+
const sharpness = Math.max(f.opacities.terminal, f.opacities.module, f.opacities.monitor, f.opacities.envelope, nonCloudness * f.shapeU);
|
|
601
|
+
const cloudD = makePathFromPoints(bodyPoints, sharpness);
|
|
602
|
+
const cloudlingBody = makeCloudlingBody(p, f);
|
|
603
|
+
const eyes = makeEyes(p, f);
|
|
604
|
+
|
|
605
|
+
stage.innerHTML = `
|
|
606
|
+
<style>
|
|
607
|
+
.edge-rim-tight { stroke-opacity: .65; }
|
|
608
|
+
.edge-rim-soft { stroke-opacity: .50; }
|
|
609
|
+
.edge-contact { stroke-opacity: .65; }
|
|
610
|
+
@media (prefers-color-scheme: light) {
|
|
611
|
+
.edge-rim-tight { stroke-opacity: .75; }
|
|
612
|
+
.edge-rim-soft { stroke-opacity: .60; }
|
|
613
|
+
.edge-contact { stroke-opacity: .75; }
|
|
614
|
+
.cloud-drop-shadow { flood-opacity: .13; }
|
|
615
|
+
.food-cloud-shadow { flood-opacity: .28; flood-color: #5060FF; }
|
|
616
|
+
.food-cloud > path {
|
|
617
|
+
stroke: #5870E8 !important;
|
|
618
|
+
stroke-width: 1.2 !important;
|
|
619
|
+
stroke-opacity: .9 !important;
|
|
620
|
+
vector-effect: non-scaling-stroke !important;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
</style>
|
|
624
|
+
<defs>
|
|
625
|
+
${commonDefs(p, cloudD)}
|
|
626
|
+
<path id="${p}-cloud-shape" d="${cloudD}"/>
|
|
627
|
+
</defs>
|
|
628
|
+
<g class="scene">
|
|
629
|
+
<g class="spring" transform="translate(12 12) scale(${f.springScale.toFixed(4)}) translate(-12 -12)">
|
|
630
|
+
<g class="breath" transform="translate(12 12) scale(${f.breathScale.toFixed(4)}) translate(-12 -12)">
|
|
631
|
+
${cloudlingBody}
|
|
632
|
+
${eyes}
|
|
633
|
+
</g>
|
|
634
|
+
</g>
|
|
635
|
+
</g>
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/* =========================================================================
|
|
640
|
+
* 主循环 + 控件 (复用 carrying-v5 模板)
|
|
641
|
+
* ========================================================================= */
|
|
642
|
+
let startT = performance.now();
|
|
643
|
+
let elapsedOffset = 0;
|
|
644
|
+
let pausedAt = 0;
|
|
645
|
+
|
|
646
|
+
const scrubberEl = document.getElementById('scrubber');
|
|
647
|
+
const tDisplayEl = document.getElementById('t-display');
|
|
648
|
+
|
|
649
|
+
function getCurrentT() {
|
|
650
|
+
if (scrubbing) return Number(scrubberEl.value);
|
|
651
|
+
if (paused) return elapsedOffset + (pausedAt - startT) / 1000;
|
|
652
|
+
return elapsedOffset + (performance.now() - startT) / 1000;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function loop() {
|
|
656
|
+
const t = getCurrentT();
|
|
657
|
+
scrubberEl.max = params.T_CYCLE.toFixed(2);
|
|
658
|
+
const tMod = ((t % params.T_CYCLE) + params.T_CYCLE) % params.T_CYCLE;
|
|
659
|
+
if (!scrubbing) scrubberEl.value = tMod.toFixed(3);
|
|
660
|
+
tDisplayEl.textContent = `${tMod.toFixed(2)} s`;
|
|
661
|
+
renderFrame(t);
|
|
662
|
+
requestAnimationFrame(loop);
|
|
663
|
+
}
|
|
664
|
+
renderFrame(0);
|
|
665
|
+
requestAnimationFrame(loop);
|
|
666
|
+
]]></script>
|
|
667
|
+
</svg>
|