Spaces:
Running
Running
Download aries/src/stage.js from Ethscriptions/POLS: direct link, hf CLI and curl.
- Browser
- Download file 6.07 kB
-
https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/stage.js
- Command line
-
hf download hf://spaces/Ethscriptions/POLS/aries/src/stage.js
-
curl -L -o stage.js https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/stage.js
6.07 kB
| // ♈ 白羊座 —— 合成器:着色器背景 + 场景 + 转场 + 字幕 + 角标 | |
| import { TIMELINE } from './timeline.js'; | |
| import { ACTS } from './scenes.js'; | |
| import { createSky, TONES } from './gl.js'; | |
| import { clamp01, mix, span, E, rgba, SANS, SERIF, DISPLAY, bloom, say, wrap, ariesGlyph, gold, rr } from './kit.js'; | |
| const XF = .8; | |
| const HEAT = { ember: 1, dawn: .45, night: .15, sea: .1, grove: .2, royal: .55 }; | |
| const STAR = { ember: .7, dawn: .8, night: 1.3, sea: 1, grove: .9, royal: 1 }; | |
| export function actIndex(t) { const a = TIMELINE.acts; for (let i = 0; i < a.length; i++) if (t < a[i].t1) return i; return a.length - 1; } | |
| export function createStage() { | |
| let sky = null; | |
| try { sky = createSky(); } catch (e) { console.warn('WebGL2 不可用,使用 Canvas 背景', e); } | |
| function background(ctx, t, W, H, i) { | |
| const act = TIMELINE.acts[i], prev = TIMELINE.acts[Math.max(0, i - 1)]; | |
| const k = i === 0 ? 1 : E.sine(span(t, act.t0 - .2, act.t0 + 1.6)); | |
| const heat = mix(HEAT[prev.tone], HEAT[act.tone], k), stars = mix(STAR[prev.tone], STAR[act.tone], k); | |
| if (sky) { | |
| const cv = sky.render(t, W, H, prev.tone, act.tone, k, { heat, stars, scale: W * H > 1.5e6 ? .5 : .6 }); | |
| ctx.drawImage(cv, 0, 0, W, H); | |
| } else { | |
| const A = TONES[prev.tone], B = TONES[act.tone]; | |
| const c = j => [0, 1, 2].map(n => Math.round(mix(A[j][n], B[j][n], k) * 255)); | |
| const g = ctx.createRadialGradient(W / 2, H * .55, 0, W / 2, H * .55, Math.max(W, H) * .8); | |
| g.addColorStop(0, rgba(c(1))); g.addColorStop(1, rgba(c(0))); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); | |
| } | |
| } | |
| function subtitles(ctx, t, W, H) { | |
| const P = H > W, S = Math.min(W, H) / 1080; | |
| let cur = null; | |
| for (const a of TIMELINE.acts) for (const l of a.lines) if (t >= l.a - .08 && t < l.b + .15) cur = l; | |
| if (!cur) return; | |
| const al = Math.min(E.sine(span(t, cur.a - .08, cur.a + .18)), 1 - E.sine(span(t, cur.b - .02, cur.b + .15))); | |
| const size = (P ? 50 : 44) * S; | |
| const lines = wrap(ctx, cur.s, P ? W * .84 : W * .66, size); | |
| const lh = size * 1.42, y0 = (P ? H * .875 : H * .895) - (lines.length - 1) * lh / 2; | |
| ctx.save(); ctx.font = `600 ${size}px ${SANS}`; | |
| const w = Math.max(...lines.map(l => ctx.measureText(l).width)) + 70 * S; | |
| ctx.globalAlpha = al * .78; | |
| const g = ctx.createLinearGradient(W / 2 - w / 2, 0, W / 2 + w / 2, 0); | |
| g.addColorStop(0, 'rgba(20,4,6,0)'); g.addColorStop(.12, 'rgba(20,4,6,.85)'); g.addColorStop(.88, 'rgba(20,4,6,.85)'); g.addColorStop(1, 'rgba(20,4,6,0)'); | |
| ctx.fillStyle = g; ctx.fillRect(W / 2 - w / 2, y0 - lh * .66, w, lines.length * lh + lh * .3); | |
| ctx.globalAlpha = al * .9; ctx.fillStyle = gold(ctx, W / 2 - w / 2, 0, W / 2 + w / 2, 0); | |
| ctx.fillRect(W / 2 - w * .3, y0 - lh * .66, w * .6, 1.5 * S); ctx.fillRect(W / 2 - w * .3, y0 - lh * .66 + lines.length * lh + lh * .3, w * .6, 1.5 * S); | |
| ctx.restore(); | |
| lines.forEach((l, i) => say(ctx, l, W / 2, y0 + i * lh, { size, font: SANS, weight: 600, fill: '#fff6e4', alpha: al, stroke: 'rgba(30,6,8,.85)', strokeW: size * .12 })); | |
| } | |
| function hud(ctx, t, W, H, i) { | |
| const P = H > W, S = Math.min(W, H) / 1080, a = .9 * span(t, 1.5, 3); | |
| if (a <= 0) return; | |
| const x = (P ? 46 : 56) * S, y = (P ? 86 : 56) * S; | |
| ariesGlyph(ctx, x + 16 * S, y + 2 * S, .17 * S, 1, { width: 16, blur: 8 }); | |
| say(ctx, '白羊座 · 春天从这里出发', x + 46 * S, y - 8 * S, { size: 24 * S, align: 'left', fill: '#ffdca0', alpha: a, font: SERIF }); | |
| const act = TIMELINE.acts[i]; const ka = Math.min(span(t, act.t0, act.t0 + .8), 1 - span(t, act.t1 - .8, act.t1)); | |
| say(ctx, `${String(i + 1).padStart(2, '0')} · ${act.title}`, x + 46 * S, y + 24 * S, { size: 19 * S, align: 'left', fill: 'rgba(255,236,214,.75)', alpha: a * ka, font: SANS, weight: 400, track: 1 }); | |
| // 顶部火焰进度线 | |
| const pw = W - 2 * x, py = (P ? 40 : 24) * S, k = t / TIMELINE.length; | |
| ctx.save(); ctx.globalAlpha = a * .5; ctx.fillStyle = 'rgba(255,255,255,.12)'; ctx.fillRect(x, py, pw, 2 * S); | |
| ctx.fillStyle = gold(ctx, x, 0, x + pw, 0); ctx.fillRect(x, py, pw * k, 2 * S); ctx.restore(); | |
| bloom(ctx, x + pw * k, py + S, 14 * S, [255, 170, 80], a); | |
| } | |
| function draw(ctx, t, W, H, opts = {}) { | |
| t = Math.max(0, Math.min(TIMELINE.length, t)); | |
| const i = actIndex(t), act = TIMELINE.acts[i]; | |
| const S = Math.min(W, H) / 1080, P = H > W; | |
| ctx.save(); | |
| background(ctx, t, W, H, i); | |
| const run = (k, alpha) => { | |
| if (alpha <= .002) return; const a = TIMELINE.acts[k]; | |
| const X = { S, P, dur: a.t1 - a.t0, opts, cue: n => (a.lines[n] ? a.lines[n].a - a.t0 : 0) }; | |
| ctx.save(); ctx.globalAlpha = alpha; | |
| try { ACTS[a.id](ctx, Math.max(0, t - a.t0), W, H, X); } catch (e) { console.error(a.id, e); } | |
| ctx.restore(); ctx.globalAlpha = 1; | |
| }; | |
| const kin = i ? E.sine(span(t, act.t0, act.t0 + XF)) : 1; | |
| if (i && kin < 1) run(i - 1, 1 - kin); | |
| run(i, kin); | |
| // 转场:横向镜头光晕扫过 | |
| const d = t - act.t0; | |
| if (i && d > -.3 && d < 1.1) { | |
| const k = clamp01((d + .3) / 1.4), x = mix(-W * .3, W * 1.3, E.sine(k)), al = Math.sin(k * Math.PI) * .6; | |
| ctx.save(); ctx.globalCompositeOperation = 'lighter'; | |
| const g = ctx.createLinearGradient(x - 300 * S, 0, x + 300 * S, 0); g.addColorStop(0, 'rgba(255,170,80,0)'); g.addColorStop(.5, rgba([255, 190, 110], .35 * al)); g.addColorStop(1, 'rgba(255,170,80,0)'); | |
| ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); ctx.restore(); | |
| bloom(ctx, x, H * .45, 240 * S, [255, 210, 150], al); | |
| ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = rgba([255, 230, 190], al * .8); ctx.fillRect(0, H * .45 - 1.5 * S, W, 3 * S); ctx.restore(); | |
| } | |
| if (opts.subtitles !== false) subtitles(ctx, t, W, H); | |
| if (opts.hud !== false) hud(ctx, t, W, H, i); | |
| const fi = 1 - span(t, 0, 1.4); if (fi > 0) { ctx.fillStyle = rgba([3, 1, 2], fi); ctx.fillRect(0, 0, W, H); } | |
| ctx.restore(); | |
| } | |
| return { draw, hasGL: !!sky, sky }; | |
| } | |