Spaces:
Running
Running
Download aries/src/scenes.js from Ethscriptions/POLS: direct link, hf CLI and curl.
- Browser
- Download file 55.4 kB
-
https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/scenes.js
- Command line
-
hf download hf://spaces/Ethscriptions/POLS/aries/src/scenes.js
-
curl -L -o scenes.js https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/scenes.js
55.4 kB
| // ♈ 白羊座 —— 十幕场景 | |
| import { PI2, clamp01, mix, span, E, life, hash, rgba, GOLD, FIRE, RED, CREAM, SKY, LEAF, SERIF, SANS, DISPLAY, | |
| bloom, star, embers, burst, gold, say, drop, card, trace, hornPath, ramHead, ramSide, ariesGlyph, marsGlyph, laurel, flame, proj, rr, measure } from './kit.js'; | |
| import { drawBadge } from './badge.js'; | |
| /* ---------- 星表(赤经 h,赤纬 °,星等) ---------- */ | |
| const ARI = { hamal: [2.1194, 23.46, 2.0], sheratan: [1.9106, 20.81, 2.64], mesarthim: [1.8922, 19.29, 3.86], a41: [2.833, 27.26, 3.63], a35: [2.725, 27.71, 4.66], a39: [2.798, 29.25, 4.51] }; | |
| const ARI_LINES = [['a41', 'hamal'], ['hamal', 'sheratan'], ['sheratan', 'mesarthim']]; | |
| const TAU_ = { ald: [4.598, 16.51, .85], eps: [4.477, 19.18, 3.5], gam: [4.33, 15.63, 3.6], del: [4.382, 17.54, 3.8], th: [4.478, 15.87, 3.4], elnath: [5.438, 28.61, 1.65], zeta: [5.627, 21.14, 3.0], lam: [4.011, 12.49, 3.4], omi: [3.414, 9.03, 3.6] }; | |
| const TAU_LINES = [['gam', 'del'], ['del', 'eps'], ['eps', 'elnath'], ['gam', 'th'], ['th', 'ald'], ['ald', 'zeta'], ['gam', 'lam'], ['lam', 'omi']]; | |
| const PSC = { eta: [1.525, 15.35, 3.6], omi: [1.757, 9.16, 4.3], alr: [2.034, 2.76, 3.8], nu: [1.69, 5.49, 4.4], mu: [1.503, 6.14, 4.8], eps: [1.049, 7.89, 4.3], del: [.811, 7.58, 4.4], tau: [1.194, 30.09, 4.5], phi: [1.229, 24.58, 4.6], ups: [1.325, 27.26, 4.7] }; | |
| const PSC_LINES = [['eta', 'omi'], ['omi', 'alr'], ['alr', 'nu'], ['nu', 'mu'], ['mu', 'eps'], ['eps', 'del'], ['eta', 'phi'], ['phi', 'ups'], ['ups', 'tau']]; | |
| const PLEIADES = [[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 radius = m => 1.4 + (4.6 - m) * 1.25; | |
| function chart(W, H, ra0, dec0, ppd, ox = 0, oy = 0) { return s => { const [x, y] = proj(s[0], s[1], ra0, dec0); return [W / 2 + ox + x * ppd, H / 2 + oy + y * ppd]; }; } | |
| function figure(ctx, cat, lines, P, k, col, lw, starA, starCol, S) { | |
| ctx.save(); ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.lineCap = 'round'; | |
| lines.forEach(([a, b], i) => { const kk = E.sine(clamp01(k * lines.length - i)); if (kk <= 0) return; const p = P(cat[a]), q = P(cat[b]); | |
| ctx.beginPath(); ctx.moveTo(...p); ctx.lineTo(mix(p[0], q[0], kk), mix(p[1], q[1], kk)); ctx.stroke(); }); | |
| ctx.restore(); | |
| if (starA > 0) for (const key in cat) { const [x, y] = P(cat[key]); star(ctx, x, y, radius(cat[key][2]) * S * .9, starCol, starA, cat[key][2] < 3 ? .8 : 0); } | |
| } | |
| const FIELD = Array.from({ length: 320 }, (_, i) => [.6 + hash(i) * 5.4, -2 + hash(i + 500) * 36, 4.6 + hash(i + 900) * 1.6, hash(i + 77) * PI2]); | |
| function fieldStars(ctx, P, t, a, S) { | |
| ctx.fillStyle = '#f2ecff'; | |
| for (const [ra, dec, m, ph] of FIELD) { const [x, y] = P([ra, dec]); ctx.globalAlpha = a * (.3 + .25 * Math.sin(t * 1.4 + ph)); ctx.beginPath(); ctx.arc(x, y, (6.6 - m) * .75 * S, 0, PI2); ctx.fill(); } | |
| ctx.globalAlpha = 1; | |
| } | |
| function tag(ctx, x, y, title, sub, a, S, o = {}) { | |
| if (a <= 0) return; | |
| const side = o.side ?? 1, col = o.color ?? '#ffd98f'; | |
| ctx.save(); ctx.globalAlpha *= a; ctx.strokeStyle = rgba([255, 214, 140], .7); ctx.lineWidth = 1.6 * S; | |
| const ex = x + side * 34 * S, ey = y - 34 * S; | |
| ctx.beginPath(); ctx.moveTo(x + side * 10 * S, y - 10 * S); ctx.lineTo(ex, ey); ctx.lineTo(ex + side * 30 * S, ey); ctx.stroke(); ctx.restore(); | |
| const tx = ex + side * 40 * S, al = side > 0 ? 'left' : 'right'; | |
| say(ctx, title, tx, ey - (sub ? 12 * S : 0), { size: (o.size ?? 34) * S, align: al, fill: col, alpha: a, shadow: 'rgba(0,0,0,.85)', blur: 12 * S }); | |
| if (sub) say(ctx, sub, tx, ey + 22 * S, { size: 21 * S, align: al, fill: 'rgba(255,240,225,.82)', alpha: a, font: SANS, weight: 400, shadow: 'rgba(0,0,0,.8)', blur: 8 * S }); | |
| } | |
| function ring(ctx, x, y, r, a, c = GOLD, lw = 2) { if (a <= 0) return; ctx.save(); ctx.globalAlpha *= a; ctx.strokeStyle = rgba(c); ctx.lineWidth = lw; ctx.beginPath(); ctx.arc(x, y, r, 0, PI2); ctx.stroke(); ctx.restore(); } | |
| function ping(ctx, x, y, u, S, c = GOLD) { for (let i = 0; i < 2; i++) { const k = (u - i * .3) / 1.3; if (k > 0 && k < 1) ring(ctx, x, y, (10 + 110 * E.out3(k)) * S, (1 - k) * .9, c, 2.4 * S); } } | |
| // 华丽标题卡(双线金框 + 角饰) | |
| function plaque(ctx, cx, cy, w, h, a, S) { | |
| if (a <= 0) return; | |
| ctx.save(); ctx.globalAlpha *= a; | |
| rr(ctx, cx - w / 2, cy - h / 2, w, h, 14 * S); const g = ctx.createLinearGradient(0, cy - h / 2, 0, cy + h / 2); g.addColorStop(0, 'rgba(40,12,8,.78)'); g.addColorStop(1, 'rgba(14,4,6,.78)'); ctx.fillStyle = g; ctx.fill(); | |
| ctx.strokeStyle = gold(ctx, cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2); ctx.lineWidth = 2.5 * S; ctx.stroke(); | |
| rr(ctx, cx - w / 2 + 9 * S, cy - h / 2 + 9 * S, w - 18 * S, h - 18 * S, 9 * S); ctx.lineWidth = 1 * S; ctx.globalAlpha *= .6; ctx.stroke(); ctx.globalAlpha /= .6; | |
| for (const [dx, dy] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) { const x = cx + dx * (w / 2 - 9 * S), y = cy + dy * (h / 2 - 9 * S); ctx.fillStyle = '#ffd98f'; ctx.beginPath(); ctx.moveTo(x, y - 7 * S); ctx.lineTo(x + 7 * S, y); ctx.lineTo(x, y + 7 * S); ctx.lineTo(x - 7 * S, y); ctx.fill(); } | |
| ctx.restore(); | |
| } | |
| // 海浪 | |
| function sea(ctx, t, W, H, top, S, a = 1, tint = [20, 70, 120]) { | |
| for (let L = 0; L < 4; L++) { | |
| const y0 = top + L * 34 * S, amp = (10 + L * 5) * S, sp = .6 + L * .25; | |
| ctx.beginPath(); ctx.moveTo(0, H); | |
| for (let x = 0; x <= W + 20; x += 20) ctx.lineTo(x, y0 + Math.sin(x * .006 / (S || 1) + t * sp + L) * amp + Math.sin(x * .017 / (S || 1) - t * sp * 1.3) * amp * .4); | |
| ctx.lineTo(W, H); ctx.closePath(); | |
| ctx.fillStyle = rgba([tint[0] - L * 4, tint[1] - L * 12, tint[2] - L * 18], a * (.55 + L * .12)); ctx.fill(); | |
| ctx.strokeStyle = rgba([150, 220, 255], a * .25); ctx.lineWidth = 1.5 * S; ctx.stroke(); | |
| } | |
| } | |
| export const ACTS = {}; | |
| /* ============ 1 序章 · 第一缕春光 ============ */ | |
| ACTS.dawn = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| // 地平线日出 | |
| const sun = E.sine(span(u, cue(0) - .5, cue(2) + 1.5)); | |
| const hy = H * (P ? .78 : .8); | |
| if (sun > 0) { | |
| const g = ctx.createLinearGradient(0, hy - 400 * S, 0, H); | |
| g.addColorStop(0, 'rgba(255,140,60,0)'); g.addColorStop(.6, rgba([255, 150, 70], .35 * sun)); g.addColorStop(1, rgba([255, 210, 140], .55 * sun)); | |
| ctx.fillStyle = g; ctx.fillRect(0, hy - 400 * S, W, H); | |
| bloom(ctx, W / 2, hy + 40 * S, 700 * S * sun, [255, 170, 80], .9 * sun); | |
| // 光束 | |
| ctx.save(); ctx.globalCompositeOperation = 'lighter'; | |
| for (let i = 0; i < 9; i++) { const ang = -Math.PI / 2 + (i - 4) * .2 + Math.sin(u * .3 + i) * .03; const L = Math.max(W, H) * 1.1; | |
| const gr = ctx.createLinearGradient(W / 2, hy, W / 2 + Math.cos(ang) * L, hy + Math.sin(ang) * L); gr.addColorStop(0, rgba([255, 200, 120], .09 * sun)); gr.addColorStop(1, 'rgba(255,200,120,0)'); | |
| ctx.fillStyle = gr; ctx.beginPath(); ctx.moveTo(W / 2, hy); ctx.lineTo(W / 2 + Math.cos(ang - .04) * L, hy + Math.sin(ang - .04) * L); ctx.lineTo(W / 2 + Math.cos(ang + .04) * L, hy + Math.sin(ang + .04) * L); ctx.fill(); } | |
| ctx.restore(); | |
| // 山脊剪影 | |
| ctx.fillStyle = '#0b0204'; ctx.beginPath(); ctx.moveTo(0, H); | |
| for (let x = 0; x <= W; x += 16) ctx.lineTo(x, hy + 30 * S + Math.sin(x / W * 7 + 1) * 22 * S + Math.sin(x / W * 19) * 9 * S); | |
| ctx.lineTo(W, H); ctx.fill(); | |
| } | |
| embers(ctx, u, W, H, { n: 70, seed: 3, alpha: .9, spread: 1.2 }); | |
| // 公羊头:从余烬中显形 | |
| const hx = P ? W / 2 : W * .3, hyy = P ? H * .3 : H * .44, hs = (P ? 1.65 : 1.35) * S; | |
| const form = span(u, cue(3) - .4, cue(3) + 2.4); | |
| if (form > 0 && form < 1) for (let i = 0; i < 90; i++) { const ang = hash(i) * PI2, r = (200 + hash(i + 9) * 600) * S * (1 - E.out3(form)); bloom(ctx, hx + Math.cos(ang + form * 2) * r, hyy + Math.sin(ang + form * 2) * r, 10 * S, FIRE, Math.sin(form * Math.PI)); } | |
| const charge = span(u, cue(4), cue(4) + .7), shake = charge > 0 && charge < 1 ? Math.sin(u * 60) * 4 * S * (1 - charge) : 0; | |
| const tilt = .18 * E.out3(charge) * (1 - span(u, cue(5) - .3, cue(5) + .6)); | |
| const lunge = 1 + .08 * E.out3(charge) * (1 - span(u, cue(5) - .3, cue(5) + .6)); | |
| bloom(ctx, hx, hyy, 380 * S * form, [255, 150, 60], .55 * form); | |
| ramHead(ctx, hx + shake, hyy, hs * lunge * mix(.9, 1, E.out3(form)), { k: form, tilt, eye: 1 + charge * .6 }); | |
| if (charge > 0) burst(ctx, u, cue(4) + .25, hx, hyy + 40 * S, S, { n: 70, color: FIRE, speed: 700 }); | |
| if (charge > 0 && charge < 1) { const k = charge; ring(ctx, hx, hyy + 60 * S, 600 * S * E.out3(k), (1 - k) * .7, [255, 180, 90], 4 * S); } | |
| // 标题 | |
| const tk = span(u, cue(5) + .1, cue(5) + 1.6); | |
| const tx = P ? W / 2 : W * .67, ty = P ? H * .57 : H * .34; | |
| bloom(ctx, tx, ty, 500 * S * tk, [255, 120, 50], .35 * tk); | |
| drop(ctx, '白羊座', tx, ty, tk, { size: (P ? 170 : 160) * S, metal: true, track: 30 * S, weight: 900, shadow: 'rgba(255,120,40,.7)', blur: 40 * S }); | |
| say(ctx, 'A R I E S', tx, ty + (P ? 118 : 112) * S, { size: 40 * S, fill: '#ffd98f', alpha: span(u, cue(5) + 1, cue(5) + 2), font: DISPLAY, weight: 600, track: 10 * S }); | |
| // 第一宫徽记 | |
| const nk = E.back(span(u, cue(5) + 1.4, cue(5) + 2.2)); | |
| if (nk > 0) { | |
| const by = ty + (P ? 250 : 225) * S, bx = tx - 150 * S; | |
| ctx.save(); ctx.translate(bx, by); ctx.scale(nk, nk); | |
| ctx.beginPath(); for (let i = 0; i < 16; i++) { const r = i % 2 ? 46 * S : 58 * S, a = i / 16 * PI2; ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); } ctx.closePath(); | |
| ctx.fillStyle = gold(ctx, -60 * S, -60 * S, 60 * S, 60 * S); ctx.shadowColor = 'rgba(255,150,60,.9)'; ctx.shadowBlur = 30 * S; ctx.fill(); ctx.restore(); | |
| say(ctx, 'No.1', bx, by - 2 * S, { size: 30 * S * nk, fill: '#5a2a06', font: DISPLAY, weight: 800 }); | |
| say(ctx, '黄道第一宫', bx + 85 * S, by - 20 * S, { size: 34 * S, fill: '#fff0d0', align: 'left', alpha: clamp01(nk), shadow: 'rgba(90,20,0,.9)', blur: 10 * S }); | |
| say(ctx, '十二星座 · 首位', bx + 85 * S, by + 24 * S, { size: 24 * S, fill: '#ffe2b0', align: 'left', alpha: clamp01(nk), font: SANS, weight: 600 }); | |
| } | |
| // 日期缎带 | |
| const dk = span(u, cue(6) - .2, cue(6) + 1); | |
| if (dk > 0) { | |
| const ry = ty + (P ? 385 : 360) * S, rw = 460 * S * E.out3(dk), rh = 64 * S, rx = tx; | |
| ctx.save(); ctx.globalAlpha = dk; | |
| for (const d of [-1, 1]) { ctx.fillStyle = '#7a1410'; ctx.beginPath(); const ex = rx + d * rw / 2; ctx.moveTo(ex - d * 10 * S, ry - rh / 2 + 10 * S); ctx.lineTo(ex + d * 60 * S, ry - rh / 2 + 10 * S); ctx.lineTo(ex + d * 40 * S, ry + 5 * S); ctx.lineTo(ex + d * 60 * S, ry + rh / 2 + 10 * S); ctx.lineTo(ex - d * 10 * S, ry + rh / 2 + 10 * S); ctx.fill(); } | |
| const g = ctx.createLinearGradient(0, ry - rh / 2, 0, ry + rh / 2); g.addColorStop(0, '#d8352a'); g.addColorStop(1, '#8e1712'); | |
| ctx.fillStyle = g; ctx.fillRect(rx - rw / 2, ry - rh / 2, rw, rh); | |
| ctx.strokeStyle = gold(ctx, rx - rw / 2, ry, rx + rw / 2, ry); ctx.lineWidth = 2 * S; ctx.strokeRect(rx - rw / 2 + 5 * S, ry - rh / 2 + 5 * S, rw - 10 * S, rh - 10 * S); | |
| ctx.restore(); | |
| say(ctx, '3.21 — 4.19', rx, ry + 2 * S, { size: 38 * S, fill: '#fff1d0', alpha: span(dk, .5, 1), font: DISPLAY, weight: 700, track: 4 * S }); | |
| } | |
| }; | |
| /* ============ 2 为什么排第一 ============ */ | |
| const SIGNS = [['♈', '白羊'], ['♉', '金牛'], ['♊', '双子'], ['♋', '巨蟹'], ['♌', '狮子'], ['♍', '处女'], ['♎', '天秤'], ['♏', '天蝎'], ['♐', '射手'], ['♑', '摩羯'], ['♒', '水瓶'], ['♓', '双鱼']]; | |
| ACTS.first = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| const cx = W / 2, cy = P ? H * .42 : H * .47; | |
| const rx = (P ? W * .4 : H * .62), ry = rx * (P ? .5 : .38); | |
| const ph2 = span(u, cue(7) - .6, cue(7) + .4); // 进入“坐标网格”阶段 | |
| const ringA = span(u, .2, 1.6) * (1 - ph2 * .94); | |
| // 黄道环 | |
| ctx.save(); ctx.globalAlpha = ringA; ctx.strokeStyle = gold(ctx, cx - rx, cy, cx + rx, cy); ctx.lineWidth = 3 * S; | |
| ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, PI2); ctx.stroke(); ctx.lineWidth = 1 * S; ctx.beginPath(); ctx.ellipse(cx, cy, rx * 1.08, ry * 1.08, 0, 0, PI2); ctx.stroke(); ctx.restore(); | |
| // 地球在中心 | |
| bloom(ctx, cx, cy, 80 * S, SKY, .6 * ringA); | |
| ctx.save(); ctx.globalAlpha = ringA; const eg = ctx.createRadialGradient(cx - 10 * S, cy - 10 * S, 2, cx, cy, 30 * S); eg.addColorStop(0, '#8fd0ff'); eg.addColorStop(1, '#1d4f9a'); ctx.fillStyle = eg; ctx.beginPath(); ctx.arc(cx, cy, 30 * S, 0, PI2); ctx.fill(); ctx.restore(); | |
| // 太阳沿黄道运行,在春分落入白羊 | |
| const base = Math.PI / 2; // 白羊位于环的正前方(下方) | |
| const posOf = i => base + i / 12 * PI2; | |
| const sunK = E.sine(span(u, cue(2), cue(4) + .6)); | |
| const sunAng = posOf(-3) + sunK * (posOf(0) - posOf(-3)); | |
| const numK = span(u, cue(5), cue(6) + 1.2); // 1→12 依次点亮 | |
| SIGNS.forEach(([g, n], i) => { | |
| const ang = posOf(i), x = cx + Math.cos(ang) * rx, y = cy + Math.sin(ang) * ry; | |
| const front = (Math.sin(ang) + 1) / 2; const sc = .75 + front * .45; | |
| const isA = i === 0, hl = isA ? span(u, cue(4), cue(4) + .6) : 0; | |
| const lit = clamp01(numK * 12 - i); | |
| const r = 34 * S * sc * (1 + hl * .35); | |
| ctx.save(); ctx.globalAlpha = ringA * (.55 + front * .45); | |
| ctx.fillStyle = isA ? 'rgba(90,30,10,.9)' : 'rgba(25,14,30,.85)'; ctx.strokeStyle = isA ? '#ffd98f' : rgba([255, 220, 160], .35 + lit * .6); | |
| ctx.lineWidth = (isA ? 3 : 1.5) * S; ctx.beginPath(); ctx.arc(x, y, r, 0, PI2); ctx.fill(); ctx.stroke(); ctx.restore(); | |
| if (isA && hl > 0) bloom(ctx, x, y, r * 4, FIRE, hl * .7); | |
| say(ctx, g + '\uFE0E', x, y - 2 * S, { size: 34 * S * sc * (1 + hl * .35), fill: isA ? '#ffd98f' : '#f2e6ff', alpha: ringA, font: '"Noto Sans Symbols 2","Segoe UI Symbol","DejaVu Sans",serif', weight: 400 }); | |
| say(ctx, n, x, y + r + 18 * S, { size: 19 * S * sc, fill: isA ? '#ffd98f' : 'rgba(240,230,255,.75)', alpha: ringA, font: SANS, weight: 600 }); | |
| if (lit > 0) say(ctx, String(i + 1), x + r * .9, y - r * .9, { size: 20 * S * sc, fill: '#fff', alpha: ringA * lit, font: DISPLAY, weight: 800, shadow: 'rgba(255,120,40,.9)', blur: 10 * S }); | |
| }); | |
| if (ringA > 0) { const x = cx + Math.cos(sunAng) * rx, y = cy + Math.sin(sunAng) * ry; bloom(ctx, x, y, 150 * S, [255, 190, 90], ringA * span(u, cue(2) - .5, cue(2) + .3)); star(ctx, x, y, 10 * S, [255, 220, 150], ringA * span(u, cue(2) - .5, cue(2) + .3), 1.2); } | |
| // 春分:昼夜等长 & 万物复苏(嫩芽与花瓣) | |
| const sp = life(u, cue(3) - .2, cue(5) + .5, .6, .8); | |
| if (sp > 0) { | |
| const ax = cx, ay = cy + ry; | |
| for (let i = 0; i < 14; i++) { const ang = -Math.PI / 2 + (i - 6.5) * .22, k = E.out3(clamp01((u - cue(3)) * 1.2 - i * .05)); const L = 120 * S * k; | |
| ctx.save(); ctx.globalAlpha = sp; ctx.strokeStyle = '#8be38f'; ctx.lineWidth = 2.5 * S; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.quadraticCurveTo(ax + Math.cos(ang) * L * .4, ay - L * .6, ax + Math.cos(ang) * L, ay + Math.sin(ang) * L); ctx.stroke(); | |
| ctx.fillStyle = i % 3 ? '#ffb7c8' : '#fff1b0'; ctx.beginPath(); ctx.arc(ax + Math.cos(ang) * L, ay + Math.sin(ang) * L, 5 * S * k, 0, PI2); ctx.fill(); ctx.restore(); } | |
| const ly = P ? cy - ry - 120 * S : cy - ry - 90 * S; | |
| card(ctx, cx - 210 * S, ly - 40 * S, 420 * S, 80 * S, { alpha: sp, r: 40 * S }); | |
| say(ctx, '春分 · 昼夜等长', cx, ly, { size: 36 * S, fill: '#ffe7b8', alpha: sp }); | |
| } | |
| const startK = life(u, cue(5) - .2, cue(7) - .3, .5, .5); | |
| if (startK > 0) { const x = cx, y = cy + ry; say(ctx, '起点 START', x, y + 120 * S, { size: 44 * S, metal: true, alpha: startK, font: DISPLAY, weight: 800, shadow: 'rgba(255,120,40,.7)' }); } | |
| // 天球坐标:赤道 × 黄道 的交点 = 春分点 = 白羊座第一点 | |
| if (ph2 > 0) { | |
| const gx = W / 2, gy = P ? H * .44 : H * .46, L = (P ? W * .44 : W * .32); | |
| const k1 = E.sine(span(u, cue(7) - .3, cue(7) + 1.2)), k2 = E.sine(span(u, cue(7) + .4, cue(7) + 1.8)); | |
| ctx.save(); ctx.lineCap = 'round'; | |
| ctx.strokeStyle = rgba(SKY, .85 * ph2); ctx.lineWidth = 3 * S; ctx.beginPath(); ctx.moveTo(gx - L * k1, gy); ctx.lineTo(gx + L * k1, gy); ctx.stroke(); | |
| const tilt = -23.4 * Math.PI / 180; | |
| ctx.strokeStyle = rgba([255, 200, 110], .9 * ph2); ctx.beginPath(); ctx.moveTo(gx - Math.cos(tilt) * L * k2, gy - Math.sin(tilt) * L * k2); ctx.lineTo(gx + Math.cos(tilt) * L * k2, gy + Math.sin(tilt) * L * k2); ctx.stroke(); ctx.restore(); | |
| say(ctx, '天赤道', gx - L + 10 * S, gy + 30 * S, { size: 24 * S, align: 'left', fill: rgba(SKY), alpha: ph2 * k1, font: SANS, weight: 600 }); | |
| say(ctx, '黄道(太阳的路径)', gx + L * .55, gy + Math.sin(-tilt) * L * .55 + 30 * S * 0 - 40 * S, { size: 24 * S, fill: '#ffd98f', alpha: ph2 * k2, font: SANS, weight: 600 }); | |
| const gk = span(u, cue(7) + 1, cue(7) + 2.6); | |
| if (gk > 0) { ping(ctx, gx, gy, u - cue(7) - 1, S, [255, 200, 120]); bloom(ctx, gx, gy, 140 * S, [255, 170, 80], gk); ariesGlyph(ctx, gx, gy - 150 * S, .75 * S, gk); } | |
| const tk = span(u, cue(7) + 2, cue(7) + 3); | |
| const py = P ? gy + 300 * S : gy + 255 * S; | |
| card(ctx, W / 2 - 330 * S, py - 62 * S, 660 * S, 124 * S, { alpha: tk }); | |
| say(ctx, '春分点 =「白羊座第一点」', W / 2, py - 18 * S, { size: 38 * S, metal: true, alpha: tk }); | |
| say(ctx, 'First Point of Aries · 天文记号 ♈︎', W / 2, py + 30 * S, { size: 22 * S, fill: 'rgba(255,236,210,.85)', alpha: tk, font: SANS, weight: 400 }); | |
| } | |
| }; | |
| /* ============ 3 找到它 ============ */ | |
| ACTS.find = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| const ppd = P ? W * .92 / 40 : Math.min(W * .9 / 66, H * .78 / 34); | |
| const zk = E.sine(span(u, cue(2) - .6, cue(2) + 2.2)); | |
| const zoom = 1 + (P ? .35 : .55) * zk; | |
| const Pj = chart(W, H, mix(P ? 2.95 : 2.9, P ? 2.6 : 2.75, zk), mix(18, 21.5, zk), ppd * zoom, 0, P ? -H * .06 : -H * .02); | |
| fieldStars(ctx, Pj, u, span(u, 0, 1.5), S); | |
| const pk = span(u, cue(1) - .2, cue(1) + 1.6), tk = span(u, cue(1) + .5, cue(1) + 2.2); | |
| figure(ctx, PSC, PSC_LINES, Pj, pk, rgba(SKY, .45), 1.8 * S, pk * .7, [190, 210, 255], S); | |
| figure(ctx, TAU_, TAU_LINES, Pj, tk, rgba([255, 170, 130], .45), 1.8 * S, tk * .7, [255, 190, 160], S); | |
| const fade = 1 - span(u, cue(5) - .5, cue(5) + 1) * .5; | |
| { const [x, y] = Pj([1.35, 11]); say(ctx, '双鱼座', x, y + 50 * S, { size: 32 * S, fill: '#bcd0ff', alpha: pk * fade }); } | |
| { const [x, y] = Pj(TAU_.ald); say(ctx, '金牛座', x - 10 * S, y + 60 * S, { size: 32 * S, fill: '#ffc2a8', alpha: tk * fade }); } | |
| // 三颗亮星 + 羊角弧 | |
| const ak = span(u, cue(2) - .2, cue(2) + 2); | |
| ['mesarthim', 'sheratan', 'hamal'].forEach((k, i) => { const [x, y] = Pj(ARI[k]); const kk = span(u, cue(2) + i * .45, cue(2) + i * .45 + .5); if (kk > 0) { star(ctx, x, y, radius(ARI[k][2]) * 2 * S, [255, 215, 150], kk, 1.2); ping(ctx, x, y, u - cue(2) - i * .45, S); } }); | |
| figure(ctx, ARI, ARI_LINES, Pj, ak, rgba([255, 214, 140], .95), 3 * S, 0, GOLD, S); | |
| { const [x, y] = Pj(ARI.a41); star(ctx, x, y, radius(ARI.a41[2]) * 1.3 * S, [255, 215, 150], ak, .5); } | |
| // 羊角幽灵:沿弧线的螺旋 | |
| const hk = span(u, cue(3) - .2, cue(3) + 1.8); | |
| if (hk > 0) { | |
| const [x1, y1] = Pj(ARI.mesarthim), [x2, y2] = Pj(ARI.hamal); | |
| const r0 = Math.hypot(x2 - x1, y2 - y1) * 1.05; | |
| const h = hornPath(x1 + (x2 - x1) * .25, y1 - r0 * .55, r0 * .62, 1.1, -1, Math.PI * .55, .35); | |
| ctx.save(); ctx.globalAlpha = .3 * hk * (1 - span(u, cue(5), cue(5) + 1.5)); ctx.fillStyle = gold(ctx, x1 - r0, y1 - r0, x2 + r0, y2 + r0); ctx.fill(h.shape); ctx.restore(); | |
| trace(ctx, h.spine, h.len * 1.9, hk, { width: 2 * S, color: rgba([255, 220, 150], .7 * (1 - span(u, cue(5), cue(5) + 1.5))), glow: 'rgba(255,190,90,.8)' }); | |
| } | |
| { const [x, y] = Pj(ARI.hamal); say(ctx, '白羊座', x + 10 * S, y - 110 * S, { size: 48 * S, metal: true, alpha: span(u, cue(2) + 1.2, cue(2) + 2.2), shadow: 'rgba(255,120,40,.6)', track: 6 * S }); } | |
| // 昴星团 | |
| const mk = span(u, cue(4) - .2, cue(4) + 1); | |
| if (mk > 0) { | |
| const c = Pj([3.78, 24.1]); bloom(ctx, c[0], c[1], 90 * S, [150, 190, 255], .6 * mk); | |
| PLEIADES.forEach((s, i) => { const [x, y] = Pj(s); const tw = .7 + .3 * Math.sin(u * 5 + i * 2); star(ctx, mix(c[0], x, 1) , y, (5.4 - s[2]) * 1.2 * S, [190, 215, 255], mk * tw, .6); }); | |
| tag(ctx, c[0], c[1], '昴星团', 'M45 · 七姐妹', mk, S, { side: -1, color: '#cfe0ff' }); | |
| } | |
| // 娄宿 / 胃宿 | |
| const lk = span(u, cue(6) - .3, cue(6) + .8); | |
| if (lk > 0) { | |
| const box = (keys, label, sub, side) => { | |
| const pts = keys.map(k => Pj(ARI[k])); const xs = pts.map(p => p[0]), ys = pts.map(p => p[1]); const pad = 34 * S; | |
| const x0 = Math.min(...xs) - pad, y0 = Math.min(...ys) - pad, x1 = Math.max(...xs) + pad, y1 = Math.max(...ys) + pad; | |
| ctx.save(); ctx.globalAlpha = lk; ctx.setLineDash([6 * S, 6 * S]); ctx.strokeStyle = '#ff9b7a'; ctx.lineWidth = 2 * S; rr(ctx, x0, y0, x1 - x0, y1 - y0, 14 * S); ctx.stroke(); ctx.restore(); | |
| const sx = side > 0 ? x1 + 18 * S : x0 - 18 * S; const sy = side > 0 ? y1 : y0; | |
| ctx.save(); ctx.globalAlpha = lk; ctx.fillStyle = '#b3261e'; rr(ctx, sx - (side > 0 ? 0 : 70 * S), sy - 35 * S, 70 * S, 70 * S, 8 * S); ctx.fill(); ctx.restore(); | |
| say(ctx, label, sx + (side > 0 ? 35 : -35) * S, sy + 2 * S, { size: 44 * S, fill: '#fff2e6', alpha: lk, weight: 900 }); | |
| say(ctx, sub, sx + (side > 0 ? 35 : -35) * S, sy + 62 * S, { size: 20 * S, fill: '#ffd7c8', alpha: lk, font: SANS, weight: 600 }); | |
| }; | |
| box(['hamal', 'sheratan', 'mesarthim'], '娄', '娄宿', 1); | |
| box(['a35', 'a39', 'a41'], '胃', '胃宿', -1); | |
| } | |
| }; | |
| /* ============ 4 星星的名字 ============ */ | |
| ACTS.names = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| const phB = E.sine(span(u, cue(5) - .6, cue(5) + .6)); | |
| // —— A:哈马尔 | |
| const A = 1 - phB; | |
| if (A > 0) { | |
| const sx = P ? W / 2 : W * .34, sy = P ? H * .36 : H * .47, R = (P ? 190 : 210) * S * E.out3(span(u, 0, 1.6)); | |
| ctx.save(); ctx.globalAlpha = A; | |
| bloom(ctx, sx, sy, R * 3.2, [255, 140, 60], .8); | |
| const g = ctx.createRadialGradient(sx - R * .3, sy - R * .3, R * .1, sx, sy, R); g.addColorStop(0, '#fff0c8'); g.addColorStop(.45, '#ffaa48'); g.addColorStop(1, '#b3420f'); | |
| ctx.fillStyle = g; ctx.beginPath(); ctx.arc(sx, sy, R, 0, PI2); ctx.fill(); | |
| // 表面对流纹 | |
| ctx.save(); ctx.beginPath(); ctx.arc(sx, sy, R, 0, PI2); ctx.clip(); ctx.globalCompositeOperation = 'overlay'; | |
| for (let i = 0; i < 160; i++) { const a = hash(i) * PI2 + u * .04 * (hash(i + 1) - .5), d = Math.sqrt(hash(i + 3)) * R; ctx.fillStyle = i % 2 ? 'rgba(255,236,190,.10)' : 'rgba(140,40,0,.10)'; ctx.beginPath(); ctx.arc(sx + Math.cos(a) * d, sy + Math.sin(a) * d, R * (.04 + hash(i + 8) * .06), 0, PI2); ctx.fill(); } | |
| ctx.restore(); | |
| const limb = ctx.createRadialGradient(sx, sy, R * .55, sx, sy, R); limb.addColorStop(0, 'rgba(90,20,0,0)'); limb.addColorStop(1, 'rgba(90,20,0,.55)'); ctx.fillStyle = limb; ctx.beginPath(); ctx.arc(sx, sy, R, 0, PI2); ctx.fill(); | |
| ctx.restore(); | |
| const nx = P ? W / 2 : W * .66, ny = P ? H * .64 : H * .3; | |
| say(ctx, '哈马尔', nx, ny, { size: 86 * S, metal: true, alpha: A * span(u, cue(0), cue(0) + .8), shadow: 'rgba(255,120,40,.6)', track: 8 * S }); | |
| say(ctx, 'Hamal · α Ari · 娄宿三', nx, ny + 62 * S, { size: 26 * S, fill: '#ffe2c0', alpha: A * span(u, cue(0) + .4, cue(0) + 1.2), font: SANS, weight: 600 }); | |
| const yk = E.back(span(u, cue(2) - .2, cue(2) + .5)); | |
| if (yk > 0) { | |
| const bx = nx, by = ny + 170 * S; ctx.save(); ctx.translate(bx, by); ctx.scale(yk, yk); ctx.rotate(-.06); | |
| ctx.fillStyle = '#b3261e'; rr(ctx, -62 * S, -62 * S, 124 * S, 124 * S, 12 * S); ctx.fill(); ctx.strokeStyle = '#ffe6d0'; ctx.lineWidth = 3 * S; rr(ctx, -50 * S, -50 * S, 100 * S, 100 * S, 6 * S); ctx.stroke(); ctx.restore(); | |
| say(ctx, '羊', bx, by + 4 * S, { size: 84 * S * yk, fill: '#fff2e6', alpha: A, weight: 900 }); | |
| say(ctx, 'الحمل al-ḥamal', bx, by + 100 * S, { size: 24 * S, fill: '#ffd7b8', alpha: A * clamp01(yk), font: SANS, weight: 600 }); | |
| } | |
| // 橙色巨星 / 距离 | |
| const ok = span(u, cue(3), cue(3) + .8); | |
| if (ok > 0) { tag(ctx, sx + R * .7, sy - R * .7, '橙色巨星', '直径约为太阳的 15 倍', A * ok, S, { side: 1 }); | |
| const sunR = R / 15; star(ctx, sx + R + 90 * S, sy + R * .6, sunR, [255, 240, 200], A * ok, 0); say(ctx, '太阳', sx + R + 90 * S, sy + R * .6 + 28 * S, { size: 20 * S, fill: '#fff', alpha: A * ok, font: SANS, weight: 600 }); } | |
| const dk = span(u, cue(4) - .2, cue(4) + 1.2); | |
| if (dk > 0) { | |
| const bx0 = P ? W * .12 : W * .52, bx1 = P ? W * .88 : W * .9, by = P ? H * .78 : H * .78; | |
| ctx.save(); ctx.globalAlpha = A * dk; ctx.strokeStyle = 'rgba(255,220,170,.5)'; ctx.lineWidth = 2 * S; ctx.setLineDash([8 * S, 8 * S]); ctx.beginPath(); ctx.moveTo(bx0, by); ctx.lineTo(bx1, by); ctx.stroke(); ctx.restore(); | |
| const px = mix(bx1, bx0, E.sine(clamp01((u - cue(4)) / 2.2) % 1)); bloom(ctx, px, by, 30 * S, [255, 200, 120], A * dk); | |
| say(ctx, '地球', bx0, by + 32 * S, { size: 22 * S, fill: '#cfe0ff', alpha: A * dk, font: SANS, weight: 600 }); say(ctx, '哈马尔', bx1, by + 32 * S, { size: 22 * S, fill: '#ffd0a0', alpha: A * dk, font: SANS, weight: 600 }); | |
| say(ctx, '≈ 66 光年', (bx0 + bx1) / 2, by - 36 * S, { size: 40 * S, metal: true, alpha: A * dk, font: SERIF }); | |
| } | |
| } | |
| // —— B:娄宿二 · 双星 | |
| if (phB > 0) { | |
| const cx = W / 2, cy = P ? H * .42 : H * .46, R = (P ? 330 : 300) * S; | |
| const scope = span(u, cue(6) - .2, cue(6) + .8); | |
| // 目镜视野 | |
| ctx.save(); ctx.globalAlpha = phB; | |
| if (scope > 0) { | |
| ctx.fillStyle = rgba([0, 0, 0], .75 * scope); ctx.beginPath(); ctx.rect(0, 0, W, H); ctx.arc(cx, cy, R * mix(1.6, 1, E.out3(scope)), 0, PI2, true); ctx.fill(); | |
| ctx.strokeStyle = gold(ctx, cx - R, cy - R, cx + R, cy + R); ctx.lineWidth = 8 * S; ctx.beginPath(); ctx.arc(cx, cy, R * mix(1.6, 1, E.out3(scope)), 0, PI2); ctx.stroke(); | |
| ctx.strokeStyle = rgba([255, 220, 160], .25 * scope); ctx.lineWidth = 1 * S; ctx.beginPath(); ctx.moveTo(cx - R, cy); ctx.lineTo(cx + R, cy); ctx.moveTo(cx, cy - R); ctx.lineTo(cx, cy + R); ctx.stroke(); | |
| } | |
| ctx.restore(); | |
| const split = E.out5(span(u, cue(7) - .2, cue(7) + 1)); | |
| const d = 70 * S * split; | |
| star(ctx, cx - d, cy, 9 * S, [230, 240, 255], phB, 1); star(ctx, cx + d, cy, 8.5 * S, [230, 240, 255], phB * (split > .05 ? 1 : 0), 1); | |
| say(ctx, '娄宿二 · Mesarthim', cx, cy - R - (P ? 70 : 50) * S, { size: 40 * S, metal: true, alpha: phB * span(u, cue(5), cue(5) + .8) }); | |
| if (split > .5) say(ctx, '双星!', cx, cy + 110 * S, { size: 52 * S, fill: '#fff4dc', alpha: phB * span(split, .5, 1), shadow: 'rgba(120,170,255,.8)' }); | |
| const yk = span(u, cue(8) - .2, cue(8) + .6); | |
| if (yk > 0) { | |
| const yy = P ? cy + R + 120 * S : cy + R * .35, xx = P ? W / 2 : W * .82; | |
| say(ctx, '1664', P ? xx : W * .17, P ? yy : cy - 40 * S, { size: 110 * S, metal: true, alpha: phB * yk, font: DISPLAY, weight: 800, shadow: 'rgba(255,140,60,.6)' }); | |
| const hk = span(u, cue(9) - .2, cue(9) + .7); | |
| say(ctx, '罗伯特 · 胡克', P ? xx : W * .17, P ? yy + 90 * S : cy + 60 * S, { size: 36 * S, fill: '#ffe2c0', alpha: phB * hk }); | |
| say(ctx, 'Robert Hooke', P ? xx : W * .17, P ? yy + 135 * S : cy + 105 * S, { size: 24 * S, fill: 'rgba(255,230,200,.8)', alpha: phB * hk, font: DISPLAY, weight: 500 }); | |
| // 古典望远镜 | |
| if (!P) { ctx.save(); ctx.globalAlpha = phB * hk; ctx.translate(W * .83, cy + 40 * S); ctx.rotate(-.5); | |
| ctx.fillStyle = gold(ctx, -160 * S, 0, 160 * S, 0); rr(ctx, -150 * S, -18 * S, 300 * S, 36 * S, 10 * S); ctx.fill(); rr(ctx, 150 * S, -24 * S, 30 * S, 48 * S, 6 * S); ctx.fill(); | |
| ctx.strokeStyle = '#7a4a10'; ctx.lineWidth = 2 * S; for (let i = -100; i <= 100; i += 50) { ctx.beginPath(); ctx.moveTo(i * S, -18 * S); ctx.lineTo(i * S, 18 * S); ctx.stroke(); } ctx.restore(); | |
| ctx.save(); ctx.globalAlpha = phB * hk; ctx.strokeStyle = '#c99a4a'; ctx.lineWidth = 6 * S; ctx.beginPath(); ctx.moveTo(W * .83, cy + 40 * S); ctx.lineTo(W * .8, cy + 230 * S); ctx.moveTo(W * .83, cy + 40 * S); ctx.lineTo(W * .87, cy + 230 * S); ctx.stroke(); ctx.restore(); } | |
| } | |
| const fk = span(u, cue(10) - .1, cue(10) + .8); | |
| if (fk > 0) { const y = P ? H * .1 + 30 * S : cy + R + 52 * S; card(ctx, W / 2 - 360 * S, y - 40 * S, 720 * S, 80 * S, { alpha: phB * fk, r: 40 * S }); say(ctx, '人类最早用望远镜发现的双星之一', W / 2, y, { size: 32 * S, fill: '#ffe7b8', alpha: phB * fk }); } | |
| } | |
| }; | |
| /* ============ 5 金羊毛 · 飞越大海 ============ */ | |
| function figureKid(ctx, x, y, s, a, girl, t) { | |
| if (a <= 0) return; ctx.save(); ctx.translate(x, y); ctx.scale(s, s); ctx.globalAlpha *= a; ctx.fillStyle = '#120608'; | |
| ctx.beginPath(); ctx.arc(0, -46, 9, 0, PI2); ctx.fill(); | |
| ctx.beginPath(); ctx.moveTo(-9, -36); ctx.lineTo(9, -36); ctx.lineTo(girl ? 16 : 10, 0); ctx.lineTo(girl ? -16 : -10, 0); ctx.closePath(); ctx.fill(); | |
| if (girl) { ctx.beginPath(); ctx.moveTo(-6, -52); ctx.quadraticCurveTo(-22 + Math.sin(t * 3) * 3, -40, -16, -24); ctx.lineTo(-8, -40); ctx.fill(); } | |
| ctx.fillRect(-6, 0, 4, 14); ctx.fillRect(2, 0, 4, 14); ctx.restore(); | |
| } | |
| ACTS.fleece1 = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| const seaTop = H * (P ? .72 : .74); | |
| // 月亮 | |
| bloom(ctx, W * .8, H * .2, 180 * S, [200, 220, 255], .5); ctx.fillStyle = '#eef3ff'; ctx.beginPath(); ctx.arc(W * .8, H * .2, 40 * S, 0, PI2); ctx.fill(); | |
| // 标题卡 | |
| const tk = life(u, .3, cue(2) - .1, .7, .5); | |
| if (tk > 0) { const y = P ? H * .3 : H * .36; plaque(ctx, W / 2, y, 720 * S, 210 * S, tk, S); | |
| say(ctx, '金羊毛传说', W / 2, y - 26 * S, { size: 72 * S, metal: true, alpha: tk, track: 10 * S, shadow: 'rgba(255,140,60,.5)' }); | |
| say(ctx, 'THE GOLDEN FLEECE', W / 2, y + 52 * S, { size: 28 * S, fill: '#ffe2b0', alpha: tk, font: DISPLAY, weight: 600, track: 8 * S }); } | |
| // 悬崖 + 兄妹 + 暗云闪电 | |
| const ck = life(u, cue(2) - .4, cue(5) + .4, .6, .6); | |
| const cliffX = P ? W * .18 : W * .14; | |
| if (ck > 0) { | |
| ctx.save(); ctx.globalAlpha = ck; ctx.fillStyle = '#0a0406'; ctx.beginPath(); ctx.moveTo(0, H); ctx.lineTo(0, seaTop - 160 * S); ctx.lineTo(cliffX * .6, seaTop - 175 * S); ctx.lineTo(cliffX + 40 * S, seaTop - 150 * S); ctx.lineTo(cliffX + 80 * S, seaTop + 20 * S); ctx.lineTo(cliffX + 60 * S, H); ctx.fill(); ctx.restore(); | |
| figureKid(ctx, cliffX - 20 * S, seaTop - 165 * S, 2.4 * S, ck * (1 - span(u, cue(5) - .3, cue(5) + .3)), false, u); | |
| figureKid(ctx, cliffX + 40 * S, seaTop - 160 * S, 2.15 * S, ck * (1 - span(u, cue(5) - .3, cue(5) + .3)), true, u); | |
| const storm = life(u, cue(3) - .2, cue(4) + .6, .5, .6); | |
| if (storm > 0) { | |
| for (let i = 0; i < 7; i++) { const x = W * (i / 6), y = H * .12 + Math.sin(i * 2 + u * .5) * 20 * S; bloom(ctx, x, y, 260 * S, [60, 10, 20], storm * .9); } | |
| const fl = Math.max(0, Math.sin(u * 7.3) - .85) * 6.6 * storm; | |
| if (fl > 0) { ctx.save(); ctx.strokeStyle = rgba([255, 200, 220], fl); ctx.lineWidth = 3 * S; ctx.shadowColor = '#ffb0c8'; ctx.shadowBlur = 20 * S; ctx.beginPath(); let x = W * .45, y = 0; ctx.moveTo(x, y); for (let i = 0; i < 7; i++) { x += (hash(i + Math.floor(u * 7)) - .5) * 80 * S; y += 60 * S; ctx.lineTo(x, y); } ctx.stroke(); ctx.restore(); | |
| ctx.fillStyle = rgba([255, 230, 240], fl * .12); ctx.fillRect(0, 0, W, H); } | |
| say(ctx, '继母的迫害', W / 2, P ? H * .2 : H * .2, { size: 40 * S, fill: '#ffb8b8', alpha: storm, shadow: 'rgba(0,0,0,.8)' }); | |
| } | |
| } | |
| // 金羊从天而降 → 驮着兄妹飞越大海 | |
| const desc = span(u, cue(4) - .2, cue(4) + 2.4); | |
| const fly = span(u, cue(5) - .2, X.dur + 4); | |
| if (desc > 0) { | |
| const x0 = cliffX + 60 * S, y0 = seaTop - 260 * S; | |
| let x = mix(W * .55, x0, E.out3(desc)), y = mix(-120 * S, y0, E.out3(desc)); | |
| const fx = mix(x0, W * 1.15, fly), fy = mix(y0, H * (P ? .36 : .3), Math.sin(fly * Math.PI) * .9 + fly * .2); | |
| if (fly > 0) { x = fx; y = fy + Math.sin(u * 2) * 10 * S; } | |
| if (desc < 1) { // 光柱 | |
| ctx.save(); ctx.globalCompositeOperation = 'lighter'; const g = ctx.createLinearGradient(x, 0, x, y); g.addColorStop(0, 'rgba(255,220,140,0)'); g.addColorStop(1, rgba([255, 220, 140], .35 * Math.sin(desc * Math.PI))); ctx.fillStyle = g; ctx.fillRect(x - 90 * S, 0, 180 * S, y); ctx.restore(); } | |
| // 金粉尾迹 | |
| for (let i = 1; i < 28; i++) { const k = i / 28; const fq = Math.max(0, fly - k * .06), tx = fly > 0 ? mix(x0, W * 1.15, fq) : x, ty = fly > 0 ? mix(y0, H * (P ? .36 : .3), Math.sin(fq * Math.PI) * .9 + fq * .2) : y - k * 200 * S; | |
| bloom(ctx, tx - 40 * S, ty + 10 * S, 16 * S * (1 - k), GOLD, (1 - k) * .7); } | |
| bloom(ctx, x, y, 260 * S, [255, 190, 90], .55); | |
| const rs = (P ? 1.15 : 1.45) * S; | |
| ramSide(ctx, x, y, rs, { t: u, fly: 1, shine: 1.2 }); | |
| // 背上的兄妹 | |
| const onBack = fly > 0 ? 1 : span(desc, .9, 1); | |
| figureKid(ctx, x - 20 * rs, y - 52 * rs, .95 * rs, onBack, false, u); | |
| const fall = span(u, cue(7) - .1, cue(7) + 1.8); | |
| if (fall <= 0) figureKid(ctx, x - 55 * rs, y - 50 * rs, .9 * rs, onBack, true, u); | |
| else { | |
| const fx0 = x - 55 * rs, hx = fx0 - fall * 60 * S, hy = mix(y - 50 * rs, seaTop + 10 * S, E.in3(fall)); | |
| if (fall < .98) { ctx.save(); ctx.translate(hx, hy); ctx.rotate(fall * 3); figureKid(ctx, 0, 0, .9 * rs, 1, true, u); ctx.restore(); } | |
| if (fall >= .98) { const k = (u - (cue(7) + 1.8)); for (let i = 0; i < 3; i++) { const kk = (k - i * .25) / 1.5; if (kk > 0 && kk < 1) { ctx.save(); ctx.strokeStyle = rgba([200, 235, 255], 1 - kk); ctx.lineWidth = 2 * S; ctx.beginPath(); ctx.ellipse(fx0 - 60 * S, seaTop + 10 * S, 120 * S * kk, 22 * S * kk, 0, 0, PI2); ctx.stroke(); ctx.restore(); } } } | |
| } | |
| } | |
| sea(ctx, u, W, H, seaTop, S, 1); | |
| // 赫勒斯滂 | |
| const hk = span(u, cue(8) + .2, cue(8) + 1.2); | |
| if (hk > 0) { const y = P ? H * .86 : H * .86; say(ctx, '赫勒斯滂 Hellespont', W / 2, y - 30 * S - (P ? 40 * S : 0), { size: 50 * S, metal: true, alpha: hk, shadow: 'rgba(0,30,60,.9)' }); say(ctx, '意为“赫勒之海” · 今达达尼尔海峡', W / 2, y + 22 * S - (P ? 40 * S : 0), { size: 24 * S, fill: '#d8ecff', alpha: hk, font: SANS, weight: 600, shadow: 'rgba(0,0,0,.8)' }); } | |
| }; | |
| /* ============ 6 金羊毛 · 升上星空 ============ */ | |
| ACTS.fleece2 = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| // A:航线 希腊 → 科尔基斯 | |
| const mk = life(u, .2, cue(1) + .2, .6, .6); | |
| if (mk > 0) { | |
| const ax = P ? W * .2 : W * .25, ay = P ? H * .52 : H * .58, bx = P ? W * .8 : W * .75, by = P ? H * .32 : H * .32; | |
| ctx.save(); ctx.globalAlpha = mk; ctx.strokeStyle = '#ffd98f'; ctx.lineWidth = 3 * S; ctx.setLineDash([12 * S, 10 * S]); | |
| const k = E.sine(span(u, .4, cue(0) + 2.6)); ctx.beginPath(); for (let i = 0; i <= 60 * k; i++) { const q = i / 60; const x = mix(ax, bx, q), y = mix(ay, by, q) - Math.sin(q * Math.PI) * 140 * S; i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); } ctx.stroke(); ctx.restore(); | |
| for (const [x, y, n, sub] of [[ax, ay, '希腊', 'Greece'], [bx, by, '科尔基斯', 'Colchis · 黑海东岸']]) { star(ctx, x, y, 7 * S, GOLD, mk, .8); say(ctx, n, x, y + 48 * S, { size: 38 * S, fill: '#ffe6b8', alpha: mk }); say(ctx, sub, x, y + 86 * S, { size: 20 * S, fill: 'rgba(255,240,220,.75)', alpha: mk, font: SANS, weight: 400 }); } | |
| const q = k; const rx = mix(ax, bx, q), ry = mix(ay, by, q) - Math.sin(q * Math.PI) * 140 * S; | |
| ramSide(ctx, rx, ry - 30 * S, .55 * S, { t: u, fly: 1, alpha: mk }); | |
| } | |
| // B:圣林 · 金羊毛 · 巨龙 · 阿尔戈号 | |
| const gk = life(u, cue(1) - .3, cue(4) + .3, .7, .7); | |
| if (gk > 0) { | |
| const tx = P ? W * .5 : W * .36, gy = H * (P ? .7 : .8); | |
| // 地面与圣树 | |
| ctx.save(); ctx.globalAlpha = gk; ctx.fillStyle = '#071209'; ctx.fillRect(0, gy, W, H - gy); | |
| ctx.strokeStyle = '#2a1a0c'; ctx.lineCap = 'round'; | |
| const branch = (x, y, ang, len, w, d) => { if (d > 6) return; const x2 = x + Math.cos(ang) * len, y2 = y + Math.sin(ang) * len; ctx.lineWidth = w; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x2, y2); ctx.stroke(); | |
| if (d >= 4) { for (let j = 0; j < 3; j++) { ctx.fillStyle = rgba([30 + j * 18, 80 + j * 25, 45 + j * 10], .75); ctx.beginPath(); ctx.arc(x2 + (hash(x2 + j) - .5) * 30 * S, y2 - j * 8 * S, (30 - j * 6) * S, 0, PI2); ctx.fill(); } } | |
| branch(x2, y2, ang - .45 + hash(d * 9 + x) * .2, len * .74, w * .7, d + 1); branch(x2, y2, ang + .4 - hash(d * 5 + y) * .2, len * .72, w * .7, d + 1); }; | |
| branch(tx, gy, -Math.PI / 2, 190 * S, 32 * S, 0); ctx.restore(); | |
| // 金羊毛 | |
| const fx = tx + 80 * S, fy = gy - 300 * S, fk = span(u, cue(1), cue(1) + 1) * gk; | |
| bloom(ctx, fx, fy, 260 * S, [255, 200, 90], fk * (.8 + .2 * Math.sin(u * 3))); | |
| ctx.save(); ctx.globalAlpha = fk; ctx.translate(fx, fy); ctx.rotate(Math.sin(u * 1.2) * .05); | |
| const fl = new Path2D(); fl.moveTo(-60 * S, 0); fl.bezierCurveTo(-80 * S, 60 * S, -40 * S, 130 * S, 0, 120 * S); fl.bezierCurveTo(40 * S, 130 * S, 80 * S, 60 * S, 60 * S, 0); fl.bezierCurveTo(30 * S, -10 * S, -30 * S, -10 * S, -60 * S, 0); | |
| ctx.fillStyle = gold(ctx, -80 * S, 0, 80 * S, 130 * S); ctx.shadowColor = '#ffcc66'; ctx.shadowBlur = 40 * S; ctx.fill(fl); ctx.shadowBlur = 0; | |
| ctx.clip(fl); ctx.strokeStyle = 'rgba(120,70,10,.5)'; ctx.lineWidth = 2 * S; for (let i = 0; i < 18; i++) { ctx.beginPath(); ctx.arc((-50 + (i * 31) % 100) * S, (15 + Math.floor(i / 3.3) * 22) * S, 10 * S, .3, 4.8); ctx.stroke(); } | |
| ctx.restore(); | |
| say(ctx, '金羊毛', fx + 150 * S, fy + 40 * S, { size: 40 * S, metal: true, alpha: fk, shadow: 'rgba(0,0,0,.6)' }); | |
| // 巨龙缠树 | |
| const dk = span(u, cue(2) - .2, cue(2) + 1.6) * gk; | |
| if (dk > 0) { | |
| const pts = []; for (let i = 0; i <= 80; i++) { const q = i / 80; const ang = q * PI2 * 2.2 + u * .3; pts.push([tx + Math.cos(ang) * (70 - q * 20) * S, gy - 20 * S - q * 230 * S + Math.sin(ang) * 18 * S, Math.sin(ang)]); } | |
| const n = Math.floor(pts.length * dk); | |
| for (let i = 1; i < n; i++) { const [x1, y1, z] = pts[i - 1], [x2, y2] = pts[i]; ctx.strokeStyle = z > 0 ? '#2f7a3c' : '#1a4524'; ctx.lineWidth = (30 - i * .22) * S; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); | |
| if (i % 4 === 0) { ctx.fillStyle = 'rgba(160,230,140,.35)'; ctx.beginPath(); ctx.arc(x2, y2 - 6 * S, 4 * S, 0, PI2); ctx.fill(); } } | |
| if (n > 2) { const [hx, hy] = pts[n - 1]; ctx.fillStyle = '#2f7a3c'; ctx.beginPath(); ctx.ellipse(hx + 20 * S, hy - 6 * S, 34 * S, 18 * S, -.2, 0, PI2); ctx.fill(); bloom(ctx, hx + 32 * S, hy - 12 * S, 20 * S, [255, 210, 60], .9 * (.7 + .3 * Math.sin(u * 4))); | |
| ctx.fillStyle = '#ffe680'; ctx.beginPath(); ctx.arc(hx + 32 * S, hy - 12 * S, 3.5 * S, 0, PI2); ctx.fill(); } | |
| say(ctx, '永不入睡的巨龙', P ? W / 2 : tx - 280 * S, P ? gy + 80 * S : gy - 120 * S, { size: 40 * S, fill: '#bff0c0', alpha: dk, shadow: 'rgba(0,0,0,.8)' }); | |
| } | |
| // 阿尔戈号 | |
| const ak = span(u, cue(3) - .2, cue(3) + 1) * gk; | |
| if (ak > 0) { | |
| const sx = mix(W * 1.1, P ? W * .62 : W * .72, E.out3(span(u, cue(3) - .2, cue(3) + 3))), sy = (P ? H * .4 : H * .55) + Math.sin(u * 1.6) * 6 * S; | |
| ctx.save(); ctx.globalAlpha = ak; ctx.translate(sx, sy); const k = P ? .8 : 1; ctx.scale(k * S, k * S); | |
| ctx.fillStyle = '#3b1d0c'; ctx.beginPath(); ctx.moveTo(-170, 0); ctx.quadraticCurveTo(0, 60, 170, 0); ctx.lineTo(200, -40); ctx.lineTo(150, -10); ctx.lineTo(-150, -10); ctx.lineTo(-190, -50); ctx.closePath(); ctx.fill(); | |
| ctx.strokeStyle = gold(ctx, -200, 0, 200, 0); ctx.lineWidth = 3; ctx.stroke(); | |
| ctx.strokeStyle = '#2a140a'; ctx.lineWidth = 6; ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(0, -200); ctx.stroke(); | |
| ctx.fillStyle = 'rgba(255,240,210,.92)'; ctx.beginPath(); ctx.moveTo(-90, -190); ctx.quadraticCurveTo(0, -150 + Math.sin(u * 2) * 6, 90, -190); ctx.lineTo(80, -40); ctx.quadraticCurveTo(0, -60, -80, -40); ctx.closePath(); ctx.fill(); | |
| ctx.fillStyle = '#b3261e'; ctx.beginPath(); ctx.arc(0, -115, 26, 0, PI2); ctx.fill(); ctx.strokeStyle = '#ffd98f'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(0, -115, 18, 0, PI2); ctx.stroke(); | |
| ctx.strokeStyle = '#2a140a'; ctx.lineWidth = 3; for (let i = -5; i <= 5; i++) { const sw = Math.sin(u * 3 + i) * 10; ctx.beginPath(); ctx.moveTo(i * 26, 10); ctx.lineTo(i * 26 + sw - 12, 60); ctx.stroke(); } | |
| ctx.restore(); | |
| say(ctx, '阿尔戈号 · 伊阿宋的远航', sx, sy + (P ? 90 : 110) * S, { size: 32 * S, fill: '#ffe6b8', alpha: ak, shadow: 'rgba(0,0,0,.85)' }); | |
| } | |
| } | |
| // C:升上夜空 → 星座 | |
| const rk = span(u, cue(4) - .4, cue(4) + .6); | |
| if (rk > 0) { | |
| const up = E.sine(span(u, cue(4), cue(6) + .2)); | |
| const x = W / 2, y = mix(H * .78, H * (P ? .42 : .46), up), s = mix(.9, 1.6, up) * S * (P ? .9 : 1); | |
| const toStars = span(u, cue(6) - .3, cue(6) + 1.2); | |
| bloom(ctx, x, y, 420 * S * up, [255, 200, 110], .5 * rk); | |
| ramSide(ctx, x - 20 * s, y, s, { t: u, fly: 1, alpha: rk * (1 - toStars * .7) }); | |
| const pts = [[-90, -30], [-40, -50], [20, -45], [70, -25], [115, -75], [135, -55], [-70, 30], [55, 28]]; | |
| const ln = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [0, 6], [3, 7]]; | |
| if (toStars > 0) { | |
| ctx.save(); ctx.globalAlpha = toStars; ctx.strokeStyle = 'rgba(255,220,150,.85)'; ctx.lineWidth = 2.4 * S; | |
| ln.forEach(([a, b], i) => { const kk = clamp01(toStars * ln.length - i); const p = pts[a], q = pts[b]; ctx.beginPath(); ctx.moveTo(x - 20 * s + p[0] * s, y + p[1] * s); ctx.lineTo(x - 20 * s + mix(p[0], q[0], kk) * s, y + mix(p[1], q[1], kk) * s); ctx.stroke(); }); | |
| ctx.restore(); | |
| pts.forEach(([px, py], i) => star(ctx, x - 20 * s + px * s, y + py * s, (i === 4 ? 5 : 3.2) * S, [255, 225, 160], toStars, i === 4 ? 1 : .4)); | |
| say(ctx, '白羊座', x, y + 150 * S, { size: 64 * S, metal: true, alpha: toStars, track: 10 * S, shadow: 'rgba(255,120,40,.7)' }); | |
| } | |
| if (up > 0 && up < 1) embers(ctx, u, W, H, { n: 40, seed: 11, color: GOLD, alpha: rk * .8, spread: .5 }); | |
| } | |
| }; | |
| /* ============ 7 ♈ 的秘密 ============ */ | |
| ACTS.glyph = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| const cx = W / 2, cy = P ? H * .42 : H * .45; | |
| const merge = E.sine(span(u, cue(5) - .4, cue(5) + 1.4)); | |
| const sep = (P ? 230 : 330) * S * (1 - merge); | |
| // 羊角(左) | |
| const hk = span(u, cue(1) - .3, cue(1) + 1.8) * (1 - merge); | |
| if (hk > 0) for (const d of [-1, 1]) { | |
| const h = hornPath(cx - sep + d * 95 * S, cy - 60 * S, 120 * S, 1.15, d, d > 0 ? -Math.PI * .95 : -Math.PI * .05, .45); | |
| ctx.save(); ctx.globalAlpha = span(hk, .4, 1); ctx.fillStyle = gold(ctx, cx - sep - 160 * S, cy - 140 * S, cx - sep + 160 * S, cy + 60 * S); ctx.shadowColor = 'rgba(255,160,60,.8)'; ctx.shadowBlur = 24 * S; ctx.fill(h.shape); ctx.restore(); | |
| trace(ctx, h.spine, h.len * 1.9, hk, { width: 4 * S, color: '#ffe3a0', glow: 'rgba(255,180,80,.9)' }); | |
| } | |
| // 嫩芽(右) | |
| const sk = span(u, cue(2) - .2, cue(2) + 2) * (1 - merge); | |
| if (sk > 0) { | |
| const bx = cx + sep, by = cy + 120 * S, g = E.out3(sk); | |
| ctx.save(); ctx.strokeStyle = '#7fdc85'; ctx.lineWidth = 9 * S; ctx.lineCap = 'round'; ctx.shadowColor = 'rgba(140,255,150,.7)'; ctx.shadowBlur = 18 * S; | |
| ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(bx, by - 150 * S * g); ctx.stroke(); | |
| for (const d of [-1, 1]) { const k2 = span(sk, .4, 1); ctx.beginPath(); ctx.moveTo(bx, by - 150 * S * g); ctx.bezierCurveTo(bx, by - 220 * S * k2, bx + d * 90 * S * k2, by - 230 * S * k2, bx + d * 80 * S * k2, by - 160 * S * k2); ctx.stroke(); | |
| ctx.fillStyle = 'rgba(140,230,140,.35)'; ctx.fill(); } | |
| ctx.restore(); | |
| ctx.fillStyle = '#2a1408'; ctx.beginPath(); ctx.ellipse(bx, by + 8 * S, 120 * S * g, 18 * S, 0, 0, PI2); ctx.fill(); | |
| } | |
| // 说明 | |
| const k1 = life(u, cue(3) - .2, cue(5) + .2, .5, .5), k2 = life(u, cue(4) - .2, cue(5) + .2, .5, .5); | |
| const ly = P ? cy + 300 * S : cy + 260 * S; | |
| say(ctx, '羊角', cx - sep, ly, { size: 50 * S, metal: true, alpha: k1 }); say(ctx, '勇往直前', cx - sep, ly + 60 * S, { size: 32 * S, fill: '#ffd9a0', alpha: k1 }); | |
| say(ctx, '嫩芽', cx + sep, ly, { size: 50 * S, fill: '#b9f5b6', alpha: k2, shadow: 'rgba(80,200,90,.6)' }); say(ctx, '新的开始', cx + sep, ly + 60 * S, { size: 32 * S, fill: '#d8ffd2', alpha: k2 }); | |
| // 合体 ♈ | |
| if (merge > 0) { | |
| bloom(ctx, cx, cy, 520 * S * merge, [255, 170, 80], .6 * merge); | |
| ariesGlyph(ctx, cx, cy - 10 * S, (P ? 1.7 : 1.6) * S * mix(.7, 1, merge), span(u, cue(5) - .1, cue(5) + 1.6), { width: 16 }); | |
| // 春花飘落 | |
| for (let i = 0; i < 40; i++) { const ph = (u * .12 + hash(i)) % 1; const x = W * hash(i + 5) + Math.sin(u + i) * 30 * S, y = -40 * S + ph * (H + 80 * S); | |
| ctx.save(); ctx.translate(x, y); ctx.rotate(u * (1 + hash(i)) + i); ctx.globalAlpha = merge * .8; ctx.fillStyle = i % 3 ? '#ffc1d0' : '#fff0b8'; ctx.beginPath(); ctx.ellipse(0, 0, 8 * S, 4.5 * S, 0, 0, PI2); ctx.fill(); ctx.restore(); } | |
| const tk = span(u, cue(6) - .3, cue(6) + .7); | |
| say(ctx, '春天的样子 · 白羊座的样子', cx, P ? cy + 330 * S : cy + 270 * S, { size: 44 * S, metal: true, alpha: tk, track: 4 * S, shadow: 'rgba(255,120,40,.6)' }); | |
| } | |
| }; | |
| /* ============ 8 火象 · 火星 ============ */ | |
| ACTS.fire = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| embers(ctx, u, W, H, { n: 90, seed: 21, alpha: 1, spread: 1.3, rise: 1.2 }); | |
| const top = P ? H * .2 : H * .28; | |
| // 火象 | |
| const fk = span(u, cue(1) - .2, cue(1) + .8), mk = span(u, cue(2) - .2, cue(2) + .8); | |
| const fx = P ? W * .28 : W * .27, mx = P ? W * .72 : W * .73; | |
| const shrink = E.sine(span(u, cue(3) - .4, cue(3) + .6)); | |
| const s1 = mix(1, .6, shrink), yy = mix(top + 60 * S, (P ? H * .12 : H * .16), shrink); | |
| if (fk > 0) { flame(ctx, fx, yy + 70 * S * s1, 1.05 * S * s1 * E.back(fk), u, fk); say(ctx, '火象星座', fx, yy + 130 * S * s1, { size: 44 * S * s1, metal: true, alpha: fk }); } | |
| if (mk > 0) { | |
| const r = 70 * S * s1 * E.back(mk); bloom(ctx, mx, yy, r * 3, [255, 80, 50], mk * .7); | |
| const g = ctx.createRadialGradient(mx - r * .3, yy - r * .3, r * .1, mx, yy, r); g.addColorStop(0, '#ffb08a'); g.addColorStop(.6, '#d2462a'); g.addColorStop(1, '#6e1a10'); | |
| ctx.save(); ctx.globalAlpha = mk; ctx.fillStyle = g; ctx.beginPath(); ctx.arc(mx, yy, Math.max(1, r), 0, PI2); ctx.fill(); ctx.restore(); | |
| marsGlyph(ctx, mx + r * 1.4, yy - r * .9, .8 * S * s1, mk); | |
| say(ctx, '守护星 · 火星', mx, yy + 130 * S * s1, { size: 44 * S * s1, metal: true, alpha: mk }); | |
| } | |
| // 特质勋章 | |
| const traits = [['热情', 'PASSION', cue(3) + .5], ['勇敢', 'COURAGE', cue(3) + 1.3], ['直率', 'HONESTY', cue(3) + 2.1], ['行动派', 'ACTION', cue(4)]]; | |
| const cols = P ? 2 : 4, cw = P ? W * .4 : W * .19, chh = (P ? 210 : 220) * S; | |
| const by = P ? H * .44 : H * .56; | |
| const fade = (1 - span(u, cue(6) - .4, cue(6) + .4) * .55) * (1 - span(u, cue(8), cue(8) + .6)); | |
| traits.forEach(([zh, en, t0], i) => { | |
| const k = E.back(span(u, t0, t0 + .6)); if (k <= 0) return; | |
| const col = i % cols, row = Math.floor(i / cols); | |
| const x = W / 2 + (col - (cols - 1) / 2) * (cw + 24 * S), y = by + row * (chh + 24 * S); | |
| ctx.save(); ctx.translate(x, y); ctx.scale(k, k); | |
| card(ctx, -cw / 2, -chh / 2, cw, chh, { alpha: fade, r: 24 * S, top: 'rgba(90,20,12,.8)', bottom: 'rgba(30,6,6,.8)' }); | |
| ctx.restore(); | |
| const a = clamp01(k) * fade; | |
| flame(ctx, x, y - 20 * S, .32 * S * k, u + i, a * .9); | |
| say(ctx, zh, x, y + 30 * S, { size: 46 * S * k, metal: true, alpha: a }); | |
| say(ctx, en, x, y + 74 * S, { size: 18 * S * k, fill: '#ffcfaa', alpha: a, font: DISPLAY, weight: 600, track: 4 * S }); | |
| }); | |
| // 冲锋:行动派 | |
| const ch = span(u, cue(5) - .2, cue(5) + 1.3); | |
| if (ch > 0 && ch < 1) { | |
| const x = mix(-300 * S, W + 300 * S, E.in3(ch) * .4 + ch * .6), y = P ? H * .74 : H * .86; | |
| for (let i = 0; i < 12; i++) { ctx.strokeStyle = rgba([255, 200, 120], .5 * (1 - ch)); ctx.lineWidth = 3 * S; ctx.beginPath(); const yy2 = y - 80 * S + i * 14 * S; ctx.moveTo(x - 200 * S - hash(i) * 300 * S, yy2); ctx.lineTo(x - 120 * S, yy2); ctx.stroke(); } | |
| ramSide(ctx, x, y - 40 * S, .9 * S, { t: u * 1.6, gait: 1 }); | |
| } | |
| // 急性子的小乌云 → 立刻放晴 | |
| const ck = life(u, cue(6) - .2, cue(8) + .2, .4, .4); | |
| if (ck > 0) { | |
| const x = W / 2, y = P ? H * .7 : H * .66; | |
| const clear = span(u, cue(7) + .3, cue(7) + 1.5); | |
| const cs = (1 - clear) * ck; | |
| for (const [dx, dy, r] of [[-50, 0, 46], [0, -20, 56], [50, 0, 46], [0, 16, 50]]) { ctx.fillStyle = rgba([90, 80, 100], .9 * cs); ctx.beginPath(); ctx.arc(x + dx * S, y + dy * S, r * S, 0, PI2); ctx.fill(); } | |
| if (cs > .2) { ctx.strokeStyle = rgba([255, 230, 120], cs); ctx.lineWidth = 4 * S; ctx.beginPath(); ctx.moveTo(x, y + 40 * S); ctx.lineTo(x - 12 * S, y + 70 * S); ctx.lineTo(x + 6 * S, y + 70 * S); ctx.lineTo(x - 6 * S, y + 100 * S); ctx.stroke(); } | |
| if (clear > 0) { bloom(ctx, x, y, 200 * S * clear, [255, 220, 120], ck * clear); for (let i = 0; i < 12; i++) { const a = i / 12 * PI2 + u * .4; ctx.strokeStyle = rgba([255, 220, 130], ck * clear); ctx.lineWidth = 4 * S; ctx.beginPath(); ctx.moveTo(x + Math.cos(a) * 60 * S, y + Math.sin(a) * 60 * S); ctx.lineTo(x + Math.cos(a) * 90 * S, y + Math.sin(a) * 90 * S); ctx.stroke(); } | |
| ctx.fillStyle = rgba([255, 214, 110], ck * clear); ctx.beginPath(); ctx.arc(x, y, 46 * S, 0, PI2); ctx.fill(); } | |
| say(ctx, clear > .5 ? '来得快 · 去得快 · 从不记仇' : '急性子?', x, y - (P ? 120 : 110) * S, { size: 36 * S, fill: '#fff1d6', alpha: ck, shadow: 'rgba(0,0,0,.7)' }); | |
| } | |
| // 春雷 | |
| const th = u - cue(8) - .2; | |
| if (th > 0 && th < 1.4) { | |
| const fl = Math.max(0, 1 - th * 2.2) + Math.max(0, .5 - Math.abs(th - .45) * 4) ; ctx.fillStyle = rgba([255, 245, 230], Math.min(.8, fl * .7)); ctx.fillRect(0, 0, W, H); | |
| ctx.save(); ctx.strokeStyle = rgba([255, 255, 255], Math.min(1, fl * 1.4)); ctx.shadowColor = '#ffe3a0'; ctx.shadowBlur = 40 * S; ctx.lineWidth = 7 * S; ctx.beginPath(); let x = W * .55, y = 0; ctx.moveTo(x, y); | |
| for (let i = 0; i < 9; i++) { x += (hash(i + 40) - .5) * 140 * S; y += H * .07; ctx.lineTo(x, y); } ctx.stroke(); ctx.restore(); | |
| } | |
| const wk = span(u, cue(9) - .2, cue(9) + 1); | |
| if (wk > 0) { bloom(ctx, W / 2, H / 2, Math.max(W, H) * .6 * wk, [255, 200, 120], .4 * (1 - span(u, cue(9) + 1.5, cue(9) + 2.5))); | |
| say(ctx, '春天的第一声惊雷', W / 2, P ? H * .62 : H * .44, { size: 64 * S, metal: true, alpha: wk * (1 - span(u, X.dur - .8, X.dur)), shadow: 'rgba(255,120,40,.8)', track: 6 * S }); } | |
| }; | |
| /* ============ 9 荣耀铭牌 ============ */ | |
| ACTS.badge = (ctx, u, W, H, X) => { | |
| const { S, P, cue } = X; | |
| // 聚光 | |
| const sp = span(u, 0, 1.2); | |
| const g = ctx.createRadialGradient(W / 2, H * .45, 0, W / 2, H * .45, Math.max(W, H) * .7); g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, rgba([0, 0, 0], .6 * sp)); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); | |
| // 光柱 | |
| ctx.save(); ctx.globalCompositeOperation = 'lighter'; const lg = ctx.createLinearGradient(W / 2, 0, W / 2, H * .6); lg.addColorStop(0, rgba([255, 210, 140], .22 * sp)); lg.addColorStop(1, 'rgba(255,210,140,0)'); ctx.fillStyle = lg; | |
| ctx.beginPath(); ctx.moveTo(W / 2 - 60 * S, 0); ctx.lineTo(W / 2 + 60 * S, 0); ctx.lineTo(W / 2 + 420 * S, H * .75); ctx.lineTo(W / 2 - 420 * S, H * .75); ctx.fill(); ctx.restore(); | |
| const forge = span(u, cue(0) - .4, cue(1) + .4); | |
| const size = P ? Math.min(W * .92, H * .58) : H * .7; | |
| const cx = W / 2, cy = P ? H * .43 : H * .45; | |
| drawBadge(ctx, cx, cy, size, { | |
| t: u, forge, engrave: span(u, cue(2) - .2, cue(3) + .8), ribbon: span(u, cue(4) - .3, cue(4) + 1), name: span(u, cue(5) - .3, cue(5) + 1), | |
| shine: (u - cue(4)) / 2.2, holder: X.opts.name, birthday: X.opts.birthday, | |
| }); | |
| // 礼花 | |
| burst(ctx, u, cue(1) + .2, cx, cy, S, { n: 90, color: GOLD, speed: 900, dur: 2.2, seed: 1 }); | |
| burst(ctx, u, cue(4) + .4, cx - size * .45, cy - size * .3, S, { n: 60, color: [255, 150, 90], speed: 600, dur: 2, seed: 2 }); | |
| burst(ctx, u, cue(4) + .7, cx + size * .45, cy - size * .3, S, { n: 60, color: [255, 220, 140], speed: 600, dur: 2, seed: 3 }); | |
| burst(ctx, u, cue(5) + 1.4, cx, cy - size * .2, S, { n: 120, color: GOLD, speed: 1100, dur: 2.6, seed: 4 }); | |
| // 金色纸屑 | |
| const cf = span(u, cue(4), cue(4) + 1); | |
| if (cf > 0) for (let i = 0; i < 70; i++) { const ph = ((u - cue(4)) * (.18 + hash(i) * .12) + hash(i + 3)) % 1; const x = W * hash(i + 9) + Math.sin(u * 2 + i) * 30 * S, y = -30 * S + ph * (H + 60 * S); | |
| ctx.save(); ctx.translate(x, y); ctx.rotate(u * 3 + i); ctx.scale(1, Math.sin(u * 5 + i)); ctx.globalAlpha = cf * .9; ctx.fillStyle = i % 3 === 0 ? '#ff6a4a' : i % 3 === 1 ? '#ffd98f' : '#fff4d6'; ctx.fillRect(-5 * S, -3 * S, 10 * S, 6 * S); ctx.restore(); } | |
| }; | |
| /* ============ 10 尾声 · 出发 ============ */ | |
| ACTS.onward = (ctx, u, W, H, X) => { | |
| const { S, P, cue, dur } = X; | |
| const hy = H * (P ? .76 : .78); | |
| const rise = E.sine(span(u, 0, cue(1) + 2)); | |
| bloom(ctx, W * .5, hy, 800 * S * rise, [255, 170, 80], .8 * rise); | |
| const g = ctx.createLinearGradient(0, hy - 300 * S, 0, hy); g.addColorStop(0, 'rgba(255,150,70,0)'); g.addColorStop(1, rgba([255, 190, 110], .45 * rise)); ctx.fillStyle = g; ctx.fillRect(0, hy - 300 * S, W, 300 * S); | |
| ctx.fillStyle = '#0a0304'; ctx.beginPath(); ctx.moveTo(0, H); for (let x = 0; x <= W; x += 16) ctx.lineTo(x, hy + Math.sin(x / W * 5 + 2) * 18 * S); ctx.lineTo(W, H); ctx.fill(); | |
| // 天上的星座 | |
| const ck = span(u, cue(1) - .2, cue(2) + .5); | |
| if (ck > 0) { | |
| const ppd = (P ? W * .62 : W * .3) / 14; | |
| const Pj = chart(W, H, 2.35, 23, ppd, P ? 0 : W * .25, P ? -H * .3 : -H * .2); | |
| figure(ctx, ARI, ARI_LINES, Pj, ck, rgba([255, 220, 150], .85), 3 * S, ck, [255, 225, 160], S); | |
| } | |
| // 奔跑的公羊(剪影 + 金边) | |
| const rx = mix(-200 * S, W * (P ? .5 : .42), E.out3(span(u, .3, cue(2) + 1))), ry = hy - 70 * S; | |
| ramSide(ctx, rx, ry - 20 * S, (P ? 1.1 : 1.3) * S, { t: u * 1.4, gait: 1 - span(u, cue(2) + .6, cue(2) + 1.6) * .9, alpha: 1 - span(u, cue(4), cue(4) + 1.2) }); | |
| const wave = span(u, cue(2) + .2, cue(3)); | |
| if (wave > 0 && wave < 1) { bloom(ctx, rx + 110 * S, ry - 80 * S, 60 * S, GOLD, Math.sin(wave * Math.PI)); ring(ctx, rx + 110 * S, ry - 80 * S, 80 * S * wave, 1 - wave, GOLD, 3 * S); } | |
| // 结尾标题 | |
| const tk = span(u, cue(4) - .3, cue(4) + 1.2); | |
| const ty = P ? H * .44 : H * .44; | |
| if (tk > 0) { | |
| ariesGlyph(ctx, W / 2, ty - 150 * S, .7 * S, span(u, cue(4) - .3, cue(4) + 1.4)); | |
| drop(ctx, '白羊座', W / 2, ty, tk, { size: 120 * S, metal: true, track: 24 * S, weight: 900, shadow: 'rgba(255,120,40,.7)', blur: 36 * S }); | |
| say(ctx, '春天,从这里出发', W / 2, ty + 100 * S, { size: 44 * S, fill: '#fff0d4', alpha: span(u, cue(4) + .8, cue(4) + 1.8), track: 8 * S, shadow: 'rgba(0,0,0,.7)' }); | |
| say(ctx, '愿你的每一次出发,都被星光照亮', W / 2, ty + 170 * S, { size: 28 * S, fill: '#ffd9a0', alpha: span(u, cue(5) + .3, cue(5) + 1.3), font: SANS, weight: 600, track: 3 * S }); | |
| } | |
| embers(ctx, u, W, H, { n: 50, seed: 31, color: GOLD, alpha: .8 }); | |
| const fo = span(u, dur - 1.5, dur); if (fo > 0) { ctx.fillStyle = rgba([4, 1, 2], fo); ctx.fillRect(0, 0, W, H); } | |
| }; | |
| export { ARI, ARI_LINES, chart, figure }; | |