POLS / aries /src /badge.js
Ethscriptions's picture
aries: 渲染引擎与十幕场景、荣耀铭牌
a97ef0b verified
Raw History Blame Contribute Delete
9.96 kB
// ♈ 白羊座 · 先锋荣耀铭牌(视频第 9 幕 + 单独导出卡片共用)
import { PI2, clamp01, mix, span, E, hash, rgba, GOLD, SERIF, SANS, DISPLAY, bloom, star, gold, say, trace, ramHead, laurel, ariesGlyph, marsGlyph, rr } from './kit.js';
function plateShape() {
const p = new Path2D();
p.moveTo(-300, -380); p.bezierCurveTo(-150, -440, 150, -440, 300, -380);
p.lineTo(370, -310); p.lineTo(370, 420); p.lineTo(300, 500); p.lineTo(-300, 500); p.lineTo(-370, 420); p.lineTo(-370, -310); p.closePath();
return p;
}
function innerShape() {
const p = new Path2D();
p.moveTo(-282, -352); p.bezierCurveTo(-140, -405, 140, -405, 282, -352);
p.lineTo(342, -296); p.lineTo(342, 408); p.lineTo(286, 472); p.lineTo(-286, 472); p.lineTo(-342, 408); p.lineTo(-342, -296); p.closePath();
return p;
}
const STARS = [[-150, 60, 2.4], [-40, 10, 3.6], [60, 30, 3], [110, 70, 2.2], [210, -10, 2.6]];
/**
* o: { t, forge(0-1), engrave(0-1), ribbon(0-1), name(0-1), shine(0-1 扫光进度), holder, birthday }
*/
export function drawBadge(ctx, cx, cy, size, o = {}) {
const s = size / 1000, t = o.t ?? 0;
const forge = o.forge ?? 1; if (forge <= 0) return;
const cool = span(forge, .45, 1); // 熔融 → 冷却成金
const body = span(forge, .25, .8);
ctx.save(); ctx.translate(cx, cy); ctx.scale(s, s);
const plate = plateShape(), inner = innerShape();
// 背后光晕
ctx.save(); ctx.scale(1 / s, 1 / s); bloom(ctx, 0, 0, size * .9, [255, 150, 70], .55 * body); ctx.restore();
// —— 板体 ——
if (body > 0) {
ctx.save(); ctx.globalAlpha *= body;
ctx.shadowColor = 'rgba(0,0,0,.6)'; ctx.shadowBlur = 40 * s; ctx.shadowOffsetY = 18 * s;
ctx.fillStyle = cool < 1 ? gold(ctx, -370, -440, 370, 500, 1) : gold(ctx, -370, -440, 370, 500);
ctx.fill(plate); ctx.shadowColor = 'transparent';
if (cool > 0 && cool < 1) { ctx.globalAlpha = body * cool; ctx.fillStyle = gold(ctx, -370, -440, 370, 500); ctx.fill(plate); ctx.globalAlpha = body; }
// 内层:深红珐琅
const en = ctx.createLinearGradient(0, -400, 0, 480); en.addColorStop(0, '#5a0d0d'); en.addColorStop(.5, '#2a0507'); en.addColorStop(1, '#420909');
ctx.fillStyle = en; ctx.fill(inner);
// 珐琅纹理:放射细线
ctx.save(); ctx.clip(inner); ctx.globalAlpha *= .12; ctx.strokeStyle = '#ffcf8a'; ctx.lineWidth = 1.2;
for (let i = 0; i < 72; i++) { const a = i / 72 * PI2; ctx.beginPath(); ctx.moveTo(0, -300); ctx.lineTo(Math.cos(a) * 1200, -300 + Math.sin(a) * 1200); ctx.stroke(); }
ctx.restore();
// 内金边 + 回纹
ctx.lineWidth = 6; ctx.strokeStyle = gold(ctx, -340, -400, 340, 470); ctx.stroke(inner);
ctx.save(); ctx.globalAlpha *= .8; ctx.lineWidth = 2; const k = 330; ctx.beginPath();
for (let y = -250; y < 380; y += 26) { for (const d of [-1, 1]) { ctx.moveTo(d * k, y); ctx.lineTo(d * (k - 10), y); ctx.lineTo(d * (k - 10), y + 13); ctx.lineTo(d * k, y + 13); } }
ctx.stroke(); ctx.restore();
// 铆钉
for (const [x, y] of [[-330, 450], [330, 450], [-330, -270], [330, -270]]) { ctx.fillStyle = gold(ctx, x - 12, y - 12, x + 12, y + 12); ctx.beginPath(); ctx.arc(x, y, 11, 0, PI2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,.6)'; ctx.beginPath(); ctx.arc(x - 3, y - 3, 3, 0, PI2); ctx.fill(); }
ctx.restore();
}
// 熔融描边
trace(ctx, plate, 3200, span(forge, 0, .5), { width: 8, color: cool > .5 ? '#ffe3a0' : '#ffb14a', glow: 'rgba(255,140,40,1)', blur: 30 * s });
// —— 星图暗纹 ——
const eng = o.engrave ?? 1;
if (body > .5) {
ctx.save(); ctx.globalAlpha *= .35 * cool; ctx.strokeStyle = '#ffcf8a'; ctx.lineWidth = 2; ctx.setLineDash([4, 6]);
ctx.beginPath(); STARS.forEach(([x, y], i) => i ? ctx.lineTo(x, y + 120) : ctx.moveTo(x, y + 120)); ctx.stroke(); ctx.restore();
STARS.forEach(([x, y, r]) => { ctx.save(); ctx.scale(1, 1); star(ctx, x, y + 120, r, [255, 220, 150], .5 * cool, .3); ctx.restore(); });
}
// —— 顶部徽章 ——
const mk = span(forge, .35, .95);
if (mk > 0) {
const my = -300;
ctx.save(); ctx.globalAlpha *= mk;
ctx.shadowColor = 'rgba(0,0,0,.6)'; ctx.shadowBlur = 30 * s;
ctx.fillStyle = gold(ctx, -170, my - 170, 170, my + 170, cool < 1 ? 1 : 0); ctx.beginPath(); ctx.arc(0, my, 168, 0, PI2); ctx.fill(); ctx.shadowColor = 'transparent';
const rg = ctx.createRadialGradient(0, my - 40, 10, 0, my, 140); rg.addColorStop(0, '#7a1712'); rg.addColorStop(1, '#2a0506'); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(0, my, 140, 0, PI2); ctx.fill();
ctx.strokeStyle = gold(ctx, -140, my - 140, 140, my + 140); ctx.lineWidth = 4; ctx.stroke();
// 齿边
ctx.fillStyle = gold(ctx, -170, my - 170, 170, my + 170); for (let i = 0; i < 48; i++) { const a = i / 48 * PI2; ctx.beginPath(); ctx.arc(Math.cos(a) * 158, my + Math.sin(a) * 158, 4, 0, PI2); ctx.fill(); }
ctx.restore();
laurel(ctx, 0, my + 20, 1.25, -1, span(forge, .55, 1)); laurel(ctx, 0, my + 20, 1.25, 1, span(forge, .55, 1));
ramHead(ctx, 0, my + 12, .62, { k: span(forge, .4, 1), molten: cool < .6, eye: .9 });
}
// —— 缎带 ——
const rb = o.ribbon ?? 1;
if (rb > 0) {
const w = 860 * E.out3(rb), y = -88, h = 96;
ctx.save();
for (const d of [-1, 1]) { ctx.fillStyle = '#7d1310'; ctx.beginPath(); const ex = d * w / 2; ctx.moveTo(ex - d * 20, y - h / 2 + 22); ctx.lineTo(ex + d * 70, y - h / 2 + 22); ctx.lineTo(ex + d * 44, y + 14); ctx.lineTo(ex + d * 70, y + h / 2 + 22); ctx.lineTo(ex - d * 20, y + h / 2 + 22); ctx.closePath(); ctx.fill(); }
const g = ctx.createLinearGradient(0, y - h / 2, 0, y + h / 2); g.addColorStop(0, '#e8443a'); g.addColorStop(.5, '#b8221b'); g.addColorStop(1, '#7d1310');
ctx.shadowColor = 'rgba(0,0,0,.5)'; ctx.shadowBlur = 18 * s; ctx.fillStyle = g; ctx.beginPath();
ctx.moveTo(-w / 2, y - h / 2); ctx.quadraticCurveTo(0, y - h / 2 - 24, w / 2, y - h / 2); ctx.lineTo(w / 2, y + h / 2); ctx.quadraticCurveTo(0, y + h / 2 - 24, -w / 2, y + h / 2); ctx.closePath(); ctx.fill();
ctx.shadowColor = 'transparent'; ctx.strokeStyle = gold(ctx, -w / 2, y, w / 2, y); ctx.lineWidth = 3; ctx.stroke();
ctx.restore();
say(ctx, '白羊座 · ARIES', 0, y - 8, { size: 56, metal: true, alpha: span(rb, .5, 1), track: 6, shadow: 'rgba(60,0,0,.8)', blur: 6 });
}
// —— 刻字信息 ——
if (eng > 0) {
const rows = [['守护星', '火星 ♂'], ['元素', '火 · 开创'], ['宫位', '黄道第一宫'], o.birthday ? ['生日', o.birthday] : ['星座季', '3.21 — 4.19']];
rows.forEach(([k, v], i) => {
const kk = span(eng, i * .16, i * .16 + .35); if (kk <= 0) return;
const col = i % 2, row = Math.floor(i / 2); const x = col ? 170 : -170, y = 30 + row * 108;
ctx.save(); ctx.globalAlpha *= kk; rr(ctx, x - 150, y - 42, 300, 84, 12); ctx.fillStyle = 'rgba(0,0,0,.28)'; ctx.fill(); ctx.strokeStyle = 'rgba(255,207,138,.55)'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.restore();
say(ctx, k, x, y - 18, { size: 22, fill: '#ffcf9a', alpha: kk, font: SANS, weight: 600, track: 4 });
say(ctx, v, x, y + 16, { size: 34, metal: true, alpha: kk, font: SERIF, weight: 800 });
});
const kw = span(eng, .7, 1);
if (kw > 0) { say(ctx, '勇 气 · 热 情 · 开 创', 0, 262, { size: 34, fill: '#ffe3b0', alpha: kw, font: SERIF, weight: 700, shadow: 'rgba(255,120,40,.9)', blur: 18 }); }
}
// —— 先锋之证 + 署名 ——
const nm = o.name ?? 1;
if (nm > 0) {
say(ctx, '先 锋 之 证', 0, 340, { size: 70, metal: true, alpha: span(nm, 0, .5), weight: 900, shadow: 'rgba(255,120,40,.8)', blur: 26 });
const holder = (o.holder || '').trim();
ctx.save(); ctx.globalAlpha *= span(nm, .3, .8); ctx.strokeStyle = 'rgba(255,207,138,.6)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-210, 386); ctx.lineTo(210, 386); ctx.stroke(); ctx.restore();
say(ctx, holder ? `授予 · ${holder}` : '授予 · 每一位白羊座', 0, 418, { size: holder ? 36 : 32, fill: '#fff1d6', alpha: span(nm, .3, .9), font: SERIF, weight: 700, track: 2 });
say(ctx, 'VANGUARD OF THE ZODIAC · No.001', 0, 460, { size: 17, fill: 'rgba(255,214,160,.8)', alpha: span(nm, .5, 1), font: DISPLAY, weight: 600, track: 3 });
}
// —— 扫光 ——
const sh = o.shine;
if (sh !== undefined && sh > 0 && sh < 1 && body >= 1) {
ctx.save(); ctx.clip(plate); ctx.globalCompositeOperation = 'lighter';
const x = mix(-700, 700, sh); const g = ctx.createLinearGradient(x - 160, -500, x + 160, 500);
g.addColorStop(0, 'rgba(255,240,200,0)'); g.addColorStop(.5, 'rgba(255,240,200,.45)'); g.addColorStop(1, 'rgba(255,240,200,0)');
ctx.fillStyle = g; ctx.fillRect(-400, -520, 800, 1040); ctx.restore();
}
// 闪烁星芒
if (body >= 1) for (let i = 0; i < 5; i++) { const ph = (t * .5 + i * .23) % 1; const x = [-330, 300, -200, 250, 0][i], y = [-300, -200, 450, 380, -470][i]; ctx.save(); ctx.scale(1, 1); star(ctx, x, y, 3.5, [255, 240, 200], Math.sin(ph * Math.PI) * .9, 1.2); ctx.restore(); }
ctx.restore();
}
// 单独的铭牌卡片(壁纸 9:16 / 头像 1:1),用于页面下载
export function drawBadgeCard(ctx, W, H, opts, paintBg) {
paintBg(ctx, W, H);
const S = Math.min(W, H) / 1080;
const P = H > W * 1.2;
const size = P ? Math.min(W * 1.12, H * .66) : H * .84;
drawBadge(ctx, W / 2, P ? H * .47 : H * .5, size, { t: 2.3, forge: 1, engrave: 1, ribbon: 1, name: 1, holder: opts.name, birthday: opts.birthday });
if (P) {
say(ctx, '♈ 白羊座 · 春天从这里出发', W / 2, H * .085, { size: 40 * S, fill: '#ffe2b0', font: SERIF, weight: 700, track: 4 * S });
say(ctx, '愿你永远保有第一次出发时的那份热血', W / 2, H * .9, { size: 34 * S, fill: '#fff1d6', font: SERIF, weight: 600, track: 2 * S });
say(ctx, '星座文化 · 一份属于白羊座的荣耀', W / 2, H * .94, { size: 24 * S, fill: 'rgba(255,214,160,.8)', font: SANS, weight: 400 });
}
}