@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,604 @@
|
|
|
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 视觉定稿)
|
|
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 easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
|
|
39
|
+
function easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/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
|
+
/* 包裹形变: contactPoint 是 cloudling 局部坐标的接触点.
|
|
61
|
+
v4 不再把边缘压凹, 而是让接触侧外鼓成一个临时 puff, 把食物云包进轮廓里. */
|
|
62
|
+
function makeSoftCloudPath(pressure, contactPoint) {
|
|
63
|
+
const a = clamp01(pressure);
|
|
64
|
+
if (a <= 0.0001) return CLOUD_D;
|
|
65
|
+
const cx = 12, cy = 12;
|
|
66
|
+
const cpX = contactPoint?.x ?? 12;
|
|
67
|
+
const cpY = contactPoint?.y ?? 4.45;
|
|
68
|
+
// out 指向食物云所在的外侧, side 是沿边缘的切线方向.
|
|
69
|
+
const dx0 = cx - cpX, dy0 = cy - cpY;
|
|
70
|
+
const len0 = Math.hypot(dx0, dy0) || 1;
|
|
71
|
+
const outX = -dx0 / len0, outY = -dy0 / len0;
|
|
72
|
+
const sideX = dy0 / len0, sideY = -dx0 / len0;
|
|
73
|
+
const volume = Math.sin(a * Math.PI) * 0.12;
|
|
74
|
+
|
|
75
|
+
const points = SOFT_CLOUD_POINTS.map((pt) => {
|
|
76
|
+
const distToContact = Math.hypot(pt.x - cpX, pt.y - cpY);
|
|
77
|
+
const contactWeight = clamp01(1 - distToContact / 9.2);
|
|
78
|
+
const rx = pt.x - cx, ry = pt.y - cy;
|
|
79
|
+
const projOut = rx * outX + ry * outY; // >0 表示在接触侧
|
|
80
|
+
const projSide = rx * sideX + ry * sideY;
|
|
81
|
+
const contactSide = clamp01((projOut + 1.2) / 9.4);
|
|
82
|
+
const centerBand = Math.pow(contactWeight, 1.24) * (0.36 + contactSide * 0.64);
|
|
83
|
+
const shoulderBand = Math.pow(contactWeight, 0.86) * clamp01((Math.abs(projSide) - 0.7) / 5.0) * contactSide;
|
|
84
|
+
const sideShift = Math.sign(projSide || 1) * shoulderBand * 0.82 * a;
|
|
85
|
+
const wrapOut = (centerBand * 2.70 + shoulderBand * 1.05) * a;
|
|
86
|
+
const neckIn = Math.pow(clamp01(1 - Math.abs(distToContact - 6.4) / 2.7), 1.35) * contactSide * 0.20 * a;
|
|
87
|
+
const lowerSettle = clamp01((pt.y - 16.0) / 4.8) * volume;
|
|
88
|
+
return {
|
|
89
|
+
x: pt.x + outX * (wrapOut - neckIn) + sideX * sideShift,
|
|
90
|
+
y: pt.y + outY * (wrapOut - neckIn) + sideY * sideShift + lowerSettle,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
return catmullRomClosedPath(points, lerp(0.96, 0.78, a));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* =========================================================================
|
|
97
|
+
* 食物云几何: 不规则圆润 puff (跟 long-idle bush peek 暖云同语言 但更冷淡)
|
|
98
|
+
* 用 7-8 个圆 union 成蓬松云团, 比 cloudling 主体小约 60%
|
|
99
|
+
* ========================================================================= */
|
|
100
|
+
function makeFoodCloudPath() {
|
|
101
|
+
// 局部坐标 viewBox 8x6, 中心 (0,0). 不规则但圆润.
|
|
102
|
+
// 用一组椭圆/圆通过 catmull rom 包出外轮廓
|
|
103
|
+
const pts = [
|
|
104
|
+
{x:-3.7, y:-0.4},
|
|
105
|
+
{x:-3.5, y:-1.6},
|
|
106
|
+
{x:-2.6, y:-2.3},
|
|
107
|
+
{x:-1.4, y:-2.5},
|
|
108
|
+
{x:-0.6, y:-2.0},
|
|
109
|
+
{x: 0.4, y:-2.4},
|
|
110
|
+
{x: 1.7, y:-2.2},
|
|
111
|
+
{x: 2.7, y:-1.4},
|
|
112
|
+
{x: 3.6, y:-0.7},
|
|
113
|
+
{x: 3.7, y: 0.5},
|
|
114
|
+
{x: 3.0, y: 1.5},
|
|
115
|
+
{x: 2.0, y: 2.0},
|
|
116
|
+
{x: 0.7, y: 2.1},
|
|
117
|
+
{x:-0.6, y: 2.3},
|
|
118
|
+
{x:-1.9, y: 2.0},
|
|
119
|
+
{x:-3.0, y: 1.5},
|
|
120
|
+
{x:-3.6, y: 0.7},
|
|
121
|
+
];
|
|
122
|
+
return catmullRomClosedPath(pts, 1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* =========================================================================
|
|
126
|
+
* 默认参数
|
|
127
|
+
* ========================================================================= */
|
|
128
|
+
const DEFAULTS = {
|
|
129
|
+
// 主周期
|
|
130
|
+
T_CYCLE: 4.5,
|
|
131
|
+
|
|
132
|
+
// 融入时机 (单口, phase A 用直接值, phase B 镜像)
|
|
133
|
+
T_FOOD_ENTER_START: 0.00, // 食物云开始飘入
|
|
134
|
+
T_FOOD_REACHED: 0.90, // 食物云飘到接触位置
|
|
135
|
+
T_INTERCEPT_START: 0.55, // 云宝开始飘向食物云
|
|
136
|
+
T_CONTACT: 1.00, // 接触瞬间 (云宝 + 食物云相遇)
|
|
137
|
+
T_SWALLOW_END: 1.24, // 食物云完全融入, v5 缩短包裹停顿
|
|
138
|
+
T_RETURN_END: 1.95, // 云宝飘回中心
|
|
139
|
+
// duang spring (从完全融入后起算)
|
|
140
|
+
springAmp: 0.16, // 弹幅 (15-18% 之间挑)
|
|
141
|
+
springFreq: 4.0, // 弹频 Hz
|
|
142
|
+
springDamping: 5.0, // 阻尼 (越大衰减越快)
|
|
143
|
+
springDuration: 0.65, // 总时长 (之后纯呼吸)
|
|
144
|
+
|
|
145
|
+
// 食物云轨迹 (右口 phase A; 左口对角镜像 — 右上进 / 左下进)
|
|
146
|
+
// X offset = "距 cloudling 中心 12 的距离" (右口取 +offset, 左口取 -offset)
|
|
147
|
+
foodEnterOffsetX: 24, // 画外起点距中心偏移 (右 = 36, 左 = -12)
|
|
148
|
+
foodTargetOffsetX: 13.5, // 接触时食物云贴在边缘外侧 (右 = 25.5, 左 = -1.5)
|
|
149
|
+
foodTargetY: 12, // 接触位置 Y (跟眼睛同高)
|
|
150
|
+
foodEnterYDelta: -4.5, // 入场 Y 相对 target 的偏移 (右口 -4.5 = 偏上, 左口 sign=-1 → +4.5 = 偏下)
|
|
151
|
+
foodSize: 1.0, // 食物云缩放
|
|
152
|
+
foodSizeRight: 1.08, // 右侧食物云稍大
|
|
153
|
+
foodSizeLeft: 0.82, // 左侧食物云稍小
|
|
154
|
+
foodOpacity: 0.85,
|
|
155
|
+
foodDriftY: 0.22, // 飘入时的微微上下浮动幅度
|
|
156
|
+
foodExitOvershoot: 0.15, // 贴边时只轻轻贴住, 不穿进主体
|
|
157
|
+
|
|
158
|
+
// 云宝拦截
|
|
159
|
+
interceptDx: 5.0, // translate x 偏移 (右拦截 +5, 左 -5)
|
|
160
|
+
interceptDy: 0, // translate y 偏移 (右上接触可让 -2, 跟 enterY 联动)
|
|
161
|
+
interceptDyMatch: 1, // 1 = interceptDy 跟食物云对角入场镜像 (右 -, 左 +); 0 = 用上面的固定值
|
|
162
|
+
interceptEyePeekX: 1.6, // 眼睛朝食物云偏移
|
|
163
|
+
interceptEyePeekY: 0,
|
|
164
|
+
bodyRollFactor: 1.0, // 云宝飘动时 body 滚轮 rotate (度/单位 cloudTx; 飘右 → 顶部往左倾)
|
|
165
|
+
bodyRollMax: 8, // body roll 最大角度限制
|
|
166
|
+
|
|
167
|
+
// 接触 deform (侧向接触, 不是顶部)
|
|
168
|
+
contactSideX: 20.8, // 接触点局部坐标 (右侧凸起)
|
|
169
|
+
contactSideY: 12,
|
|
170
|
+
contactPressureMax: 0.90, // 包裹最大强度
|
|
171
|
+
absorbDepth: 1.2, // v4: 同化时只向主体贴近一点, 不钻入中心
|
|
172
|
+
absorbSqueezeX: 0.64, // 同化中食物云轻微横压
|
|
173
|
+
absorbSqueezeY: 1.16, // 同化中食物云轻微纵鼓
|
|
174
|
+
wrapBulgeOffset: 1.55, // 同化时保留在侧边外鼓区域的距离
|
|
175
|
+
|
|
176
|
+
// 呼吸 (跟 swallow 同步: 周期 = 2.25s = 半 cycle, 每口配一呼吸)
|
|
177
|
+
breathLow: 0.97,
|
|
178
|
+
breathHigh: 1.02,
|
|
179
|
+
|
|
180
|
+
// edge-system + 阴影
|
|
181
|
+
cloudShadowOpacity: 0.18,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
let params = { ...DEFAULTS };
|
|
185
|
+
let paused = false;
|
|
186
|
+
let scrubbing = false;
|
|
187
|
+
|
|
188
|
+
/* =========================================================================
|
|
189
|
+
* defs (复用 cloudling 标准件)
|
|
190
|
+
* ========================================================================= */
|
|
191
|
+
function commonDefs(p, cloudD) {
|
|
192
|
+
return `
|
|
193
|
+
<linearGradient id="${p}-body" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
|
|
194
|
+
<stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="#e0f2fe"/><stop offset="1" stop-color="#bae6fd"/>
|
|
195
|
+
</linearGradient>
|
|
196
|
+
<linearGradient id="${p}-eye" x1="0" y1="3" x2="0" y2="21" gradientUnits="userSpaceOnUse">
|
|
197
|
+
<stop offset="0" stop-color="#B1A7FF"/><stop offset="0.5" stop-color="#7A9DFF"/><stop offset="1" stop-color="#3941FF"/>
|
|
198
|
+
</linearGradient>
|
|
199
|
+
<radialGradient id="${p}-hi" cx="0.45" cy="0.25" r="0.55">
|
|
200
|
+
<stop offset="0" stop-color="#fff" stop-opacity="0.9"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
|
201
|
+
</radialGradient>
|
|
202
|
+
<linearGradient id="${p}-food" x1="0" y1="-3" x2="0" y2="3" gradientUnits="userSpaceOnUse">
|
|
203
|
+
<stop offset="0" stop-color="#FFFFFF"/>
|
|
204
|
+
<stop offset="0.56" stop-color="#F0F5FF"/>
|
|
205
|
+
<stop offset="1" stop-color="#DCE6FA"/>
|
|
206
|
+
</linearGradient>
|
|
207
|
+
<radialGradient id="${p}-food-hi" cx="0.4" cy="0.3" r="0.55">
|
|
208
|
+
<stop offset="0" stop-color="#fff" stop-opacity="0.85"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
|
209
|
+
</radialGradient>
|
|
210
|
+
<linearGradient id="${p}-food-rim" x1="-4" y1="-2" x2="4" y2="2" gradientUnits="userSpaceOnUse">
|
|
211
|
+
<stop offset="0" stop-color="#D6DFFF"/>
|
|
212
|
+
<stop offset="1" stop-color="#AAB9F6"/>
|
|
213
|
+
</linearGradient>
|
|
214
|
+
<filter id="${p}-shadow" x="-50%" y="-50%" width="200%" height="200%">
|
|
215
|
+
<feDropShadow class="cloud-drop-shadow" dx="0" dy="0.6" stdDeviation="0.8" flood-color="#3941FF" flood-opacity="${params.cloudShadowOpacity}"/>
|
|
216
|
+
</filter>
|
|
217
|
+
<filter id="${p}-food-shadow" x="-50%" y="-50%" width="200%" height="200%">
|
|
218
|
+
<feDropShadow class="food-cloud-shadow" dx="0" dy="0.28" stdDeviation="0.36" flood-color="#7A9DFF" flood-opacity="0.08"/>
|
|
219
|
+
</filter>
|
|
220
|
+
<filter id="${p}-food-melt" x="-60%" y="-60%" width="220%" height="220%">
|
|
221
|
+
<feGaussianBlur stdDeviation="0.10" result="soft"/>
|
|
222
|
+
<feDropShadow class="food-cloud-shadow" dx="0" dy="0.2" stdDeviation="0.35" flood-color="#3941FF" flood-opacity="0.08"/>
|
|
223
|
+
</filter>
|
|
224
|
+
<filter id="${p}-edge-blur" x="-40%" y="-40%" width="180%" height="180%">
|
|
225
|
+
<feGaussianBlur stdDeviation="0.16"/>
|
|
226
|
+
</filter>
|
|
227
|
+
<clipPath id="${p}-clip"><path d="${cloudD}"/></clipPath>
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* =========================================================================
|
|
232
|
+
* cloudling + 眼睛 (capsule + smile)
|
|
233
|
+
* ========================================================================= */
|
|
234
|
+
function makeEyes(p, mode, openness, offX, offY) {
|
|
235
|
+
const lcx = 9, rcx = 15, cy = 12;
|
|
236
|
+
let lEye, rEye;
|
|
237
|
+
|
|
238
|
+
if (mode === 'smile') {
|
|
239
|
+
const w = 3.5, h = 1.0, smileCY = 12.7;
|
|
240
|
+
const smilePath = (cx) => `M ${(cx-w/2).toFixed(2)} ${smileCY} Q ${cx} ${(smileCY-2*h).toFixed(2)} ${(cx+w/2).toFixed(2)} ${smileCY}`;
|
|
241
|
+
lEye = `<path d="${smilePath(lcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>`;
|
|
242
|
+
rEye = `<path d="${smilePath(rcx)}" fill="none" stroke="url(#${p}-eye)" stroke-width="1.25" stroke-linecap="round"/>`;
|
|
243
|
+
} else {
|
|
244
|
+
// capsule
|
|
245
|
+
const baseH = 5;
|
|
246
|
+
const h = Math.max(0.3, baseH * openness);
|
|
247
|
+
const y = 9.5 + (baseH - h) / 2;
|
|
248
|
+
const rx = Math.min(1.3, h / 2);
|
|
249
|
+
lEye = `<rect x="7.7" y="${y.toFixed(2)}" width="2.6" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="url(#${p}-eye)" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>`;
|
|
250
|
+
rEye = `<rect x="13.7" y="${y.toFixed(2)}" width="2.6" height="${h.toFixed(2)}" rx="${rx.toFixed(2)}" ry="${rx.toFixed(2)}" fill="url(#${p}-eye)" stroke="#fff" stroke-width="0.08" stroke-opacity="0.7"/>`;
|
|
251
|
+
}
|
|
252
|
+
return `<g class="eyes" transform="translate(${offX.toFixed(2)} ${offY.toFixed(2)})">${lEye}${rEye}</g>`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function makeCloudlingBody(p) {
|
|
256
|
+
return `
|
|
257
|
+
<g class="cloudling-body">
|
|
258
|
+
<g class="edge-system">
|
|
259
|
+
<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)"/>
|
|
260
|
+
<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)"/>
|
|
261
|
+
<use class="edge-rim-tight" href="#${p}-cloud-shape" fill="none" stroke="#aab3c6" stroke-width="0.42" stroke-linejoin="round"/>
|
|
262
|
+
</g>
|
|
263
|
+
<g filter="url(#${p}-shadow)">
|
|
264
|
+
<use href="#${p}-cloud-shape" fill="url(#${p}-body)"/>
|
|
265
|
+
<rect x="1" y="2" width="22" height="11" fill="url(#${p}-hi)" clip-path="url(#${p}-clip)"/>
|
|
266
|
+
</g>
|
|
267
|
+
</g>
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function makeAbsorbFrontRim(p, opacity) {
|
|
272
|
+
return `
|
|
273
|
+
<g class="absorb-front-rim" opacity="${opacity.toFixed(3)}">
|
|
274
|
+
<use href="#${p}-cloud-shape" fill="none" stroke="#f7f9ff" stroke-width="0.46" stroke-linejoin="round"/>
|
|
275
|
+
<use class="edge-rim-soft" href="#${p}-cloud-shape" fill="none" stroke="#7A9DFF" stroke-width="0.80" stroke-linejoin="round" filter="url(#${p}-edge-blur)"/>
|
|
276
|
+
</g>
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function makeFoodCloud(p, cx, cy, scale, opacity, opts = {}) {
|
|
281
|
+
const path = makeFoodCloudPath();
|
|
282
|
+
const sx = opts.sx ?? 1;
|
|
283
|
+
const sy = opts.sy ?? 1;
|
|
284
|
+
const rot = opts.rot ?? 0;
|
|
285
|
+
const filterId = opts.melting ? `${p}-food-melt` : `${p}-food-shadow`;
|
|
286
|
+
const hiOpacity = opts.melting ? 0.55 : 1;
|
|
287
|
+
return `
|
|
288
|
+
<g class="food-cloud" opacity="${opacity.toFixed(3)}" transform="translate(${cx.toFixed(2)} ${cy.toFixed(2)}) rotate(${rot.toFixed(2)}) scale(${(scale * sx).toFixed(3)} ${(scale * sy).toFixed(3)})" filter="url(#${filterId})">
|
|
289
|
+
<path d="${path}" fill="url(#${p}-food)"/>
|
|
290
|
+
<ellipse cx="-0.6" cy="-0.7" rx="2.6" ry="1.2" fill="url(#${p}-food-hi)" opacity="${hiOpacity}"/>
|
|
291
|
+
</g>
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function makeAbsorbWash(p, cx, cy, scale, opacity, opts = {}) {
|
|
296
|
+
const path = makeFoodCloudPath();
|
|
297
|
+
const sx = opts.sx ?? 1;
|
|
298
|
+
const sy = opts.sy ?? 1;
|
|
299
|
+
const rot = opts.rot ?? 0;
|
|
300
|
+
return `
|
|
301
|
+
<g class="absorb-wash" clip-path="url(#${p}-clip)" opacity="${opacity.toFixed(3)}" transform="translate(${cx.toFixed(2)} ${cy.toFixed(2)}) rotate(${rot.toFixed(2)}) scale(${(scale * sx).toFixed(3)} ${(scale * sy).toFixed(3)})" filter="url(#${p}-food-melt)">
|
|
302
|
+
<path d="${path}" fill="url(#${p}-body)" opacity="0.72"/>
|
|
303
|
+
<ellipse cx="-0.7" cy="-0.7" rx="2.4" ry="1.0" fill="url(#${p}-hi)" opacity="0.46"/>
|
|
304
|
+
</g>
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* =========================================================================
|
|
309
|
+
* duang duang spring overshoot
|
|
310
|
+
* 阻尼弹簧: scale = 1 + A × cos(2π × freq × t) × exp(-damping × t)
|
|
311
|
+
* t = 0 时 scale = 1 + A (第一弹峰)
|
|
312
|
+
* ========================================================================= */
|
|
313
|
+
function springOvershoot(tLocal, amp, freq, damping) {
|
|
314
|
+
if (tLocal < 0) return 0;
|
|
315
|
+
return amp * Math.cos(2 * Math.PI * freq * tLocal) * Math.exp(-damping * tLocal);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* =========================================================================
|
|
319
|
+
* 单口 (half cycle = 2.25s) 求 frame
|
|
320
|
+
* side: 'right' | 'left'
|
|
321
|
+
* tHalf: 0 ~ T_CYCLE/2, 当口的本地时间
|
|
322
|
+
* ========================================================================= */
|
|
323
|
+
function computeHalfCycle(tHalf, side) {
|
|
324
|
+
const sign = (side === 'right') ? +1 : -1;
|
|
325
|
+
const f = {};
|
|
326
|
+
const cloudCenterX = 12;
|
|
327
|
+
const cpX = (side === 'right') ? params.contactSideX : (24 - params.contactSideX);
|
|
328
|
+
const cpY = params.contactSideY;
|
|
329
|
+
|
|
330
|
+
// ---- 云宝拦截 translate (对角拦截: Y 跟食物云入场对角联动) ----
|
|
331
|
+
// interceptDyMatch=1 时, 右口 cloudTy 向上 (-) 接住右上来的食物云; 左口向下 (+) 接住左下来的
|
|
332
|
+
// 接住 Y 偏移 = foodEnterYDelta 的一半 (云宝不必完全跟到食物云入场高度, 只是迎接)
|
|
333
|
+
const targetDy = params.interceptDyMatch
|
|
334
|
+
? sign * params.foodEnterYDelta * 0.45
|
|
335
|
+
: params.interceptDy;
|
|
336
|
+
let cloudTx = 0, cloudTy = 0;
|
|
337
|
+
if (tHalf < params.T_INTERCEPT_START) {
|
|
338
|
+
cloudTx = 0; cloudTy = 0;
|
|
339
|
+
} else if (tHalf < params.T_CONTACT) {
|
|
340
|
+
const u = progress(tHalf, params.T_INTERCEPT_START, params.T_CONTACT);
|
|
341
|
+
cloudTx = lerp(0, sign * params.interceptDx, easeOut(u));
|
|
342
|
+
cloudTy = lerp(0, targetDy, easeOut(u));
|
|
343
|
+
} else if (tHalf < params.T_RETURN_END) {
|
|
344
|
+
if (tHalf < params.T_SWALLOW_END) {
|
|
345
|
+
cloudTx = sign * params.interceptDx;
|
|
346
|
+
cloudTy = targetDy;
|
|
347
|
+
} else {
|
|
348
|
+
const u = progress(tHalf, params.T_SWALLOW_END, params.T_RETURN_END);
|
|
349
|
+
cloudTx = lerp(sign * params.interceptDx, 0, easeInOut(u));
|
|
350
|
+
cloudTy = lerp(targetDy, 0, easeInOut(u));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
f.cloudTx = cloudTx;
|
|
354
|
+
f.cloudTy = cloudTy;
|
|
355
|
+
|
|
356
|
+
// ---- 食物云 ----
|
|
357
|
+
// v4: 接触后停在侧边外鼓区域, 被主体包住并同化成临时 puff.
|
|
358
|
+
const foodStart = cloudCenterX + sign * params.foodEnterOffsetX;
|
|
359
|
+
const foodSurfaceX = cloudCenterX + sign * params.foodTargetOffsetX;
|
|
360
|
+
const foodSurfaceY = targetDy + params.foodTargetY;
|
|
361
|
+
const foodStartY = foodSurfaceY + sign * params.foodEnterYDelta;
|
|
362
|
+
const foodContactX = foodSurfaceX - sign * params.foodExitOvershoot;
|
|
363
|
+
const foodWrappedX = cloudTx + cpX + sign * params.wrapBulgeOffset - sign * params.absorbDepth * 0.35;
|
|
364
|
+
const foodWrappedY = targetDy + cpY;
|
|
365
|
+
const sideFoodScale = params.foodSize * (side === 'right' ? params.foodSizeRight : params.foodSizeLeft);
|
|
366
|
+
let foodVisible = false, foodCx = foodStart, foodCy = foodStartY;
|
|
367
|
+
let foodOpacity = params.foodOpacity, foodScale = sideFoodScale;
|
|
368
|
+
let foodLayer = 'none', foodSx = 1, foodSy = 1, foodRot = 0, absorbWashOpacity = 0;
|
|
369
|
+
|
|
370
|
+
if (tHalf < params.T_FOOD_ENTER_START) {
|
|
371
|
+
foodVisible = false;
|
|
372
|
+
} else if (tHalf < params.T_CONTACT) {
|
|
373
|
+
foodVisible = true;
|
|
374
|
+
foodLayer = 'behind';
|
|
375
|
+
const u = progress(tHalf, params.T_FOOD_ENTER_START, params.T_CONTACT);
|
|
376
|
+
foodCx = lerp(foodStart, foodContactX, u);
|
|
377
|
+
foodCy = lerp(foodStartY, foodSurfaceY, u);
|
|
378
|
+
const driftPhase = (tHalf - params.T_FOOD_ENTER_START) / 0.55 * Math.PI * 2;
|
|
379
|
+
foodCy += Math.sin(driftPhase) * params.foodDriftY;
|
|
380
|
+
foodOpacity = clamp01(progress(tHalf, params.T_FOOD_ENTER_START, params.T_FOOD_ENTER_START + 0.25)) * params.foodOpacity;
|
|
381
|
+
} else if (tHalf < params.T_SWALLOW_END) {
|
|
382
|
+
foodVisible = true;
|
|
383
|
+
foodLayer = 'front';
|
|
384
|
+
const u = progress(tHalf, params.T_CONTACT, params.T_SWALLOW_END);
|
|
385
|
+
const slide = easeInOut(u);
|
|
386
|
+
const fade = easeIn(progress(u, 0.48, 1));
|
|
387
|
+
const squeeze = Math.sin(Math.min(1, u * 1.15) * Math.PI);
|
|
388
|
+
foodCx = lerp(foodContactX, foodWrappedX, slide);
|
|
389
|
+
foodCy = lerp(foodSurfaceY, foodWrappedY, slide) + sign * Math.sin(u * Math.PI) * 0.10;
|
|
390
|
+
foodScale = lerp(sideFoodScale, sideFoodScale * 0.72, easeIn(u));
|
|
391
|
+
foodOpacity = params.foodOpacity * (1 - fade);
|
|
392
|
+
foodSx = lerp(1, params.absorbSqueezeX, easeOut(u));
|
|
393
|
+
foodSy = lerp(1, params.absorbSqueezeY, squeeze);
|
|
394
|
+
foodRot = -sign * Math.sin(u * Math.PI) * 4;
|
|
395
|
+
absorbWashOpacity = Math.sin(u * Math.PI) * 0.46 * params.foodOpacity + easeIn(progress(u, 0.40, 1)) * 0.20;
|
|
396
|
+
} else {
|
|
397
|
+
foodVisible = false;
|
|
398
|
+
}
|
|
399
|
+
f.foodVisible = foodVisible;
|
|
400
|
+
f.foodLayer = foodLayer;
|
|
401
|
+
f.foodCx = foodCx;
|
|
402
|
+
f.foodCy = foodCy;
|
|
403
|
+
f.foodLocalCx = foodCx - cloudTx;
|
|
404
|
+
f.foodLocalCy = foodCy - cloudTy;
|
|
405
|
+
f.foodScale = foodScale;
|
|
406
|
+
f.foodOpacity = foodOpacity;
|
|
407
|
+
f.foodSx = foodSx;
|
|
408
|
+
f.foodSy = foodSy;
|
|
409
|
+
f.foodRot = foodRot;
|
|
410
|
+
f.absorbWashOpacity = absorbWashOpacity;
|
|
411
|
+
|
|
412
|
+
// ---- body roll (滚轮感: 飘动时 body rotate, eyes 不 rotate) ----
|
|
413
|
+
let bodyRoll = -cloudTx * params.bodyRollFactor;
|
|
414
|
+
bodyRoll = Math.max(-params.bodyRollMax, Math.min(params.bodyRollMax, bodyRoll));
|
|
415
|
+
f.bodyRoll = bodyRoll;
|
|
416
|
+
|
|
417
|
+
// ---- 眼睛 (peek 朝食物云偏移) ----
|
|
418
|
+
let eyeMode = 'capsule', eyeOpenness = 1, eyeOffX = 0, eyeOffY = 0;
|
|
419
|
+
if (tHalf < params.T_FOOD_ENTER_START + 0.05) {
|
|
420
|
+
eyeMode = 'capsule';
|
|
421
|
+
} else if (tHalf < params.T_CONTACT) {
|
|
422
|
+
// 看到食物云 → peek
|
|
423
|
+
const u = progress(tHalf, params.T_FOOD_ENTER_START + 0.05, params.T_INTERCEPT_START);
|
|
424
|
+
eyeOffX = lerp(0, sign * params.interceptEyePeekX, easeOut(u));
|
|
425
|
+
eyeOffY = params.interceptEyePeekY;
|
|
426
|
+
} else if (tHalf < params.T_SWALLOW_END) {
|
|
427
|
+
// 融入时保持专注, 不用嘴部符号.
|
|
428
|
+
const u = progress(tHalf, params.T_CONTACT, params.T_SWALLOW_END);
|
|
429
|
+
eyeMode = 'capsule';
|
|
430
|
+
eyeOpenness = lerp(1, 0.55, Math.sin(u * Math.PI));
|
|
431
|
+
eyeOffX = sign * params.interceptEyePeekX * 0.55;
|
|
432
|
+
} else if (tHalf < params.T_SWALLOW_END + 0.24) {
|
|
433
|
+
// 完全融入后才 smile + 弹.
|
|
434
|
+
eyeMode = 'smile';
|
|
435
|
+
eyeOffX = sign * params.interceptEyePeekX * 0.35;
|
|
436
|
+
} else if (tHalf < params.T_RETURN_END) {
|
|
437
|
+
// 飘回时眼睛 capsule + offset 回中
|
|
438
|
+
const u = progress(tHalf, params.T_SWALLOW_END + 0.24, params.T_RETURN_END);
|
|
439
|
+
eyeMode = 'capsule';
|
|
440
|
+
eyeOffX = lerp(sign * params.interceptEyePeekX * 0.35, 0, easeOut(u));
|
|
441
|
+
}
|
|
442
|
+
f.eyeMode = eyeMode;
|
|
443
|
+
f.eyeOpenness = eyeOpenness;
|
|
444
|
+
f.eyeOffX = eyeOffX;
|
|
445
|
+
f.eyeOffY = eyeOffY;
|
|
446
|
+
|
|
447
|
+
// ---- 接触 deform pressure ----
|
|
448
|
+
// 贴边前开始外鼓, 同化时保持包裹, 融完后释放.
|
|
449
|
+
let pressure = 0;
|
|
450
|
+
const tPressureStart = params.T_CONTACT - 0.20;
|
|
451
|
+
const tPressurePeak = params.T_CONTACT + 0.08;
|
|
452
|
+
const tPressureReleaseStart = params.T_SWALLOW_END - 0.05;
|
|
453
|
+
const tPressureReleaseEnd = params.T_SWALLOW_END + 0.16;
|
|
454
|
+
if (tHalf >= tPressureStart && tHalf < params.T_CONTACT) {
|
|
455
|
+
pressure = lerp(0, 0.72, easeInOut(progress(tHalf, tPressureStart, params.T_CONTACT))) * params.contactPressureMax;
|
|
456
|
+
} else if (tHalf >= params.T_CONTACT && tHalf < tPressurePeak) {
|
|
457
|
+
pressure = lerp(0.72, 1, easeOut(progress(tHalf, params.T_CONTACT, tPressurePeak))) * params.contactPressureMax;
|
|
458
|
+
} else if (tHalf >= tPressurePeak && tHalf < tPressureReleaseStart) {
|
|
459
|
+
pressure = params.contactPressureMax;
|
|
460
|
+
} else if (tHalf >= tPressureReleaseStart && tHalf < tPressureReleaseEnd) {
|
|
461
|
+
pressure = params.contactPressureMax * (1 - easeInOut(progress(tHalf, tPressureReleaseStart, tPressureReleaseEnd)));
|
|
462
|
+
}
|
|
463
|
+
f.pressure = pressure;
|
|
464
|
+
f.contactPoint = { x: cpX, y: cpY };
|
|
465
|
+
|
|
466
|
+
// ---- duang spring scale (完全融入后才弹) ----
|
|
467
|
+
let springScale = 1;
|
|
468
|
+
const springStart = params.T_SWALLOW_END + 0.03;
|
|
469
|
+
if (tHalf >= springStart && tHalf < springStart + params.springDuration) {
|
|
470
|
+
const tLocal = tHalf - springStart;
|
|
471
|
+
springScale = 1 + springOvershoot(tLocal, params.springAmp, params.springFreq, params.springDamping);
|
|
472
|
+
}
|
|
473
|
+
f.springScale = springScale;
|
|
474
|
+
|
|
475
|
+
// ---- 呼吸 (持续, 周期 = T_CYCLE / 2 = 2.25s; 每口对齐一个呼吸) ----
|
|
476
|
+
const breathPeriod = params.T_CYCLE / 2;
|
|
477
|
+
const breathPhase = (Math.cos(2 * Math.PI * tHalf / breathPeriod) + 1) / 2;
|
|
478
|
+
f.breathScale = lerp(params.breathLow, params.breathHigh, breathPhase);
|
|
479
|
+
|
|
480
|
+
return f;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* =========================================================================
|
|
484
|
+
* 主帧计算: 把全局 t 切成两个 half cycle
|
|
485
|
+
* ========================================================================= */
|
|
486
|
+
function computeFrame(t) {
|
|
487
|
+
const T = params.T_CYCLE;
|
|
488
|
+
const tMod = ((t % T) + T) % T;
|
|
489
|
+
const half = T / 2;
|
|
490
|
+
let tHalf, side;
|
|
491
|
+
if (tMod < half) {
|
|
492
|
+
tHalf = tMod;
|
|
493
|
+
side = 'right';
|
|
494
|
+
} else {
|
|
495
|
+
tHalf = tMod - half;
|
|
496
|
+
side = 'left';
|
|
497
|
+
}
|
|
498
|
+
const f = computeHalfCycle(tHalf, side);
|
|
499
|
+
f.tMod = tMod;
|
|
500
|
+
f.tHalf = tHalf;
|
|
501
|
+
f.side = side;
|
|
502
|
+
return f;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* =========================================================================
|
|
506
|
+
* 渲染
|
|
507
|
+
* ========================================================================= */
|
|
508
|
+
const stage = document.getElementById('stage');
|
|
509
|
+
|
|
510
|
+
function renderFrame(t) {
|
|
511
|
+
const f = computeFrame(t);
|
|
512
|
+
const p = 'crr';
|
|
513
|
+
|
|
514
|
+
const cloudD = makeSoftCloudPath(f.pressure, f.contactPoint);
|
|
515
|
+
const cloudlingBody = makeCloudlingBody(p);
|
|
516
|
+
const eyes = makeEyes(p, f.eyeMode, f.eyeOpenness, f.eyeOffX, f.eyeOffY);
|
|
517
|
+
|
|
518
|
+
// food cloud: 接触前在背景层, 接触后压到主体表面层.
|
|
519
|
+
const foodBehindHTML = f.foodVisible && f.foodLayer === 'behind'
|
|
520
|
+
? makeFoodCloud(p, f.foodCx, f.foodCy, f.foodScale, f.foodOpacity)
|
|
521
|
+
: '';
|
|
522
|
+
const foodSurfaceHTML = f.foodVisible && f.foodLayer === 'front'
|
|
523
|
+
? makeFoodCloud(p, f.foodLocalCx, f.foodLocalCy, f.foodScale, f.foodOpacity, {
|
|
524
|
+
sx: f.foodSx,
|
|
525
|
+
sy: f.foodSy,
|
|
526
|
+
rot: f.foodRot,
|
|
527
|
+
melting: true,
|
|
528
|
+
})
|
|
529
|
+
: '';
|
|
530
|
+
const absorbWashHTML = f.absorbWashOpacity > 0.01
|
|
531
|
+
? makeAbsorbWash(p, f.foodLocalCx, f.foodLocalCy, f.foodScale, f.absorbWashOpacity, {
|
|
532
|
+
sx: f.foodSx,
|
|
533
|
+
sy: f.foodSy,
|
|
534
|
+
rot: f.foodRot,
|
|
535
|
+
})
|
|
536
|
+
: '';
|
|
537
|
+
const absorbRimHTML = f.pressure > 0.02
|
|
538
|
+
? makeAbsorbFrontRim(p, Math.min(0.34, (f.pressure / params.contactPressureMax) * 0.30))
|
|
539
|
+
: '';
|
|
540
|
+
|
|
541
|
+
stage.innerHTML = `
|
|
542
|
+
<style>
|
|
543
|
+
.edge-rim-tight { stroke-opacity: .65; }
|
|
544
|
+
.edge-rim-soft { stroke-opacity: .50; }
|
|
545
|
+
.edge-contact { stroke-opacity: .65; }
|
|
546
|
+
@media (prefers-color-scheme: light) {
|
|
547
|
+
.edge-rim-tight { stroke-opacity: .75; }
|
|
548
|
+
.edge-rim-soft { stroke-opacity: .60; }
|
|
549
|
+
.edge-contact { stroke-opacity: .75; }
|
|
550
|
+
.cloud-drop-shadow { flood-opacity: .13; }
|
|
551
|
+
}
|
|
552
|
+
</style>
|
|
553
|
+
<defs>
|
|
554
|
+
${commonDefs(p, cloudD)}
|
|
555
|
+
<path id="${p}-cloud-shape" d="${cloudD}"/>
|
|
556
|
+
</defs>
|
|
557
|
+
<!-- 食物云 (背景层) -->
|
|
558
|
+
${foodBehindHTML}
|
|
559
|
+
<!-- cloudling: translate → spring → breath → (body roll) -->
|
|
560
|
+
<g class="scene" transform="translate(${f.cloudTx.toFixed(3)} ${f.cloudTy.toFixed(3)})">
|
|
561
|
+
<g class="spring" transform="translate(12 12) scale(${f.springScale.toFixed(4)}) translate(-12 -12)">
|
|
562
|
+
<g class="breath" transform="translate(12 12) scale(${f.breathScale.toFixed(4)}) translate(-12 -12)">
|
|
563
|
+
<g class="body-roll" transform="rotate(${f.bodyRoll.toFixed(3)} 12 12)">
|
|
564
|
+
${cloudlingBody}
|
|
565
|
+
${absorbWashHTML}
|
|
566
|
+
${foodSurfaceHTML}
|
|
567
|
+
${absorbRimHTML}
|
|
568
|
+
</g>
|
|
569
|
+
${eyes}
|
|
570
|
+
</g>
|
|
571
|
+
</g>
|
|
572
|
+
</g>
|
|
573
|
+
`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* =========================================================================
|
|
577
|
+
* 主循环 + 控件
|
|
578
|
+
* ========================================================================= */
|
|
579
|
+
let startT = performance.now();
|
|
580
|
+
let elapsedOffset = 0;
|
|
581
|
+
let pausedAt = 0;
|
|
582
|
+
|
|
583
|
+
const scrubberEl = document.getElementById('scrubber');
|
|
584
|
+
const tDisplayEl = document.getElementById('t-display');
|
|
585
|
+
|
|
586
|
+
function getCurrentT() {
|
|
587
|
+
if (scrubbing) return Number(scrubberEl.value);
|
|
588
|
+
if (paused) return elapsedOffset + (pausedAt - startT) / 1000;
|
|
589
|
+
return elapsedOffset + (performance.now() - startT) / 1000;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function loop() {
|
|
593
|
+
const t = getCurrentT();
|
|
594
|
+
scrubberEl.max = params.T_CYCLE.toFixed(2);
|
|
595
|
+
const tMod = ((t % params.T_CYCLE) + params.T_CYCLE) % params.T_CYCLE;
|
|
596
|
+
if (!scrubbing) scrubberEl.value = tMod.toFixed(3);
|
|
597
|
+
tDisplayEl.textContent = `${tMod.toFixed(2)} s`;
|
|
598
|
+
renderFrame(t);
|
|
599
|
+
requestAnimationFrame(loop);
|
|
600
|
+
}
|
|
601
|
+
renderFrame(0);
|
|
602
|
+
requestAnimationFrame(loop);
|
|
603
|
+
]]></script>
|
|
604
|
+
</svg>
|