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