File size: 6,068 Bytes
a97ef0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// ♈ 白羊座 —— 合成器:着色器背景 + 场景 + 转场 + 字幕 + 角标
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 };
}