Spaces:
Running
Running
Download taurus/js/card.js from Ethscriptions/POLS: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/taurus/js/card.js
- Command line
-
hf download hf://spaces/Ethscriptions/POLS/taurus/js/card.js
-
curl -L -o card.js https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/taurus/js/card.js
11.4 kB
| // ♉ 金牛座 · 鎏金珍藏卡(视频第 9 幕 + 页面单独导出共用,全新编写) | |
| import { TAU, clamp, lerp, seg, ease, h1, col, C, FONT, gilt, halo, sparkle, text, rrect, flower, leaf, bullHead, taurusSign, venusSign } from './canvas-kit.js'; | |
| const CW = 630, CH = 880, R = 36; | |
| function archPath(g, x, y, w, h) { // 拱形窗 | |
| 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(); | |
| } | |
| function back(g, t) { | |
| 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(); | |
| g.lineWidth = 10; g.strokeStyle = gilt(g, -CW / 2, -CH / 2, CW / 2, CH / 2, 'rose'); g.stroke(); | |
| g.save(); rrect(g, -CW / 2 + 26, -CH / 2 + 26, CW - 52, CH - 52, 22); g.clip(); | |
| g.strokeStyle = col(C.gold, .22); g.lineWidth = 1.2; | |
| 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(); } | |
| g.restore(); | |
| 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(); | |
| 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(); | |
| g.lineWidth = 1.5; g.beginPath(); g.arc(0, -10, 130, 0, TAU); g.stroke(); | |
| 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(); } | |
| taurusSign(g, 0, -4, 1.25, 1, { width: 12 }); | |
| text(g, 'TAURUS', 0, 220, { size: 46, font: FONT.latin, weight: 700, gilt: 'gold', spacing: 14 }); | |
| text(g, '金牛座 · 鎏金珍藏', 0, 280, { size: 26, color: col(C.gold, .85), weight: 700, spacing: 6 }); | |
| void t; | |
| } | |
| function front(g, st) { | |
| const t = st.t ?? 0, F = st.fields ?? 1; | |
| // 外框 | |
| rrect(g, -CW / 2, -CH / 2, CW, CH, R); g.fillStyle = gilt(g, -CW / 2, -CH / 2, CW / 2, CH / 2, 'rose'); g.fill(); | |
| 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(); | |
| g.lineWidth = 2; g.strokeStyle = gilt(g, -300, -400, 300, 400); g.stroke(); | |
| // 拱窗插画 | |
| const ax = -238, ay = -392, aw = 476, ah = 430; | |
| g.save(); archPath(g, ax, ay, aw, ah); g.clip(); | |
| 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); | |
| 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(); } | |
| // 昴星团小点 | |
| [[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)); | |
| // 远山与花丛 | |
| 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(); | |
| halo(g, 0, -170, 260, [255, 210, 150], .7); | |
| bullHead(g, 0, -168, 1.02, { k: 1 }); | |
| 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]]) { | |
| 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] }); | |
| } | |
| g.restore(); | |
| archPath(g, ax, ay, aw, ah); g.lineWidth = 5; g.strokeStyle = gilt(g, ax, ay, ax + aw, ay + ah); g.stroke(); | |
| // 角标 | |
| 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(); | |
| text(g, 'No.', -246, -366, { size: 18, font: FONT.latin, weight: 700, color: col(C.gold) }); | |
| text(g, '02', -246, -340, { size: 34, font: FONT.latin, weight: 700, gilt: 'gold' }); | |
| g.fillStyle = '#0c2c23'; g.beginPath(); g.arc(246, -350, 44, 0, TAU); g.fill(); g.stroke(); | |
| taurusSign(g, 246, -346, .42, 1, { width: 9, blur: 6 }); | |
| // 名称缎带 | |
| const ry = 48; g.save(); | |
| 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(); } | |
| 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; | |
| 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; | |
| g.strokeStyle = gilt(g, -280, ry, 280, ry); g.lineWidth = 2.5; g.stroke(); g.restore(); | |
| text(g, '金牛座 · TAURUS', 0, ry - 6, { size: 40, gilt: 'gold', weight: 900, spacing: 6, glow: 'rgba(0,0,0,.5)', blur: 6 }); | |
| // 属性 | |
| const rows = [['守护星', '金星 ♀'], ['元素', '土 · 固定'], ['宫位', '黄道第二宫'], st.birthday ? ['生日', st.birthday] : ['星座季', '4.20 — 5.20']]; | |
| rows.forEach(([k, v], i) => { | |
| const q = ease.out(seg(F, i * .14, i * .14 + .3)); if (q <= 0) return; | |
| const x = i % 2 ? 140 : -140, y = 132 + Math.floor(i / 2) * 82; | |
| 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(); | |
| text(g, k, x, y - 13, { size: 17, font: FONT.sans, weight: 600, color: col(C.rose), alpha: q, spacing: 4 }); | |
| text(g, v, x, y + 13, { size: 27, weight: 900, gilt: 'gold', alpha: q }); | |
| }); | |
| // 品质宝石 | |
| const qs = ['坚定', '温柔', '可靠']; | |
| qs.forEach((s, i) => { | |
| const q = ease.back(seg(F, .55 + i * .12, .75 + i * .12)); if (q <= 0) return; | |
| const x = (i - 1) * 168, y = 306; | |
| 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(); | |
| g.strokeStyle = gilt(g, -70, -26, 70, 26); g.lineWidth = 2; g.stroke(); g.restore(); | |
| text(g, s, x, y + 1, { size: 28 * q, weight: 900, color: '#4a1420' }); | |
| }); | |
| // 署名 | |
| const nk = seg(F, .85, 1); | |
| text(g, '大地守护者', 0, 370, { size: 34, weight: 900, gilt: 'gold', spacing: 10, alpha: nk }); | |
| const holder = (st.holder || '').trim(); | |
| text(g, holder ? `珍藏人 · ${holder}` : '珍藏人 · 每一位金牛座', 0, 410, { size: 22, weight: 700, color: col(C.ivory, .92), alpha: nk, spacing: 2 }); | |
| 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 }); | |
| venusSign(g, -262, 404, .32, nk, { width: 7 }); | |
| g.save(); g.globalAlpha *= nk; taurusSign(g, 262, 400, .3, 1, { width: 8, blur: 4 }); g.restore(); | |
| } | |
| // 镭射全息:彩虹带随 shine 扫过 + 细闪 | |
| function holo(g, shine, t, strength = 1) { | |
| g.save(); rrect(g, -CW / 2, -CH / 2, CW, CH, R); g.clip(); | |
| g.globalCompositeOperation = 'overlay'; | |
| const x = lerp(-900, 900, shine); | |
| const gr = g.createLinearGradient(x - 380, -440, x + 380, 440); | |
| ['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)); | |
| g.globalAlpha = strength; g.fillStyle = gr; g.fillRect(-CW / 2, -CH / 2, CW, CH); | |
| g.globalCompositeOperation = 'lighter'; | |
| 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)'); | |
| g.fillStyle = wg; g.fillRect(-CW / 2, -CH / 2, CW, CH); | |
| 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); } | |
| g.restore(); | |
| } | |
| function stamp(g, k) { | |
| if (k <= 0) return; | |
| const s = lerp(2.4, 1, ease.out5(k)), a = clamp(k * 3); | |
| g.save(); g.translate(196, 290); g.rotate(-.22); g.scale(s, s); g.globalAlpha *= a * .92; | |
| 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(); | |
| g.lineWidth = 2; g.beginPath(); g.arc(0, 0, 62, 0, TAU); g.stroke(); | |
| text(g, '认证', 0, -6, { size: 40, weight: 900, color: '#c4302b' }); | |
| text(g, '大地守护者', 0, 32, { size: 17, weight: 900, color: '#c4302b', spacing: 2 }); | |
| 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(); } | |
| g.restore(); | |
| } | |
| /** | |
| * st: { t, flip 0..1, fields 0..1, shine 0..1|undefined, stamp 0..1, holder, birthday, tilt } | |
| */ | |
| export function drawCard(g, cx, cy, height, st = {}) { | |
| const s = height / CH, flip = st.flip ?? 1, ang = flip * Math.PI, sx = Math.cos(ang); | |
| g.save(); g.translate(cx, cy); g.rotate(st.tilt ?? 0); g.scale(s * Math.max(.02, Math.abs(sx)), s); | |
| // 投影 | |
| 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(); | |
| if (sx > 0) back(g, st.t); else { g.scale(-1, 1); g.scale(-1, 1); front(g, st); } | |
| if (sx <= 0 && st.shine !== undefined) holo(g, st.shine, st.t ?? 0, st.holo ?? 1); | |
| if (sx <= 0) stamp(g, st.stamp ?? 0); | |
| // 翻转时的明暗 | |
| 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(); } | |
| g.restore(); | |
| } | |
| // 单独的海报(壁纸 9:16 / 头像 1:1 / 收藏卡 3:4) | |
| export function drawCardPoster(g, W, H, opts, paint) { | |
| paint(g, W, H); | |
| const S = Math.min(W, H) / 1080, portrait = H > W * 1.25; | |
| const ch = portrait ? Math.min(H * .66, W * .9 / CW * CH) : H * .86; | |
| const cy = portrait ? H * .48 : H / 2; | |
| halo(g, W / 2, cy, ch * .8, [255, 200, 150], .7); | |
| 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 }); | |
| if (portrait) { | |
| text(g, '♉ 金牛座 · 慢慢来,也能抵达美好', W / 2, H * .08, { size: 40 * S, weight: 700, color: col(C.gold), spacing: 4 * S }); | |
| text(g, '愿你按照自己的节奏,稳稳地发光', W / 2, H * .9, { size: 36 * S, weight: 700, color: col(C.ivory), spacing: 2 * S }); | |
| text(g, '星座文化 · 一张属于金牛座的珍藏卡', W / 2, H * .94, { size: 24 * S, font: FONT.sans, weight: 400, color: col(C.rose, .9) }); | |
| } | |
| } | |
| export const CARD_RATIO = CW / CH; | |