@real-music-packages/web-core 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ // src/video.ts
2
+ var SAFE_ZONE = { top: 0.15, bottom: 0.28, left: 0.06, right: 0.12 };
3
+ function safeBox(W, H) {
4
+ const left = W * SAFE_ZONE.left;
5
+ const right = W * (1 - SAFE_ZONE.right);
6
+ const top = H * SAFE_ZONE.top;
7
+ const bottom = H * (1 - SAFE_ZONE.bottom);
8
+ return {
9
+ left,
10
+ right,
11
+ top,
12
+ bottom,
13
+ w: right - left,
14
+ h: bottom - top,
15
+ cx: (left + right) / 2,
16
+ cy: (top + bottom) / 2,
17
+ centeredW: 2 * Math.min(W / 2 - left, right - W / 2)
18
+ };
19
+ }
20
+ function safeTitleBaseline(size, H) {
21
+ return H * SAFE_ZONE.top + size * 0.8 + 4;
22
+ }
23
+ function drawSafeGuides(ctx) {
24
+ const W = ctx.canvas.width, H = ctx.canvas.height;
25
+ const b = safeBox(W, H);
26
+ ctx.save();
27
+ ctx.globalAlpha = 1;
28
+ ctx.fillStyle = "rgba(220,40,40,0.35)";
29
+ ctx.fillRect(0, 0, W, b.top);
30
+ ctx.fillRect(0, b.bottom, W, H - b.bottom);
31
+ ctx.fillRect(0, b.top, b.left, b.h);
32
+ ctx.fillRect(b.right, b.top, W - b.right, b.h);
33
+ ctx.strokeStyle = "rgba(40,200,90,1)";
34
+ ctx.lineWidth = 6;
35
+ ctx.strokeRect(b.left, b.top, b.w, b.h);
36
+ ctx.restore();
37
+ }
38
+ function pickMimeType() {
39
+ const candidates = [
40
+ "video/webm;codecs=vp9,opus",
41
+ "video/webm;codecs=vp8,opus",
42
+ "video/webm"
43
+ ];
44
+ for (const t of candidates) {
45
+ if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(t)) return t;
46
+ }
47
+ return "video/webm";
48
+ }
49
+ async function recordScenes(scenes, opts) {
50
+ const { audioStream, width, height, fps = 30, background = "#000000", onProgress } = opts;
51
+ const canvas = document.createElement("canvas");
52
+ canvas.width = width;
53
+ canvas.height = height;
54
+ const maybeCtx = canvas.getContext("2d");
55
+ if (!maybeCtx) throw new Error("Failed to get 2D context");
56
+ const ctx = maybeCtx;
57
+ const videoStream = canvas.captureStream(fps);
58
+ const combined = new MediaStream([
59
+ ...videoStream.getVideoTracks(),
60
+ ...audioStream.getAudioTracks()
61
+ ]);
62
+ const mimeType = pickMimeType();
63
+ const recorder = new MediaRecorder(combined, {
64
+ mimeType,
65
+ videoBitsPerSecond: 4e6,
66
+ audioBitsPerSecond: 128e3
67
+ });
68
+ const chunks = [];
69
+ recorder.ondataavailable = (e) => {
70
+ if (e.data.size > 0) chunks.push(e.data);
71
+ };
72
+ const stopped = new Promise((res) => {
73
+ recorder.onstop = () => res();
74
+ });
75
+ recorder.start();
76
+ const totalMs = scenes.reduce((s, sc) => s + sc.durationMs, 0);
77
+ const t0 = performance.now();
78
+ onProgress?.({ phase: "capturing", progress01: 0 });
79
+ let sceneStart = 0;
80
+ let sceneIdx = 0;
81
+ let enteredScene = -1;
82
+ ctx.fillStyle = background;
83
+ ctx.fillRect(0, 0, width, height);
84
+ scenes[0].draw(ctx, 0);
85
+ await new Promise((resolve) => {
86
+ function tick() {
87
+ const now = performance.now() - t0;
88
+ if (now >= totalMs) {
89
+ setTimeout(resolve, 100);
90
+ return;
91
+ }
92
+ while (sceneIdx < scenes.length - 1 && now - sceneStart >= scenes[sceneIdx].durationMs) {
93
+ sceneStart += scenes[sceneIdx].durationMs;
94
+ sceneIdx += 1;
95
+ }
96
+ const scene = scenes[sceneIdx];
97
+ if (enteredScene !== sceneIdx) {
98
+ enteredScene = sceneIdx;
99
+ scene.onEnter?.();
100
+ }
101
+ const tInScene = (now - sceneStart) / scene.durationMs;
102
+ ctx.fillStyle = background;
103
+ ctx.fillRect(0, 0, width, height);
104
+ scene.draw(ctx, Math.min(1, Math.max(0, tInScene)));
105
+ onProgress?.({
106
+ phase: "capturing",
107
+ progress01: Math.min(0.95, now / totalMs)
108
+ });
109
+ requestAnimationFrame(tick);
110
+ }
111
+ requestAnimationFrame(tick);
112
+ });
113
+ onProgress?.({ phase: "finalizing", progress01: 0.96 });
114
+ recorder.stop();
115
+ await stopped;
116
+ return new Blob(chunks, { type: mimeType });
117
+ }
118
+ function truncate(s, max) {
119
+ return s.length > max ? s.slice(0, max - 1).trimEnd() + "\u2026" : s;
120
+ }
121
+ function initials(name) {
122
+ return name.split(/\s+/).filter(Boolean).slice(0, 3).map((w) => w[0]).join("").toUpperCase();
123
+ }
124
+ function drawBottomGradient(ctx, opts = {}) {
125
+ const W = ctx.canvas.width, H = ctx.canvas.height;
126
+ const heightFrac = opts.heightFrac ?? 0.3;
127
+ const maxAlpha = opts.maxAlpha ?? 0.45;
128
+ const top = H * (1 - heightFrac);
129
+ const g = ctx.createLinearGradient(0, top, 0, H);
130
+ g.addColorStop(0, "rgba(0,0,0,0)");
131
+ g.addColorStop(1, `rgba(0,0,0,${maxAlpha})`);
132
+ ctx.save();
133
+ ctx.fillStyle = g;
134
+ ctx.fillRect(0, top, W, H - top);
135
+ ctx.restore();
136
+ }
137
+ function hookScene(theme, opts) {
138
+ return {
139
+ durationMs: 2e3,
140
+ draw(ctx, _t01) {
141
+ const W = ctx.canvas.width;
142
+ const H = ctx.canvas.height;
143
+ drawBottomGradient(ctx);
144
+ ctx.textAlign = "center";
145
+ ctx.fillStyle = theme.ink;
146
+ ctx.font = `bold 88px ${theme.fontDisplay}`;
147
+ opts.lines.forEach((line, i) => {
148
+ ctx.fillText(line, W / 2, H * 0.42 + i * 104);
149
+ });
150
+ const brand = opts.brand ?? theme.brand;
151
+ ctx.font = `italic 38px ${theme.fontBody}`;
152
+ ctx.fillStyle = theme.sepia;
153
+ ctx.fillText(brand, W / 2, H * 0.42 + opts.lines.length * 104 + 12);
154
+ }
155
+ };
156
+ }
157
+ var _portraitCache = /* @__PURE__ */ new Map();
158
+ function loadPortrait(url) {
159
+ if (!url) return Promise.resolve(null);
160
+ const cached = _portraitCache.get(url);
161
+ if (cached) return cached;
162
+ const p = new Promise((resolve) => {
163
+ if (typeof Image === "undefined") {
164
+ resolve(null);
165
+ return;
166
+ }
167
+ const img = new Image();
168
+ img.crossOrigin = "anonymous";
169
+ img.onload = () => resolve(img);
170
+ img.onerror = () => resolve(null);
171
+ img.src = url;
172
+ });
173
+ _portraitCache.set(url, p);
174
+ return p;
175
+ }
176
+ function wrapLines(ctx, text, maxW, maxLines) {
177
+ const words = text.split(/\s+/).filter(Boolean);
178
+ const lines = [];
179
+ let cur = "";
180
+ for (const w of words) {
181
+ const next = cur ? `${cur} ${w}` : w;
182
+ if (ctx.measureText(next).width > maxW && cur) {
183
+ lines.push(cur);
184
+ cur = w;
185
+ if (lines.length === maxLines - 1) break;
186
+ } else {
187
+ cur = next;
188
+ }
189
+ }
190
+ if (cur && lines.length < maxLines) lines.push(cur);
191
+ const used = lines.join(" ");
192
+ if (used.length < text.length && lines.length) {
193
+ lines[lines.length - 1] = truncate(lines[lines.length - 1] + "\u2026", 999);
194
+ }
195
+ return lines;
196
+ }
197
+ function revealScene(theme, opts) {
198
+ return {
199
+ durationMs: 2200,
200
+ draw(ctx, t01) {
201
+ const W = ctx.canvas.width;
202
+ const H = ctx.canvas.height;
203
+ const b = safeBox(W, H);
204
+ const cx = W / 2;
205
+ const cy = b.top + b.h * 0.4286;
206
+ const r = 92;
207
+ const k = Math.min(1, t01 * 1.4);
208
+ ctx.globalAlpha = k;
209
+ ctx.textAlign = "center";
210
+ ctx.beginPath();
211
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
212
+ ctx.fillStyle = theme.paper;
213
+ ctx.fill();
214
+ if (opts.portrait) {
215
+ ctx.save();
216
+ ctx.beginPath();
217
+ ctx.arc(cx, cy, r - 3, 0, Math.PI * 2);
218
+ ctx.clip();
219
+ const img = opts.portrait;
220
+ const iw = img.naturalWidth || img.width || 1;
221
+ const ih = img.naturalHeight || img.height || 1;
222
+ const scale = Math.max(2 * (r - 3) / iw, 2 * (r - 3) / ih);
223
+ const dw = iw * scale;
224
+ const dh = ih * scale;
225
+ ctx.drawImage(img, cx - dw / 2, cy - dh / 2, dw, dh);
226
+ ctx.restore();
227
+ } else {
228
+ ctx.fillStyle = theme.ink;
229
+ ctx.font = `bold 76px ${theme.fontDisplay}`;
230
+ const badge = opts.initials ?? initials(opts.title);
231
+ ctx.fillText(badge, cx, cy + 27);
232
+ }
233
+ ctx.lineWidth = 5;
234
+ ctx.strokeStyle = theme.gold;
235
+ ctx.stroke();
236
+ const truncated = truncate(opts.title, 24);
237
+ let fontSize = 80;
238
+ ctx.font = `bold ${fontSize}px ${theme.fontDisplay}`;
239
+ if (ctx.measureText(truncated).width > W * 0.86) {
240
+ fontSize = 68;
241
+ ctx.font = `bold ${fontSize}px ${theme.fontDisplay}`;
242
+ }
243
+ ctx.fillStyle = theme.ink;
244
+ ctx.fillText(truncated, cx, cy + r + 110);
245
+ ctx.font = `italic 50px ${theme.fontBody}`;
246
+ ctx.fillStyle = theme.sepia;
247
+ ctx.fillText(opts.subtitle, cx, cy + r + 176);
248
+ if (opts.funFact) {
249
+ ctx.font = `34px ${theme.fontBody}`;
250
+ ctx.fillStyle = theme.sepia;
251
+ const lines = wrapLines(ctx, opts.funFact, b.w * 0.92, 2);
252
+ lines.forEach((line, i) => {
253
+ ctx.fillText(line, cx, cy + r + 246 + i * 44);
254
+ });
255
+ }
256
+ ctx.globalAlpha = 1;
257
+ }
258
+ };
259
+ }
260
+ function ctaScene(theme, opts) {
261
+ return {
262
+ durationMs: 2500,
263
+ draw(ctx, _t01) {
264
+ const W = ctx.canvas.width;
265
+ const H = ctx.canvas.height;
266
+ const b = safeBox(W, H);
267
+ const baseY = b.cy - 40;
268
+ ctx.textAlign = "center";
269
+ if (opts.lines.length > 0) {
270
+ ctx.fillStyle = theme.ink;
271
+ ctx.font = `bold 66px ${theme.fontDisplay}`;
272
+ ctx.fillText(opts.lines[0], W / 2, baseY);
273
+ }
274
+ opts.lines.slice(1).forEach((line, i) => {
275
+ ctx.fillStyle = theme.accent;
276
+ ctx.font = `bold 72px ${theme.fontDisplay}`;
277
+ ctx.fillText(line, W / 2, baseY + 100 * (i + 1));
278
+ });
279
+ }
280
+ };
281
+ }
282
+ async function runPromoCapture(opts, record = recordScenes) {
283
+ const w = window;
284
+ w.__promoMeta = opts.meta;
285
+ try {
286
+ const blob = await record(opts.buildScenes(), {
287
+ audioStream: opts.audioStream,
288
+ width: opts.width ?? 1080,
289
+ height: opts.height ?? 1920,
290
+ fps: opts.fps,
291
+ background: opts.background
292
+ });
293
+ const url = URL.createObjectURL(blob);
294
+ w.__promoBlobUrl = url;
295
+ if (opts.download !== false) {
296
+ const a = document.createElement("a");
297
+ a.href = url;
298
+ a.download = `promo-${opts.meta.id ?? "clip"}.webm`;
299
+ document.body.appendChild(a);
300
+ a.click();
301
+ }
302
+ w.__promoReady = true;
303
+ } catch (e) {
304
+ w.__promoError = String(e);
305
+ throw e;
306
+ }
307
+ }
308
+
309
+ export {
310
+ SAFE_ZONE,
311
+ safeBox,
312
+ safeTitleBaseline,
313
+ drawSafeGuides,
314
+ pickMimeType,
315
+ recordScenes,
316
+ truncate,
317
+ initials,
318
+ drawBottomGradient,
319
+ hookScene,
320
+ loadPortrait,
321
+ revealScene,
322
+ ctaScene,
323
+ runPromoCapture
324
+ };
325
+ //# sourceMappingURL=chunk-HXTRNE74.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/video.ts"],"sourcesContent":["/**\n * Framework-agnostic promo video module.\n *\n * Provides:\n * - PromoTheme — colour/font/brand tokens\n * - Scene — {durationMs, draw, onEnter?}\n * - recordScenes — rAF-based canvas capture → Blob (browser only)\n * - hookScene / revealScene / ctaScene — scene builders (pure, unit-testable)\n * - runPromoCapture — orchestrator with dependency-injected recorder\n *\n * Visual style ported from whozart/src/lib/promo.ts + video.ts.\n */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface PromoTheme {\n paper: string;\n ink: string;\n accent: string;\n sepia: string;\n gold: string;\n fontDisplay: string;\n fontBody: string;\n brand: string;\n}\n\nexport interface Scene {\n durationMs: number;\n draw: (ctx: CanvasRenderingContext2D, t01: number) => void;\n onEnter?: () => void;\n}\n\nexport interface RecordOpts {\n audioStream: MediaStream;\n width: number;\n height: number;\n fps?: number;\n background?: string;\n onProgress?: (p: { phase: string; progress01: number }) => void;\n}\n\nexport interface PromoMeta {\n id?: string;\n title?: string;\n composer?: string;\n [k: string]: unknown;\n}\n\nexport interface PromoCaptureOpts {\n buildScenes: () => Scene[];\n audioStream: MediaStream;\n meta: PromoMeta;\n width?: number;\n height?: number;\n fps?: number;\n background?: string;\n download?: boolean;\n}\n\n// ─── Safe zone (phone short-form UI) ─────────────────────────────────────────\n\n/**\n * Insets (fractions of frame) kept clear of TikTok/Reels/Shorts UI overlays.\n * `bottom: 0.28` puts the safe-box bottom at 0.72·H — TikTok's caption + right\n * action rail reach ~25–28% up, tighter than the old 0.20, and a posted RET clip\n * had its keyboard clipped at 0.20. `top: 0.15` clears TikTok's top tabs/search\n * row — measured from a real screenshot, the \"Find related content\" search bar\n * bottom sits at ~0.114·H, so 0.10 clashed and 0.13 cleared by only ~31px; 0.15\n * gives a comfortable ~73px. This is the SINGLE source of truth for both safe lines:\n * consumers anchor bottom content to `safeBox().bottom` and top titles to\n * `safeTitleBaseline()` instead of hardcoding. Verify any layout with `?safe=1`.\n */\nexport const SAFE_ZONE = { top: 0.15, bottom: 0.28, left: 0.06, right: 0.12 } as const;\n\nexport interface SafeBox {\n left: number; right: number; top: number; bottom: number;\n w: number; h: number; cx: number; cy: number;\n /** Widest block centered at frame W/2 that still fits inside the box. */\n centeredW: number;\n}\n\n/** The usable content rectangle for a W×H frame, in pixels. */\nexport function safeBox(W: number, H: number): SafeBox {\n const left = W * SAFE_ZONE.left;\n const right = W * (1 - SAFE_ZONE.right);\n const top = H * SAFE_ZONE.top;\n const bottom = H * (1 - SAFE_ZONE.bottom);\n return {\n left, right, top, bottom,\n w: right - left, h: bottom - top,\n cx: (left + right) / 2, cy: (top + bottom) / 2,\n centeredW: 2 * Math.min(W / 2 - left, right - W / 2),\n };\n}\n\n/**\n * Lowest first-line baseline (for an ALPHABETIC-baseline title of `size` px) that\n * keeps the title's cap-top inside the safe box top. Phone top chrome (TikTok/Reels\n * \"For You\" tabs + search) sits above `safeBox().top`, and an alphabetic baseline\n * draws glyphs ~0.8*size ABOVE the baseline — so a small `topY` silently breaches\n * the top. Apps clamp their top titles: `y = Math.max(topY, safeTitleBaseline(size, H))`.\n * Single source of truth for the top-safe line, mirroring `safeBox().bottom`.\n */\nexport function safeTitleBaseline(size: number, H: number): number {\n return H * SAFE_ZONE.top + size * 0.8 + 4;\n}\n\n/** Debug overlay: dim the four unsafe margins + outline the safe box. */\nexport function drawSafeGuides(ctx: CanvasRenderingContext2D): void {\n const W = ctx.canvas.width, H = ctx.canvas.height;\n const b = safeBox(W, H);\n ctx.save();\n ctx.globalAlpha = 1;\n ctx.fillStyle = 'rgba(220,40,40,0.35)';\n ctx.fillRect(0, 0, W, b.top); // top\n ctx.fillRect(0, b.bottom, W, H - b.bottom); // bottom\n ctx.fillRect(0, b.top, b.left, b.h); // left\n ctx.fillRect(b.right, b.top, W - b.right, b.h); // right (action rail)\n ctx.strokeStyle = 'rgba(40,200,90,1)';\n ctx.lineWidth = 6;\n ctx.strokeRect(b.left, b.top, b.w, b.h);\n ctx.restore();\n}\n\n// ─── MIME type picker ─────────────────────────────────────────────────────────\n\nexport function pickMimeType(): string {\n const candidates = [\n 'video/webm;codecs=vp9,opus',\n 'video/webm;codecs=vp8,opus',\n 'video/webm',\n ];\n for (const t of candidates) {\n if (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported(t)) return t;\n }\n return 'video/webm';\n}\n\n// ─── recordScenes ─────────────────────────────────────────────────────────────\n\nexport async function recordScenes(scenes: Scene[], opts: RecordOpts): Promise<Blob> {\n const { audioStream, width, height, fps = 30, background = '#000000', onProgress } = opts;\n\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n\n const maybeCtx = canvas.getContext('2d');\n if (!maybeCtx) throw new Error('Failed to get 2D context');\n const ctx: CanvasRenderingContext2D = maybeCtx;\n\n const videoStream = canvas.captureStream(fps);\n const combined = new MediaStream([\n ...videoStream.getVideoTracks(),\n ...audioStream.getAudioTracks(),\n ]);\n\n const mimeType = pickMimeType();\n const recorder = new MediaRecorder(combined, {\n mimeType,\n videoBitsPerSecond: 4_000_000,\n audioBitsPerSecond: 128_000,\n });\n\n const chunks: Blob[] = [];\n recorder.ondataavailable = (e) => {\n if (e.data.size > 0) chunks.push(e.data);\n };\n const stopped = new Promise<void>((res) => {\n recorder.onstop = () => res();\n });\n\n recorder.start();\n\n const totalMs = scenes.reduce((s, sc) => s + sc.durationMs, 0);\n const t0 = performance.now();\n\n onProgress?.({ phase: 'capturing', progress01: 0 });\n\n let sceneStart = 0;\n let sceneIdx = 0;\n let enteredScene = -1;\n\n // Draw first frame immediately\n ctx.fillStyle = background;\n ctx.fillRect(0, 0, width, height);\n scenes[0].draw(ctx, 0);\n\n await new Promise<void>((resolve) => {\n function tick() {\n const now = performance.now() - t0;\n if (now >= totalMs) {\n setTimeout(resolve, 100);\n return;\n }\n\n // Advance scene index\n while (\n sceneIdx < scenes.length - 1 &&\n now - sceneStart >= scenes[sceneIdx].durationMs\n ) {\n sceneStart += scenes[sceneIdx].durationMs;\n sceneIdx += 1;\n }\n\n const scene = scenes[sceneIdx];\n\n // Fire onEnter once per scene\n if (enteredScene !== sceneIdx) {\n enteredScene = sceneIdx;\n scene.onEnter?.();\n }\n\n const tInScene = (now - sceneStart) / scene.durationMs;\n\n // Clear to background\n ctx.fillStyle = background;\n ctx.fillRect(0, 0, width, height);\n\n scene.draw(ctx, Math.min(1, Math.max(0, tInScene)));\n\n onProgress?.({\n phase: 'capturing',\n progress01: Math.min(0.95, now / totalMs),\n });\n\n requestAnimationFrame(tick);\n }\n\n requestAnimationFrame(tick);\n });\n\n onProgress?.({ phase: 'finalizing', progress01: 0.96 });\n recorder.stop();\n await stopped;\n\n return new Blob(chunks, { type: mimeType });\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max - 1).trimEnd() + '…' : s;\n}\n\nexport function initials(name: string): string {\n return name\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 3)\n .map((w) => w[0])\n .join('')\n .toUpperCase();\n}\n\n// ─── Scene builders ───────────────────────────────────────────────────────────\n\n/**\n * Dark gradient rising from the frame bottom (opaque-ish at the very bottom,\n * fading out by `heightFrac` up). TikTok overlays the view count bottom-left of\n * every profile-grid thumbnail in WHITE — on our light start screens it was\n * unreadable. Draw this after the background, before content.\n */\nexport function drawBottomGradient(\n ctx: CanvasRenderingContext2D,\n opts: { heightFrac?: number; maxAlpha?: number } = {},\n): void {\n const W = ctx.canvas.width, H = ctx.canvas.height;\n const heightFrac = opts.heightFrac ?? 0.30;\n const maxAlpha = opts.maxAlpha ?? 0.45;\n const top = H * (1 - heightFrac);\n const g = ctx.createLinearGradient(0, top, 0, H);\n g.addColorStop(0, \"rgba(0,0,0,0)\");\n g.addColorStop(1, `rgba(0,0,0,${maxAlpha})`);\n ctx.save();\n ctx.fillStyle = g;\n ctx.fillRect(0, top, W, H - top);\n ctx.restore();\n}\n\nexport interface HookSceneOpts {\n lines: string[];\n brand?: string;\n}\n\nexport function hookScene(theme: PromoTheme, opts: HookSceneOpts): Scene {\n return {\n durationMs: 2000,\n draw(ctx, _t01) {\n const W = ctx.canvas.width;\n const H = ctx.canvas.height;\n // The hook is the profile-grid thumbnail; the bottom gradient keeps\n // TikTok's white view-count overlay readable on light backdrops.\n drawBottomGradient(ctx);\n ctx.textAlign = 'center';\n ctx.fillStyle = theme.ink;\n ctx.font = `bold 88px ${theme.fontDisplay}`;\n opts.lines.forEach((line, i) => {\n ctx.fillText(line, W / 2, H * 0.42 + i * 104);\n });\n const brand = opts.brand ?? theme.brand;\n ctx.font = `italic 38px ${theme.fontBody}`;\n ctx.fillStyle = theme.sepia;\n ctx.fillText(brand, W / 2, H * 0.42 + opts.lines.length * 104 + 12);\n },\n };\n}\n\nexport interface RevealSceneOpts {\n title: string;\n subtitle: string;\n initials?: string;\n /** Composer portrait (already decoded) drawn clipped into the medallion. If\n * omitted/null, falls back to the initials badge. Load it with `loadPortrait`\n * so a cross-origin image (e.g. Wikimedia) doesn't taint the capture canvas. */\n portrait?: HTMLImageElement | null;\n /** Optional one-line fun fact wrapped below the subtitle (e.g. corpus\n * `fun_fact`). Trimmed to two lines. */\n funFact?: string;\n}\n\n/** Load an image for canvas capture: `crossOrigin='anonymous'` so a CORS-enabled\n * cross-origin source (Wikimedia portraits send `Access-Control-Allow-Origin: *`)\n * can be drawn onto a captured canvas WITHOUT tainting it. Fail-soft: resolves\n * null on error/missing url (callers fall back to initials). Cached per-url. */\nconst _portraitCache = new Map<string, Promise<HTMLImageElement | null>>();\nexport function loadPortrait(url?: string | null): Promise<HTMLImageElement | null> {\n if (!url) return Promise.resolve(null);\n const cached = _portraitCache.get(url);\n if (cached) return cached;\n const p = new Promise<HTMLImageElement | null>((resolve) => {\n if (typeof Image === 'undefined') { resolve(null); return; }\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => resolve(img);\n img.onerror = () => resolve(null);\n img.src = url;\n });\n _portraitCache.set(url, p);\n return p;\n}\n\n/** Greedy word-wrap for the current ctx.font into at most `maxLines` lines. */\nfunction wrapLines(ctx: CanvasRenderingContext2D, text: string, maxW: number, maxLines: number): string[] {\n const words = text.split(/\\s+/).filter(Boolean);\n const lines: string[] = [];\n let cur = '';\n for (const w of words) {\n const next = cur ? `${cur} ${w}` : w;\n if (ctx.measureText(next).width > maxW && cur) {\n lines.push(cur);\n cur = w;\n if (lines.length === maxLines - 1) break;\n } else {\n cur = next;\n }\n }\n if (cur && lines.length < maxLines) lines.push(cur);\n // If we ran out of line budget mid-text, ellipsize the last line.\n const used = lines.join(' ');\n if (used.length < text.length && lines.length) {\n lines[lines.length - 1] = truncate(lines[lines.length - 1] + '…', 999);\n }\n return lines;\n}\n\nexport function revealScene(theme: PromoTheme, opts: RevealSceneOpts): Scene {\n return {\n durationMs: 2200,\n draw(ctx, t01) {\n const W = ctx.canvas.width;\n const H = ctx.canvas.height;\n const b = safeBox(W, H);\n const cx = W / 2;\n // Anchor through the safe box; ~0.4286 of box height ≈ original H*0.40 (no visible shift at 1080×1920).\n const cy = b.top + b.h * 0.4286;\n const r = 92;\n const k = Math.min(1, t01 * 1.4);\n ctx.globalAlpha = k;\n ctx.textAlign = 'center';\n\n // Medallion circle (faint surface) + gold ring.\n ctx.beginPath();\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n ctx.fillStyle = theme.paper;\n ctx.fill();\n\n if (opts.portrait) {\n // Portrait clipped into the medallion, cover-fit (centred crop).\n ctx.save();\n ctx.beginPath();\n ctx.arc(cx, cy, r - 3, 0, Math.PI * 2);\n ctx.clip();\n const img = opts.portrait;\n const iw = img.naturalWidth || img.width || 1;\n const ih = img.naturalHeight || img.height || 1;\n const scale = Math.max((2 * (r - 3)) / iw, (2 * (r - 3)) / ih);\n const dw = iw * scale;\n const dh = ih * scale;\n ctx.drawImage(img, cx - dw / 2, cy - dh / 2, dw, dh);\n ctx.restore();\n } else {\n // Initials inside medallion (fallback).\n ctx.fillStyle = theme.ink;\n ctx.font = `bold 76px ${theme.fontDisplay}`;\n const badge = opts.initials ?? initials(opts.title);\n ctx.fillText(badge, cx, cy + 27);\n }\n\n ctx.lineWidth = 5;\n ctx.strokeStyle = theme.gold;\n ctx.stroke();\n\n // Title (with font-size fallback if too wide)\n const truncated = truncate(opts.title, 24);\n let fontSize = 80;\n ctx.font = `bold ${fontSize}px ${theme.fontDisplay}`;\n if (ctx.measureText(truncated).width > W * 0.86) {\n fontSize = 68;\n ctx.font = `bold ${fontSize}px ${theme.fontDisplay}`;\n }\n ctx.fillStyle = theme.ink;\n ctx.fillText(truncated, cx, cy + r + 110);\n\n // Subtitle\n ctx.font = `italic 50px ${theme.fontBody}`;\n ctx.fillStyle = theme.sepia;\n ctx.fillText(opts.subtitle, cx, cy + r + 176);\n\n // Optional fun fact, wrapped to two lines below the subtitle.\n if (opts.funFact) {\n ctx.font = `34px ${theme.fontBody}`;\n ctx.fillStyle = theme.sepia;\n const lines = wrapLines(ctx, opts.funFact, b.w * 0.92, 2);\n lines.forEach((line, i) => {\n ctx.fillText(line, cx, cy + r + 246 + i * 44);\n });\n }\n\n ctx.globalAlpha = 1;\n },\n };\n}\n\nexport interface CtaSceneOpts {\n lines: string[];\n}\n\nexport function ctaScene(theme: PromoTheme, opts: CtaSceneOpts): Scene {\n return {\n durationMs: 2500,\n draw(ctx, _t01) {\n const W = ctx.canvas.width;\n const H = ctx.canvas.height;\n const b = safeBox(W, H);\n // Safe-box-relative base ≈ original H*0.42 (no visible shift at 1080×1920).\n const baseY = b.cy - 40;\n ctx.textAlign = 'center';\n if (opts.lines.length > 0) {\n ctx.fillStyle = theme.ink;\n ctx.font = `bold 66px ${theme.fontDisplay}`;\n ctx.fillText(opts.lines[0], W / 2, baseY);\n }\n opts.lines.slice(1).forEach((line, i) => {\n ctx.fillStyle = theme.accent;\n ctx.font = `bold 72px ${theme.fontDisplay}`;\n ctx.fillText(line, W / 2, baseY + 100 * (i + 1));\n });\n },\n };\n}\n\n// ─── runPromoCapture ──────────────────────────────────────────────────────────\n\nexport async function runPromoCapture(\n opts: PromoCaptureOpts,\n record: (scenes: Scene[], o: RecordOpts) => Promise<Blob> = recordScenes,\n): Promise<void> {\n const w = window as unknown as Record<string, unknown>;\n w.__promoMeta = opts.meta;\n try {\n const blob = await record(opts.buildScenes(), {\n audioStream: opts.audioStream,\n width: opts.width ?? 1080,\n height: opts.height ?? 1920,\n fps: opts.fps,\n background: opts.background,\n });\n const url = URL.createObjectURL(blob);\n w.__promoBlobUrl = url;\n if (opts.download !== false) {\n const a = document.createElement('a');\n a.href = url;\n a.download = `promo-${opts.meta.id ?? 'clip'}.webm`;\n document.body.appendChild(a);\n a.click();\n }\n w.__promoReady = true;\n } catch (e) {\n w.__promoError = String(e);\n throw e;\n }\n}\n"],"mappings":"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","names":[]}