Ethscriptions commited on
Commit
c19dd43
·
1 Parent(s): 5c50084

taurus: 画面引擎、十幕动画、合成器(step 3/4)

Browse files
taurus/js/acts.js ADDED
@@ -0,0 +1,685 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ♉ 金牛座 —— 十幕(全新编写)。每幕:(g, u, W, H, X),u = 本幕内时间;X = { S, P, at(n), dur, opts }
2
+ import { TAU, clamp, lerp, seg, ease, env, h1, col, C, FONT, gilt, halo, sparkle, dust, text, reveal, rrect, strokePart, bez, arcPts,
3
+ flower, leaf, branch, petals, ornamentFrame, divider, bullSide, bullHead, taurusSign, venusSign, sky } from './canvas-kit.js';
4
+ import { drawCard } from './card.js';
5
+
6
+ /* ---------- 星表(赤经 h,赤纬 °,星等) ---------- */
7
+ const STAR = {
8
+ ald: [4.599, 16.51, .86], elnath: [5.438, 28.61, 1.65], zeta: [5.627, 21.14, 3.0], gam: [4.330, 15.63, 3.65], del: [4.382, 17.54, 3.76], eps: [4.477, 19.18, 3.53], th2: [4.478, 15.87, 3.4],
9
+ lam: [4.011, 12.49, 3.4], omi: [3.414, 9.03, 3.6], xi: [3.453, 9.73, 3.7],
10
+ bet: [5.919, 7.41, .5], rig: [5.242, -8.20, .13], bel: [5.418, 6.35, 1.64], sai: [5.796, -9.67, 2.06], alk: [5.679, -1.94, 1.77], aln: [5.604, -1.20, 1.69], min: [5.533, -.30, 2.23], mei: [5.585, 9.93, 3.4],
11
+ };
12
+ const PLEI = [[3.791, 24.105, 2.9], [3.819, 24.053, 3.6], [3.748, 24.113, 3.7], [3.763, 24.367, 3.9], [3.774, 23.948, 4.2], [3.747, 23.946, 4.3], [3.814, 24.137, 5.0]];
13
+ const ORI_L = [['bet', 'bel'], ['bel', 'min'], ['min', 'aln'], ['aln', 'alk'], ['alk', 'bet'], ['min', 'rig'], ['alk', 'sai'], ['rig', 'sai'], ['bet', 'mei'], ['bel', 'mei']];
14
+ const HYA_L = [['gam', 'del'], ['del', 'eps'], ['gam', 'th2'], ['th2', 'ald']];
15
+ const HORN_L = [['eps', 'elnath'], ['ald', 'zeta']];
16
+ const BODY_L = [['gam', 'lam'], ['lam', 'omi'], ['lam', 'xi']];
17
+ const FIELD = Array.from({ length: 360 }, (_, i) => [2.8 + h1(i * 1.1) * 3.6, -14 + h1(i * 2.3) * 48, 4.4 + h1(i * 3.7) * 1.8, h1(i * 5.9) * TAU]);
18
+ const rad = (m, S) => (1.2 + (4.6 - m) * 1.15) * S;
19
+
20
+ function mapper(W, H, ra0, dec0, ppd, ox = 0, oy = 0) { return s => { const [x, y] = sky(s[0], s[1], ra0, dec0); return [W / 2 + ox + x * ppd, H / 2 + oy + y * ppd]; }; }
21
+ function lines(g, L, M, k, color, w) {
22
+ L.forEach(([a, b], i) => { const q = ease.io(clamp(k * L.length - i)); if (q <= 0) return; const p = M(STAR[a]), r = M(STAR[b]);
23
+ strokePart(g, [p, r], q, { color, width: w, glow: color, blur: 8 }); });
24
+ }
25
+ function label(g, x, y, t1, t2, a, S, o = {}) {
26
+ if (a <= 0) return; const side = o.side ?? 1, al = side > 0 ? 'left' : 'right';
27
+ const ex = x + side * 46 * S, ey = y - 46 * S;
28
+ g.save(); g.globalAlpha *= a; g.strokeStyle = col(C.gold, .75); g.lineWidth = 1.4 * S; g.beginPath(); g.moveTo(x + side * 12 * S, y - 12 * S); g.lineTo(ex, ey); g.lineTo(ex + side * 26 * S, ey); g.stroke(); g.restore();
29
+ text(g, t1, ex + side * 36 * S, ey - (t2 ? 14 * S : 0), { size: (o.size ?? 36) * S, align: al, gilt: o.gilt ?? 'gold', alpha: a, glow: 'rgba(0,0,0,.8)', blur: 10 * S, weight: 900 });
30
+ if (t2) text(g, t2, ex + side * 36 * S, ey + 22 * S, { size: 20 * S, align: al, color: col(C.ivory, .85), alpha: a, font: FONT.sans, weight: 400, glow: 'rgba(0,0,0,.8)', blur: 6 * S });
31
+ }
32
+ function ring(g, x, y, u, S, c = C.gold) { for (let i = 0; i < 3; i++) { const k = (u - i * .25) / 1.4; if (k <= 0 || k >= 1) continue; g.save(); g.strokeStyle = col(c, (1 - k) * .8); g.lineWidth = 2 * S; g.beginPath(); g.arc(x, y, (8 + 90 * ease.out(k)) * S, 0, TAU); g.stroke(); g.restore(); } }
33
+ // 圆角玻璃标题卡
34
+ function plate(g, cx, cy, w, h, a, S, o = {}) {
35
+ if (a <= 0) return; g.save(); g.globalAlpha *= a; rrect(g, cx - w / 2, cy - h / 2, w, h, (o.r ?? 18) * S);
36
+ const gr = g.createLinearGradient(0, cy - h / 2, 0, cy + h / 2); gr.addColorStop(0, o.top ?? 'rgba(14,34,28,.82)'); gr.addColorStop(1, o.bot ?? 'rgba(20,10,20,.82)'); g.fillStyle = gr; g.fill(); g.restore();
37
+ ornamentFrame(g, cx - w / 2, cy - h / 2, w, h, { S: S * (o.orn ?? .7), alpha: a, k: o.k ?? 1, metal: o.metal ?? 'gold' });
38
+ }
39
+ function hills(g, W, H, y0, S, color, amp = 1, ph = 0) {
40
+ g.fillStyle = color; g.beginPath(); g.moveTo(0, H);
41
+ for (let x = 0; x <= W + 20; x += 16) g.lineTo(x, y0 + (Math.sin(x / W * 4 + ph) * 26 + Math.sin(x / W * 11 + ph * 2) * 9) * S * amp);
42
+ g.lineTo(W, H); g.closePath(); g.fill();
43
+ }
44
+ function grass(g, W, y0, u, S, n = 140, c = 'rgba(30,60,40,.95)') {
45
+ g.strokeStyle = c; g.lineCap = 'round';
46
+ for (let i = 0; i < n; i++) { const x = W * h1(i * 1.7), h = (24 + h1(i * 2.1) * 40) * S, sw = Math.sin(u * 1.4 + i) * 6 * S;
47
+ g.lineWidth = (2 + h1(i) * 2) * S; g.beginPath(); g.moveTo(x, y0 + 30 * S); g.quadraticCurveTo(x + sw * .5, y0 + 30 * S - h * .5, x + sw, y0 + 30 * S - h); g.stroke(); }
48
+ }
49
+ const corners = (W, H, S, P) => {
50
+ const m = 1;
51
+ return [
52
+ bez([W * (P ? .42 : .3), -10 * S], [W * .14, H * .1], [W * .05, H * .2], [-20 * S, H * .42]),
53
+ bez([W * (P ? .58 : .7), -10 * S], [W * .86, H * .1], [W * .95, H * .2], [W + 20 * S, H * .42]),
54
+ bez([-20 * S, H * .72], [W * .1, H * .78], [W * .12, H * .95], [W * (P ? .36 : .22), H + 10 * S]),
55
+ bez([W + 20 * S, H * .7], [W * .9, H * .8], [W * .9, H * .95], [W * (P ? .66 : .8), H + 10 * S]),
56
+ ].map(p => p.map(([x, y]) => [x, y * m]));
57
+ };
58
+
59
+ export const ACTS = {};
60
+
61
+ /* ============ 1 序章 · 繁花里的公牛 ============ */
62
+ ACTS.bloom = (g, u, W, H, X) => {
63
+ const { S, P, at } = X;
64
+ const gy = H * (P ? .76 : .8);
65
+ // 地平线与晨晖
66
+ halo(g, W / 2, gy, Math.max(W, H) * .6, [255, 190, 160], .35 + .25 * seg(u, at(1), at(4)));
67
+ hills(g, W, H, gy - 50 * S, S, 'rgba(36,52,48,.75)', 1.2, 1);
68
+ hills(g, W, H, gy, S, '#0f1d18', 1, 2.4);
69
+ grass(g, W, gy, u, S, P ? 90 : 160);
70
+ // 四角花枝生长
71
+ const vines = corners(W, H, S, P);
72
+ vines.forEach((pts, i) => branch(g, pts, ease.io(seg(u, at(1) - .4 + i * .5, at(4) + i * .4)), { S: S * (P ? .9 : 1), width: 5, flowers: [[.45, 48, i], [.78, 60, i + 2], [.98, 40, i + 4]],
73
+ inner: i % 2 ? [214, 92, 120] : [228, 140, 110], outer: i % 2 ? [255, 214, 222] : [255, 232, 205] }));
74
+ petals(g, u, W, H, { n: P ? 30 : 46, alpha: seg(u, at(2), at(3) + 1), wind: .1 });
75
+ // 公牛缓步走来,蹄下开花
76
+ const wk = ease.io(seg(u, at(5) - .6, at(8) + .8));
77
+ const bs = (P ? 1.15 : 1.35) * S, bx = lerp(-W * .3, W * (P ? .46 : .47), wk), by = gy - 118 * bs;
78
+ const walking = wk > 0 && wk < 1 ? 1 : 0;
79
+ if (wk > 0) {
80
+ for (let i = 0; i < 9; i++) { // 足迹花
81
+ const q = (i + 1) / 10; if (wk < q) break; const fx = lerp(-W * .3, W * .47, q) + 40 * S, age = (wk - q) * 12;
82
+ flower(g, fx, gy + 8 * S, 14 * S, clamp(age), { layers: 2, petals: 6, rot: i, inner: [240, 160, 170], outer: [255, 240, 230] });
83
+ }
84
+ halo(g, bx, by, 300 * S, [255, 210, 150], .5);
85
+ bullSide(g, bx, by, bs, { t: u, walk: walking * (1 - seg(wk, .9, 1)) });
86
+ }
87
+ // 标题
88
+ const tk = seg(u, at(9) - .2, at(9) + 1.6);
89
+ const ty = P ? H * .2 : H * .23;
90
+ if (tk > 0) {
91
+ halo(g, W / 2, ty, 420 * S, [255, 200, 140], .45 * tk);
92
+ reveal(g, '金牛座', W / 2, ty, tk, { size: (P ? 150 : 140) * S, gilt: 'gold', weight: 900, spacing: 26 * S, glow: 'rgba(255,170,120,.6)', blur: 30 * S });
93
+ text(g, 'T A U R U S', W / 2, ty + (P ? 102 : 96) * S, { size: 40 * S, font: FONT.latin, weight: 700, color: col(C.gold), alpha: seg(u, at(9) + .8, at(9) + 1.8), spacing: 6 * S });
94
+ divider(g, W / 2, ty + (P ? 140 : 132) * S, 420 * S, seg(u, at(9) + 1, at(9) + 2), S);
95
+ // No.2 徽章
96
+ const nk = ease.back(seg(u, at(9) + 1.2, at(9) + 2));
97
+ if (nk > 0) {
98
+ const nx = W / 2 - (P ? 170 : 190) * S, ny = ty + 215 * S;
99
+ g.save(); g.translate(nx, ny); g.scale(nk, nk); g.fillStyle = '#0c2c23'; g.beginPath(); g.arc(0, 0, 54 * S, 0, TAU); g.fill();
100
+ g.lineWidth = 4 * S; g.strokeStyle = gilt(g, -54 * S, -54 * S, 54 * S, 54 * S, 'rose'); g.stroke(); g.restore();
101
+ text(g, 'No.2', nx, ny - 4 * S, { size: 30 * S * nk, font: FONT.latin, weight: 700, gilt: 'gold' });
102
+ text(g, '黄道第二宫', nx, ny + 74 * S, { size: 22 * S, color: col(C.ivory), alpha: clamp(nk), font: FONT.sans, weight: 600 });
103
+ }
104
+ }
105
+ // 日期花签
106
+ const dk = seg(u, at(10) - .2, at(10) + .9);
107
+ if (dk > 0) {
108
+ const dx = W / 2 + (P ? 90 : 100) * S, dy = ty + 215 * S;
109
+ plate(g, dx, dy, 300 * S * ease.out(dk), 84 * S, dk, S, { r: 42, orn: .45, metal: 'rose' });
110
+ text(g, '4.20 — 5.20', dx, dy + 2 * S, { size: 38 * S, font: FONT.latin, weight: 700, gilt: 'gold', alpha: seg(dk, .4, 1), spacing: 2 * S });
111
+ }
112
+ dust(g, u, W, H, { n: 30, alpha: .7 });
113
+ };
114
+
115
+ /* ============ 2 最古老的星座 ============ */
116
+ ACTS.ancient = (g, u, W, H, X) => {
117
+ const { S, P, at } = X;
118
+ const phA = 1 - seg(u, at(5) - .6, at(5) + .2), phB = env(u, at(5) - .4, at(7) - .7, .7, .6), phC = seg(u, at(7) - .5, at(7) + .4);
119
+ // 标题卡
120
+ const hk = env(u, .2, at(1) - .3, .7, .5);
121
+ if (hk > 0) { const y = P ? H * .4 : H * .42; plate(g, W / 2, y, (P ? 900 : 1000) * S, 200 * S, hk, S);
122
+ text(g, '最古老的星座之一', W / 2, y - 24 * S, { size: (P ? 64 : 70) * S, gilt: 'gold', weight: 900, alpha: hk, spacing: 8 * S });
123
+ text(g, 'ONE OF THE OLDEST CONSTELLATIONS', W / 2, y + 48 * S, { size: 22 * S, font: FONT.latin, weight: 700, color: col(C.rose), alpha: hk, spacing: 5 * S }); }
124
+ // A:五千年前的春分
125
+ const ak = phA * seg(u, at(1) - .4, at(1) + .4);
126
+ if (ak > 0) {
127
+ const yrK = ease.io(seg(u, at(1) - .3, at(2) + .6)), yr = Math.round(lerp(2026, -3000, yrK));
128
+ const cy = P ? H * .2 : H * .2;
129
+ text(g, yr > 0 ? `公元 ${yr} 年` : `公元前 ${-yr} 年`, W / 2, cy, { size: 64 * S, gilt: 'gold', weight: 900, alpha: ak, font: FONT.serif });
130
+ // 时间尺
131
+ const rw = (P ? W * .86 : W * .7), rx = W / 2 - rw / 2, ry = cy + 70 * S;
132
+ g.save(); g.globalAlpha *= ak; g.strokeStyle = col(C.gold, .6); g.lineWidth = 1.5 * S; g.beginPath(); g.moveTo(rx, ry); g.lineTo(rx + rw, ry); g.stroke();
133
+ for (let i = 0; i <= 40; i++) { const x = rx + ((i / 40 + yrK * 3) % 1) * rw; g.beginPath(); g.moveTo(x, ry); g.lineTo(x, ry - (i % 5 ? 8 : 16) * S); g.stroke(); }
134
+ g.restore();
135
+ // 田野、太阳进入金牛
136
+ const fy = P ? H * .7 : H * .74;
137
+ hills(g, W, H, fy, S, 'rgba(60,40,24,.9)', .6, .5);
138
+ for (let r = 0; r < 6; r++) for (let i = 0; i < 26; i++) { const k = seg(u, at(4) - .4 + i * .04, at(4) + .4 + i * .04); if (k <= 0) continue; const x = W * (i + .5) / 26 + (r % 2) * 10 * S, y = fy + 30 * S + r * 26 * S;
139
+ g.save(); g.globalAlpha *= ak * k; g.strokeStyle = '#8fd18a'; g.lineWidth = 3 * S; g.beginPath(); g.moveTo(x, y); g.lineTo(x, y - 30 * S * k); g.stroke(); leaf(g, x, y - 22 * S * k, 20 * S, -2.3, k, [90, 170, 90]); leaf(g, x, y - 26 * S * k, 20 * S, -.8, k, [90, 170, 90]); g.restore(); }
140
+ const sk = ease.io(seg(u, at(2) - .2, at(3) + 1.2));
141
+ const cx = W / 2 + (P ? 0 : W * .02), cyy = P ? H * .44 : H * .47, sc = (P ? 1.0 : 1.1) * S;
142
+ const pts = [[-130, 30], [-70, 10], [-20, 18], [30, -10], [90, -60], [120, -20], [60, 40], [-40, -30]];
143
+ const ln = [[0, 1], [1, 2], [2, 3], [3, 4], [3, 5], [2, 6], [1, 7]];
144
+ g.save(); g.globalAlpha *= ak; g.strokeStyle = col(C.gold, .6); g.lineWidth = 1.6 * S; ln.forEach(([a, b]) => { g.beginPath(); g.moveTo(cx + pts[a][0] * sc, cyy + pts[a][1] * sc); g.lineTo(cx + pts[b][0] * sc, cyy + pts[b][1] * sc); g.stroke(); }); g.restore();
145
+ pts.forEach(([x, y], i) => sparkle(g, cx + x * sc, cyy + y * sc, (i === 2 ? 3.5 : 2.2) * S, C.ivory, ak, i === 2 ? .8 : .3));
146
+ text(g, '金牛座', cx, cyy - 110 * S, { size: 34 * S, gilt: 'gold', alpha: ak * seg(u, at(2), at(2) + 1) });
147
+ const sx = lerp(cx - W * .45, cx - 10 * S, sk), sy = cyy + 10 * S - Math.sin(sk * Math.PI) * 60 * S;
148
+ halo(g, sx, sy, 160 * S, [255, 200, 120], ak); sparkle(g, sx, sy, 9 * S, [255, 230, 170], ak, 1.4);
149
+ const tk = seg(u, at(3) + .4, at(3) + 1.2); if (tk > 0) text(g, '春分 · 太阳落在金牛座', cx, cyy + 120 * S, { size: 34 * S, color: col(C.ivory), alpha: ak * tk, weight: 700, glow: 'rgba(0,0,0,.8)' });
150
+ const pk = seg(u, at(4) - .1, at(4) + .6); if (pk > 0) text(g, '— 播种的季节 —', W / 2, fy - 40 * S, { size: 30 * S, color: '#cfe8c0', alpha: ak * pk, weight: 700, glow: 'rgba(0,0,0,.8)' });
151
+ }
152
+ // B:美索不达米亚泥板
153
+ if (phB > 0) {
154
+ const cx = W / 2, cy = P ? H * .4 : H * .44, tw = (P ? 560 : 520) * S, th = tw * .78;
155
+ g.save(); g.globalAlpha *= phB; g.translate(cx, cy); g.rotate(-.04);
156
+ g.shadowColor = 'rgba(0,0,0,.6)'; g.shadowBlur = 40 * S; rrect(g, -tw / 2, -th / 2, tw, th, 40 * S);
157
+ const cg = g.createLinearGradient(-tw / 2, -th / 2, tw / 2, th / 2); cg.addColorStop(0, '#c8a070'); cg.addColorStop(1, '#8a6038'); g.fillStyle = cg; g.fill(); g.shadowBlur = 0;
158
+ g.strokeStyle = 'rgba(80,50,20,.6)'; g.lineWidth = 2 * S;
159
+ for (let r = 0; r < 4; r++) for (let i = 0; i < 14; i++) { if (h1(r * 20 + i) < .25) continue; const x = -tw * .42 + i * tw * .06, y = th * .1 + r * th * .1; g.beginPath(); g.moveTo(x, y); g.lineTo(x + 10 * S, y + (h1(i + r) > .5 ? 10 : -6) * S); g.stroke(); g.fillStyle = 'rgba(80,50,20,.55)'; g.beginPath(); g.moveTo(x - 3 * S, y - 3 * S); g.lineTo(x + 5 * S, y); g.lineTo(x - 3 * S, y + 3 * S); g.fill(); }
160
+ g.restore();
161
+ // 浮雕公牛
162
+ bullSide(g, cx - 10 * S, cy - th * .2, .62 * S, { t: 0, alpha: phB * seg(u, at(5), at(5) + 1), metal: 'pale' });
163
+ text(g, '天上的公牛', cx, cy + th / 2 + 60 * S, { size: 52 * S, gilt: 'gold', weight: 900, alpha: phB * seg(u, at(6), at(6) + .8), spacing: 10 * S });
164
+ text(g, 'GU₄.AN.NA · 美索不达米亚', cx, cy + th / 2 + 112 * S, { size: 24 * S, font: FONT.latin, weight: 700, color: col(C.rose), alpha: phB * seg(u, at(6) + .3, at(6) + 1.1), spacing: 3 * S });
165
+ }
166
+ // C:拉斯科洞穴壁画
167
+ if (phC > 0) {
168
+ g.save(); g.globalAlpha *= phC;
169
+ const rg = g.createRadialGradient(W / 2, H * .45, 0, W / 2, H * .45, Math.max(W, H) * .7); rg.addColorStop(0, '#7a5a3a'); rg.addColorStop(.6, '#3e2a1a'); rg.addColorStop(1, '#120a06'); g.fillStyle = rg; g.fillRect(0, 0, W, H);
170
+ for (let i = 0; i < 70; i++) { const x = W * h1(i * 3), y = H * h1(i * 5), r = (60 + h1(i * 7) * 220) * S; const c = h1(i) > .5 ? [20, 10, 5] : [190, 150, 100];
171
+ const rg2 = g.createRadialGradient(x, y, 0, x, y, r); rg2.addColorStop(0, col(c, .12 + h1(i + 3) * .1)); rg2.addColorStop(1, col(c, 0)); g.fillStyle = rg2; g.fillRect(x - r, y - r, r * 2, r * 2); }
172
+ g.strokeStyle = 'rgba(30,15,5,.18)'; g.lineWidth = 2 * S; for (let i = 0; i < 26; i++) { const x = W * h1(i * 11), y = H * h1(i * 13); g.beginPath(); g.moveTo(x, y); g.bezierCurveTo(x + 60 * S, y + 20 * S, x + 90 * S, y - 30 * S, x + 180 * S * h1(i), y + 40 * S); g.stroke(); }
173
+ g.restore();
174
+ // 赭石线描公牛
175
+ const ox = W / 2, oy = P ? H * .44 : H * .5, s = (P ? 1.4 : 1.6) * S;
176
+ const T = ([x, y]) => [ox + x * s, oy + y * s];
177
+ const body = [...bez([-150, 20], [-150, -40], [-80, -50], [-20, -52]), ...bez([-20, -52], [40, -56], [70, -86], [110, -70]), ...bez([110, -70], [140, -58], [170, -20], [176, 4]),
178
+ ...bez([176, 4], [178, 24], [140, 26], [120, 34]), ...bez([120, 34], [100, 60], [40, 66], [-40, 62]), ...bez([-40, 62], [-110, 60], [-150, 60], [-150, 20])].map(T);
179
+ const hornA = bez([140, -46], [140, -110], [190, -140], [230, -120]).map(T);
180
+ const hornB = bez([124, -56], [100, -120], [140, -160], [176, -152]).map(T);
181
+ const legs = [bez([-120, 50], [-128, 80], [-118, 100], [-126, 124]), bez([-80, 58], [-74, 84], [-66, 100], [-60, 124]), bez([56, 60], [52, 84], [46, 104], [50, 124]), bez([96, 52], [104, 80], [100, 100], [112, 120])].map(l => l.map(T));
182
+ const lk = seg(u, at(8) - .3, at(8) + 2.5);
183
+ const ochre = { color: 'rgba(150,50,20,.9)', width: 7 * S, glow: 'rgba(60,20,0,.5)', blur: 4 * S, alpha: phC };
184
+ strokePart(g, body, seg(lk, 0, .55), ochre); strokePart(g, hornA, seg(lk, .5, .7), ochre); strokePart(g, hornB, seg(lk, .55, .75), ochre);
185
+ legs.forEach((l, i) => strokePart(g, l, seg(lk, .7 + i * .06, .82 + i * .06), ochre));
186
+ g.save(); g.globalAlpha *= .35 * phC * seg(lk, .5, 1); g.fillStyle = 'rgba(120,40,15,1)'; g.beginPath(); body.forEach(([x, y], i) => i ? g.lineTo(x, y) : g.moveTo(x, y)); g.fill(); g.restore();
187
+ // 小点(昴星团?)
188
+ const dk = seg(u, at(9) - .2, at(9) + .8), glow = seg(u, at(11) - .3, at(11) + .6);
189
+ [[150, -200], [172, -214], [190, -196], [165, -184], [200, -222], [182, -232]].forEach(([x, y], i) => {
190
+ const px = ox + x * s, py = oy + y * s, q = seg(dk, i * .12, i * .12 + .3);
191
+ g.save(); g.globalAlpha *= phC * q; g.fillStyle = 'rgba(40,15,5,.9)'; g.beginPath(); g.arc(px, py, 6 * S, 0, TAU); g.fill(); g.restore();
192
+ if (glow > 0) sparkle(g, px, py, 4 * S, C.sky, glow * phC, .9);
193
+ });
194
+ if (glow > 0) { const [lx, ly] = [ox + 175 * s, oy - 210 * s]; label(g, lx + 20 * S, ly, '昴星团?', '有人这样认为', glow * phC, S, { side: P ? -1 : 1 }); }
195
+ text(g, '拉斯科洞穴壁画 · 约 17000 年前', W / 2, P ? H * .14 : H * .12, { size: 36 * S, gilt: 'gold', weight: 900, alpha: phC * seg(u, at(7), at(7) + .8), glow: 'rgba(0,0,0,.8)', blur: 12 * S });
196
+ }
197
+ };
198
+
199
+ /* ============ 3 找到它 ============ */
200
+ ACTS.locate = (g, u, W, H, X) => {
201
+ const { S, P, at } = X;
202
+ const ppd = P ? W * .92 / 30 : Math.min(W * .9 / 52, H * .82 / 42);
203
+ // 镜头:全景(猎户 + 金牛)→ 推近毕星团 → 拉远看牛角与昴星团
204
+ const z1 = ease.io(seg(u, at(5) + .2, at(6) + .4)), z2 = ease.io(seg(u, at(9) - .6, at(9) + .8));
205
+ const cRA = lerp(lerp(P ? 5.0 : 4.75, 4.43, z1), P ? 4.4 : 4.45, z2), cDec = lerp(lerp(P ? 8 : 9, 16.6, z1), P ? 19 : 18.5, z2);
206
+ const zoom = lerp(lerp(1, P ? 2.6 : 2.4, z1), P ? 1.25 : 1.35, z2);
207
+ const M = mapper(W, H, cRA, cDec, ppd * zoom, 0, P ? -H * .04 : 0);
208
+ const fa = seg(u, 0, 1.4);
209
+ g.save(); g.globalCompositeOperation = 'lighter';
210
+ for (const [ra, dec, m, ph] of FIELD) { const [x, y] = M([ra, dec]); g.fillStyle = col([235, 240, 255], fa * (.25 + .25 * Math.sin(u * 1.3 + ph))); g.beginPath(); g.arc(x, y, (6.4 - m) * .7 * S, 0, TAU); g.fill(); }
211
+ g.restore();
212
+ // 猎户座
213
+ const ok = seg(u, at(2) - .4, at(2) + 1.6), dimO = 1 - .55 * seg(u, at(5), at(6));
214
+ lines(g, ORI_L, M, ok, col(C.sky, .5 * dimO), 1.6 * S);
215
+ for (const k of ['bet', 'rig', 'bel', 'sai', 'mei']) { const [x, y] = M(STAR[k]); sparkle(g, x, y, rad(STAR[k][2], S) * .7, k === 'bet' ? [255, 190, 150] : C.sky, ok * dimO, .5); }
216
+ const beltK = seg(u, at(2) + .6, at(2) + 1.6);
217
+ for (const k of ['alk', 'aln', 'min']) { const [x, y] = M(STAR[k]); sparkle(g, x, y, rad(STAR[k][2], S) * .8, C.sky, Math.max(ok * .7, beltK) * dimO, .8 + beltK * .6); }
218
+ if (beltK > 0) { const [x, y] = M(STAR.aln); label(g, x, y + 10 * S, '猎户座腰带', '三颗星排成一线', beltK * (1 - seg(u, at(4), at(4) + 1)), S, { side: -1, gilt: 'pale' }); }
219
+ { const [x, y] = M([5.6, 3]); text(g, '猎户座', x - 120 * S, y + 60 * S, { size: 30 * S, color: col(C.sky, .85), alpha: ok * dimO * .9, weight: 700 }); }
220
+ // 延长线 → 毕宿五
221
+ const ek = ease.io(seg(u, at(3) - .1, at(3) + 1.8));
222
+ if (ek > 0) {
223
+ const a = M(STAR.alk), b = M(STAR.ald); const pts = []; for (let i = 0; i <= 40; i++) pts.push([lerp(a[0], b[0], i / 40), lerp(a[1], b[1], i / 40)]);
224
+ g.save(); g.setLineDash([10 * S, 8 * S]); strokePart(g, pts, ek, { color: col(C.gold, .85 * (1 - seg(u, at(6), at(7)) * .7)), width: 2.2 * S }); g.restore();
225
+ const tip = pts[Math.floor(ek * 40)];
226
+ if (ek < 1) halo(g, tip[0], tip[1], 30 * S, C.gold, 1);
227
+ }
228
+ // 毕宿五
229
+ const ak = seg(u, at(4) - .2, at(4) + .6);
230
+ if (ak > 0) { const [x, y] = M(STAR.ald); halo(g, x, y, 90 * S, [255, 140, 80], ak); sparkle(g, x, y, 7 * S, [255, 170, 110], ak, 1.6); ring(g, x, y, u - at(4), S, [255, 170, 110]);
231
+ label(g, x, y, '毕宿五', '公牛之眼 · Aldebaran', seg(u, at(5) - .2, at(5) + .6) * (1 - .6 * seg(u, at(7) - .4, at(7))), S, { side: 1, size: 40 }); }
232
+ // 毕星团 V 字
233
+ const vk = seg(u, at(6) - .2, at(7) + .8);
234
+ lines(g, HYA_L, M, vk, col(C.gold, .9), 2.6 * S);
235
+ for (const k of ['gam', 'del', 'eps', 'th2']) { const [x, y] = M(STAR[k]); sparkle(g, x, y, rad(STAR[k][2], S) * .8, C.ivory, vk, .5); }
236
+ if (vk > 0) { const [x, y] = M(STAR.gam); label(g, x - 10 * S, y + 70 * S, '毕星团', 'V 字形 · 公牛的脸', seg(u, at(7) - .2, at(7) + .6) * (1 - seg(u, at(9) - .6, at(9))), S, { side: -1 }); }
237
+ // 牛角
238
+ const hk = seg(u, at(9) - .2, at(10) + .8);
239
+ lines(g, HORN_L, M, hk, col(C.gold, .9), 2.6 * S);
240
+ lines(g, BODY_L, M, seg(u, at(10), at(11)), col(C.gold, .55), 1.8 * S);
241
+ for (const k of ['elnath', 'zeta', 'lam', 'omi', 'xi']) { const [x, y] = M(STAR[k]); sparkle(g, x, y, rad(STAR[k][2], S) * .8, C.ivory, Math.max(hk, ok * .4), .5); }
242
+ if (hk > 0) { const [x, y] = M(STAR.elnath); label(g, x, y, '牛角', '五车五 · 天关', seg(u, at(10) - .2, at(10) + .6), S, { side: 1 }); }
243
+ // 昴星团
244
+ const pk = seg(u, at(11) - .2, at(11) + .8);
245
+ if (pk > 0) { const c = M([3.78, 24.1]); halo(g, c[0], c[1], 80 * S, [140, 180, 255], pk * .8);
246
+ PLEI.forEach((s, i) => { const [x, y] = M(s); sparkle(g, x, y, (5.4 - s[2]) * 1.1 * S, C.sky, pk * (.7 + .3 * Math.sin(u * 4 + i * 2)), .6); });
247
+ label(g, c[0], c[1], '昴星团', '七姐妹 · M45', pk, S, { side: -1, gilt: 'pale' }); }
248
+ // 金色公牛线稿叠影
249
+ const gk = seg(u, at(11) + 1, at(11) + 2.6);
250
+ void gk;
251
+ text(g, '金牛座', W / 2, P ? H * .1 : H * .1, { size: 48 * S, gilt: 'gold', weight: 900, alpha: seg(u, at(0), at(0) + 1) * (1 - seg(u, at(2), at(2) + .8)), spacing: 12 * S });
252
+ };
253
+
254
+ /* ============ 4 毕宿五与昴星团 ============ */
255
+ ACTS.stars = (g, u, W, H, X) => {
256
+ const { S, P, at } = X;
257
+ const B = ease.io(seg(u, at(7) - .7, at(7) + .3)), A = 1 - B;
258
+ if (A > 0) {
259
+ const sx = P ? W / 2 : W * .3, sy = P ? H * .33 : H * .46, R = (P ? 230 : 250) * S * ease.out(seg(u, 0, 1.6));
260
+ g.save(); g.globalAlpha *= A;
261
+ halo(g, sx, sy, R * 3, [255, 110, 60], .9);
262
+ const sg = g.createRadialGradient(sx - R * .3, sy - R * .3, R * .05, sx, sy, R); sg.addColorStop(0, '#ffe2b8'); sg.addColorStop(.4, '#ff8a4a'); sg.addColorStop(.85, '#c23a1c'); sg.addColorStop(1, '#6a140a');
263
+ g.fillStyle = sg; g.beginPath(); g.arc(sx, sy, Math.max(1, R), 0, TAU); g.fill();
264
+ g.save(); g.beginPath(); g.arc(sx, sy, Math.max(1, R), 0, TAU); g.clip();
265
+ for (let i = 0; i < 120; i++) { const a = h1(i) * TAU + u * .03, d = Math.sqrt(h1(i + 2)) * R; g.fillStyle = i % 2 ? 'rgba(255,230,180,.045)' : 'rgba(120,20,0,.06)'; g.beginPath(); g.arc(sx + Math.cos(a) * d, sy + Math.sin(a) * d, R * (.03 + h1(i + 5) * .05), 0, TAU); g.fill(); }
266
+ const limb = g.createRadialGradient(sx, sy, R * .6, sx, sy, R); limb.addColorStop(0, 'rgba(60,5,0,0)'); limb.addColorStop(1, 'rgba(60,5,0,.5)'); g.fillStyle = limb; g.fillRect(sx - R, sy - R, R * 2, R * 2);
267
+ g.restore(); g.restore();
268
+ const nx = P ? W / 2 : W * .7, ny = P ? H * .62 : H * .26;
269
+ text(g, '毕宿五', nx, ny, { size: 84 * S, gilt: 'gold', weight: 900, alpha: A * seg(u, at(0), at(0) + .8), spacing: 10 * S, glow: 'rgba(255,120,60,.5)' });
270
+ text(g, '红巨星 · Aldebaran · α Tau', nx, ny + 62 * S, { size: 26 * S, color: col(C.ivory), alpha: A * seg(u, at(0) + .4, at(0) + 1.2), font: FONT.sans, weight: 600 });
271
+ // 太阳对比
272
+ const ck = seg(u, at(1) - .2, at(1) + .7);
273
+ if (ck > 0) { const r0 = R / 44, px = sx + R + 70 * S, py = sy + R * .5; sparkle(g, px, py, Math.max(2 * S, r0), [255, 240, 200], A * ck, .6);
274
+ text(g, '太阳', px, py + 28 * S, { size: 20 * S, color: '#fff', alpha: A * ck, font: FONT.sans, weight: 600 });
275
+ text(g, '直径 ≈ 太阳 × 44', nx, ny + 120 * S, { size: 40 * S, gilt: 'rose', weight: 900, alpha: A * ck }); }
276
+ const dk = seg(u, at(2) - .2, at(2) + .7);
277
+ if (dk > 0) { text(g, '距离 ≈ 65 光年', nx, ny + 180 * S, { size: 40 * S, gilt: 'rose', weight: 900, alpha: A * dk * (P ? 1 - seg(u, at(3), at(3) + .5) : 1) }); }
278
+ // 名字由来
279
+ const lk = env(u, at(3) - .2, at(6) - .5, .6, .5);
280
+ if (lk > 0) {
281
+ const y = P ? H * .74 : H * .58, x = nx;
282
+ text(g, 'الدبران', x, y, { size: 56 * S, color: col(C.gold), alpha: A * lk, font: FONT.sans, weight: 700 });
283
+ text(g, 'al-Dabarān', x, y + 50 * S, { size: 28 * S, font: FONT.latin, weight: 700, italic: true, color: col(C.rose), alpha: A * lk });
284
+ text(g, '“追随者”', x, y + 104 * S, { size: 52 * S, gilt: 'gold', weight: 900, alpha: A * seg(u, at(5) - .2, at(5) + .6) });
285
+ }
286
+ // 追随:昴星团在前、毕宿五在后
287
+ const fk = env(u, at(6) - .3, at(7) - .7, .5, .5);
288
+ if (fk > 0) {
289
+ const cx = P ? W / 2 : W * .7, cy = P ? H * .72 : H * .62, rr = (P ? 260 : 300) * S;
290
+ g.save(); g.globalAlpha *= A * fk; g.strokeStyle = col(C.gold, .4); g.lineWidth = 1.5 * S; g.setLineDash([6 * S, 6 * S]); g.beginPath(); g.arc(cx, cy + rr * .9, rr, Math.PI * 1.15, Math.PI * 1.85); g.stroke(); g.restore();
291
+ const m = (u - at(6)) * .12;
292
+ const p1 = Math.PI * (1.3 + m), p2 = Math.PI * (1.18 + m);
293
+ PLEI.slice(0, 6).forEach((s, i) => sparkle(g, cx + Math.cos(p1) * rr + (i % 3) * 8 * S, cy + rr * .9 + Math.sin(p1) * rr + Math.floor(i / 3) * 8 * S, 2.2 * S, C.sky, A * fk, .4));
294
+ const qx = cx + Math.cos(p2) * rr, qy = cy + rr * .9 + Math.sin(p2) * rr, px1 = cx + Math.cos(p1) * rr, py1 = cy + rr * .9 + Math.sin(p1) * rr;
295
+ sparkle(g, qx, qy, 5 * S, [255, 160, 100], A * fk, 1);
296
+ text(g, '昴星团', px1, py1 - 34 * S, { size: 26 * S, color: col(C.sky), weight: 700, alpha: A * fk });
297
+ text(g, '毕宿五 · 紧随其后', qx, qy + 40 * S, { size: 26 * S, gilt: 'gold', weight: 900, alpha: A * fk });
298
+ text(g, '东升', cx + rr * .95, cy + rr * .55, { size: 20 * S, color: col(C.ivory, .7), alpha: A * fk, font: FONT.sans, weight: 400 });
299
+ }
300
+ }
301
+ if (B > 0) {
302
+ const cx = W / 2, cy = P ? H * .4 : H * .46, R = (P ? 300 : 290) * S;
303
+ text(g, '昴星团 · 七姐妹', cx, P ? H * .1 : H * .1, { size: 56 * S, gilt: 'gold', weight: 900, alpha: B, spacing: 6 * S });
304
+ text(g, 'PLEIADES · M45', cx, (P ? H * .1 : H * .1) + 54 * S, { size: 24 * S, font: FONT.latin, weight: 700, color: col(C.sky), alpha: B, spacing: 6 * S });
305
+ const tk = seg(u, at(10) - .6, at(10) + .8);
306
+ // 肉眼:6~7 颗
307
+ const scale = (P ? 2400 : 2600) * S;
308
+ PLEI.forEach((s, i) => { const x = cx + (3.78 - s[0]) * scale * .6, y = cy - (s[1] - 24.1) * scale * .55 * .55; sparkle(g, x, y, (5.6 - s[2]) * 2.2 * S, C.sky, B * seg(u, at(7) + .2 + i * .15, at(7) + .7 + i * .15), .9); });
309
+ if (tk > 0) {
310
+ g.save(); g.globalAlpha *= B; g.beginPath(); g.arc(cx, cy, R * ease.out(tk), 0, TAU); g.save(); g.clip();
311
+ const bg = g.createRadialGradient(cx, cy, 0, cx, cy, R); bg.addColorStop(0, 'rgba(60,90,180,.6)'); bg.addColorStop(1, 'rgba(5,10,30,.95)'); g.fillStyle = bg; g.fillRect(cx - R, cy - R, R * 2, R * 2);
312
+ for (let i = 0; i < 26; i++) halo(g, cx + (h1(i * 3) - .5) * R * 1.4, cy + (h1(i * 5) - .5) * R * 1.4, (40 + h1(i) * 90) * S, [120, 160, 255], .25);
313
+ for (let i = 0; i < 420; i++) { const a = h1(i * 2.1) * TAU, d = Math.pow(h1(i * 3.3), .7) * R; const x = cx + Math.cos(a) * d, y = cy + Math.sin(a) * d; g.fillStyle = col([200, 220, 255], .4 + .6 * h1(i)); g.beginPath(); g.arc(x, y, (.6 + Math.pow(h1(i * 7), 4) * 3) * S, 0, TAU); g.fill(); }
314
+ PLEI.forEach((s, i) => sparkle(g, cx + (3.78 - s[0]) * scale * .6, cy - (s[1] - 24.1) * scale * .3, (5.6 - s[2]) * 2.4 * S, C.sky, 1, 1.1));
315
+ g.restore();
316
+ g.strokeStyle = gilt(g, cx - R, cy - R, cx + R, cy + R); g.lineWidth = 6 * S; g.beginPath(); g.arc(cx, cy, R * ease.out(tk), 0, TAU); g.stroke(); g.restore();
317
+ text(g, '1000+ 颗成员星 · 年龄约 1 亿年', cx, cy + R + 50 * S, { size: 36 * S, color: col(C.ivory), weight: 700, alpha: B * seg(u, at(10), at(10) + .8) * (1 - seg(u, at(11) - .4, at(11))) });
318
+ } else text(g, '肉眼可见 6~7 颗', cx, cy + R + 50 * S, { size: 36 * S, color: col(C.ivory), weight: 700, alpha: B * seg(u, at(7) + .6, at(7) + 1.4) });
319
+ // 昴(すばる)
320
+ const jk = seg(u, at(11) - .3, at(11) + .7);
321
+ if (jk > 0) {
322
+ const jx = P ? W / 2 : W * .17, jy = P ? cy + R + 150 * S : cy;
323
+ text(g, '昴', jx, jy - (P ? 0 : 30) * S, { size: (P ? 110 : 150) * S, gilt: 'gold', weight: 900, alpha: B * jk, glow: 'rgba(120,160,255,.6)', blur: 30 * S });
324
+ if (!P) text(g, 'すばる · SUBARU', jx, jy + 80 * S, { size: 26 * S, font: FONT.latin, weight: 700, color: col(C.sky), alpha: B * jk, spacing: 3 * S });
325
+ }
326
+ const lk = seg(u, at(12) - .2, at(12) + .8);
327
+ if (lk > 0) {
328
+ const lx = P ? W / 2 : W * .83, ly = P ? H * .78 + 40 * S : cy, ew = 150 * S, eh = 90 * S;
329
+ g.save(); g.globalAlpha *= B * lk; g.fillStyle = '#123a8c'; g.beginPath(); g.ellipse(lx, ly, ew, eh, 0, 0, TAU); g.fill(); g.lineWidth = 4 * S; g.strokeStyle = gilt(g, lx - ew, ly - eh, lx + ew, ly + eh, 'pale'); g.stroke(); g.restore();
330
+ [[-70, -20, 1.6], [0, 10, 1], [30, -30, 1], [60, 20, 1], [-10, 45, 1], [80, -5, 1]].forEach(([dx, dy, z], i) => {
331
+ const x = lx + dx * S, y = ly + dy * S, r = 12 * z * S; g.save(); g.globalAlpha *= B * seg(lk, .2 + i * .1, .4 + i * .1); g.fillStyle = '#fff'; g.beginPath();
332
+ for (let j = 0; j < 8; j++) { const a = j / 8 * TAU - Math.PI / 2, rr = j % 2 ? r * .45 : r; g.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr); } g.fill(); g.restore(); });
333
+ text(g, '六连星', lx, ly + eh + 40 * S, { size: 30 * S, color: col(C.ivory), weight: 700, alpha: B * lk });
334
+ }
335
+ }
336
+ };
337
+
338
+ /* ============ 5 天关客星 ============ */
339
+ ACTS.guest = (g, u, W, H, X) => {
340
+ const { S, P, at } = X;
341
+ const B = ease.io(seg(u, at(8) - .6, at(8) + .6)), A = 1 - B;
342
+ if (A > 0) {
343
+ // 白昼泛光
344
+ const day = env(u, at(6) - .3, at(8) - .8, .8, .8);
345
+ if (day > 0) { g.fillStyle = col([150, 190, 220], .45 * day * A); g.fillRect(0, 0, W, H); }
346
+ // 水墨远山
347
+ const layers = [['rgba(40,70,64,.55)', .62, 1.4], ['rgba(24,44,40,.8)', .7, 1], ['rgba(10,20,18,.95)', .8, .8]];
348
+ layers.forEach(([c, y, amp], i) => { g.save(); g.globalAlpha *= A; g.fillStyle = c; g.beginPath(); g.moveTo(0, H);
349
+ for (let x = 0; x <= W + 20; x += 12) { const q = x / W; g.lineTo(x, H * y - (Math.pow(Math.abs(Math.sin(q * 3.2 + i * 1.7)), 2) * 160 + Math.sin(q * 13 + i) * 18) * S * amp); }
350
+ g.lineTo(W, H); g.fill(); g.restore(); });
351
+ // 天关星与客星
352
+ const tx = P ? W * .64 : W * .62, ty = P ? H * .24 : H * .26;
353
+ const zk = seg(u, at(4) - .2, at(4) + .6);
354
+ if (zk > 0) { sparkle(g, tx, ty, 4 * S, C.ivory, A * zk, .6); ring(g, tx, ty, u - at(4), S); label(g, tx, ty, '天关', 'ζ Tau', zk * A * (1 - seg(u, at(5), at(5) + .6)), S, { side: 1 }); }
355
+ const gk = seg(u, at(5) - .1, at(5) + .5);
356
+ if (gk > 0) {
357
+ const gx = tx - 50 * S, gy = ty + 40 * S, fl = 1 + .15 * Math.sin(u * 9);
358
+ halo(g, gx, gy, 260 * S * gk * fl, [255, 236, 200], A); halo(g, gx, gy, 90 * S * gk, [255, 255, 255], A);
359
+ sparkle(g, gx, gy, 14 * S * gk, [255, 245, 230], A, 2.2 * fl);
360
+ label(g, gx, gy, '客星', '超新星 SN 1054', A * seg(u, at(5) + .3, at(5) + 1), S, { side: -1, size: 42 });
361
+ }
362
+ // 年份
363
+ const yk = env(u, at(2) - .3, at(6) - .3, .6, .6);
364
+ const yx = P ? W / 2 : W * .26, yy = P ? H * .44 : H * .2;
365
+ text(g, '1054', yx, yy, { size: (P ? 120 : 130) * S, font: FONT.latin, weight: 700, gilt: 'gold', alpha: A * yk, glow: 'rgba(0,0,0,.6)' });
366
+ text(g, '北宋 · 至和元年', yx, yy + 80 * S, { size: 34 * S, color: col(C.ivory), weight: 700, alpha: A * yk, spacing: 6 * S });
367
+ // 史书卷轴
368
+ const sk = env(u, at(3) - .3, at(8) - .8, .8, .5);
369
+ if (sk > 0) {
370
+ const sw = (P ? 600 : 520) * S, sh = (P ? 360 : 420) * S, sx = P ? W / 2 : W * .26, sy = P ? H * .72 : H * .62;
371
+ const open = ease.out(seg(u, at(3) - .2, at(3) + 1.4)), ww = sw * open;
372
+ g.save(); g.globalAlpha *= A * sk; g.shadowColor = 'rgba(0,0,0,.5)'; g.shadowBlur = 20 * S;
373
+ const pg = g.createLinearGradient(0, sy - sh / 2, 0, sy + sh / 2); pg.addColorStop(0, '#efe0bf'); pg.addColorStop(1, '#d8c39a'); g.fillStyle = pg; g.fillRect(sx - ww / 2, sy - sh / 2, ww, sh); g.shadowBlur = 0;
374
+ g.fillStyle = '#5a3a1e'; g.fillRect(sx - ww / 2 - 12 * S, sy - sh / 2 - 10 * S, 12 * S, sh + 20 * S); g.fillRect(sx + ww / 2, sy - sh / 2 - 10 * S, 12 * S, sh + 20 * S);
375
+ g.save(); g.beginPath(); g.rect(sx - ww / 2, sy - sh / 2, ww, sh); g.clip();
376
+ const cols = ['至和元年五月己丑', '客星出天关东南', '可数寸 岁余稍没', '昼见如太白', '凡见二十三日'];
377
+ const fs = (P ? 34 : 34) * S, colW = sw / (cols.length + 1);
378
+ cols.forEach((c, i) => { const x = sx + sw / 2 - colW * (i + 1); [...c].forEach((ch, j) => { const q = seg(u, at(3) + .3 + i * .5 + j * .05, at(3) + .6 + i * .5 + j * .05);
379
+ if (c[j] === ' ') return; text(g, ch, x, sy - sh / 2 + 34 * S + j * fs * 1.12, { size: fs, color: i >= 3 ? '#8a1a14' : '#2a1a0e', weight: 900, alpha: q }); }); });
380
+ g.restore();
381
+ // 朱印
382
+ const st = seg(u, at(7) - .2, at(7) + .3);
383
+ if (st > 0) { g.save(); g.translate(sx - sw * .32, sy + sh * .3); g.rotate(-.1); g.scale(lerp(1.8, 1, ease.out5(st)), lerp(1.8, 1, ease.out5(st))); g.fillStyle = 'rgba(190,40,30,.9)'; g.fillRect(-34 * S, -34 * S, 68 * S, 68 * S); g.restore();
384
+ text(g, '廿三日', sx - sw * .32, sy + sh * .3, { size: 20 * S, color: '#ffeedd', weight: 900, alpha: st }); }
385
+ g.restore();
386
+ const cap = seg(u, at(6) - .1, at(6) + .6);
387
+ if (cap > 0) text(g, '白昼可见 · 23 天', P ? W / 2 : W * .66, P ? H * .48 : H * .5, { size: 46 * S, gilt: 'gold', weight: 900, alpha: A * cap * (1 - seg(u, at(8) - 1, at(8) - .4)), glow: 'rgba(0,0,0,.7)' });
388
+ }
389
+ text(g, '史载 · 《宋会要》《宋史 · 天文志》', P ? W / 2 : W * .26, P ? H * .93 - 40 * S : H * .92 - 40 * S, { size: 20 * S, color: col(C.ivory, .7), font: FONT.sans, weight: 400, alpha: A * seg(u, at(4), at(4) + 1) * (1 - seg(u, at(8) - 1, at(8) - .4)) });
390
+ const pr = env(u, at(0) - .2, at(2) - .3, .6, .5);
391
+ if (pr > 0) { plate(g, W / 2, H * .4, (P ? 880 : 900) * S, 170 * S, pr, S, { metal: 'gold' }); text(g, '属于中国人的骄傲', W / 2, H * .4, { size: 64 * S, gilt: 'gold', weight: 900, alpha: pr, spacing: 8 * S }); }
392
+ }
393
+ if (B > 0) {
394
+ // 蟹状星云
395
+ const cx = W / 2, cy = P ? H * .42 : H * .47, R = (P ? 380 : 360) * S * (1 + (u - at(8)) * .006);
396
+ g.save(); g.globalAlpha *= B;
397
+ halo(g, cx, cy, R * 1.6, [120, 140, 255], .45);
398
+ halo(g, cx, cy, R * 1.1, [150, 190, 255], .5);
399
+ g.globalCompositeOperation = 'lighter';
400
+ for (let i = 0; i < 60; i++) { const a = h1(i * 4.1) * TAU, d = Math.sqrt(h1(i * 6.3)) * R * .8; halo(g, cx + Math.cos(a) * d, cy + Math.sin(a) * d * .75, (60 + h1(i) * 120) * S, i % 3 ? [255, 120, 70] : [120, 230, 170], .22); }
401
+ for (let i = 0; i < 160; i++) {
402
+ const a = h1(i * 1.3) * TAU, r0 = R * (.15 + h1(i * 2.7) * .3), r1 = R * (.55 + h1(i * 3.9) * .5) * (1 + .3 * Math.cos(a * 2) ** 2);
403
+ const x0 = cx + Math.cos(a) * r0, y0 = cy + Math.sin(a) * r0 * .78, x1 = cx + Math.cos(a + (h1(i) - .5) * .5) * r1, y1 = cy + Math.sin(a + (h1(i) - .5) * .5) * r1 * .78;
404
+ const mx = (x0 + x1) / 2 + (h1(i * 5) - .5) * 60 * S, my = (y0 + y1) / 2 + (h1(i * 6) - .5) * 60 * S;
405
+ g.strokeStyle = col(i % 3 ? [255, 110, 70] : [255, 190, 120], .08 + h1(i * 9) * .14); g.lineWidth = (1 + h1(i * 4) * 2) * S;
406
+ g.beginPath(); g.moveTo(x0, y0); g.quadraticCurveTo(mx, my, x1, y1); g.stroke();
407
+ }
408
+ g.restore();
409
+ text(g, '蟹状星云 · M1', W / 2, P ? H * .1 : H * .1, { size: 56 * S, gilt: 'gold', weight: 900, alpha: B * seg(u, at(10) - .2, at(10) + .6), spacing: 6 * S });
410
+ text(g, '超新星遗迹 · 至今仍在膨胀', W / 2, (P ? H * .1 : H * .1) + 56 * S, { size: 26 * S, color: col(C.ivory), alpha: B * seg(u, at(11), at(11) + .8), font: FONT.sans, weight: 600 });
411
+ // 脉冲星
412
+ const pk = seg(u, at(12) - .2, at(12) + .6);
413
+ if (pk > 0) {
414
+ const ang = u * 3.2; // 视觉化慢放
415
+ g.save(); g.globalCompositeOperation = 'lighter';
416
+ for (const d of [0, Math.PI]) { const gr = g.createLinearGradient(cx, cy, cx + Math.cos(ang + d) * R, cy + Math.sin(ang + d) * R * .6); gr.addColorStop(0, col([200, 230, 255], .8 * pk * B)); gr.addColorStop(1, col([200, 230, 255], 0));
417
+ g.fillStyle = gr; g.beginPath(); g.moveTo(cx, cy); g.lineTo(cx + Math.cos(ang + d - .08) * R, cy + Math.sin(ang + d - .08) * R * .6); g.lineTo(cx + Math.cos(ang + d + .08) * R, cy + Math.sin(ang + d + .08) * R * .6); g.fill(); }
418
+ g.restore();
419
+ sparkle(g, cx, cy, 8 * S, [210, 235, 255], pk * B, 1.4);
420
+ text(g, '脉冲星 · 每秒自转约 30 圈', cx, cy + R * .78 + 60 * S, { size: 36 * S, gilt: 'pale', weight: 900, alpha: pk * B, glow: 'rgba(0,0,0,.8)' });
421
+ }
422
+ }
423
+ };
424
+
425
+ /* ============ 6 欧罗巴与白牛 ============ */
426
+ function princess(g, x, y, s, a, t, sit = 0) {
427
+ if (a <= 0) return; g.save(); g.translate(x, y); g.scale(s, s); g.globalAlpha *= a;
428
+ const dress = g.createLinearGradient(0, -120, 0, 0); dress.addColorStop(0, '#f4d6e0'); dress.addColorStop(1, '#c98aa6');
429
+ g.fillStyle = dress; g.beginPath();
430
+ if (sit) { g.moveTo(-14, -78); g.quadraticCurveTo(-24, -40, -40, -14); g.quadraticCurveTo(0, 6, 46, -6 + Math.sin(t * 2) * 4); g.quadraticCurveTo(20, -40, 14, -78); }
431
+ else { g.moveTo(-14, -80); g.quadraticCurveTo(-30, -30, -42 + Math.sin(t * 1.5) * 4, 0); g.lineTo(42 + Math.sin(t * 1.5 + 1) * 5, 0); g.quadraticCurveTo(30, -30, 14, -80); }
432
+ g.closePath(); g.fill(); g.strokeStyle = col(C.gold, .8); g.lineWidth = 1.5; g.stroke();
433
+ g.fillStyle = '#f6dcc8'; g.beginPath(); g.arc(0, -94, 13, 0, TAU); g.fill();
434
+ g.fillStyle = '#4a2a1a'; g.beginPath(); g.moveTo(-13, -98); g.quadraticCurveTo(0, -116, 13, -98); g.quadraticCurveTo(26 + Math.sin(t * 1.3) * 6, -70, 18, -50); g.quadraticCurveTo(10, -80, -13, -98); g.fill();
435
+ for (let i = 0; i < 5; i++) { g.fillStyle = i % 2 ? '#ffd0dc' : '#fff2c0'; g.beginPath(); g.arc(-12 + i * 6, -104 + Math.abs(i - 2) * 2, 3, 0, TAU); g.fill(); }
436
+ g.restore();
437
+ }
438
+ ACTS.europa = (g, u, W, H, X) => {
439
+ const { S, P, at } = X;
440
+ const seaY = H * (P ? .7 : .72), rise = ease.io(seg(u, at(11) - .3, at(13) + .8));
441
+ // 月与海
442
+ halo(g, W * .78, H * .18, 200 * S, [255, 240, 220], .5 * (1 - rise * .6)); g.fillStyle = col([255, 248, 235], 1 - rise * .5); g.beginPath(); g.arc(W * .78, H * .18, 34 * S, 0, TAU); g.fill();
443
+ for (let L = 0; L < 4; L++) { const y0 = seaY + L * 30 * S; g.beginPath(); g.moveTo(0, H);
444
+ for (let x = 0; x <= W + 20; x += 18) g.lineTo(x, y0 + Math.sin(x / (90 * S) + u * (.8 + L * .2) + L) * (6 + L * 3) * S);
445
+ g.lineTo(W, H); g.closePath(); g.fillStyle = col([20 + L * 4, 70 - L * 8, 90 - L * 10], .8); g.fill(); g.strokeStyle = col([190, 240, 255], .2); g.lineWidth = 1.2 * S; g.stroke(); }
446
+ // 月光倒影
447
+ for (let i = 0; i < 18; i++) { const y = seaY + 10 * S + i * 12 * S, w = (60 - i * 2) * S * (.6 + .4 * Math.sin(u * 2 + i)); g.fillStyle = col([255, 240, 220], .25 * (1 - rise * .5)); g.fillRect(W * .78 - w / 2, y, w, 2 * S); }
448
+ // 海岸花丛(前 9 句)
449
+ const shore = 1 - seg(u, at(7) - .5, at(8));
450
+ const sx0 = P ? W * .08 : W * .05;
451
+ if (shore > 0) { g.save(); g.globalAlpha *= shore; g.fillStyle = '#0d1a14'; g.beginPath(); g.moveTo(0, H); g.lineTo(0, seaY - 40 * S); g.quadraticCurveTo(W * (P ? .5 : .35), seaY - 70 * S, W * (P ? .75 : .55), seaY + 20 * S); g.lineTo(W * (P ? .75 : .55), H); g.fill(); g.restore();
452
+ for (let i = 0; i < (P ? 14 : 20); i++) flower(g, sx0 + h1(i * 3) * W * (P ? .65 : .45), seaY - 40 * S + h1(i * 7) * 60 * S, (12 + h1(i) * 14) * S, shore, { rot: i, layers: 2, inner: i % 3 ? [220, 110, 140] : [240, 190, 120], outer: [255, 236, 236] }); }
453
+ // 宙斯:云中金光与王冠
454
+ const zk = env(u, at(1) - .3, at(3) - .2, .6, .6);
455
+ if (zk > 0) { const x = W / 2, y = H * (P ? .16 : .17); halo(g, x, y, 300 * S, [255, 220, 150], zk * .8);
456
+ for (let i = 0; i < 7; i++) halo(g, x + (i - 3) * 90 * S, y + Math.sin(i) * 20 * S, 120 * S, [200, 200, 230], zk * .35);
457
+ text(g, '宙斯', x, y, { size: 72 * S, gilt: 'gold', weight: 900, alpha: zk, spacing: 16 * S, glow: 'rgba(255,220,150,.7)', blur: 20 * S }); text(g, 'ZEUS · 众神之王', x, y + 54 * S, { size: 22 * S, font: FONT.latin, weight: 700, color: col(C.ivory), alpha: zk, spacing: 4 * S }); }
458
+ // 公主
459
+ const pk = seg(u, at(2) - .2, at(2) + .8) * shore;
460
+ const bs = (P ? 1.0 : 1.4) * S, bullX = P ? W * .5 : W * .36, bullY = seaY - 30 * S - 118 * bs;
461
+ const sit = seg(u, at(6) - .2, at(6) + .8);
462
+ const swim = ease.io(seg(u, at(7) - .2, at(8) + 1.2));
463
+ const tx = lerp(bullX, W * (P ? .42 : .6), swim), ty = lerp(bullY, seaY + 10 * S - 80 * bs, swim) + Math.sin(u * 2) * 4 * S * swim;
464
+ const px = lerp(P ? W * .2 : W * .14, tx - 10 * bs, sit), py = lerp(seaY - 30 * S, ty - 62 * bs, sit);
465
+ if (sit <= 0) { princess(g, px, py, (P ? 1.2 : 1.6) * S, pk, u, 0); if (pk > 0) text(g, '欧罗巴', px, py + 40 * S, { size: 32 * S, gilt: 'rose', weight: 900, alpha: pk * (1 - seg(u, at(4), at(4) + .6)) }); }
466
+ // 白牛
467
+ const wk = seg(u, at(3) - .2, at(3) + 1.2) * (1 - seg(u, at(11) - .4, at(11) + .6));
468
+ if (wk > 0) {
469
+ if (wk < 1) for (let i = 0; i < 30; i++) sparkle(g, tx + (h1(i) - .5) * 300 * S * (1 - wk), ty + (h1(i + 4) - .5) * 200 * S * (1 - wk), 3 * S, C.gold, Math.sin(wk * Math.PI), .5);
470
+ halo(g, tx, ty, 260 * S, [255, 240, 220], .45 * wk);
471
+ if (swim > 0 && swim < 1) for (let i = 0; i < 8; i++) { const k = ((u * .7 + i / 8) % 1); g.strokeStyle = col([220, 245, 255], (1 - k) * .5); g.lineWidth = 2 * S; g.beginPath(); g.ellipse(tx - 40 * S - k * 200 * S, seaY + 6 * S, 40 * S + k * 60 * S, 6 * S + k * 6 * S, 0, 0, TAU); g.stroke(); }
472
+ if (swim > 0) { g.save(); g.beginPath(); g.rect(0, 0, W, seaY + 4 * S + Math.sin(u * 2) * 3 * S); g.clip(); }
473
+ bullSide(g, tx, ty, bs, { t: u, walk: swim > 0 && swim < 1 ? .6 : 0, white: true, alpha: wk });
474
+ if (swim > 0) g.restore();
475
+ // 花环
476
+ const gk = seg(u, at(5) - .2, at(5) + 1.4);
477
+ if (gk > 0) for (let i = 0; i < 9; i++) { const q = i / 8; if (q > gk) break; const a = Math.PI * (.15 + q * .9); const fx = tx + 118 * bs + Math.cos(a) * 30 * bs, fy = ty - 10 * bs + Math.sin(a) * 50 * bs;
478
+ flower(g, fx, fy, 10 * bs, clamp((gk - q) * 6), { layers: 2, petals: 6, rot: i, inner: i % 2 ? [230, 120, 150] : [250, 200, 120], outer: [255, 240, 240], alpha: wk }); }
479
+ if (sit > 0) princess(g, tx - 10 * bs, ty - 62 * bs, (P ? 1.0 : 1.3) * S, 1, u, 1);
480
+ text(g, '雪白温顺的公牛', P ? W / 2 : tx, P ? H * .32 : ty - 170 * S, { size: 34 * S, color: col(C.ivory), weight: 700, alpha: env(u, at(3) + .3, at(5) - .3, .5, .5), glow: 'rgba(0,0,0,.7)' });
481
+ }
482
+ // 克里特岛
483
+ const ck = env(u, at(8) - .2, at(11) - .5, .7, .6);
484
+ if (ck > 0) { const ix = W * (P ? .9 : .92), iy = seaY + 2 * S; g.save(); g.globalAlpha *= ck; g.fillStyle = '#1d2a22'; g.beginPath(); g.moveTo(ix - 200 * S, iy + 4 * S); g.quadraticCurveTo(ix - 100 * S, iy - 70 * S, ix, iy - 50 * S); g.quadraticCurveTo(ix + 120 * S, iy - 60 * S, ix + 220 * S, iy + 4 * S); g.fill(); g.restore();
485
+ label(g, ix + 10 * S, iy - 56 * S, '克里特岛', 'Crete', ck, S, { side: -1 }); }
486
+ // EUROPA → Europe
487
+ const ek = env(u, at(9) - .2, at(11) + .2, .6, .6);
488
+ if (ek > 0) { const y = P ? H * .24 : H * .27; plate(g, W / 2, y, (P ? 860 : 900) * S, 210 * S, ek, S, { metal: 'gold' });
489
+ text(g, 'EUROPA', W / 2 - (P ? 190 : 200) * S, y - 20 * S, { size: 60 * S, font: FONT.latin, weight: 700, gilt: 'gold', alpha: ek, spacing: 6 * S });
490
+ text(g, '→', W / 2, y - 20 * S, { size: 50 * S, color: col(C.rose), alpha: ek * seg(u, at(10) - .4, at(10) + .2) });
491
+ text(g, 'Europe', W / 2 + (P ? 190 : 200) * S, y - 20 * S, { size: 60 * S, font: FONT.latin, weight: 700, italic: true, gilt: 'rose', alpha: ek * seg(u, at(10) - .2, at(10) + .5) });
492
+ text(g, '欧罗巴 · 欧洲一词的来源', W / 2, y + 56 * S, { size: 28 * S, color: col(C.ivory), weight: 700, alpha: ek * seg(u, at(10), at(10) + .6) }); }
493
+ // 升空成星座
494
+ if (rise > 0) {
495
+ const cx = W / 2, cy = lerp(seaY - 60 * S, H * (P ? .38 : .4), rise), s = lerp(.9, 1.7, rise) * S * (P ? .8 : 1);
496
+ const toStar = seg(u, at(13) - .4, at(13) + 1.2);
497
+ halo(g, cx, cy, 380 * S * rise, [255, 230, 200], .5);
498
+ bullSide(g, cx, cy, s, { t: u, white: true, alpha: rise * (1 - toStar * .75) });
499
+ const pts = [[-140, -20], [-60, -60], [30, -66], [110, -60], [160, 0], [186, -96], [120, 30], [-110, 60]];
500
+ const ln = [[0, 1], [1, 2], [2, 3], [3, 4], [3, 5], [4, 6], [0, 7]];
501
+ if (toStar > 0) { g.save(); ln.forEach(([a, b], i) => strokePart(g, [[cx + pts[a][0] * s, cy + pts[a][1] * s], [cx + pts[b][0] * s, cy + pts[b][1] * s]], clamp(toStar * ln.length - i), { color: col(C.gold, .9), width: 2.4 * S, glow: 'rgba(255,220,150,.8)' })); g.restore();
502
+ pts.forEach(([x, y], i) => sparkle(g, cx + x * s, cy + y * s, (i === 4 ? 5 : 3) * S, i === 4 ? [255, 170, 110] : C.ivory, toStar, .6));
503
+ text(g, '金牛座', cx, cy + 170 * S, { size: 64 * S, gilt: 'gold', weight: 900, alpha: toStar, spacing: 14 * S, glow: 'rgba(255,200,140,.6)' }); }
504
+ dust(g, u, W, H, { n: 30, alpha: rise * .8 });
505
+ }
506
+ };
507
+
508
+ /* ============ 7 ♉ 的含义 ============ */
509
+ ACTS.sigil = (g, u, W, H, X) => {
510
+ const { S, P, at } = X;
511
+ const cx = W / 2, cy = P ? H * .4 : H * .44, sc = (P ? 2.8 : 2.6) * S;
512
+ const alt = ease.io(seg(u, at(5) - .4, at(6) + .4)); // 新月托满月
513
+ // 圆
514
+ const ck = ease.io(seg(u, at(1) - .3, at(1) + 1.2)), ak = ease.io(seg(u, at(2) - .2, at(2) + 1.2));
515
+ const style = { color: gilt(g, cx - 160 * S, cy - 200 * S, cx + 160 * S, cy + 200 * S), width: 14 * S, glow: 'rgba(255,210,140,.8)', blur: 24 * S, alpha: 1 - alt };
516
+ strokePart(g, arcPts(cx, cy + 22 * sc, 38 * sc, -Math.PI / 2, Math.PI * 1.5, 90), ck, style);
517
+ strokePart(g, arcPts(cx, cy - 70 * sc, 54 * sc, Math.PI * .85, Math.PI * .15, 70), ak, style);
518
+ // 牛脸与牛角对应
519
+ const fk = env(u, at(3) - .3, at(5) - .3, .6, .6);
520
+ if (fk > 0) { g.save(); g.globalAlpha *= .3 * fk; bullHead(g, cx, cy + 40 * sc, sc * .38, { k: 1 }); g.restore();
521
+ label(g, cx + 38 * sc, cy + 22 * sc, '圆 · 公牛的脸', '', seg(u, at(3) - .2, at(3) + .5) * fk, S, { side: 1 });
522
+ label(g, cx - 54 * sc * .9, cy - 46 * sc, '弧 · 一对牛角', '', seg(u, at(4) - .2, at(4) + .5) * fk, S, { side: -1 }); }
523
+ // 另一种解读:新月托满月
524
+ if (alt > 0) {
525
+ const mx = cx, my = cy + 10 * S, R = 90 * sc * .55;
526
+ halo(g, mx, my, R * 3, [255, 240, 210], .6 * alt);
527
+ const mg = g.createRadialGradient(mx - R * .3, my - R * .3, R * .1, mx, my, R); mg.addColorStop(0, '#fffaf0'); mg.addColorStop(1, '#d8cbb0');
528
+ g.save(); g.globalAlpha *= alt; g.fillStyle = mg; g.beginPath(); g.arc(mx, my, R, 0, TAU); g.fill();
529
+ for (let i = 0; i < 9; i++) { g.fillStyle = 'rgba(150,130,100,.18)'; g.beginPath(); g.arc(mx + (h1(i) - .5) * R * 1.2, my + (h1(i + 3) - .5) * R * 1.2, R * (.08 + h1(i + 6) * .12), 0, TAU); g.fill(); }
530
+ // 新月(托在下方)
531
+ g.beginPath(); g.arc(mx, my + R * .55, R * 1.35, Math.PI * .08, Math.PI * .92); g.arc(mx, my + R * .15, R * 1.2, Math.PI * .88, Math.PI * .12, true); g.closePath();
532
+ g.fillStyle = gilt(g, mx - R * 1.4, my, mx + R * 1.4, my + R * 2); g.shadowColor = 'rgba(255,210,140,.8)'; g.shadowBlur = 24 * S; g.fill(); g.restore();
533
+ text(g, '新月托起满月', cx, P ? cy - R * 1.8 : cy - R * 1.7, { size: 46 * S, gilt: 'gold', weight: 900, alpha: alt, spacing: 6 * S });
534
+ // 麦穗
535
+ const wk = seg(u, at(8) - .3, at(8) + 1.4);
536
+ for (const d of [-1, 1]) for (let j = 0; j < 3; j++) {
537
+ const bx = cx + d * (R * 1.9 + j * 40 * S), by = my + R * 1.7, hgt = (220 + j * 30) * S * ease.out(wk); if (wk <= 0) continue;
538
+ g.save(); g.globalAlpha *= alt; g.strokeStyle = '#d9b46a'; g.lineWidth = 3 * S; g.beginPath(); g.moveTo(bx, by); g.quadraticCurveTo(bx + d * 20 * S, by - hgt * .6, bx + d * 30 * S, by - hgt); g.stroke();
539
+ for (let k = 0; k < 7; k++) { const q = .55 + k * .065, x = lerp(bx, bx + d * 30 * S, q), y = by - hgt * q; for (const s2 of [-1, 1]) { g.fillStyle = gilt(g, x - 10 * S, y - 10 * S, x + 10 * S, y + 10 * S); g.beginPath(); g.ellipse(x + s2 * 7 * S, y, 5 * S, 11 * S, s2 * .5, 0, TAU); g.fill(); } }
540
+ g.restore();
541
+ }
542
+ text(g, '丰收 · 富足', cx, my + R * 2.3 + (P ? 30 : 10) * S, { size: 44 * S, gilt: 'gold', weight: 900, alpha: alt * seg(u, at(8), at(8) + .8), spacing: 14 * S });
543
+ }
544
+ const tk = env(u, .3, at(3) - .3, .6, .5);
545
+ text(g, '♉ 的含义', cx, P ? H * .12 : H * .12, { size: 52 * S, gilt: 'gold', weight: 900, alpha: tk, spacing: 10 * S });
546
+ dust(g, u, W, H, { n: 26, alpha: .6 });
547
+ };
548
+
549
+ /* ============ 8 土象 · 金星 ============ */
550
+ const TRAITS = [['稳重', 'STEADY'], ['耐心', 'PATIENT'], ['重承诺', 'TRUSTED'], ['懂生活', 'ART OF LIVING'], ['爱美好', 'BEAUTY'], ['忠诚', 'LOYAL']];
551
+ ACTS.venus = (g, u, W, H, X) => {
552
+ const { S, P, at } = X;
553
+ petals(g, u, W, H, { n: 20, alpha: .6, seed: 9 });
554
+ const shrink = ease.io(seg(u, at(4) - .5, at(4) + .5));
555
+ const top = lerp(P ? H * .26 : H * .36, P ? H * .1 : H * .14, shrink), sz = lerp(1, .55, shrink);
556
+ const ex = P ? W * .28 : W * .3, vx = P ? W * .72 : W * .7;
557
+ // 土象
558
+ const ek = seg(u, at(1) - .2, at(1) + .8);
559
+ if (ek > 0) {
560
+ const r = 80 * S * sz * ease.back(ek);
561
+ g.save(); g.globalAlpha *= ek; g.fillStyle = '#2a2414'; g.beginPath(); g.arc(ex, top, Math.max(1, r), 0, TAU); g.fill(); g.lineWidth = 4 * S * sz; g.strokeStyle = gilt(g, ex - r, top - r, ex + r, top + r); g.stroke();
562
+ // 炼金术“土”符号:倒三角 + 横线
563
+ g.beginPath(); g.moveTo(ex - r * .55, top - r * .35); g.lineTo(ex + r * .55, top - r * .35); g.lineTo(ex, top + r * .55); g.closePath(); g.moveTo(ex - r * .38, top - r * .02); g.lineTo(ex + r * .38, top - r * .02); g.stroke(); g.restore();
564
+ for (let i = 0; i < 5; i++) leaf(g, ex + (i - 2) * 22 * S * sz, top + r + 6 * S, 22 * S * sz, -Math.PI / 2 + (i - 2) * .35, ek, [90, 150, 90]);
565
+ text(g, '土象星座', ex, top + r + 60 * S * sz, { size: 44 * S * sz, gilt: 'gold', weight: 900, alpha: ek });
566
+ }
567
+ // 金星
568
+ const vk = seg(u, at(2) - .2, at(2) + .8);
569
+ if (vk > 0) {
570
+ const r = 76 * S * sz * ease.back(vk); halo(g, vx, top, r * 3, [255, 220, 200], vk * .7);
571
+ const pg = g.createRadialGradient(vx - r * .3, top - r * .3, r * .1, vx, top, r); pg.addColorStop(0, '#fff8ea'); pg.addColorStop(.6, '#f0d2a8'); pg.addColorStop(1, '#a6784a');
572
+ g.save(); g.globalAlpha *= vk; g.fillStyle = pg; g.beginPath(); g.arc(vx, top, Math.max(1, r), 0, TAU); g.fill(); g.restore();
573
+ venusSign(g, vx + r * 1.35, top - r * .3, .9 * S * sz, vk);
574
+ const rk = seg(u, at(3) - .3, at(3) + 1.2); flower(g, vx - r * 1.2, top + r * .7, 40 * S * sz, rk, { inner: [200, 60, 90], outer: [255, 190, 200] });
575
+ text(g, '守护星 · 金星', vx, top + r + 60 * S * sz, { size: 44 * S * sz, gilt: 'rose', weight: 900, alpha: vk });
576
+ text(g, '爱与美', vx, top + r + 110 * S * sz, { size: 30 * S * sz, color: col(C.blush), weight: 700, alpha: seg(u, at(3), at(3) + .7) * (1 - shrink) });
577
+ }
578
+ // 六张品质卡
579
+ const cols = P ? 2 : 3, cw = P ? W * .4 : W * .25, ch = (P ? 190 : 170) * S, gy0 = P ? H * .33 : H * .42;
580
+ const times = [at(5), at(5) + .9, at(5) + 2.2, at(6), at(6) + 1.2, at(10) - .4];
581
+ const dim = 1 - .9 * seg(u, at(8) - .4, at(8) + .2);
582
+ TRAITS.forEach(([zh, en], i) => {
583
+ const k = ease.back(seg(u, times[i] - .1, times[i] + .5)); if (k <= 0) return;
584
+ const c = i % cols, r = Math.floor(i / cols), x = W / 2 + (c - (cols - 1) / 2) * (cw + 22 * S), y = gy0 + r * (ch + 22 * S);
585
+ g.save(); g.translate(x, y); g.scale(Math.max(.01, Math.abs(Math.cos((1 - clamp(k)) * Math.PI / 2))), 1); g.globalAlpha *= dim;
586
+ rrect(g, -cw / 2, -ch / 2, cw, ch, 20 * S); const cg = g.createLinearGradient(0, -ch / 2, 0, ch / 2); cg.addColorStop(0, 'rgba(40,60,40,.85)'); cg.addColorStop(1, 'rgba(30,18,22,.85)'); g.fillStyle = cg; g.fill();
587
+ g.restore();
588
+ ornamentFrame(g, x - cw / 2, y - ch / 2, cw, ch, { S: S * .5, alpha: clamp(k) * dim, metal: i % 2 ? 'rose' : 'gold' });
589
+ text(g, zh, x, y - 12 * S, { size: 50 * S, gilt: i % 2 ? 'rose' : 'gold', weight: 900, alpha: clamp(k) * dim });
590
+ text(g, en, x, y + 44 * S, { size: 18 * S, font: FONT.latin, weight: 700, color: col(C.ivory, .8), alpha: clamp(k) * dim, spacing: 3 * S });
591
+ });
592
+ // 慢热的心:慢慢被注满 → 守护之盾
593
+ const hk = env(u, at(8) - .3, X.dur + 1, .6, .6);
594
+ if (hk > 0) {
595
+ const hx = W / 2, hy = P ? H * .54 : H * .56, hs = (P ? 110 : 90) * S;
596
+ const heart = () => { g.beginPath(); g.moveTo(hx, hy + hs * .9); g.bezierCurveTo(hx - hs * 1.5, hy - hs * .2, hx - hs * .7, hy - hs * 1.3, hx, hy - hs * .5); g.bezierCurveTo(hx + hs * .7, hy - hs * 1.3, hx + hs * 1.5, hy - hs * .2, hx, hy + hs * .9); g.closePath(); };
597
+ const fill = ease.io(seg(u, at(8), at(10) + .4));
598
+ const shield = seg(u, at(10) - .1, at(10) + .7);
599
+ if (shield > 0) { g.save(); g.globalAlpha *= hk * shield; g.translate(hx, hy); g.scale(lerp(1.4, 1, ease.out(shield)), lerp(1.4, 1, ease.out(shield)));
600
+ g.beginPath(); g.moveTo(0, -hs * 1.6); g.quadraticCurveTo(hs * 1.4, -hs * 1.4, hs * 1.5, -hs * .9); g.quadraticCurveTo(hs * 1.4, hs * .9, 0, hs * 1.7); g.quadraticCurveTo(-hs * 1.4, hs * .9, -hs * 1.5, -hs * .9); g.quadraticCurveTo(-hs * 1.4, -hs * 1.4, 0, -hs * 1.6); g.closePath();
601
+ g.lineWidth = 6 * S; g.strokeStyle = gilt(g, -hs * 1.5, -hs * 1.6, hs * 1.5, hs * 1.7); g.fillStyle = 'rgba(30,70,52,.6)'; g.fill(); g.stroke(); g.restore(); }
602
+ g.save(); g.globalAlpha *= hk; heart(); g.fillStyle = 'rgba(60,20,30,.6)'; g.fill(); g.save(); heart(); g.clip();
603
+ const fy = hy + hs * .9 - (hs * 2.2) * fill; const fg = g.createLinearGradient(0, fy, 0, hy + hs); fg.addColorStop(0, '#ffb1a8'); fg.addColorStop(1, '#c4485a'); g.fillStyle = fg; g.fillRect(hx - hs * 2, fy, hs * 4, hs * 3); g.restore();
604
+ heart(); g.lineWidth = 3 * S; g.strokeStyle = gilt(g, hx - hs, hy - hs, hx + hs, hy + hs, 'rose'); g.stroke(); g.restore();
605
+ if (fill > 0 && fill < 1) halo(g, hx, hy, 120 * S, [255, 160, 160], .4);
606
+ const lx = P ? W / 2 : W / 2, ly = P ? hy - hs * 2.1 : hy - hs * 2.1;
607
+ text(g, shield > .3 ? '认定了,就守护到底' : '慢热……', lx, ly - 10 * S, { size: 48 * S, gilt: 'rose', weight: 900, alpha: hk, glow: 'rgba(0,0,0,.7)' });
608
+ }
609
+ };
610
+
611
+ /* ============ 9 鎏金珍藏卡 ============ */
612
+ ACTS.card = (g, u, W, H, X) => {
613
+ const { S, P, at } = X;
614
+ // 聚光
615
+ const sp = seg(u, 0, 1.2);
616
+ const vg = g.createRadialGradient(W / 2, H * .45, 0, W / 2, H * .45, Math.max(W, H) * .7); vg.addColorStop(0, 'rgba(0,0,0,0)'); vg.addColorStop(1, `rgba(0,0,0,${.55 * sp})`); g.fillStyle = vg; g.fillRect(0, 0, W, H);
617
+ g.save(); g.globalCompositeOperation = 'lighter'; const lg = g.createLinearGradient(W / 2, 0, W / 2, H * .7); lg.addColorStop(0, col([255, 225, 180], .2 * sp)); lg.addColorStop(1, 'rgba(255,225,180,0)');
618
+ g.fillStyle = lg; g.beginPath(); g.moveTo(W / 2 - 80 * S, 0); g.lineTo(W / 2 + 80 * S, 0); g.lineTo(W / 2 + 460 * S, H * .8); g.lineTo(W / 2 - 460 * S, H * .8); g.fill(); g.restore();
619
+ // 旋转光环
620
+ const rk = seg(u, at(0) - .5, at(1));
621
+ const ch = P ? Math.min(H * .6, W * .86 / .716) : H * .7, cx = W / 2, cy = P ? H * .43 : H * .44;
622
+ if (rk > 0) { g.save(); g.translate(cx, cy); g.rotate(u * .15); for (let i = 0; i < 24; i++) { g.rotate(TAU / 24); g.fillStyle = col(C.gold, .05 * rk); g.beginPath(); g.moveTo(0, 0); g.lineTo(-30 * S, -ch); g.lineTo(30 * S, -ch); g.fill(); } g.restore(); }
623
+ halo(g, cx, cy, ch * .9, [255, 200, 150], .6 * rk);
624
+ // 卡片:降落 → 翻转 → 刻字 → 全息扫光 → 盖章
625
+ const drop = ease.out(seg(u, at(0) - .6, at(0) + 1.4));
626
+ const flip = ease.io(seg(u, at(1) - .3, at(1) + 1.1));
627
+ const y = lerp(-ch, cy, drop) + Math.sin(u * 1.2) * 6 * S * drop;
628
+ if (drop > 0) drawCard(g, cx, y, ch, {
629
+ t: u, flip, fields: seg(u, at(2) - .3, at(4) + 2.4), shine: u < at(5) - .4 ? undefined : u < at(6) + .6 ? seg(u, at(5) - .4, at(6) + .6) : (((u - at(6) - .6) * .3) % 1.5) / 1.5,
630
+ holo: .9, stamp: seg(u, at(7) - .1, at(7) + .35), holder: X.opts.name, birthday: X.opts.birthday, tilt: Math.sin(u * .8) * .02 * drop,
631
+ });
632
+ // 翻转瞬间的光爆
633
+ const fb = seg(u, at(1) + .3, at(1) + 1.1); if (fb > 0 && fb < 1) halo(g, cx, cy, ch * (.4 + fb), [255, 240, 210], Math.sin(fb * Math.PI));
634
+ // 盖章冲击波 + 花瓣雨
635
+ const sk = u - at(7) - .2;
636
+ if (sk > 0 && sk < 1.4) for (let i = 0; i < 2; i++) { const k = (sk - i * .2) / 1.2; if (k <= 0 || k >= 1) continue; g.save(); g.strokeStyle = col([255, 120, 100], (1 - k) * .7); g.lineWidth = 3 * S; g.beginPath(); g.arc(cx + ch * .22, cy + ch * .33, ch * .6 * ease.out(k), 0, TAU); g.stroke(); g.restore(); }
637
+ petals(g, u, W, H, { n: P ? 40 : 60, alpha: seg(u, at(7) + .1, at(7) + 1), seed: 21, speed: .06 });
638
+ dust(g, u, W, H, { n: 40, alpha: seg(u, at(1), at(2)) });
639
+ // 底部标语
640
+ const tk = seg(u, at(7) + .6, at(7) + 1.8);
641
+ if (tk > 0 && P) text(g, '金牛座 · 大地的守护者', W / 2, cy + ch / 2 + 70 * S, { size: 46 * S, gilt: 'gold', weight: 900, alpha: tk, spacing: 6 * S, glow: 'rgba(0,0,0,.7)' });
642
+ if (tk > 0 && !P) { text(g, '大地的', W * .17, H * .44, { size: 52 * S, gilt: 'gold', weight: 900, alpha: tk, spacing: 10 * S }); text(g, '守护者', W * .83, H * .44, { size: 52 * S, gilt: 'gold', weight: 900, alpha: tk, spacing: 10 * S });
643
+ text(g, 'GUARDIAN', W * .17, H * .44 + 56 * S, { size: 22 * S, font: FONT.latin, weight: 700, color: col(C.rose), alpha: tk, spacing: 6 * S }); text(g, 'OF THE EARTH', W * .83, H * .44 + 56 * S, { size: 22 * S, font: FONT.latin, weight: 700, color: col(C.rose), alpha: tk, spacing: 6 * S }); }
644
+ };
645
+
646
+ /* ============ 10 尾声 · 慢慢来 ============ */
647
+ ACTS.coda = (g, u, W, H, X) => {
648
+ const { S, P, at, dur } = X;
649
+ const gy = H * (P ? .74 : .76);
650
+ const sun = ease.io(seg(u, 0, 4));
651
+ halo(g, W / 2, gy - 20 * S, 700 * S, [255, 170, 110], .7 * sun);
652
+ g.fillStyle = col([255, 214, 160], .9 * sun); g.beginPath(); g.arc(W / 2, gy - 10 * S - 60 * S * (1 - sun), 70 * S, Math.PI, 0); g.fill();
653
+ hills(g, W, H, gy - 40 * S, S, 'rgba(90,50,60,.7)', 1.2, .3);
654
+ hills(g, W, H, gy, S, '#1a0f12', .8, 1.6);
655
+ // 麦浪
656
+ for (let i = 0; i < (P ? 70 : 120); i++) { const x = W * h1(i * 2.7), h = (40 + h1(i) * 50) * S, sw = Math.sin(u * 1.2 + x * .01) * 8 * S;
657
+ g.strokeStyle = col([200, 150, 90], .7); g.lineWidth = 2 * S; g.beginPath(); g.moveTo(x, gy + 40 * S); g.quadraticCurveTo(x + sw * .5, gy + 40 * S - h * .5, x + sw, gy + 40 * S - h); g.stroke();
658
+ g.fillStyle = col([240, 200, 120], .8); g.beginPath(); g.ellipse(x + sw, gy + 40 * S - h, 3 * S, 9 * S, sw * .02, 0, TAU); g.fill(); }
659
+ for (let i = 0; i < (P ? 10 : 16); i++) flower(g, W * h1(i * 5.3), gy + 50 * S + h1(i * 3) * 50 * S, (12 + h1(i) * 10) * S, seg(u, .5 + i * .15, 1.5 + i * .15), { rot: i, layers: 2, inner: [220, 110, 130], outer: [255, 225, 225] });
660
+ // 公牛静立
661
+ const bk = seg(u, .4, 2.4);
662
+ const bx = P ? W * .5 : W * .3, by = gy - 112 * S;
663
+ halo(g, bx, by, 260 * S, [255, 200, 150], .5 * bk);
664
+ bullSide(g, bx, by, (P ? .95 : 1.05) * S, { t: u, alpha: bk });
665
+ // 星座
666
+ const ck = seg(u, at(1) - .2, at(2) + 1);
667
+ if (ck > 0) {
668
+ const M = mapper(W, H, 4.6, 19, (P ? W * .42 : W * .2) / 16, P ? W * .04 : -W * .02, P ? -H * .34 : -H * .26);
669
+ lines(g, [...HYA_L, ...HORN_L], M, ck, col(C.gold, .85), 2.4 * S);
670
+ for (const k of ['ald', 'elnath', 'zeta', 'gam', 'del', 'eps', 'th2']) { const [x, y] = M(STAR[k]); sparkle(g, x, y, rad(STAR[k][2], S) * .8, k === 'ald' ? [255, 170, 110] : C.ivory, ck, .6); }
671
+ PLEI.forEach(s => { const [x, y] = M(s); sparkle(g, x, y, 2 * S, C.sky, ck, .3); });
672
+ }
673
+ // 结尾标题
674
+ const tk = seg(u, at(3) - .3, at(3) + 1.4);
675
+ const tx = P ? W / 2 : W * .66, ty = P ? H * .44 : H * .44;
676
+ if (tk > 0) {
677
+ taurusSign(g, tx, ty - 150 * S, .75 * S, seg(u, at(3) - .3, at(3) + 1.6), { width: 10 });
678
+ reveal(g, '慢慢来', tx, ty, tk, { size: 96 * S, gilt: 'gold', weight: 900, spacing: 20 * S, glow: 'rgba(255,160,110,.6)', blur: 30 * S });
679
+ text(g, '也能抵达所有的美好', tx, ty + 86 * S, { size: 44 * S, color: col(C.ivory), weight: 700, alpha: seg(u, at(4), at(4) + 1), spacing: 8 * S, glow: 'rgba(0,0,0,.6)' });
680
+ divider(g, tx, ty + 140 * S, 380 * S, seg(u, at(4) + .4, at(4) + 1.4), S);
681
+ text(g, '金牛座 · TAURUS · 4.20 — 5.20', tx, ty + 182 * S, { size: 26 * S, font: FONT.serif, weight: 700, color: col(C.gold), alpha: seg(u, at(4) + .8, at(4) + 1.8), spacing: 4 * S });
682
+ }
683
+ petals(g, u, W, H, { n: 26, alpha: .7, seed: 41 });
684
+ const fo = seg(u, dur - 1.6, dur); if (fo > 0) { g.fillStyle = `rgba(4,3,4,${fo})`; g.fillRect(0, 0, W, H); }
685
+ };
taurus/js/backdrop.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ♉ 水彩天幕:启动时把每种色调预绘成离屏纹理,播放时漂移 + 交叉溶解(全新编写)
2
+ import { h1, col, clamp, lerp, TAU, halo } from './canvas-kit.js';
3
+
4
+ export const TONES = {
5
+ meadow: { base: ['#0b2a22', '#3a1838'], blobs: [[40, 140, 100], [220, 110, 150], [255, 190, 150], [20, 90, 90], [150, 60, 120]], star: .55, warm: [255, 200, 170] },
6
+ dusk: { base: ['#24160e', '#3a2416'], blobs: [[150, 96, 52], [96, 60, 40], [220, 170, 110], [70, 50, 60]], star: .5, warm: [255, 200, 140] },
7
+ night: { base: ['#060b1c', '#0d1430'], blobs: [[30, 50, 110], [60, 40, 100], [20, 80, 110], [90, 70, 140]], star: 1.2, warm: [170, 190, 255] },
8
+ ink: { base: ['#07100e', '#121a1a'], blobs: [[30, 60, 56], [60, 70, 70], [20, 40, 40], [110, 90, 60]], star: .9, warm: [220, 210, 180] },
9
+ sea: { base: ['#06202a', '#0c2f3c'], blobs: [[30, 110, 130], [60, 150, 160], [200, 170, 140], [20, 60, 90]], star: .6, warm: [200, 240, 255] },
10
+ rose: { base: ['#2a0f1c', '#3a1630'], blobs: [[190, 90, 120], [120, 50, 90], [240, 180, 170], [80, 40, 80]], star: .7, warm: [255, 190, 200] },
11
+ earth: { base: ['#1a1a0e', '#2a1c12'], blobs: [[110, 120, 60], [170, 110, 70], [70, 90, 50], [220, 180, 120]], star: .5, warm: [255, 220, 160] },
12
+ gilt: { base: ['#05140f', '#140a14'], blobs: [[30, 100, 76], [150, 110, 50], [60, 40, 70], [220, 180, 110]], star: .9, warm: [255, 220, 150] },
13
+ sunset: { base: ['#2a1420', '#4a2418'], blobs: [[255, 160, 110], [230, 110, 120], [255, 210, 150], [120, 60, 100]], star: .45, warm: [255, 200, 150] },
14
+ };
15
+
16
+ const TEX = 720;
17
+ function paintTexture(tone, seed) {
18
+ const T = TONES[tone], cv = document.createElement('canvas'); cv.width = cv.height = TEX; const g = cv.getContext('2d');
19
+ const gr = g.createLinearGradient(0, 0, TEX * .3, TEX); gr.addColorStop(0, T.base[0]); gr.addColorStop(1, T.base[1]); g.fillStyle = gr; g.fillRect(0, 0, TEX, TEX);
20
+ // 水彩色团:多层低透明度柔边圆,叠出晕染边缘
21
+ for (let i = 0; i < 260; i++) {
22
+ const c = T.blobs[i % T.blobs.length];
23
+ const cx = TEX * (h1(i * 3.1 + seed) * 1.2 - .1), cy = TEX * (h1(i * 7.3 + seed) * 1.2 - .1), r = TEX * (.05 + h1(i * 1.7 + seed) * .22);
24
+ const g2 = g.createRadialGradient(cx, cy, r * .2, cx, cy, r);
25
+ const a = .07 + h1(i + seed * 2) * .09;
26
+ g2.addColorStop(0, col(c, a)); g2.addColorStop(.7, col(c, a * .6)); g2.addColorStop(1, col(c, 0));
27
+ g.fillStyle = g2; g.beginPath(); g.ellipse(cx, cy, r, r * (.6 + h1(i + 11) * .6), h1(i + 5) * 3, 0, TAU); g.fill();
28
+ }
29
+ // 纸纹颗粒
30
+ const img = g.getImageData(0, 0, TEX, TEX), d = img.data;
31
+ for (let i = 0; i < d.length; i += 4) { const n = (h1(i * .25 + seed) - .5) * 10; d[i] += n; d[i + 1] += n; d[i + 2] += n; }
32
+ g.putImageData(img, 0, 0);
33
+ return cv;
34
+ }
35
+
36
+ const STARS = Array.from({ length: 420 }, (_, i) => [h1(i * 1.3), h1(i * 2.9 + 4), Math.pow(h1(i * 5.1 + 2), 3), h1(i + 99) * TAU]);
37
+ const BOKEH = Array.from({ length: 26 }, (_, i) => [h1(i * 4.1 + 1), h1(i * 6.7 + 2), .4 + h1(i * 2.3) * .6, h1(i + 33) * TAU]);
38
+
39
+ export function createBackdrop() {
40
+ const cache = {};
41
+ const tex = tone => cache[tone] || (cache[tone] = paintTexture(tone, Object.keys(TONES).indexOf(tone) * 13.7 + 2));
42
+ function layer(g, tone, t, W, H, a) {
43
+ if (a <= 0) return;
44
+ const cv = tex(tone), s = Math.max(W, H) * 1.25 / TEX;
45
+ const dx = Math.sin(t * .021) * W * .06 - W * .06, dy = Math.cos(t * .017) * H * .05 - H * .06;
46
+ g.save(); g.globalAlpha = a; g.drawImage(cv, (W - TEX * s) / 2 + dx, (H - TEX * s) / 2 + dy, TEX * s, TEX * s); g.restore();
47
+ }
48
+ // from → to 溶解,k = 0..1
49
+ function draw(g, t, W, H, from, to, k) {
50
+ g.fillStyle = '#05070a'; g.fillRect(0, 0, W, H);
51
+ layer(g, from, t, W, H, 1);
52
+ if (to !== from) layer(g, to, t, W, H, k);
53
+ const S = Math.min(W, H) / 1080;
54
+ const st = lerp(TONES[from].star, TONES[to].star, k);
55
+ // 星空
56
+ g.save(); g.globalCompositeOperation = 'lighter';
57
+ for (const [x, y, m, ph] of STARS) {
58
+ const tw = .55 + .45 * Math.sin(t * (1 + m * 2) + ph), r = (.6 + m * 2.2) * S;
59
+ g.fillStyle = col([255, 248, 235], clamp(st * tw * (.25 + m * .75)));
60
+ g.beginPath(); g.arc(x * W, y * H, r, 0, TAU); g.fill();
61
+ }
62
+ g.restore();
63
+ // 散景光斑
64
+ const warm = TONES[to].warm;
65
+ for (const [x, y, sz, ph] of BOKEH) {
66
+ const px = (x * W + Math.sin(t * .05 + ph) * 60 * S), py = (y * H + Math.cos(t * .04 + ph) * 40 * S);
67
+ halo(g, px, py, (40 + sz * 70) * S, warm, .16 * sz * (.6 + .4 * Math.sin(t * .5 + ph)));
68
+ }
69
+ // 暗角
70
+ const v = g.createRadialGradient(W / 2, H * .5, Math.min(W, H) * .35, W / 2, H * .5, Math.max(W, H) * .78);
71
+ v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(0,0,0,.6)'); g.fillStyle = v; g.fillRect(0, 0, W, H);
72
+ }
73
+ return { draw, warm: () => Object.keys(TONES).forEach(tex) };
74
+ }
taurus/js/canvas-kit.js ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ♉ 金牛座 · 绘图库(全新编写):数学 / 色彩 / 文字 / 光效 / 花草 / 公牛 / 符号
2
+ export const TAU = Math.PI * 2;
3
+ export const clamp = (v, a = 0, b = 1) => v < a ? a : v > b ? b : v;
4
+ export const lerp = (a, b, k) => a + (b - a) * k;
5
+ export const seg = (t, a, b) => b <= a ? (t >= b ? 1 : 0) : clamp((t - a) / (b - a));
6
+ export const ease = {
7
+ io: k => k < .5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2,
8
+ out: k => 1 - Math.pow(1 - k, 3),
9
+ out5: k => 1 - Math.pow(1 - k, 5),
10
+ in: k => k * k * k,
11
+ smooth: k => k * k * (3 - 2 * k),
12
+ back: k => { const c = 1.6; return k <= 0 ? 0 : 1 + (c + 1) * Math.pow(k - 1, 3) + c * Math.pow(k - 1, 2); },
13
+ };
14
+ // 包络:a 开始淡入(fi 秒),b 开始淡出(fo 秒)
15
+ export const env = (t, a, b, fi = .6, fo = .6) => Math.min(ease.smooth(seg(t, a, a + fi)), 1 - ease.smooth(seg(t, b, b + fo)));
16
+ export const h1 = n => { const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); };
17
+ export const col = (c, a = 1) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;
18
+
19
+ export const C = {
20
+ gold: [243, 211, 139], rose: [236, 170, 160], blush: [248, 205, 210], ivory: [255, 247, 232],
21
+ emerald: [40, 128, 98], jade: [120, 200, 170], sky: [170, 200, 255], red: [255, 120, 80], plum: [120, 50, 90],
22
+ };
23
+ export const FONT = {
24
+ serif: '"Noto Serif SC","Songti SC","STSong",serif',
25
+ sans: '"Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif',
26
+ latin: '"Cormorant Garamond","Noto Serif SC",serif',
27
+ };
28
+
29
+ /* ---------- 金属渐变 ---------- */
30
+ const GILT = {
31
+ gold: [[0, '#fff4d2'], [.22, '#f5d58a'], [.45, '#c99a45'], [.6, '#fbe3a6'], [.82, '#b9873a'], [1, '#f1cf86']],
32
+ rose: [[0, '#fff0ea'], [.25, '#f3c1b0'], [.5, '#c98774'], [.65, '#f8d6c8'], [.85, '#b3705f'], [1, '#efc0ae']],
33
+ pale: [[0, '#ffffff'], [.5, '#f6ead6'], [1, '#d9c3a0']],
34
+ };
35
+ export function gilt(g, x0, y0, x1, y1, kind = 'gold') {
36
+ const gr = g.createLinearGradient(x0, y0, x1, y1);
37
+ for (const [o, c] of GILT[kind]) gr.addColorStop(o, c);
38
+ return gr;
39
+ }
40
+
41
+ /* ---------- 光效 ---------- */
42
+ export function halo(g, x, y, r, c, a = 1) {
43
+ if (a <= 0 || r <= 0) return;
44
+ const gr = g.createRadialGradient(x, y, 0, x, y, r);
45
+ gr.addColorStop(0, col(c, .55 * a)); gr.addColorStop(.35, col(c, .2 * a)); gr.addColorStop(1, col(c, 0));
46
+ g.save(); g.globalCompositeOperation = 'lighter'; g.fillStyle = gr; g.fillRect(x - r, y - r, r * 2, r * 2); g.restore();
47
+ }
48
+ export function sparkle(g, x, y, r, c = C.ivory, a = 1, rays = 1) {
49
+ if (a <= 0) return;
50
+ halo(g, x, y, r * 5, c, a * .7);
51
+ g.save(); g.globalAlpha *= a; g.fillStyle = col([255, 255, 255]);
52
+ g.beginPath(); g.arc(x, y, r * .7, 0, TAU); g.fill();
53
+ if (rays > 0) {
54
+ g.globalCompositeOperation = 'lighter'; g.fillStyle = col(c, .9);
55
+ for (const [dx, dy] of [[1, 0], [0, 1]]) {
56
+ const L = r * 5 * rays, w = r * .32;
57
+ g.beginPath(); g.moveTo(x - dx * L, y - dy * L); g.lineTo(x + dy * w, y + dx * w); g.lineTo(x + dx * L, y + dy * L); g.lineTo(x - dy * w, y - dx * w); g.closePath(); g.fill();
58
+ }
59
+ }
60
+ g.restore();
61
+ }
62
+ export function dust(g, t, W, H, o = {}) {
63
+ const n = o.n ?? 60, sd = o.seed ?? 1, a = o.alpha ?? 1, S = Math.min(W, H) / 1080, c = o.color || C.gold;
64
+ for (let i = 0; i < n; i++) {
65
+ const k = (t * (o.speed ?? .02) * (.5 + h1(i + sd)) + h1(i * 3 + sd)) % 1;
66
+ const x = W * h1(i * 7 + sd) + Math.sin(t * .4 + i) * 30 * S, y = H * (1.05 - k * 1.1);
67
+ const tw = .5 + .5 * Math.sin(t * 2.3 + i * 1.7);
68
+ halo(g, x, y, (6 + 10 * h1(i + 5)) * S, c, a * tw * Math.sin(k * Math.PI));
69
+ }
70
+ }
71
+
72
+ /* ---------- 文字 ---------- */
73
+ export function text(g, s, x, y, o = {}) {
74
+ if ((o.alpha ?? 1) <= 0 || !s) return;
75
+ const size = o.size ?? 40;
76
+ g.save(); g.globalAlpha *= o.alpha ?? 1;
77
+ g.font = `${o.italic ? 'italic ' : ''}${o.weight ?? 700} ${size}px ${o.font ?? FONT.serif}`;
78
+ g.textAlign = o.align ?? 'center'; g.textBaseline = 'middle';
79
+ if ('letterSpacing' in g) g.letterSpacing = `${o.spacing ?? 0}px`;
80
+ const w = g.measureText(s).width;
81
+ const x0 = g.textAlign === 'center' ? x - w / 2 : g.textAlign === 'right' ? x - w : x;
82
+ if (o.glow) { g.shadowColor = o.glow; g.shadowBlur = o.blur ?? size * .5; }
83
+ if (o.outline) { g.lineJoin = 'round'; g.strokeStyle = o.outline; g.lineWidth = o.outlineW ?? size * .14; g.strokeText(s, x, y); }
84
+ g.fillStyle = o.gilt ? gilt(g, x0, y - size * .6, x0 + w * .15, y + size * .6, o.gilt) : (o.color ?? '#fff');
85
+ g.fillText(s, x, y);
86
+ if (o.gilt && o.edge !== false) { g.shadowColor = 'transparent'; g.globalAlpha *= .35; g.lineWidth = Math.max(.6, size * .012); g.strokeStyle = '#5a3a10'; g.strokeText(s, x, y); }
87
+ g.restore();
88
+ return w;
89
+ }
90
+ export function textWidth(g, s, size, font = FONT.serif, weight = 700, spacing = 0) {
91
+ g.save(); g.font = `${weight} ${size}px ${font}`; if ('letterSpacing' in g) g.letterSpacing = `${spacing}px`; const w = g.measureText(s).width; g.restore(); return w;
92
+ }
93
+ // 逐字浮现(带上浮与模糊感)
94
+ export function reveal(g, s, x, y, k, o = {}) {
95
+ if (k <= 0) return;
96
+ const chars = [...s], size = o.size ?? 60, sp = o.spacing ?? 0;
97
+ const widths = chars.map(c => textWidth(g, c, size, o.font, o.weight, 0));
98
+ const total = widths.reduce((a, b) => a + b, 0) + sp * (chars.length - 1);
99
+ let cx = (o.align === 'left' ? x : x - total / 2);
100
+ chars.forEach((c, i) => {
101
+ const kk = ease.out(clamp(k * (chars.length + 2) / 3 - i * .5 / 1.5 * 2 / (chars.length / 3 + 1)));
102
+ const q = clamp((k - i / chars.length * .6) / .4);
103
+ const e = ease.out(q);
104
+ text(g, c, cx + widths[i] / 2, y + (1 - e) * size * .35, { ...o, align: 'center', spacing: 0, alpha: (o.alpha ?? 1) * e });
105
+ cx += widths[i] + sp; void kk;
106
+ });
107
+ }
108
+ export function wrapText(g, s, maxW, size, font = FONT.sans, weight = 600) {
109
+ const chars = [...s], lines = []; let cur = '';
110
+ g.save(); g.font = `${weight} ${size}px ${font}`;
111
+ for (const ch of chars) { if (cur && g.measureText(cur + ch).width > maxW) { lines.push(cur); cur = ch; } else cur += ch; }
112
+ if (cur) lines.push(cur); g.restore();
113
+ if (lines.length === 2) { // 均衡两行,并避免行首标点
114
+ const all = [...lines.join('')]; let m = Math.ceil(all.length / 2);
115
+ while (m < all.length - 1 && ',。、!?;:”」)'.includes(all[m])) m++;
116
+ return [all.slice(0, m).join(''), all.slice(m).join('')];
117
+ }
118
+ return lines;
119
+ }
120
+
121
+ /* ---------- 形状 ---------- */
122
+ export function rrect(g, x, y, w, h, r) {
123
+ r = Math.min(r, w / 2, h / 2); g.beginPath(); g.moveTo(x + r, y);
124
+ g.arcTo(x + w, y, x + w, y + h, r); g.arcTo(x + w, y + h, x, y + h, r); g.arcTo(x, y + h, x, y, r); g.arcTo(x, y, x + w, y, r); g.closePath();
125
+ }
126
+ // 沿折线描出前 k 部分
127
+ export function strokePart(g, pts, k, o = {}) {
128
+ if (k <= 0 || pts.length < 2) return null;
129
+ let L = 0; const d = [0];
130
+ for (let i = 1; i < pts.length; i++) { L += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); d.push(L); }
131
+ const target = L * clamp(k); g.save(); g.beginPath(); g.moveTo(pts[0][0], pts[0][1]); let tip = pts[0];
132
+ for (let i = 1; i < pts.length; i++) {
133
+ if (d[i] <= target) { g.lineTo(pts[i][0], pts[i][1]); tip = pts[i]; }
134
+ else { const q = (target - d[i - 1]) / (d[i] - d[i - 1]); tip = [lerp(pts[i - 1][0], pts[i][0], q), lerp(pts[i - 1][1], pts[i][1], q)]; g.lineTo(tip[0], tip[1]); break; }
135
+ }
136
+ g.lineCap = 'round'; g.lineJoin = 'round'; g.strokeStyle = o.color ?? '#fff'; g.lineWidth = o.width ?? 2;
137
+ if (o.glow) { g.shadowColor = o.glow; g.shadowBlur = o.blur ?? 12; }
138
+ g.globalAlpha *= o.alpha ?? 1; g.stroke(); g.restore();
139
+ return tip;
140
+ }
141
+ export function bez(p0, p1, p2, p3, n = 40) { const out = []; for (let i = 0; i <= n; i++) { const t = i / n, u = 1 - t; out.push([u * u * u * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t * t * t * p3[0], u * u * u * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t * t * t * p3[1]]); } return out; }
142
+ export function arcPts(cx, cy, r, a0, a1, n = 60) { const out = []; for (let i = 0; i <= n; i++) { const a = lerp(a0, a1, i / n); out.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]); } return out; }
143
+
144
+ /* ---------- 花 ---------- */
145
+ function petalPath(g, len, wid) {
146
+ g.beginPath(); g.moveTo(0, 0);
147
+ g.bezierCurveTo(wid * .9, -len * .2, wid * 1.05, -len * .85, wid * .25, -len);
148
+ g.quadraticCurveTo(0, -len * .9, -wid * .25, -len);
149
+ g.bezierCurveTo(-wid * 1.05, -len * .85, -wid * .9, -len * .2, 0, 0); g.closePath();
150
+ }
151
+ // 牡丹 / 玫瑰:k 为绽放程度 0..1
152
+ export function flower(g, x, y, r, k, o = {}) {
153
+ if (k <= 0 || r <= 0) return;
154
+ const inner = o.inner ?? [214, 92, 120], outer = o.outer ?? [255, 214, 222], rot = o.rot ?? 0, layers = o.layers ?? 3, n = o.petals ?? 7;
155
+ g.save(); g.translate(x, y); g.rotate(rot); g.globalAlpha *= o.alpha ?? 1;
156
+ for (let L = 0; L < layers; L++) {
157
+ const lk = ease.back(clamp(k * 1.4 - L * .2)); if (lk <= 0) continue;
158
+ const len = r * (1 - L * .22) * lk, wid = len * .62, spread = lerp(.25, 1, lk);
159
+ for (let i = 0; i < n; i++) {
160
+ const a = (i / n) * TAU + L * .45;
161
+ g.save(); g.rotate(a * spread + (1 - spread) * L);
162
+ const gr = g.createLinearGradient(0, 0, 0, -len);
163
+ gr.addColorStop(0, col(inner)); gr.addColorStop(1, col(outer));
164
+ petalPath(g, len, wid); g.fillStyle = gr; g.fill();
165
+ g.strokeStyle = col(inner, .35); g.lineWidth = Math.max(.5, r * .015); g.stroke();
166
+ g.restore();
167
+ }
168
+ }
169
+ const ck = clamp(k * 2 - 1);
170
+ if (ck > 0) for (let i = 0; i < 9; i++) { const a = i / 9 * TAU, d = r * .12 * ck; g.fillStyle = col(C.gold); g.beginPath(); g.arc(Math.cos(a) * d, Math.sin(a) * d, r * .035, 0, TAU); g.fill(); }
171
+ g.restore();
172
+ }
173
+ export function leaf(g, x, y, len, ang, k = 1, c = [70, 140, 100]) {
174
+ if (k <= 0) return;
175
+ g.save(); g.translate(x, y); g.rotate(ang); g.scale(k, k);
176
+ g.beginPath(); g.moveTo(0, 0); g.quadraticCurveTo(len * .5, -len * .32, len, 0); g.quadraticCurveTo(len * .5, len * .32, 0, 0);
177
+ const gr = g.createLinearGradient(0, 0, len, 0); gr.addColorStop(0, col(c)); gr.addColorStop(1, col([c[0] + 60, c[1] + 60, c[2] + 40]));
178
+ g.fillStyle = gr; g.fill(); g.strokeStyle = col([255, 230, 170], .35); g.lineWidth = Math.max(.6, len * .02); g.beginPath(); g.moveTo(0, 0); g.lineTo(len * .9, 0); g.stroke();
179
+ g.restore();
180
+ }
181
+ // 花枝:沿点列生长,叶子与花依次出现
182
+ export function branch(g, pts, k, o = {}) {
183
+ if (k <= 0) return;
184
+ const S = o.S ?? 1;
185
+ strokePart(g, pts, k, { color: o.stem ?? '#3d6b4c', width: (o.width ?? 5) * S });
186
+ strokePart(g, pts, k, { color: col(C.gold, .5), width: 1.2 * S });
187
+ const n = pts.length;
188
+ for (let i = 4; i < n - 3; i += o.leafEvery ?? 6) {
189
+ const q = i / n, lk = ease.back(seg(k, q, q + .15)); if (lk <= 0) continue;
190
+ const [x0, y0] = pts[i - 1], [x1, y1] = pts[i]; const a = Math.atan2(y1 - y0, x1 - x0) + (i % 2 ? 1 : -1) * .9;
191
+ leaf(g, x1, y1, (o.leaf ?? 34) * S, a, lk, o.leafColor);
192
+ }
193
+ for (const [q, r, sd] of o.flowers ?? []) {
194
+ const p = pts[Math.min(n - 1, Math.floor(q * (n - 1)))]; const fk = seg(k, q, q + .25);
195
+ flower(g, p[0], p[1], r * S, fk, { rot: sd, inner: o.inner, outer: o.outer, layers: o.layers ?? 3 });
196
+ }
197
+ }
198
+ // 飘落花瓣
199
+ export function petals(g, t, W, H, o = {}) {
200
+ const n = o.n ?? 40, sd = o.seed ?? 3, a = o.alpha ?? 1, S = Math.min(W, H) / 1080;
201
+ const cols = o.colors ?? [[255, 196, 206], [255, 226, 232], [246, 170, 186], [255, 240, 220]];
202
+ if (a <= 0) return;
203
+ for (let i = 0; i < n; i++) {
204
+ const sp = (o.speed ?? .045) * (.6 + h1(i + sd) * .8), ph = (t * sp + h1(i * 5 + sd)) % 1;
205
+ const x = W * (h1(i * 13 + sd) * 1.2 - .1) + Math.sin(t * .7 + i) * 50 * S + (o.wind ?? 0) * ph * W;
206
+ const y = -40 * S + ph * (H + 80 * S), r = (9 + h1(i + 2) * 9) * S * (o.size ?? 1);
207
+ g.save(); g.translate(x, y); g.rotate(t * (.6 + h1(i)) + i); g.scale(1, Math.sin(t * 1.8 + i * 2)); g.globalAlpha *= a * (.75 + .25 * h1(i + 9));
208
+ g.fillStyle = col(cols[i % cols.length]); g.beginPath(); g.moveTo(0, -r); g.quadraticCurveTo(r * .9, -r * .2, 0, r); g.quadraticCurveTo(-r * .9, -r * .2, 0, -r); g.fill();
209
+ g.restore();
210
+ }
211
+ }
212
+
213
+ /* ---------- 新艺术花饰框 ---------- */
214
+ export function ornamentFrame(g, x, y, w, h, o = {}) {
215
+ const k = o.k ?? 1, S = o.S ?? 1, a = o.alpha ?? 1; if (k <= 0 || a <= 0) return;
216
+ g.save(); g.globalAlpha *= a;
217
+ const kind = o.metal ?? 'gold';
218
+ const per = [[x + 18 * S, y], [x + w - 18 * S, y], [x + w, y + 18 * S], [x + w, y + h - 18 * S], [x + w - 18 * S, y + h], [x + 18 * S, y + h], [x, y + h - 18 * S], [x, y + 18 * S], [x + 18 * S, y]];
219
+ strokePart(g, per, k, { color: gilt(g, x, y, x + w, y + h, kind), width: 2.2 * S });
220
+ const inset = 9 * S; rrect(g, x + inset, y + inset, w - inset * 2, h - inset * 2, 6 * S); g.strokeStyle = col(C.gold, .45 * k); g.lineWidth = 1 * S; g.stroke();
221
+ const ck = ease.out(seg(k, .5, 1));
222
+ if (ck > 0) for (const [cx, cy, sx, sy] of [[x, y, 1, 1], [x + w, y, -1, 1], [x, y + h, 1, -1], [x + w, y + h, -1, -1]]) {
223
+ g.save(); g.translate(cx, cy); g.scale(sx * S * ck, sy * S * ck); g.strokeStyle = gilt(g, 0, 0, 60, 60, kind); g.lineWidth = 2.2; g.lineCap = 'round';
224
+ g.beginPath(); g.moveTo(0, 46); g.bezierCurveTo(0, 14, 14, 0, 46, 0); g.stroke();
225
+ g.beginPath(); g.moveTo(10, 60); g.bezierCurveTo(10, 24, 24, 10, 60, 10); g.stroke();
226
+ g.beginPath(); g.arc(24, 24, 7, 0, TAU); g.stroke();
227
+ g.fillStyle = gilt(g, 0, 0, 40, 40, kind); g.beginPath(); g.arc(24, 24, 3, 0, TAU); g.fill();
228
+ g.beginPath(); g.moveTo(60, 10); g.bezierCurveTo(80, 10, 86, 24, 76, 30); g.bezierCurveTo(70, 34, 64, 26, 70, 22); g.stroke();
229
+ g.beginPath(); g.moveTo(10, 60); g.bezierCurveTo(10, 80, 24, 86, 30, 76); g.bezierCurveTo(34, 70, 26, 64, 22, 70); g.stroke();
230
+ g.restore();
231
+ }
232
+ g.restore();
233
+ }
234
+ // 花式分隔线
235
+ export function divider(g, x, y, w, k, S = 1, a = 1) {
236
+ if (k <= 0) return; g.save(); g.globalAlpha *= a; const hw = w / 2 * ease.out(k);
237
+ g.strokeStyle = gilt(g, x - hw, y, x + hw, y); g.lineWidth = 1.5 * S; g.beginPath(); g.moveTo(x - hw, y); g.lineTo(x - 14 * S, y); g.moveTo(x + 14 * S, y); g.lineTo(x + hw, y); g.stroke();
238
+ g.fillStyle = gilt(g, x - 10 * S, y - 10 * S, x + 10 * S, y + 10 * S); g.beginPath(); g.moveTo(x, y - 8 * S); g.lineTo(x + 8 * S, y); g.lineTo(x, y + 8 * S); g.lineTo(x - 8 * S, y); g.fill();
239
+ g.restore();
240
+ }
241
+
242
+ /* ---------- 公牛 ---------- */
243
+ // 侧身公牛(面向右),局部单位:身长约 360,蹄底 y = 125
244
+ function limb(g, top, knee, hoof, w0, w1, w2) {
245
+ // 以三点构造一条有粗细变化的腿
246
+ const n = (p, q) => { const dx = q[0] - p[0], dy = q[1] - p[1], L = Math.hypot(dx, dy) || 1; return [-dy / L, dx / L]; };
247
+ const n1 = n(top, knee), n2 = n(knee, hoof);
248
+ g.beginPath();
249
+ g.moveTo(top[0] + n1[0] * w0, top[1] + n1[1] * w0);
250
+ g.quadraticCurveTo(knee[0] + n1[0] * w1 * 1.3, knee[1] + n1[1] * w1 * 1.3, knee[0] + n2[0] * w1, knee[1] + n2[1] * w1);
251
+ g.lineTo(hoof[0] + n2[0] * w2, hoof[1] + n2[1] * w2);
252
+ g.lineTo(hoof[0] - n2[0] * w2, hoof[1] - n2[1] * w2);
253
+ g.lineTo(knee[0] - n2[0] * w1, knee[1] - n2[1] * w1);
254
+ g.quadraticCurveTo(knee[0] - n1[0] * w1 * 1.3, knee[1] - n1[1] * w1 * 1.3, top[0] - n1[0] * w0, top[1] - n1[1] * w0);
255
+ g.closePath();
256
+ }
257
+ export function bullSide(g, x, y, s, o = {}) {
258
+ const t = o.t ?? 0, walk = o.walk ?? 0, a = o.alpha ?? 1, white = !!o.white; if (a <= 0) return;
259
+ g.save(); g.translate(x, y); g.scale(s * (o.flip ? -1 : 1), s); g.globalAlpha *= a;
260
+ const cyc = t * 4.2, bob = Math.abs(Math.sin(cyc)) * -3 * walk, breath = Math.sin(t * 1.3) * (1 - walk);
261
+ const body = white ? (() => { const gr = g.createLinearGradient(0, -100, 0, 90); gr.addColorStop(0, '#ffffff'); gr.addColorStop(.55, '#f3eee6'); gr.addColorStop(1, '#cbbfae'); return gr; })() : gilt(g, -170, -110, 190, 90, o.metal ?? 'gold');
262
+ const farFill = white ? '#d6ccbe' : gilt(g, -170, -110, 190, 90, 'rose');
263
+ const edge = white ? col(C.gold, .85) : 'rgba(90,55,15,.7)', hoofC = white ? '#7d6a58' : '#5a3a12';
264
+ const legDef = (hx, hy, ph, back, far) => {
265
+ const sw = Math.sin(cyc + ph) * walk, lift = Math.max(0, Math.cos(cyc + ph)) * 10 * walk;
266
+ const top = [hx, hy], knee = back ? [hx - 14 + sw * 16, hy + 58] : [hx + 4 + sw * 18, hy + 50], hoof = [hx + (back ? -2 : 6) + sw * 30, 120 - lift];
267
+ limb(g, top, knee, hoof, back ? 24 : 18, 9, 7.5);
268
+ g.fillStyle = far ? farFill : body; g.fill(); g.strokeStyle = edge; g.lineWidth = 1.5; g.stroke();
269
+ g.fillStyle = hoofC; g.beginPath(); g.moveTo(hoof[0] - 9, hoof[1] - 4); g.lineTo(hoof[0] + 10, hoof[1] - 4); g.lineTo(hoof[0] + 12, hoof[1] + 5); g.lineTo(hoof[0] - 10, hoof[1] + 5); g.closePath(); g.fill();
270
+ };
271
+ g.translate(0, bob);
272
+ legDef(-112, 20, Math.PI, true, true); legDef(78, 26, 0, false, true);
273
+ // 尾巴
274
+ const tw = Math.sin(t * 1.7) * .3 + Math.sin(cyc) * .1 * walk;
275
+ g.save(); g.translate(-160, -40); g.rotate(tw); g.strokeStyle = white ? '#e9e2d6' : gilt(g, -30, 0, 0, 80); g.lineWidth = 5; g.lineCap = 'round'; g.beginPath(); g.moveTo(0, 0); g.bezierCurveTo(-18, 20, -22, 50, -16, 84); g.stroke();
276
+ g.fillStyle = white ? '#d8cfc2' : '#9b6a26'; g.beginPath(); g.moveTo(-16, 78); g.quadraticCurveTo(-30, 100, -14, 112); g.quadraticCurveTo(0, 98, -10, 78); g.fill(); g.restore();
277
+ // 身体
278
+ g.save(); g.translate(0, breath * .6);
279
+ const bodyPath = new Path2D();
280
+ bodyPath.moveTo(-165, -40);
281
+ bodyPath.bezierCurveTo(-150, -70, -80, -66, -10, -70);
282
+ bodyPath.bezierCurveTo(40, -74, 62, -104, 100, -100); // 肩峰
283
+ bodyPath.bezierCurveTo(130, -96, 146, -74, 156, -54);
284
+ bodyPath.bezierCurveTo(168, -36, 186, -14, 198, 12); // 额 → 鼻
285
+ bodyPath.bezierCurveTo(206, 30, 196, 44, 178, 42);
286
+ bodyPath.bezierCurveTo(160, 40, 146, 34, 136, 44); // 下颌
287
+ bodyPath.bezierCurveTo(126, 66, 112, 82, 92, 80); // 垂肉 / 胸
288
+ bodyPath.bezierCurveTo(60, 76, 30, 62, -20, 64); // 腹
289
+ bodyPath.bezierCurveTo(-70, 66, -110, 60, -140, 46);
290
+ bodyPath.bezierCurveTo(-170, 30, -176, -10, -165, -40);
291
+ bodyPath.closePath();
292
+ g.fillStyle = body; g.fill(bodyPath);
293
+ // 体积明暗
294
+ g.save(); g.clip(bodyPath);
295
+ const sh = g.createLinearGradient(0, -100, 0, 82); sh.addColorStop(0, 'rgba(255,255,240,.35)'); sh.addColorStop(.45, 'rgba(255,255,255,0)'); sh.addColorStop(1, white ? 'rgba(120,100,80,.35)' : 'rgba(90,45,5,.45)');
296
+ g.fillStyle = sh; g.fillRect(-200, -120, 420, 220);
297
+ const rg = g.createRadialGradient(100, -40, 10, 100, -40, 90); rg.addColorStop(0, 'rgba(255,255,230,.35)'); rg.addColorStop(1, 'rgba(255,255,230,0)'); g.fillStyle = rg; g.fillRect(0, -140, 220, 200);
298
+ g.strokeStyle = white ? 'rgba(170,150,120,.45)' : 'rgba(255,248,215,.55)'; g.lineWidth = 2;
299
+ g.beginPath(); g.moveTo(-130, -30); g.quadraticCurveTo(-100, 10, -126, 46); g.stroke();
300
+ g.beginPath(); g.moveTo(60, -70); g.quadraticCurveTo(84, -10, 70, 70); g.stroke();
301
+ g.beginPath(); g.moveTo(140, 20); g.quadraticCurveTo(150, 0, 168, -6); g.stroke();
302
+ g.restore();
303
+ g.strokeStyle = edge; g.lineWidth = 2; g.stroke(bodyPath);
304
+ // 牛角(远、近)
305
+ const horn = (dx, dy, far) => { g.save(); g.translate(dx, dy); g.beginPath(); g.moveTo(150, -66); g.bezierCurveTo(150, -104, 176, -126, 206, -120); g.bezierCurveTo(186, -114, 170, -98, 166, -60); g.closePath();
306
+ g.fillStyle = far ? '#bfa57a' : gilt(g, 140, -130, 210, -60, 'pale'); g.fill(); g.strokeStyle = 'rgba(110,80,30,.75)'; g.lineWidth = 1.5; g.stroke(); g.restore(); };
307
+ horn(-14, 6, true);
308
+ g.fillStyle = body; g.beginPath(); g.ellipse(146, -54, 20, 8, -.45, 0, TAU); g.fill(); g.strokeStyle = edge; g.lineWidth = 1.5; g.stroke();
309
+ horn(0, 0, false);
310
+ // 眼、鼻
311
+ g.fillStyle = '#24160a'; g.beginPath(); g.ellipse(172, -24, 5, 3.6, -.35, 0, TAU); g.fill();
312
+ g.fillStyle = 'rgba(255,255,255,.85)'; g.beginPath(); g.arc(173.5, -25.2, 1.4, 0, TAU); g.fill();
313
+ g.strokeStyle = edge; g.lineWidth = 1.2; g.beginPath(); g.arc(171, -26, 8, Math.PI * 1.1, Math.PI * 1.8); g.stroke();
314
+ g.fillStyle = white ? 'rgba(220,160,160,.8)' : 'rgba(110,60,25,.6)'; g.beginPath(); g.ellipse(196, 26, 4, 3, .4, 0, TAU); g.fill();
315
+ g.restore();
316
+ legDef(-88, 24, 0, true, false); legDef(100, 30, Math.PI, false, false);
317
+ g.restore();
318
+ }
319
+ // 正面公牛头徽(单位:��约 ±150)
320
+ export function bullHead(g, x, y, s, o = {}) {
321
+ const k = o.k ?? 1, a = o.alpha ?? 1; if (k <= 0 || a <= 0) return;
322
+ const kind = o.metal ?? 'gold';
323
+ g.save(); g.translate(x, y); g.scale(s, s); g.globalAlpha *= a;
324
+ const fill = gilt(g, -150, -120, 150, 130, kind), edge = 'rgba(80,45,10,.7)';
325
+ // 角(由内向外长出)
326
+ const hk = ease.out(seg(k, .2, 1));
327
+ for (const d of [-1, 1]) {
328
+ g.save(); g.scale(d, 1);
329
+ g.beginPath(); g.moveTo(42, -58); g.bezierCurveTo(70, -70, 120 * hk + 30 * (1 - hk), -60, 140 * hk + 40 * (1 - hk), -110 * hk - 60 * (1 - hk));
330
+ g.bezierCurveTo(130 * hk + 40 * (1 - hk), -50, 90, -38, 50, -36); g.closePath();
331
+ g.fillStyle = gilt(g, 40, -120, 150, -30, 'pale'); g.fill(); g.strokeStyle = edge; g.lineWidth = 2; g.stroke();
332
+ // 耳
333
+ g.beginPath(); g.moveTo(52, -30); g.bezierCurveTo(90, -40, 112, -20, 104, -6); g.bezierCurveTo(90, 4, 66, -6, 50, -10); g.closePath();
334
+ g.fillStyle = fill; g.fill(); g.stroke();
335
+ g.restore();
336
+ }
337
+ // 脸
338
+ g.beginPath(); g.moveTo(-58, -64); g.bezierCurveTo(-20, -78, 20, -78, 58, -64);
339
+ g.bezierCurveTo(66, -20, 52, 40, 40, 78); g.bezierCurveTo(30, 108, -30, 108, -40, 78);
340
+ g.bezierCurveTo(-52, 40, -66, -20, -58, -64); g.closePath();
341
+ g.fillStyle = fill; g.fill(); g.strokeStyle = edge; g.lineWidth = 2.2; g.stroke();
342
+ // 额卷毛
343
+ g.strokeStyle = 'rgba(110,70,20,.6)'; g.lineWidth = 2; for (let i = -2; i <= 2; i++) { g.beginPath(); g.arc(i * 14, -56, 8, .2, Math.PI - .2); g.stroke(); }
344
+ // 额饰菱形
345
+ g.fillStyle = o.gem ?? '#2f8f6b'; g.beginPath(); g.moveTo(0, -38); g.lineTo(10, -24); g.lineTo(0, -10); g.lineTo(-10, -24); g.closePath(); g.fill(); g.strokeStyle = gilt(g, -10, -38, 10, -10); g.lineWidth = 2; g.stroke();
346
+ g.fillStyle = 'rgba(255,255,255,.6)'; g.beginPath(); g.arc(-3, -28, 2.2, 0, TAU); g.fill();
347
+ // 眼
348
+ for (const d of [-1, 1]) { g.fillStyle = '#3a220b'; g.beginPath(); g.ellipse(d * 30, -8, 8, 5.5, d * .25, 0, TAU); g.fill(); g.fillStyle = col(o.eyeGlow ?? [255, 240, 200], .9); g.beginPath(); g.arc(d * 30 + 2, -10, 1.8, 0, TAU); g.fill(); }
349
+ // 鼻
350
+ g.beginPath(); g.ellipse(0, 76, 36, 22, 0, 0, TAU); g.fillStyle = gilt(g, -36, 54, 36, 98, 'rose'); g.fill(); g.strokeStyle = edge; g.lineWidth = 1.8; g.stroke();
351
+ g.fillStyle = 'rgba(70,35,10,.75)'; for (const d of [-1, 1]) { g.beginPath(); g.ellipse(d * 14, 78, 5, 7, d * .3, 0, TAU); g.fill(); }
352
+ // 中线
353
+ g.strokeStyle = 'rgba(255,250,225,.55)'; g.lineWidth = 2; g.beginPath(); g.moveTo(0, -6); g.lineTo(0, 50); g.stroke();
354
+ g.restore();
355
+ }
356
+
357
+ /* ---------- 符号 ---------- */
358
+ export function taurusSign(g, x, y, s, k = 1, o = {}) {
359
+ if (k <= 0) return;
360
+ const lw = (o.width ?? 9) * s / 1, a = o.alpha ?? 1;
361
+ const circle = arcPts(x, y + 22 * s, 38 * s, -Math.PI / 2, Math.PI * 1.5, 80);
362
+ const horns = arcPts(x, y - 52 * s, 48 * s, Math.PI * .88, Math.PI * .12, 60).map(([px, py]) => [px, py]);
363
+ // 上弧:开口向上的弯月
364
+ const cup = arcPts(x, y - 70 * s, 54 * s, Math.PI * .15, Math.PI * .85, 60);
365
+ const style = { color: o.color ?? gilt(g, x - 60 * s, y - 80 * s, x + 60 * s, y + 70 * s), width: lw, glow: o.glow ?? 'rgba(255,210,140,.8)', blur: 16 * s, alpha: a };
366
+ strokePart(g, cup, ease.io(seg(k, 0, .45)), style);
367
+ strokePart(g, circle, ease.io(seg(k, .35, 1)), style);
368
+ void horns;
369
+ }
370
+ export function venusSign(g, x, y, s, k = 1, o = {}) {
371
+ if (k <= 0) return;
372
+ const st = { color: o.color ?? gilt(g, x - 40 * s, y - 60 * s, x + 40 * s, y + 70 * s, 'rose'), width: (o.width ?? 8) * s, glow: 'rgba(255,190,190,.8)', blur: 14 * s, alpha: o.alpha ?? 1 };
373
+ strokePart(g, arcPts(x, y - 20 * s, 34 * s, -Math.PI / 2, Math.PI * 1.5, 70), seg(k, 0, .6), st);
374
+ strokePart(g, [[x, y + 14 * s], [x, y + 74 * s]], seg(k, .55, .8), st);
375
+ strokePart(g, [[x - 24 * s, y + 46 * s], [x + 24 * s, y + 46 * s]], seg(k, .75, 1), st);
376
+ }
377
+
378
+ /* ---------- 星空坐标 ---------- */
379
+ // 简单的方位投影:返回相对中心的角度(度),赤经向左增大
380
+ export function sky(ra, dec, ra0, dec0) {
381
+ const c = Math.cos(dec0 * Math.PI / 180);
382
+ return [-(ra - ra0) * 15 * c, -(dec - dec0)];
383
+ }
taurus/js/card.js ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ♉ 金牛座 · 鎏金珍藏卡(视频第 9 幕 + 页面单独导出共用,全新编写)
2
+ import { TAU, clamp, lerp, seg, ease, h1, col, C, FONT, gilt, halo, sparkle, text, rrect, flower, leaf, bullHead, taurusSign, venusSign } from './canvas-kit.js';
3
+
4
+ const CW = 630, CH = 880, R = 36;
5
+
6
+ function archPath(g, x, y, w, h) { // 拱形窗
7
+ g.beginPath(); g.moveTo(x, y + h); g.lineTo(x, y + w / 2); g.arc(x + w / 2, y + w / 2, w / 2, Math.PI, 0); g.lineTo(x + w, y + h); g.closePath();
8
+ }
9
+
10
+ function back(g, t) {
11
+ rrect(g, -CW / 2, -CH / 2, CW, CH, R); const gr = g.createLinearGradient(0, -CH / 2, 0, CH / 2); gr.addColorStop(0, '#0f3b2e'); gr.addColorStop(1, '#071a14'); g.fillStyle = gr; g.fill();
12
+ g.lineWidth = 10; g.strokeStyle = gilt(g, -CW / 2, -CH / 2, CW / 2, CH / 2, 'rose'); g.stroke();
13
+ g.save(); rrect(g, -CW / 2 + 26, -CH / 2 + 26, CW - 52, CH - 52, 22); g.clip();
14
+ g.strokeStyle = col(C.gold, .22); g.lineWidth = 1.2;
15
+ for (let i = -20; i <= 20; i++) { g.beginPath(); g.moveTo(i * 40 - 400, -460); g.lineTo(i * 40 + 400, 460); g.stroke(); g.beginPath(); g.moveTo(i * 40 + 400, -460); g.lineTo(i * 40 - 400, 460); g.stroke(); }
16
+ g.restore();
17
+ rrect(g, -CW / 2 + 26, -CH / 2 + 26, CW - 52, CH - 52, 22); g.strokeStyle = gilt(g, -300, -400, 300, 400); g.lineWidth = 2.5; g.stroke();
18
+ g.fillStyle = '#0b2b21'; g.beginPath(); g.arc(0, -10, 150, 0, TAU); g.fill(); g.lineWidth = 6; g.strokeStyle = gilt(g, -150, -160, 150, 140); g.stroke();
19
+ g.lineWidth = 1.5; g.beginPath(); g.arc(0, -10, 130, 0, TAU); g.stroke();
20
+ for (let i = 0; i < 36; i++) { const a = i / 36 * TAU; g.fillStyle = col(C.gold, .8); g.beginPath(); g.arc(Math.cos(a) * 140, -10 + Math.sin(a) * 140, 2.5, 0, TAU); g.fill(); }
21
+ taurusSign(g, 0, -4, 1.25, 1, { width: 12 });
22
+ text(g, 'TAURUS', 0, 220, { size: 46, font: FONT.latin, weight: 700, gilt: 'gold', spacing: 14 });
23
+ text(g, '金牛座 · 鎏金珍藏', 0, 280, { size: 26, color: col(C.gold, .85), weight: 700, spacing: 6 });
24
+ void t;
25
+ }
26
+
27
+ function front(g, st) {
28
+ const t = st.t ?? 0, F = st.fields ?? 1;
29
+ // 外框
30
+ rrect(g, -CW / 2, -CH / 2, CW, CH, R); g.fillStyle = gilt(g, -CW / 2, -CH / 2, CW / 2, CH / 2, 'rose'); g.fill();
31
+ rrect(g, -CW / 2 + 16, -CH / 2 + 16, CW - 32, CH - 32, R - 12); const pg = g.createLinearGradient(0, -CH / 2, 0, CH / 2); pg.addColorStop(0, '#0c2c23'); pg.addColorStop(.6, '#081c17'); pg.addColorStop(1, '#130a12'); g.fillStyle = pg; g.fill();
32
+ g.lineWidth = 2; g.strokeStyle = gilt(g, -300, -400, 300, 400); g.stroke();
33
+ // 拱窗插画
34
+ const ax = -238, ay = -392, aw = 476, ah = 430;
35
+ g.save(); archPath(g, ax, ay, aw, ah); g.clip();
36
+ const sg = g.createLinearGradient(0, ay, 0, ay + ah); sg.addColorStop(0, '#2a1838'); sg.addColorStop(.55, '#7a3c58'); sg.addColorStop(1, '#f2b49a'); g.fillStyle = sg; g.fillRect(ax, ay, aw, ah);
37
+ for (let i = 0; i < 60; i++) { const x = ax + h1(i * 3) * aw, y = ay + h1(i * 7) * ah * .6; g.fillStyle = col([255, 245, 230], .4 + .5 * Math.sin(t * 2 + i) ** 2); g.beginPath(); g.arc(x, y, .8 + h1(i) * 1.8, 0, TAU); g.fill(); }
38
+ // 昴星团小点
39
+ [[150, -330], [162, -318], [140, -312], [170, -300], [152, -300], [160, -340]].forEach(([x, y], i) => sparkle(g, x, y, 2.2, C.sky, .7 + .3 * Math.sin(t * 3 + i), .6));
40
+ // 远山与花丛
41
+ g.fillStyle = 'rgba(40,20,40,.55)'; g.beginPath(); g.moveTo(ax, ay + ah); for (let x = 0; x <= aw; x += 20) g.lineTo(ax + x, ay + ah - 70 - Math.sin(x * .02) * 18 - Math.sin(x * .05) * 8); g.lineTo(ax + aw, ay + ah); g.fill();
42
+ halo(g, 0, -170, 260, [255, 210, 150], .7);
43
+ bullHead(g, 0, -168, 1.02, { k: 1 });
44
+ for (const [x, y, r, i] of [[-200, 10, 46, 0], [-150, 30, 34, 1], [-225, -40, 28, 2], [200, 10, 46, 3], [150, 30, 34, 4], [225, -40, 28, 5]]) {
45
+ leaf(g, x, y + 10, 50, (x < 0 ? -.6 : -2.5) + h1(i) * .4, 1); flower(g, x, y, r, 1, { rot: i, inner: i % 2 ? [214, 92, 120] : [230, 150, 120], outer: i % 2 ? [255, 214, 222] : [255, 236, 200] });
46
+ }
47
+ g.restore();
48
+ archPath(g, ax, ay, aw, ah); g.lineWidth = 5; g.strokeStyle = gilt(g, ax, ay, ax + aw, ay + ah); g.stroke();
49
+ // 角标
50
+ g.fillStyle = '#0c2c23'; g.beginPath(); g.arc(-246, -350, 44, 0, TAU); g.fill(); g.lineWidth = 3; g.strokeStyle = gilt(g, -290, -394, -202, -306); g.stroke();
51
+ text(g, 'No.', -246, -366, { size: 18, font: FONT.latin, weight: 700, color: col(C.gold) });
52
+ text(g, '02', -246, -340, { size: 34, font: FONT.latin, weight: 700, gilt: 'gold' });
53
+ g.fillStyle = '#0c2c23'; g.beginPath(); g.arc(246, -350, 44, 0, TAU); g.fill(); g.stroke();
54
+ taurusSign(g, 246, -346, .42, 1, { width: 9, blur: 6 });
55
+ // 名称缎带
56
+ const ry = 48; g.save();
57
+ for (const d of [-1, 1]) { g.fillStyle = '#0d4a37'; g.beginPath(); g.moveTo(d * 270, ry - 22); g.lineTo(d * 312, ry - 22); g.lineTo(d * 296, ry); g.lineTo(d * 312, ry + 22); g.lineTo(d * 270, ry + 22); g.fill(); }
58
+ const rg = g.createLinearGradient(0, ry - 34, 0, ry + 34); rg.addColorStop(0, '#1f7a5c'); rg.addColorStop(1, '#0d4a37'); g.fillStyle = rg; g.shadowColor = 'rgba(0,0,0,.5)'; g.shadowBlur = 14;
59
+ g.beginPath(); g.moveTo(-282, ry - 32); g.quadraticCurveTo(0, ry - 46, 282, ry - 32); g.lineTo(282, ry + 32); g.quadraticCurveTo(0, ry + 18, -282, ry + 32); g.closePath(); g.fill(); g.shadowBlur = 0;
60
+ g.strokeStyle = gilt(g, -280, ry, 280, ry); g.lineWidth = 2.5; g.stroke(); g.restore();
61
+ text(g, '金牛座 · TAURUS', 0, ry - 6, { size: 40, gilt: 'gold', weight: 900, spacing: 6, glow: 'rgba(0,0,0,.5)', blur: 6 });
62
+ // 属性
63
+ const rows = [['守护星', '金星 ♀'], ['元素', '土 · 固定'], ['宫位', '黄道第二宫'], st.birthday ? ['生日', st.birthday] : ['星座季', '4.20 — 5.20']];
64
+ rows.forEach(([k, v], i) => {
65
+ const q = ease.out(seg(F, i * .14, i * .14 + .3)); if (q <= 0) return;
66
+ const x = i % 2 ? 140 : -140, y = 132 + Math.floor(i / 2) * 82;
67
+ g.save(); g.globalAlpha *= q; rrect(g, x - 128, y - 32, 256, 64, 12); g.fillStyle = 'rgba(255,240,210,.06)'; g.fill(); g.strokeStyle = col(C.gold, .45); g.lineWidth = 1.2; g.stroke(); g.restore();
68
+ text(g, k, x, y - 13, { size: 17, font: FONT.sans, weight: 600, color: col(C.rose), alpha: q, spacing: 4 });
69
+ text(g, v, x, y + 13, { size: 27, weight: 900, gilt: 'gold', alpha: q });
70
+ });
71
+ // 品质宝石
72
+ const qs = ['坚定', '温柔', '可靠'];
73
+ qs.forEach((s, i) => {
74
+ const q = ease.back(seg(F, .55 + i * .12, .75 + i * .12)); if (q <= 0) return;
75
+ const x = (i - 1) * 168, y = 306;
76
+ g.save(); g.translate(x, y); g.scale(q, q); rrect(g, -70, -26, 140, 52, 26); const cg = g.createLinearGradient(0, -26, 0, 26); cg.addColorStop(0, '#f6c9c4'); cg.addColorStop(1, '#c97f74'); g.fillStyle = cg; g.fill();
77
+ g.strokeStyle = gilt(g, -70, -26, 70, 26); g.lineWidth = 2; g.stroke(); g.restore();
78
+ text(g, s, x, y + 1, { size: 28 * q, weight: 900, color: '#4a1420' });
79
+ });
80
+ // 署名
81
+ const nk = seg(F, .85, 1);
82
+ text(g, '大地守护者', 0, 370, { size: 34, weight: 900, gilt: 'gold', spacing: 10, alpha: nk });
83
+ const holder = (st.holder || '').trim();
84
+ text(g, holder ? `珍藏人 · ${holder}` : '珍藏人 · 每一位金牛座', 0, 410, { size: 22, weight: 700, color: col(C.ivory, .92), alpha: nk, spacing: 2 });
85
+ text(g, 'GUARDIAN OF THE EARTH · LIMITED №0420', 0, 440, { size: 13, font: FONT.latin, weight: 700, color: col(C.gold, .8), alpha: nk, spacing: 3 });
86
+ venusSign(g, -262, 404, .32, nk, { width: 7 });
87
+ g.save(); g.globalAlpha *= nk; taurusSign(g, 262, 400, .3, 1, { width: 8, blur: 4 }); g.restore();
88
+ }
89
+
90
+ // 镭射全息:彩虹带随 shine 扫过 + 细闪
91
+ function holo(g, shine, t, strength = 1) {
92
+ g.save(); rrect(g, -CW / 2, -CH / 2, CW, CH, R); g.clip();
93
+ g.globalCompositeOperation = 'overlay';
94
+ const x = lerp(-900, 900, shine);
95
+ const gr = g.createLinearGradient(x - 380, -440, x + 380, 440);
96
+ ['rgba(255,80,160,0)', 'rgba(255,120,170,.55)', 'rgba(255,230,120,.6)', 'rgba(120,255,200,.55)', 'rgba(120,170,255,.55)', 'rgba(200,120,255,0)'].forEach((c, i, a) => gr.addColorStop(i / (a.length - 1), c));
97
+ g.globalAlpha = strength; g.fillStyle = gr; g.fillRect(-CW / 2, -CH / 2, CW, CH);
98
+ g.globalCompositeOperation = 'lighter';
99
+ const wg = g.createLinearGradient(x - 90, -440, x + 90, 440); wg.addColorStop(0, 'rgba(255,255,255,0)'); wg.addColorStop(.5, 'rgba(255,250,235,.35)'); wg.addColorStop(1, 'rgba(255,255,255,0)');
100
+ g.fillStyle = wg; g.fillRect(-CW / 2, -CH / 2, CW, CH);
101
+ for (let i = 0; i < 40; i++) { const px = (h1(i * 3) - .5) * CW, py = (h1(i * 5) - .5) * CH; const d = Math.abs(px - x * .7) / 300; if (d < 1) sparkle(g, px, py, 2 + h1(i) * 2, [255, 240, 255], (1 - d) * (.5 + .5 * Math.sin(t * 6 + i)), .8); }
102
+ g.restore();
103
+ }
104
+
105
+ function stamp(g, k) {
106
+ if (k <= 0) return;
107
+ const s = lerp(2.4, 1, ease.out5(k)), a = clamp(k * 3);
108
+ g.save(); g.translate(196, 290); g.rotate(-.22); g.scale(s, s); g.globalAlpha *= a * .92;
109
+ g.strokeStyle = '#c4302b'; g.fillStyle = 'rgba(196,48,43,.12)'; g.lineWidth = 5; g.beginPath(); g.arc(0, 0, 74, 0, TAU); g.fill(); g.stroke();
110
+ g.lineWidth = 2; g.beginPath(); g.arc(0, 0, 62, 0, TAU); g.stroke();
111
+ text(g, '认证', 0, -6, { size: 40, weight: 900, color: '#c4302b' });
112
+ text(g, '大地守护者', 0, 32, { size: 17, weight: 900, color: '#c4302b', spacing: 2 });
113
+ for (let i = 0; i < 5; i++) { const an = -Math.PI * .8 + i * .4; g.fillStyle = '#c4302b'; g.beginPath(); g.arc(Math.cos(an) * 48, Math.sin(an) * 48 - 2, 3, 0, TAU); g.fill(); }
114
+ g.restore();
115
+ }
116
+
117
+ /**
118
+ * st: { t, flip 0..1, fields 0..1, shine 0..1|undefined, stamp 0..1, holder, birthday, tilt }
119
+ */
120
+ export function drawCard(g, cx, cy, height, st = {}) {
121
+ const s = height / CH, flip = st.flip ?? 1, ang = flip * Math.PI, sx = Math.cos(ang);
122
+ g.save(); g.translate(cx, cy); g.rotate(st.tilt ?? 0); g.scale(s * Math.max(.02, Math.abs(sx)), s);
123
+ // 投影
124
+ g.save(); g.shadowColor = 'rgba(0,0,0,.55)'; g.shadowBlur = 60 * s; g.shadowOffsetY = 30 * s; rrect(g, -CW / 2, -CH / 2, CW, CH, R); g.fillStyle = '#000'; g.fill(); g.restore();
125
+ if (sx > 0) back(g, st.t); else { g.scale(-1, 1); g.scale(-1, 1); front(g, st); }
126
+ if (sx <= 0 && st.shine !== undefined) holo(g, st.shine, st.t ?? 0, st.holo ?? 1);
127
+ if (sx <= 0) stamp(g, st.stamp ?? 0);
128
+ // 翻转时的明暗
129
+ const shade = 1 - Math.abs(sx); if (shade > .02) { rrect(g, -CW / 2, -CH / 2, CW, CH, R); g.fillStyle = `rgba(255,240,210,${shade * .35})`; g.fill(); }
130
+ g.restore();
131
+ }
132
+
133
+ // 单独的海报(壁纸 9:16 / 头像 1:1 / 收藏卡 3:4)
134
+ export function drawCardPoster(g, W, H, opts, paint) {
135
+ paint(g, W, H);
136
+ const S = Math.min(W, H) / 1080, portrait = H > W * 1.25;
137
+ const ch = portrait ? Math.min(H * .66, W * .9 / CW * CH) : H * .86;
138
+ const cy = portrait ? H * .48 : H / 2;
139
+ halo(g, W / 2, cy, ch * .8, [255, 200, 150], .7);
140
+ drawCard(g, W / 2, cy, ch, { t: 1.7, flip: 1, fields: 1, shine: .62, holo: .55, stamp: 1, holder: opts.name, birthday: opts.birthday });
141
+ if (portrait) {
142
+ text(g, '♉ 金牛座 · 慢慢来,也能抵达美好', W / 2, H * .08, { size: 40 * S, weight: 700, color: col(C.gold), spacing: 4 * S });
143
+ text(g, '愿你按照自己的节奏,稳稳地发光', W / 2, H * .9, { size: 36 * S, weight: 700, color: col(C.ivory), spacing: 2 * S });
144
+ text(g, '星座文化 · 一张属于金牛座的珍藏卡', W / 2, H * .94, { size: 24 * S, font: FONT.sans, weight: 400, color: col(C.rose, .9) });
145
+ }
146
+ }
147
+ export const CARD_RATIO = CW / CH;
taurus/js/film.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ♉ 金牛座 —— 合成器:水彩天幕 + 十幕交叉溶解 + 花瓣光幕转场 + 字幕 + 角标(全新编写)
2
+ import { FILM } from './script.js';
3
+ import { ACTS } from './acts.js';
4
+ import { createBackdrop } from './backdrop.js';
5
+ import { clamp, lerp, seg, ease, h1, col, C, FONT, gilt, halo, text, wrapText, taurusSign } from './canvas-kit.js';
6
+
7
+ const BLEND = 1.0;
8
+ export const actAt = t => { const A = FILM.acts; for (let i = 0; i < A.length; i++) if (t < A[i].end) return i; return A.length - 1; };
9
+
10
+ export function createFilm() {
11
+ const bg = createBackdrop();
12
+
13
+ function subtitle(g, t, W, H) {
14
+ const P = H > W, S = Math.min(W, H) / 1080;
15
+ let cur = null; for (const a of FILM.acts) for (const s of a.subs) if (t >= s.t0 - .1 && t < s.t1 + .2) cur = s;
16
+ if (!cur) return;
17
+ const a = Math.min(ease.smooth(seg(t, cur.t0 - .1, cur.t0 + .15)), 1 - ease.smooth(seg(t, cur.t1, cur.t1 + .2)));
18
+ const size = (P ? 48 : 42) * S, lines = wrapText(g, cur.text, P ? W * .82 : W * .62, size), lh = size * 1.45;
19
+ const cy = (P ? H * .885 : H * .9) - (lines.length - 1) * lh / 2;
20
+ g.save(); g.font = `600 ${size}px ${FONT.sans}`; const w = Math.max(...lines.map(l => g.measureText(l).width)) + 90 * S; g.restore();
21
+ g.save(); g.globalAlpha = a * .85;
22
+ const gr = g.createLinearGradient(W / 2 - w / 2, 0, W / 2 + w / 2, 0);
23
+ gr.addColorStop(0, 'rgba(8,16,14,0)'); gr.addColorStop(.15, 'rgba(8,16,14,.8)'); gr.addColorStop(.85, 'rgba(8,16,14,.8)'); gr.addColorStop(1, 'rgba(8,16,14,0)');
24
+ g.fillStyle = gr; const top = cy - lh * .7, hgt = lines.length * lh + lh * .4 - lh * .45; g.fillRect(W / 2 - w / 2, top, w, hgt);
25
+ g.fillStyle = gilt(g, W / 2 - w * .3, 0, W / 2 + w * .3, 0, 'rose'); g.fillRect(W / 2 - w * .28, top, w * .56, 1.4 * S); g.fillRect(W / 2 - w * .28, top + hgt, w * .56, 1.4 * S);
26
+ g.restore();
27
+ lines.forEach((l, i) => text(g, l, W / 2, cy + i * lh, { size, font: FONT.sans, weight: 600, color: '#fff8ee', alpha: a, outline: 'rgba(10,14,12,.85)', outlineW: size * .12 }));
28
+ }
29
+
30
+ function hud(g, t, W, H, i) {
31
+ const P = H > W, S = Math.min(W, H) / 1080, a = .9 * seg(t, 1.5, 3); if (a <= 0) return;
32
+ const x = (P ? 44 : 54) * S, y = (P ? 90 : 58) * S;
33
+ g.save(); g.globalAlpha = a; taurusSign(g, x + 14 * S, y + 4 * S, .2 * S, 1, { width: 14, blur: 4 }); g.restore();
34
+ text(g, '金牛座 · 慢慢来,也能抵达美好', x + 44 * S, y - 8 * S, { size: 23 * S, align: 'left', color: col(C.gold), alpha: a, weight: 700 });
35
+ const A = FILM.acts[i], ka = Math.min(seg(t, A.start, A.start + .8), 1 - seg(t, A.end - .8, A.end));
36
+ text(g, `${String(i + 1).padStart(2, '0')} ${A.title}`, x + 44 * S, y + 22 * S, { size: 18 * S, align: 'left', color: col(C.ivory, .78), alpha: a * ka, font: FONT.sans, weight: 400 });
37
+ // 进度:一枝藤蔓
38
+ const w = W - 2 * x, py = (P ? 44 : 26) * S, k = t / FILM.duration;
39
+ g.save(); g.globalAlpha = a * .55; g.fillStyle = 'rgba(255,255,255,.12)'; g.fillRect(x, py, w, 1.6 * S); g.fillStyle = gilt(g, x, 0, x + w, 0, 'rose'); g.fillRect(x, py, w * k, 1.6 * S); g.restore();
40
+ for (let j = 1; j < FILM.acts.length; j++) { const q = FILM.acts[j].start / FILM.duration; g.fillStyle = col(q <= k ? C.blush : [255, 255, 255], a * .6); g.beginPath(); g.arc(x + w * q, py + .8 * S, 2.6 * S, 0, Math.PI * 2); g.fill(); }
41
+ halo(g, x + w * k, py, 12 * S, [255, 200, 190], a);
42
+ }
43
+
44
+ // 转场:一道斜向的柔光幕,带着金粉与花瓣横扫
45
+ function sweep(g, t, W, H, i) {
46
+ if (!i) return; const d = t - FILM.acts[i].start; if (d < -.4 || d > 1.2) return;
47
+ const S = Math.min(W, H) / 1080, k = clamp((d + .4) / 1.6), x = lerp(-W * .4, W * 1.4, ease.io(k)), a = Math.sin(k * Math.PI);
48
+ g.save(); g.globalCompositeOperation = 'lighter';
49
+ const gr = g.createLinearGradient(x - 320 * S, 0, x + 320 * S, H * .3); gr.addColorStop(0, 'rgba(255,200,180,0)'); gr.addColorStop(.5, col([255, 214, 190], .28 * a)); gr.addColorStop(1, 'rgba(255,200,180,0)');
50
+ g.fillStyle = gr; g.fillRect(0, 0, W, H); g.restore();
51
+ for (let j = 0; j < 36; j++) { const y = H * h1(j * 3 + i), px = x + (h1(j * 7 + i) - .5) * 360 * S; halo(g, px, y, (6 + h1(j) * 14) * S, j % 3 ? C.gold : C.blush, a * .9); }
52
+ }
53
+
54
+ function draw(g, t, W, H, opts = {}) {
55
+ t = clamp(t, 0, FILM.duration);
56
+ const i = actAt(t), A = FILM.acts[i], prev = FILM.acts[Math.max(0, i - 1)];
57
+ const S = Math.min(W, H) / 1080, P = H > W;
58
+ g.save();
59
+ const k = i ? ease.smooth(seg(t, A.start - .3, A.start + 1.6)) : 1;
60
+ bg.draw(g, t, W, H, i ? prev.tone : A.tone, A.tone, k);
61
+ const run = (j, alpha) => {
62
+ if (alpha <= .003) return; const a = FILM.acts[j];
63
+ const X = { S, P, dur: a.end - a.start, opts, at: n => (a.subs[n] ? a.subs[n].t0 : a.subs[a.subs.length - 1].t1) - a.start };
64
+ g.save(); g.globalAlpha = alpha;
65
+ try { ACTS[a.id](g, Math.max(0, t - a.start), W, H, X); } catch (e) { console.error('act', a.id, e); }
66
+ g.restore();
67
+ };
68
+ const kin = i ? ease.smooth(seg(t, A.start, A.start + BLEND)) : 1;
69
+ if (i && kin < 1) run(i - 1, 1 - kin);
70
+ run(i, kin);
71
+ sweep(g, t, W, H, i);
72
+ if (opts.subtitles !== false) subtitle(g, t, W, H);
73
+ if (opts.hud !== false) hud(g, t, W, H, i);
74
+ const fi = 1 - seg(t, 0, 1.6); if (fi > 0) { g.fillStyle = `rgba(3,4,4,${fi})`; g.fillRect(0, 0, W, H); }
75
+ g.restore();
76
+ }
77
+ return { draw, backdrop: bg };
78
+ }