// ♈ 白羊座 · 先锋荣耀铭牌(视频第 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 }); } }