@real-music-packages/web-core 0.9.4 → 0.9.6

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.
package/dist/video.d.ts CHANGED
@@ -56,12 +56,15 @@ interface PromoCaptureOpts {
56
56
  * Insets (fractions of frame) kept clear of TikTok/Reels/Shorts UI overlays.
57
57
  * `bottom: 0.28` puts the safe-box bottom at 0.72·H — TikTok's caption + right
58
58
  * action rail reach ~25–28% up, tighter than the old 0.20, and a posted RET clip
59
- * had its keyboard clipped at 0.20. This is the SINGLE source of truth for the
60
- * bottom-safe line: consumers anchor bottom content to `safeBox().bottom` instead
61
- * of hardcoding (see RET `keyboard()`). Verify any layout with `?safe=1`.
59
+ * had its keyboard clipped at 0.20. `top: 0.15` clears TikTok's top tabs/search
60
+ * row measured from a real screenshot, the "Find related content" search bar
61
+ * bottom sits at ~0.114·H, so 0.10 clashed and 0.13 cleared by only ~31px; 0.15
62
+ * gives a comfortable ~73px. This is the SINGLE source of truth for both safe lines:
63
+ * consumers anchor bottom content to `safeBox().bottom` and top titles to
64
+ * `safeTitleBaseline()` instead of hardcoding. Verify any layout with `?safe=1`.
62
65
  */
63
66
  declare const SAFE_ZONE: {
64
- readonly top: 0.1;
67
+ readonly top: 0.15;
65
68
  readonly bottom: 0.28;
66
69
  readonly left: 0.06;
67
70
  readonly right: 0.12;
package/dist/video.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/video.ts
2
- var SAFE_ZONE = { top: 0.1, bottom: 0.28, left: 0.06, right: 0.12 };
2
+ var SAFE_ZONE = { top: 0.15, bottom: 0.28, left: 0.06, right: 0.12 };
3
3
  function safeBox(W, H) {
4
4
  const left = W * SAFE_ZONE.left;
5
5
  const right = W * (1 - SAFE_ZONE.right);
package/dist/video.js.map CHANGED
@@ -1 +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. This is the SINGLE source of truth for the\n * bottom-safe line: consumers anchor bottom content to `safeBox().bottom` instead\n * of hardcoding (see RET `keyboard()`). Verify any layout with `?safe=1`.\n */\nexport const SAFE_ZONE = { top: 0.10, 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\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 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}\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)\n ctx.beginPath();\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n ctx.fillStyle = theme.paper;\n ctx.fill();\n ctx.lineWidth = 5;\n ctx.strokeStyle = theme.gold;\n ctx.stroke();\n\n // Initials inside medallion\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 // 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 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":[]}
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\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 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}\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)\n ctx.beginPath();\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n ctx.fillStyle = theme.paper;\n ctx.fill();\n ctx.lineWidth = 5;\n ctx.strokeStyle = theme.gold;\n ctx.stroke();\n\n // Initials inside medallion\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 // 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 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-music-packages/web-core",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Shared music-theory + audio primitives for the music-suite web apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",