Spaces:
Running
Running
Download taurus/js/acts.js from Ethscriptions/POLS: direct link, hf CLI and curl.
- Browser
- Download file 60.6 kB
-
https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/taurus/js/acts.js
- Command line
-
hf download hf://spaces/Ethscriptions/POLS/taurus/js/acts.js
-
curl -L -o acts.js https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/taurus/js/acts.js
60.6 kB
| // ♉ 金牛座 —— 十幕(全新编写)。每幕:(g, u, W, H, X),u = 本幕内时间;X = { S, P, at(n), dur, opts } | |
| import { TAU, clamp, lerp, seg, ease, env, h1, col, C, FONT, gilt, halo, sparkle, dust, text, reveal, rrect, strokePart, bez, arcPts, | |
| flower, leaf, branch, petals, ornamentFrame, divider, bullSide, bullHead, taurusSign, venusSign, sky } from './canvas-kit.js'; | |
| import { drawCard } from './card.js'; | |
| /* ---------- 星表(赤经 h,赤纬 °,星等) ---------- */ | |
| const STAR = { | |
| 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], | |
| lam: [4.011, 12.49, 3.4], omi: [3.414, 9.03, 3.6], xi: [3.453, 9.73, 3.7], | |
| 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], | |
| }; | |
| 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]]; | |
| const ORI_L = [['bet', 'bel'], ['bel', 'min'], ['min', 'aln'], ['aln', 'alk'], ['alk', 'bet'], ['min', 'rig'], ['alk', 'sai'], ['rig', 'sai'], ['bet', 'mei'], ['bel', 'mei']]; | |
| const HYA_L = [['gam', 'del'], ['del', 'eps'], ['gam', 'th2'], ['th2', 'ald']]; | |
| const HORN_L = [['eps', 'elnath'], ['ald', 'zeta']]; | |
| const BODY_L = [['gam', 'lam'], ['lam', 'omi'], ['lam', 'xi']]; | |
| 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]); | |
| const rad = (m, S) => (1.2 + (4.6 - m) * 1.15) * S; | |
| 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]; }; } | |
| function lines(g, L, M, k, color, w) { | |
| 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]); | |
| strokePart(g, [p, r], q, { color, width: w, glow: color, blur: 8 }); }); | |
| } | |
| function label(g, x, y, t1, t2, a, S, o = {}) { | |
| if (a <= 0) return; const side = o.side ?? 1, al = side > 0 ? 'left' : 'right'; | |
| const ex = x + side * 46 * S, ey = y - 46 * S; | |
| 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(); | |
| 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 }); | |
| 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 }); | |
| } | |
| 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(); } } | |
| // 圆角玻璃标题卡 | |
| function plate(g, cx, cy, w, h, a, S, o = {}) { | |
| if (a <= 0) return; g.save(); g.globalAlpha *= a; rrect(g, cx - w / 2, cy - h / 2, w, h, (o.r ?? 18) * S); | |
| 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(); | |
| 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' }); | |
| } | |
| function hills(g, W, H, y0, S, color, amp = 1, ph = 0) { | |
| g.fillStyle = color; g.beginPath(); g.moveTo(0, H); | |
| 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); | |
| g.lineTo(W, H); g.closePath(); g.fill(); | |
| } | |
| function grass(g, W, y0, u, S, n = 140, c = 'rgba(30,60,40,.95)') { | |
| g.strokeStyle = c; g.lineCap = 'round'; | |
| 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; | |
| 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(); } | |
| } | |
| const corners = (W, H, S, P) => { | |
| const m = 1; | |
| return [ | |
| bez([W * (P ? .42 : .3), -10 * S], [W * .14, H * .1], [W * .05, H * .2], [-20 * S, H * .42]), | |
| bez([W * (P ? .58 : .7), -10 * S], [W * .86, H * .1], [W * .95, H * .2], [W + 20 * S, H * .42]), | |
| bez([-20 * S, H * .72], [W * .1, H * .78], [W * .12, H * .95], [W * (P ? .36 : .22), H + 10 * S]), | |
| bez([W + 20 * S, H * .7], [W * .9, H * .8], [W * .9, H * .95], [W * (P ? .66 : .8), H + 10 * S]), | |
| ].map(p => p.map(([x, y]) => [x, y * m])); | |
| }; | |
| export const ACTS = {}; | |
| /* ============ 1 序章 · 繁花里的公牛 ============ */ | |
| ACTS.bloom = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| const gy = H * (P ? .76 : .8); | |
| // 地平线与晨晖 | |
| halo(g, W / 2, gy, Math.max(W, H) * .6, [255, 190, 160], .35 + .25 * seg(u, at(1), at(4))); | |
| hills(g, W, H, gy - 50 * S, S, 'rgba(36,52,48,.75)', 1.2, 1); | |
| hills(g, W, H, gy, S, '#0f1d18', 1, 2.4); | |
| grass(g, W, gy, u, S, P ? 90 : 160); | |
| // 四角花枝生长 | |
| const vines = corners(W, H, S, P); | |
| 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]], | |
| inner: i % 2 ? [214, 92, 120] : [228, 140, 110], outer: i % 2 ? [255, 214, 222] : [255, 232, 205] })); | |
| petals(g, u, W, H, { n: P ? 30 : 46, alpha: seg(u, at(2), at(3) + 1), wind: .1 }); | |
| // 公牛缓步走来,蹄下开花 | |
| const wk = ease.io(seg(u, at(5) - .6, at(8) + .8)); | |
| const bs = (P ? 1.15 : 1.35) * S, bx = lerp(-W * .3, W * (P ? .46 : .47), wk), by = gy - 118 * bs; | |
| const walking = wk > 0 && wk < 1 ? 1 : 0; | |
| if (wk > 0) { | |
| for (let i = 0; i < 9; i++) { // 足迹花 | |
| const q = (i + 1) / 10; if (wk < q) break; const fx = lerp(-W * .3, W * .47, q) + 40 * S, age = (wk - q) * 12; | |
| flower(g, fx, gy + 8 * S, 14 * S, clamp(age), { layers: 2, petals: 6, rot: i, inner: [240, 160, 170], outer: [255, 240, 230] }); | |
| } | |
| halo(g, bx, by, 300 * S, [255, 210, 150], .5); | |
| bullSide(g, bx, by, bs, { t: u, walk: walking * (1 - seg(wk, .9, 1)) }); | |
| } | |
| // 标题 | |
| const tk = seg(u, at(9) - .2, at(9) + 1.6); | |
| const ty = P ? H * .2 : H * .23; | |
| if (tk > 0) { | |
| halo(g, W / 2, ty, 420 * S, [255, 200, 140], .45 * tk); | |
| 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 }); | |
| 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 }); | |
| divider(g, W / 2, ty + (P ? 140 : 132) * S, 420 * S, seg(u, at(9) + 1, at(9) + 2), S); | |
| // No.2 徽章 | |
| const nk = ease.back(seg(u, at(9) + 1.2, at(9) + 2)); | |
| if (nk > 0) { | |
| const nx = W / 2 - (P ? 170 : 190) * S, ny = ty + 215 * S; | |
| 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(); | |
| g.lineWidth = 4 * S; g.strokeStyle = gilt(g, -54 * S, -54 * S, 54 * S, 54 * S, 'rose'); g.stroke(); g.restore(); | |
| text(g, 'No.2', nx, ny - 4 * S, { size: 30 * S * nk, font: FONT.latin, weight: 700, gilt: 'gold' }); | |
| text(g, '黄道第二宫', nx, ny + 74 * S, { size: 22 * S, color: col(C.ivory), alpha: clamp(nk), font: FONT.sans, weight: 600 }); | |
| } | |
| } | |
| // 日期花签 | |
| const dk = seg(u, at(10) - .2, at(10) + .9); | |
| if (dk > 0) { | |
| const dx = W / 2 + (P ? 90 : 100) * S, dy = ty + 215 * S; | |
| plate(g, dx, dy, 300 * S * ease.out(dk), 84 * S, dk, S, { r: 42, orn: .45, metal: 'rose' }); | |
| 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 }); | |
| } | |
| dust(g, u, W, H, { n: 30, alpha: .7 }); | |
| }; | |
| /* ============ 2 最古老的星座 ============ */ | |
| ACTS.ancient = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| 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); | |
| // 标题卡 | |
| const hk = env(u, .2, at(1) - .3, .7, .5); | |
| if (hk > 0) { const y = P ? H * .4 : H * .42; plate(g, W / 2, y, (P ? 900 : 1000) * S, 200 * S, hk, S); | |
| text(g, '最古老的星座之一', W / 2, y - 24 * S, { size: (P ? 64 : 70) * S, gilt: 'gold', weight: 900, alpha: hk, spacing: 8 * S }); | |
| 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 }); } | |
| // A:五千年前的春分 | |
| const ak = phA * seg(u, at(1) - .4, at(1) + .4); | |
| if (ak > 0) { | |
| const yrK = ease.io(seg(u, at(1) - .3, at(2) + .6)), yr = Math.round(lerp(2026, -3000, yrK)); | |
| const cy = P ? H * .2 : H * .2; | |
| text(g, yr > 0 ? `公元 ${yr} 年` : `公元前 ${-yr} 年`, W / 2, cy, { size: 64 * S, gilt: 'gold', weight: 900, alpha: ak, font: FONT.serif }); | |
| // 时间尺 | |
| const rw = (P ? W * .86 : W * .7), rx = W / 2 - rw / 2, ry = cy + 70 * S; | |
| 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(); | |
| 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(); } | |
| g.restore(); | |
| // 田野、太阳进入金牛 | |
| const fy = P ? H * .7 : H * .74; | |
| hills(g, W, H, fy, S, 'rgba(60,40,24,.9)', .6, .5); | |
| 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; | |
| 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(); } | |
| const sk = ease.io(seg(u, at(2) - .2, at(3) + 1.2)); | |
| const cx = W / 2 + (P ? 0 : W * .02), cyy = P ? H * .44 : H * .47, sc = (P ? 1.0 : 1.1) * S; | |
| const pts = [[-130, 30], [-70, 10], [-20, 18], [30, -10], [90, -60], [120, -20], [60, 40], [-40, -30]]; | |
| const ln = [[0, 1], [1, 2], [2, 3], [3, 4], [3, 5], [2, 6], [1, 7]]; | |
| 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(); | |
| 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)); | |
| text(g, '金牛座', cx, cyy - 110 * S, { size: 34 * S, gilt: 'gold', alpha: ak * seg(u, at(2), at(2) + 1) }); | |
| const sx = lerp(cx - W * .45, cx - 10 * S, sk), sy = cyy + 10 * S - Math.sin(sk * Math.PI) * 60 * S; | |
| halo(g, sx, sy, 160 * S, [255, 200, 120], ak); sparkle(g, sx, sy, 9 * S, [255, 230, 170], ak, 1.4); | |
| 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)' }); | |
| 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)' }); | |
| } | |
| // B:美索不达米亚泥板 | |
| if (phB > 0) { | |
| const cx = W / 2, cy = P ? H * .4 : H * .44, tw = (P ? 560 : 520) * S, th = tw * .78; | |
| g.save(); g.globalAlpha *= phB; g.translate(cx, cy); g.rotate(-.04); | |
| g.shadowColor = 'rgba(0,0,0,.6)'; g.shadowBlur = 40 * S; rrect(g, -tw / 2, -th / 2, tw, th, 40 * S); | |
| 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; | |
| g.strokeStyle = 'rgba(80,50,20,.6)'; g.lineWidth = 2 * S; | |
| 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(); } | |
| g.restore(); | |
| // 浮雕公牛 | |
| bullSide(g, cx - 10 * S, cy - th * .2, .62 * S, { t: 0, alpha: phB * seg(u, at(5), at(5) + 1), metal: 'pale' }); | |
| 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 }); | |
| 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 }); | |
| } | |
| // C:拉斯科洞穴壁画 | |
| if (phC > 0) { | |
| g.save(); g.globalAlpha *= phC; | |
| 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); | |
| 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]; | |
| 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); } | |
| 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(); } | |
| g.restore(); | |
| // 赭石线描公牛 | |
| const ox = W / 2, oy = P ? H * .44 : H * .5, s = (P ? 1.4 : 1.6) * S; | |
| const T = ([x, y]) => [ox + x * s, oy + y * s]; | |
| 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]), | |
| ...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); | |
| const hornA = bez([140, -46], [140, -110], [190, -140], [230, -120]).map(T); | |
| const hornB = bez([124, -56], [100, -120], [140, -160], [176, -152]).map(T); | |
| 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)); | |
| const lk = seg(u, at(8) - .3, at(8) + 2.5); | |
| const ochre = { color: 'rgba(150,50,20,.9)', width: 7 * S, glow: 'rgba(60,20,0,.5)', blur: 4 * S, alpha: phC }; | |
| strokePart(g, body, seg(lk, 0, .55), ochre); strokePart(g, hornA, seg(lk, .5, .7), ochre); strokePart(g, hornB, seg(lk, .55, .75), ochre); | |
| legs.forEach((l, i) => strokePart(g, l, seg(lk, .7 + i * .06, .82 + i * .06), ochre)); | |
| 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(); | |
| // 小点(昴星团?) | |
| const dk = seg(u, at(9) - .2, at(9) + .8), glow = seg(u, at(11) - .3, at(11) + .6); | |
| [[150, -200], [172, -214], [190, -196], [165, -184], [200, -222], [182, -232]].forEach(([x, y], i) => { | |
| const px = ox + x * s, py = oy + y * s, q = seg(dk, i * .12, i * .12 + .3); | |
| 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(); | |
| if (glow > 0) sparkle(g, px, py, 4 * S, C.sky, glow * phC, .9); | |
| }); | |
| 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 }); } | |
| 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 }); | |
| } | |
| }; | |
| /* ============ 3 找到它 ============ */ | |
| ACTS.locate = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| const ppd = P ? W * .92 / 30 : Math.min(W * .9 / 52, H * .82 / 42); | |
| // 镜头:全景(猎户 + 金牛)→ 推近毕星团 → 拉远看牛角与昴星团 | |
| const z1 = ease.io(seg(u, at(5) + .2, at(6) + .4)), z2 = ease.io(seg(u, at(9) - .6, at(9) + .8)); | |
| 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); | |
| const zoom = lerp(lerp(1, P ? 2.6 : 2.4, z1), P ? 1.25 : 1.35, z2); | |
| const M = mapper(W, H, cRA, cDec, ppd * zoom, 0, P ? -H * .04 : 0); | |
| const fa = seg(u, 0, 1.4); | |
| g.save(); g.globalCompositeOperation = 'lighter'; | |
| 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(); } | |
| g.restore(); | |
| // 猎户座 | |
| const ok = seg(u, at(2) - .4, at(2) + 1.6), dimO = 1 - .55 * seg(u, at(5), at(6)); | |
| lines(g, ORI_L, M, ok, col(C.sky, .5 * dimO), 1.6 * S); | |
| 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); } | |
| const beltK = seg(u, at(2) + .6, at(2) + 1.6); | |
| 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); } | |
| 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' }); } | |
| { 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 }); } | |
| // 延长线 → 毕宿五 | |
| const ek = ease.io(seg(u, at(3) - .1, at(3) + 1.8)); | |
| if (ek > 0) { | |
| 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)]); | |
| 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(); | |
| const tip = pts[Math.floor(ek * 40)]; | |
| if (ek < 1) halo(g, tip[0], tip[1], 30 * S, C.gold, 1); | |
| } | |
| // 毕宿五 | |
| const ak = seg(u, at(4) - .2, at(4) + .6); | |
| 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]); | |
| 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 }); } | |
| // 毕星团 V 字 | |
| const vk = seg(u, at(6) - .2, at(7) + .8); | |
| lines(g, HYA_L, M, vk, col(C.gold, .9), 2.6 * S); | |
| 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); } | |
| 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 }); } | |
| // 牛角 | |
| const hk = seg(u, at(9) - .2, at(10) + .8); | |
| lines(g, HORN_L, M, hk, col(C.gold, .9), 2.6 * S); | |
| lines(g, BODY_L, M, seg(u, at(10), at(11)), col(C.gold, .55), 1.8 * S); | |
| 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); } | |
| if (hk > 0) { const [x, y] = M(STAR.elnath); label(g, x, y, '牛角', '五车五 · 天关', seg(u, at(10) - .2, at(10) + .6), S, { side: 1 }); } | |
| // 昴星团 | |
| const pk = seg(u, at(11) - .2, at(11) + .8); | |
| if (pk > 0) { const c = M([3.78, 24.1]); halo(g, c[0], c[1], 80 * S, [140, 180, 255], pk * .8); | |
| 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); }); | |
| label(g, c[0], c[1], '昴星团', '七姐妹 · M45', pk, S, { side: -1, gilt: 'pale' }); } | |
| // 金色公牛线稿叠影 | |
| const gk = seg(u, at(11) + 1, at(11) + 2.6); | |
| void gk; | |
| 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 }); | |
| }; | |
| /* ============ 4 毕宿五与昴星团 ============ */ | |
| ACTS.stars = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| const B = ease.io(seg(u, at(7) - .7, at(7) + .3)), A = 1 - B; | |
| if (A > 0) { | |
| 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)); | |
| g.save(); g.globalAlpha *= A; | |
| halo(g, sx, sy, R * 3, [255, 110, 60], .9); | |
| 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'); | |
| g.fillStyle = sg; g.beginPath(); g.arc(sx, sy, Math.max(1, R), 0, TAU); g.fill(); | |
| g.save(); g.beginPath(); g.arc(sx, sy, Math.max(1, R), 0, TAU); g.clip(); | |
| 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(); } | |
| 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); | |
| g.restore(); g.restore(); | |
| const nx = P ? W / 2 : W * .7, ny = P ? H * .62 : H * .26; | |
| 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)' }); | |
| 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 }); | |
| // 太阳对比 | |
| const ck = seg(u, at(1) - .2, at(1) + .7); | |
| 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); | |
| text(g, '太阳', px, py + 28 * S, { size: 20 * S, color: '#fff', alpha: A * ck, font: FONT.sans, weight: 600 }); | |
| text(g, '直径 ≈ 太阳 × 44', nx, ny + 120 * S, { size: 40 * S, gilt: 'rose', weight: 900, alpha: A * ck }); } | |
| const dk = seg(u, at(2) - .2, at(2) + .7); | |
| 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) }); } | |
| // 名字由来 | |
| const lk = env(u, at(3) - .2, at(6) - .5, .6, .5); | |
| if (lk > 0) { | |
| const y = P ? H * .74 : H * .58, x = nx; | |
| text(g, 'الدبران', x, y, { size: 56 * S, color: col(C.gold), alpha: A * lk, font: FONT.sans, weight: 700 }); | |
| 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 }); | |
| text(g, '“追随者”', x, y + 104 * S, { size: 52 * S, gilt: 'gold', weight: 900, alpha: A * seg(u, at(5) - .2, at(5) + .6) }); | |
| } | |
| // 追随:昴星团在前、毕宿五在后 | |
| const fk = env(u, at(6) - .3, at(7) - .7, .5, .5); | |
| if (fk > 0) { | |
| const cx = P ? W / 2 : W * .7, cy = P ? H * .72 : H * .62, rr = (P ? 260 : 300) * S; | |
| 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(); | |
| const m = (u - at(6)) * .12; | |
| const p1 = Math.PI * (1.3 + m), p2 = Math.PI * (1.18 + m); | |
| 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)); | |
| 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; | |
| sparkle(g, qx, qy, 5 * S, [255, 160, 100], A * fk, 1); | |
| text(g, '昴星团', px1, py1 - 34 * S, { size: 26 * S, color: col(C.sky), weight: 700, alpha: A * fk }); | |
| text(g, '毕宿五 · 紧随其后', qx, qy + 40 * S, { size: 26 * S, gilt: 'gold', weight: 900, alpha: A * fk }); | |
| text(g, '东升', cx + rr * .95, cy + rr * .55, { size: 20 * S, color: col(C.ivory, .7), alpha: A * fk, font: FONT.sans, weight: 400 }); | |
| } | |
| } | |
| if (B > 0) { | |
| const cx = W / 2, cy = P ? H * .4 : H * .46, R = (P ? 300 : 290) * S; | |
| text(g, '昴星团 · 七姐妹', cx, P ? H * .1 : H * .1, { size: 56 * S, gilt: 'gold', weight: 900, alpha: B, spacing: 6 * S }); | |
| 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 }); | |
| const tk = seg(u, at(10) - .6, at(10) + .8); | |
| // 肉眼:6~7 颗 | |
| const scale = (P ? 2400 : 2600) * S; | |
| 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); }); | |
| if (tk > 0) { | |
| g.save(); g.globalAlpha *= B; g.beginPath(); g.arc(cx, cy, R * ease.out(tk), 0, TAU); g.save(); g.clip(); | |
| 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); | |
| 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); | |
| 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(); } | |
| 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)); | |
| g.restore(); | |
| 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(); | |
| 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))) }); | |
| } 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) }); | |
| // 昴(すばる) | |
| const jk = seg(u, at(11) - .3, at(11) + .7); | |
| if (jk > 0) { | |
| const jx = P ? W / 2 : W * .17, jy = P ? cy + R + 150 * S : cy; | |
| 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 }); | |
| 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 }); | |
| } | |
| const lk = seg(u, at(12) - .2, at(12) + .8); | |
| if (lk > 0) { | |
| const lx = P ? W / 2 : W * .83, ly = P ? H * .78 + 40 * S : cy, ew = 150 * S, eh = 90 * S; | |
| 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(); | |
| [[-70, -20, 1.6], [0, 10, 1], [30, -30, 1], [60, 20, 1], [-10, 45, 1], [80, -5, 1]].forEach(([dx, dy, z], i) => { | |
| 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(); | |
| 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(); }); | |
| text(g, '六连星', lx, ly + eh + 40 * S, { size: 30 * S, color: col(C.ivory), weight: 700, alpha: B * lk }); | |
| } | |
| } | |
| }; | |
| /* ============ 5 天关客星 ============ */ | |
| ACTS.guest = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| const B = ease.io(seg(u, at(8) - .6, at(8) + .6)), A = 1 - B; | |
| if (A > 0) { | |
| // 白昼泛光 | |
| const day = env(u, at(6) - .3, at(8) - .8, .8, .8); | |
| if (day > 0) { g.fillStyle = col([150, 190, 220], .45 * day * A); g.fillRect(0, 0, W, H); } | |
| // 水墨远山 | |
| const layers = [['rgba(40,70,64,.55)', .62, 1.4], ['rgba(24,44,40,.8)', .7, 1], ['rgba(10,20,18,.95)', .8, .8]]; | |
| layers.forEach(([c, y, amp], i) => { g.save(); g.globalAlpha *= A; g.fillStyle = c; g.beginPath(); g.moveTo(0, H); | |
| 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); } | |
| g.lineTo(W, H); g.fill(); g.restore(); }); | |
| // 天关星与客星 | |
| const tx = P ? W * .64 : W * .62, ty = P ? H * .24 : H * .26; | |
| const zk = seg(u, at(4) - .2, at(4) + .6); | |
| 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 }); } | |
| const gk = seg(u, at(5) - .1, at(5) + .5); | |
| if (gk > 0) { | |
| const gx = tx - 50 * S, gy = ty + 40 * S, fl = 1 + .15 * Math.sin(u * 9); | |
| halo(g, gx, gy, 260 * S * gk * fl, [255, 236, 200], A); halo(g, gx, gy, 90 * S * gk, [255, 255, 255], A); | |
| sparkle(g, gx, gy, 14 * S * gk, [255, 245, 230], A, 2.2 * fl); | |
| label(g, gx, gy, '客星', '超新星 SN 1054', A * seg(u, at(5) + .3, at(5) + 1), S, { side: -1, size: 42 }); | |
| } | |
| // 年份 | |
| const yk = env(u, at(2) - .3, at(6) - .3, .6, .6); | |
| const yx = P ? W / 2 : W * .26, yy = P ? H * .44 : H * .2; | |
| 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)' }); | |
| text(g, '北宋 · 至和元年', yx, yy + 80 * S, { size: 34 * S, color: col(C.ivory), weight: 700, alpha: A * yk, spacing: 6 * S }); | |
| // 史书卷轴 | |
| const sk = env(u, at(3) - .3, at(8) - .8, .8, .5); | |
| if (sk > 0) { | |
| const sw = (P ? 600 : 520) * S, sh = (P ? 360 : 420) * S, sx = P ? W / 2 : W * .26, sy = P ? H * .72 : H * .62; | |
| const open = ease.out(seg(u, at(3) - .2, at(3) + 1.4)), ww = sw * open; | |
| g.save(); g.globalAlpha *= A * sk; g.shadowColor = 'rgba(0,0,0,.5)'; g.shadowBlur = 20 * S; | |
| 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; | |
| 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); | |
| g.save(); g.beginPath(); g.rect(sx - ww / 2, sy - sh / 2, ww, sh); g.clip(); | |
| const cols = ['至和元年五月己丑', '客星出天关东南', '可数寸 岁余稍没', '昼见如太白', '凡见二十三日']; | |
| const fs = (P ? 34 : 34) * S, colW = sw / (cols.length + 1); | |
| 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); | |
| 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 }); }); }); | |
| g.restore(); | |
| // 朱印 | |
| const st = seg(u, at(7) - .2, at(7) + .3); | |
| 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(); | |
| text(g, '廿三日', sx - sw * .32, sy + sh * .3, { size: 20 * S, color: '#ffeedd', weight: 900, alpha: st }); } | |
| g.restore(); | |
| const cap = seg(u, at(6) - .1, at(6) + .6); | |
| 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)' }); | |
| } | |
| 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)) }); | |
| const pr = env(u, at(0) - .2, at(2) - .3, .6, .5); | |
| 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 }); } | |
| } | |
| if (B > 0) { | |
| // 蟹状星云 | |
| const cx = W / 2, cy = P ? H * .42 : H * .47, R = (P ? 380 : 360) * S * (1 + (u - at(8)) * .006); | |
| g.save(); g.globalAlpha *= B; | |
| halo(g, cx, cy, R * 1.6, [120, 140, 255], .45); | |
| halo(g, cx, cy, R * 1.1, [150, 190, 255], .5); | |
| g.globalCompositeOperation = 'lighter'; | |
| 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); } | |
| for (let i = 0; i < 160; i++) { | |
| 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); | |
| 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; | |
| const mx = (x0 + x1) / 2 + (h1(i * 5) - .5) * 60 * S, my = (y0 + y1) / 2 + (h1(i * 6) - .5) * 60 * S; | |
| g.strokeStyle = col(i % 3 ? [255, 110, 70] : [255, 190, 120], .08 + h1(i * 9) * .14); g.lineWidth = (1 + h1(i * 4) * 2) * S; | |
| g.beginPath(); g.moveTo(x0, y0); g.quadraticCurveTo(mx, my, x1, y1); g.stroke(); | |
| } | |
| g.restore(); | |
| 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 }); | |
| 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 }); | |
| // 脉冲星 | |
| const pk = seg(u, at(12) - .2, at(12) + .6); | |
| if (pk > 0) { | |
| const ang = u * 3.2; // 视觉化慢放 | |
| g.save(); g.globalCompositeOperation = 'lighter'; | |
| 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)); | |
| 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(); } | |
| g.restore(); | |
| sparkle(g, cx, cy, 8 * S, [210, 235, 255], pk * B, 1.4); | |
| 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)' }); | |
| } | |
| } | |
| }; | |
| /* ============ 6 欧罗巴与白牛 ============ */ | |
| function princess(g, x, y, s, a, t, sit = 0) { | |
| if (a <= 0) return; g.save(); g.translate(x, y); g.scale(s, s); g.globalAlpha *= a; | |
| const dress = g.createLinearGradient(0, -120, 0, 0); dress.addColorStop(0, '#f4d6e0'); dress.addColorStop(1, '#c98aa6'); | |
| g.fillStyle = dress; g.beginPath(); | |
| 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); } | |
| 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); } | |
| g.closePath(); g.fill(); g.strokeStyle = col(C.gold, .8); g.lineWidth = 1.5; g.stroke(); | |
| g.fillStyle = '#f6dcc8'; g.beginPath(); g.arc(0, -94, 13, 0, TAU); g.fill(); | |
| 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(); | |
| 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(); } | |
| g.restore(); | |
| } | |
| ACTS.europa = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| const seaY = H * (P ? .7 : .72), rise = ease.io(seg(u, at(11) - .3, at(13) + .8)); | |
| // 月与海 | |
| 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(); | |
| for (let L = 0; L < 4; L++) { const y0 = seaY + L * 30 * S; g.beginPath(); g.moveTo(0, H); | |
| 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); | |
| 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(); } | |
| // 月光倒影 | |
| 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); } | |
| // 海岸花丛(前 9 句) | |
| const shore = 1 - seg(u, at(7) - .5, at(8)); | |
| const sx0 = P ? W * .08 : W * .05; | |
| 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(); | |
| 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] }); } | |
| // 宙斯:云中金光与王冠 | |
| const zk = env(u, at(1) - .3, at(3) - .2, .6, .6); | |
| if (zk > 0) { const x = W / 2, y = H * (P ? .16 : .17); halo(g, x, y, 300 * S, [255, 220, 150], zk * .8); | |
| 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); | |
| 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 }); } | |
| // 公主 | |
| const pk = seg(u, at(2) - .2, at(2) + .8) * shore; | |
| const bs = (P ? 1.0 : 1.4) * S, bullX = P ? W * .5 : W * .36, bullY = seaY - 30 * S - 118 * bs; | |
| const sit = seg(u, at(6) - .2, at(6) + .8); | |
| const swim = ease.io(seg(u, at(7) - .2, at(8) + 1.2)); | |
| 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; | |
| const px = lerp(P ? W * .2 : W * .14, tx - 10 * bs, sit), py = lerp(seaY - 30 * S, ty - 62 * bs, sit); | |
| 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)) }); } | |
| // 白牛 | |
| const wk = seg(u, at(3) - .2, at(3) + 1.2) * (1 - seg(u, at(11) - .4, at(11) + .6)); | |
| if (wk > 0) { | |
| 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); | |
| halo(g, tx, ty, 260 * S, [255, 240, 220], .45 * wk); | |
| 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(); } | |
| if (swim > 0) { g.save(); g.beginPath(); g.rect(0, 0, W, seaY + 4 * S + Math.sin(u * 2) * 3 * S); g.clip(); } | |
| bullSide(g, tx, ty, bs, { t: u, walk: swim > 0 && swim < 1 ? .6 : 0, white: true, alpha: wk }); | |
| if (swim > 0) g.restore(); | |
| // 花环 | |
| const gk = seg(u, at(5) - .2, at(5) + 1.4); | |
| 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; | |
| 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 }); } | |
| if (sit > 0) princess(g, tx - 10 * bs, ty - 62 * bs, (P ? 1.0 : 1.3) * S, 1, u, 1); | |
| 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)' }); | |
| } | |
| // 克里特岛 | |
| const ck = env(u, at(8) - .2, at(11) - .5, .7, .6); | |
| 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(); | |
| label(g, ix + 10 * S, iy - 56 * S, '克里特岛', 'Crete', ck, S, { side: -1 }); } | |
| // EUROPA → Europe | |
| const ek = env(u, at(9) - .2, at(11) + .2, .6, .6); | |
| 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' }); | |
| 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 }); | |
| text(g, '→', W / 2, y - 20 * S, { size: 50 * S, color: col(C.rose), alpha: ek * seg(u, at(10) - .4, at(10) + .2) }); | |
| 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) }); | |
| 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) }); } | |
| // 升空成星座 | |
| if (rise > 0) { | |
| const cx = W / 2, cy = lerp(seaY - 60 * S, H * (P ? .38 : .4), rise), s = lerp(.9, 1.7, rise) * S * (P ? .8 : 1); | |
| const toStar = seg(u, at(13) - .4, at(13) + 1.2); | |
| halo(g, cx, cy, 380 * S * rise, [255, 230, 200], .5); | |
| bullSide(g, cx, cy, s, { t: u, white: true, alpha: rise * (1 - toStar * .75) }); | |
| const pts = [[-140, -20], [-60, -60], [30, -66], [110, -60], [160, 0], [186, -96], [120, 30], [-110, 60]]; | |
| const ln = [[0, 1], [1, 2], [2, 3], [3, 4], [3, 5], [4, 6], [0, 7]]; | |
| 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(); | |
| 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)); | |
| text(g, '金牛座', cx, cy + 170 * S, { size: 64 * S, gilt: 'gold', weight: 900, alpha: toStar, spacing: 14 * S, glow: 'rgba(255,200,140,.6)' }); } | |
| dust(g, u, W, H, { n: 30, alpha: rise * .8 }); | |
| } | |
| }; | |
| /* ============ 7 ♉ 的含义 ============ */ | |
| ACTS.sigil = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| const cx = W / 2, cy = P ? H * .4 : H * .44, sc = (P ? 2.8 : 2.6) * S; | |
| const alt = ease.io(seg(u, at(5) - .4, at(6) + .4)); // 新月托满月 | |
| // 圆 | |
| 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)); | |
| 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 }; | |
| strokePart(g, arcPts(cx, cy + 22 * sc, 38 * sc, -Math.PI / 2, Math.PI * 1.5, 90), ck, style); | |
| strokePart(g, arcPts(cx, cy - 70 * sc, 54 * sc, Math.PI * .85, Math.PI * .15, 70), ak, style); | |
| // 牛脸与牛角对应 | |
| const fk = env(u, at(3) - .3, at(5) - .3, .6, .6); | |
| if (fk > 0) { g.save(); g.globalAlpha *= .3 * fk; bullHead(g, cx, cy + 40 * sc, sc * .38, { k: 1 }); g.restore(); | |
| label(g, cx + 38 * sc, cy + 22 * sc, '圆 · 公牛的脸', '', seg(u, at(3) - .2, at(3) + .5) * fk, S, { side: 1 }); | |
| label(g, cx - 54 * sc * .9, cy - 46 * sc, '弧 · 一对牛角', '', seg(u, at(4) - .2, at(4) + .5) * fk, S, { side: -1 }); } | |
| // 另一种解读:新月托满月 | |
| if (alt > 0) { | |
| const mx = cx, my = cy + 10 * S, R = 90 * sc * .55; | |
| halo(g, mx, my, R * 3, [255, 240, 210], .6 * alt); | |
| const mg = g.createRadialGradient(mx - R * .3, my - R * .3, R * .1, mx, my, R); mg.addColorStop(0, '#fffaf0'); mg.addColorStop(1, '#d8cbb0'); | |
| g.save(); g.globalAlpha *= alt; g.fillStyle = mg; g.beginPath(); g.arc(mx, my, R, 0, TAU); g.fill(); | |
| 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(); } | |
| // 新月(托在下方) | |
| 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(); | |
| 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(); | |
| text(g, '新月托起满月', cx, P ? cy - R * 1.8 : cy - R * 1.7, { size: 46 * S, gilt: 'gold', weight: 900, alpha: alt, spacing: 6 * S }); | |
| // 麦穗 | |
| const wk = seg(u, at(8) - .3, at(8) + 1.4); | |
| for (const d of [-1, 1]) for (let j = 0; j < 3; j++) { | |
| 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; | |
| 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(); | |
| 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(); } } | |
| g.restore(); | |
| } | |
| 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 }); | |
| } | |
| const tk = env(u, .3, at(3) - .3, .6, .5); | |
| text(g, '♉ 的含义', cx, P ? H * .12 : H * .12, { size: 52 * S, gilt: 'gold', weight: 900, alpha: tk, spacing: 10 * S }); | |
| dust(g, u, W, H, { n: 26, alpha: .6 }); | |
| }; | |
| /* ============ 8 土象 · 金星 ============ */ | |
| const TRAITS = [['稳重', 'STEADY'], ['耐心', 'PATIENT'], ['重承诺', 'TRUSTED'], ['懂生活', 'ART OF LIVING'], ['爱美好', 'BEAUTY'], ['忠诚', 'LOYAL']]; | |
| ACTS.venus = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| petals(g, u, W, H, { n: 20, alpha: .6, seed: 9 }); | |
| const shrink = ease.io(seg(u, at(4) - .5, at(4) + .5)); | |
| const top = lerp(P ? H * .26 : H * .36, P ? H * .1 : H * .14, shrink), sz = lerp(1, .55, shrink); | |
| const ex = P ? W * .28 : W * .3, vx = P ? W * .72 : W * .7; | |
| // 土象 | |
| const ek = seg(u, at(1) - .2, at(1) + .8); | |
| if (ek > 0) { | |
| const r = 80 * S * sz * ease.back(ek); | |
| 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(); | |
| // 炼金术“土”符号:倒三角 + 横线 | |
| 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(); | |
| 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]); | |
| text(g, '土象星座', ex, top + r + 60 * S * sz, { size: 44 * S * sz, gilt: 'gold', weight: 900, alpha: ek }); | |
| } | |
| // 金星 | |
| const vk = seg(u, at(2) - .2, at(2) + .8); | |
| if (vk > 0) { | |
| const r = 76 * S * sz * ease.back(vk); halo(g, vx, top, r * 3, [255, 220, 200], vk * .7); | |
| 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'); | |
| g.save(); g.globalAlpha *= vk; g.fillStyle = pg; g.beginPath(); g.arc(vx, top, Math.max(1, r), 0, TAU); g.fill(); g.restore(); | |
| venusSign(g, vx + r * 1.35, top - r * .3, .9 * S * sz, vk); | |
| 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] }); | |
| text(g, '守护星 · 金星', vx, top + r + 60 * S * sz, { size: 44 * S * sz, gilt: 'rose', weight: 900, alpha: vk }); | |
| 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) }); | |
| } | |
| // 六张品质卡 | |
| const cols = P ? 2 : 3, cw = P ? W * .4 : W * .25, ch = (P ? 190 : 170) * S, gy0 = P ? H * .33 : H * .42; | |
| const times = [at(5), at(5) + .9, at(5) + 2.2, at(6), at(6) + 1.2, at(10) - .4]; | |
| const dim = 1 - .9 * seg(u, at(8) - .4, at(8) + .2); | |
| TRAITS.forEach(([zh, en], i) => { | |
| const k = ease.back(seg(u, times[i] - .1, times[i] + .5)); if (k <= 0) return; | |
| 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); | |
| 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; | |
| 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(); | |
| g.restore(); | |
| ornamentFrame(g, x - cw / 2, y - ch / 2, cw, ch, { S: S * .5, alpha: clamp(k) * dim, metal: i % 2 ? 'rose' : 'gold' }); | |
| text(g, zh, x, y - 12 * S, { size: 50 * S, gilt: i % 2 ? 'rose' : 'gold', weight: 900, alpha: clamp(k) * dim }); | |
| 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 }); | |
| }); | |
| // 慢热的心:慢慢被注满 → 守护之盾 | |
| const hk = env(u, at(8) - .3, X.dur + 1, .6, .6); | |
| if (hk > 0) { | |
| const hx = W / 2, hy = P ? H * .54 : H * .56, hs = (P ? 110 : 90) * S; | |
| 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(); }; | |
| const fill = ease.io(seg(u, at(8), at(10) + .4)); | |
| const shield = seg(u, at(10) - .1, at(10) + .7); | |
| 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))); | |
| 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(); | |
| 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(); } | |
| g.save(); g.globalAlpha *= hk; heart(); g.fillStyle = 'rgba(60,20,30,.6)'; g.fill(); g.save(); heart(); g.clip(); | |
| 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(); | |
| heart(); g.lineWidth = 3 * S; g.strokeStyle = gilt(g, hx - hs, hy - hs, hx + hs, hy + hs, 'rose'); g.stroke(); g.restore(); | |
| if (fill > 0 && fill < 1) halo(g, hx, hy, 120 * S, [255, 160, 160], .4); | |
| const lx = P ? W / 2 : W / 2, ly = P ? hy - hs * 2.1 : hy - hs * 2.1; | |
| text(g, shield > .3 ? '认定了,就守护到底' : '慢热……', lx, ly - 10 * S, { size: 48 * S, gilt: 'rose', weight: 900, alpha: hk, glow: 'rgba(0,0,0,.7)' }); | |
| } | |
| }; | |
| /* ============ 9 鎏金珍藏卡 ============ */ | |
| ACTS.card = (g, u, W, H, X) => { | |
| const { S, P, at } = X; | |
| // 聚光 | |
| const sp = seg(u, 0, 1.2); | |
| 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); | |
| 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)'); | |
| 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(); | |
| // 旋转光环 | |
| const rk = seg(u, at(0) - .5, at(1)); | |
| const ch = P ? Math.min(H * .6, W * .86 / .716) : H * .7, cx = W / 2, cy = P ? H * .43 : H * .44; | |
| 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(); } | |
| halo(g, cx, cy, ch * .9, [255, 200, 150], .6 * rk); | |
| // 卡片:降落 → 翻转 → 刻字 → 全息扫光 → 盖章 | |
| const drop = ease.out(seg(u, at(0) - .6, at(0) + 1.4)); | |
| const flip = ease.io(seg(u, at(1) - .3, at(1) + 1.1)); | |
| const y = lerp(-ch, cy, drop) + Math.sin(u * 1.2) * 6 * S * drop; | |
| if (drop > 0) drawCard(g, cx, y, ch, { | |
| 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, | |
| 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, | |
| }); | |
| // 翻转瞬间的光爆 | |
| 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)); | |
| // 盖章冲击波 + 花瓣雨 | |
| const sk = u - at(7) - .2; | |
| 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(); } | |
| petals(g, u, W, H, { n: P ? 40 : 60, alpha: seg(u, at(7) + .1, at(7) + 1), seed: 21, speed: .06 }); | |
| dust(g, u, W, H, { n: 40, alpha: seg(u, at(1), at(2)) }); | |
| // 底部标语 | |
| const tk = seg(u, at(7) + .6, at(7) + 1.8); | |
| 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)' }); | |
| 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 }); | |
| 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 }); } | |
| }; | |
| /* ============ 10 尾声 · 慢慢来 ============ */ | |
| ACTS.coda = (g, u, W, H, X) => { | |
| const { S, P, at, dur } = X; | |
| const gy = H * (P ? .74 : .76); | |
| const sun = ease.io(seg(u, 0, 4)); | |
| halo(g, W / 2, gy - 20 * S, 700 * S, [255, 170, 110], .7 * sun); | |
| 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(); | |
| hills(g, W, H, gy - 40 * S, S, 'rgba(90,50,60,.7)', 1.2, .3); | |
| hills(g, W, H, gy, S, '#1a0f12', .8, 1.6); | |
| // 麦浪 | |
| 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; | |
| 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(); | |
| 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(); } | |
| 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] }); | |
| // 公牛静立 | |
| const bk = seg(u, .4, 2.4); | |
| const bx = P ? W * .5 : W * .3, by = gy - 112 * S; | |
| halo(g, bx, by, 260 * S, [255, 200, 150], .5 * bk); | |
| bullSide(g, bx, by, (P ? .95 : 1.05) * S, { t: u, alpha: bk }); | |
| // 星座 | |
| const ck = seg(u, at(1) - .2, at(2) + 1); | |
| if (ck > 0) { | |
| const M = mapper(W, H, 4.6, 19, (P ? W * .42 : W * .2) / 16, P ? W * .04 : -W * .02, P ? -H * .34 : -H * .26); | |
| lines(g, [...HYA_L, ...HORN_L], M, ck, col(C.gold, .85), 2.4 * S); | |
| 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); } | |
| PLEI.forEach(s => { const [x, y] = M(s); sparkle(g, x, y, 2 * S, C.sky, ck, .3); }); | |
| } | |
| // 结尾标题 | |
| const tk = seg(u, at(3) - .3, at(3) + 1.4); | |
| const tx = P ? W / 2 : W * .66, ty = P ? H * .44 : H * .44; | |
| if (tk > 0) { | |
| taurusSign(g, tx, ty - 150 * S, .75 * S, seg(u, at(3) - .3, at(3) + 1.6), { width: 10 }); | |
| reveal(g, '慢慢来', tx, ty, tk, { size: 96 * S, gilt: 'gold', weight: 900, spacing: 20 * S, glow: 'rgba(255,160,110,.6)', blur: 30 * S }); | |
| 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)' }); | |
| divider(g, tx, ty + 140 * S, 380 * S, seg(u, at(4) + .4, at(4) + 1.4), S); | |
| 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 }); | |
| } | |
| petals(g, u, W, H, { n: 26, alpha: .7, seed: 41 }); | |
| const fo = seg(u, dur - 1.6, dur); if (fo > 0) { g.fillStyle = `rgba(4,3,4,${fo})`; g.fillRect(0, 0, W, H); } | |
| }; | |