@real-music-packages/web-core 0.44.0 → 0.44.2
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/{chunk-HIELHWEZ.js → chunk-DBFGX6OK.js} +2 -2
- package/dist/{chunk-F4VYTGJQ.js → chunk-HIYU73EB.js} +2 -2
- package/dist/{chunk-3X2J66MQ.js → chunk-NZFS7ZTK.js} +3 -3
- package/dist/{chunk-HXTRNE74.js → chunk-PVZXLC23.js} +26 -8
- package/dist/chunk-PVZXLC23.js.map +1 -0
- package/dist/notationPlayer.js +3 -3
- package/dist/notationPlayerSvg.js +3 -3
- package/dist/notationPlayerVerovio.js +3 -3
- package/dist/scene/index.d.ts +5 -1
- package/dist/scene/index.js +7 -7
- package/dist/scene/index.js.map +1 -1
- package/dist/video.d.ts +23 -0
- package/dist/video.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-HXTRNE74.js.map +0 -1
- /package/dist/{chunk-HIELHWEZ.js.map → chunk-DBFGX6OK.js.map} +0 -0
- /package/dist/{chunk-F4VYTGJQ.js.map → chunk-HIYU73EB.js.map} +0 -0
- /package/dist/{chunk-3X2J66MQ.js.map → chunk-NZFS7ZTK.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
safeBox
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PVZXLC23.js";
|
|
4
4
|
|
|
5
5
|
// src/scene/notationGeometry.ts
|
|
6
6
|
var FOLLOW_BARS = 2;
|
|
@@ -482,4 +482,4 @@ export {
|
|
|
482
482
|
audioPlayheadLine,
|
|
483
483
|
vstackAudioPlayheadLine
|
|
484
484
|
};
|
|
485
|
-
//# sourceMappingURL=chunk-
|
|
485
|
+
//# sourceMappingURL=chunk-DBFGX6OK.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hitTestMeasureAt,
|
|
3
3
|
measureColumnsFromLayout
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DBFGX6OK.js";
|
|
5
5
|
|
|
6
6
|
// src/notationCommon.ts
|
|
7
7
|
function computeReflowScrollDelta(oldLayout, newLayout, anchorX, anchorY) {
|
|
@@ -150,4 +150,4 @@ export {
|
|
|
150
150
|
hasReachedProgrammaticTarget,
|
|
151
151
|
createFollowController
|
|
152
152
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-HIYU73EB.js.map
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
notationLayout,
|
|
8
8
|
vstackAudioPlayheadLine,
|
|
9
9
|
vstackFollowBox
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-DBFGX6OK.js";
|
|
11
11
|
import {
|
|
12
12
|
safeBox
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-PVZXLC23.js";
|
|
14
14
|
|
|
15
15
|
// src/scene/engravingStore.ts
|
|
16
16
|
var STORE = /* @__PURE__ */ new WeakMap();
|
|
@@ -275,4 +275,4 @@ export {
|
|
|
275
275
|
notationFactory,
|
|
276
276
|
scrollCursorFactory
|
|
277
277
|
};
|
|
278
|
-
//# sourceMappingURL=chunk-
|
|
278
|
+
//# sourceMappingURL=chunk-NZFS7ZTK.js.map
|
|
@@ -47,7 +47,7 @@ function pickMimeType() {
|
|
|
47
47
|
return "video/webm";
|
|
48
48
|
}
|
|
49
49
|
async function recordScenes(scenes, opts) {
|
|
50
|
-
const { audioStream, width, height, fps = 30, background = "#000000", onProgress } = opts;
|
|
50
|
+
const { audioStream, width, height, fps = 30, background = "#000000", onProgress, splitAudioTrack = false, onAudioBlob } = opts;
|
|
51
51
|
const canvas = document.createElement("canvas");
|
|
52
52
|
canvas.width = width;
|
|
53
53
|
canvas.height = height;
|
|
@@ -55,15 +55,13 @@ async function recordScenes(scenes, opts) {
|
|
|
55
55
|
if (!maybeCtx) throw new Error("Failed to get 2D context");
|
|
56
56
|
const ctx = maybeCtx;
|
|
57
57
|
const videoStream = canvas.captureStream(fps);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
...audioStream.getAudioTracks()
|
|
61
|
-
]);
|
|
58
|
+
const videoOnly = splitAudioTrack;
|
|
59
|
+
const recordedStream = videoOnly ? new MediaStream([...videoStream.getVideoTracks()]) : new MediaStream([...videoStream.getVideoTracks(), ...audioStream.getAudioTracks()]);
|
|
62
60
|
const mimeType = pickMimeType();
|
|
63
|
-
const recorder = new MediaRecorder(
|
|
61
|
+
const recorder = new MediaRecorder(recordedStream, {
|
|
64
62
|
mimeType,
|
|
65
63
|
videoBitsPerSecond: 4e6,
|
|
66
|
-
audioBitsPerSecond: 128e3
|
|
64
|
+
...videoOnly ? {} : { audioBitsPerSecond: 128e3 }
|
|
67
65
|
});
|
|
68
66
|
const chunks = [];
|
|
69
67
|
recorder.ondataavailable = (e) => {
|
|
@@ -72,7 +70,25 @@ async function recordScenes(scenes, opts) {
|
|
|
72
70
|
const stopped = new Promise((res) => {
|
|
73
71
|
recorder.onstop = () => res();
|
|
74
72
|
});
|
|
73
|
+
let audioRecorder = null;
|
|
74
|
+
let audioStopped = Promise.resolve();
|
|
75
|
+
if (videoOnly && audioStream.getAudioTracks().length > 0) {
|
|
76
|
+
const audioOnlyStream = new MediaStream([...audioStream.getAudioTracks()]);
|
|
77
|
+
const audioMime = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : "audio/webm";
|
|
78
|
+
audioRecorder = new MediaRecorder(audioOnlyStream, { mimeType: audioMime, audioBitsPerSecond: 128e3 });
|
|
79
|
+
const audioChunks = [];
|
|
80
|
+
audioRecorder.ondataavailable = (e) => {
|
|
81
|
+
if (e.data.size > 0) audioChunks.push(e.data);
|
|
82
|
+
};
|
|
83
|
+
audioStopped = new Promise((res) => {
|
|
84
|
+
audioRecorder.onstop = () => {
|
|
85
|
+
onAudioBlob?.(new Blob(audioChunks, { type: audioMime }));
|
|
86
|
+
res();
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
75
90
|
recorder.start();
|
|
91
|
+
audioRecorder?.start();
|
|
76
92
|
const totalMs = scenes.reduce((s, sc) => s + sc.durationMs, 0);
|
|
77
93
|
const t0 = performance.now();
|
|
78
94
|
onProgress?.({ phase: "capturing", progress01: 0 });
|
|
@@ -112,7 +128,9 @@ async function recordScenes(scenes, opts) {
|
|
|
112
128
|
});
|
|
113
129
|
onProgress?.({ phase: "finalizing", progress01: 0.96 });
|
|
114
130
|
recorder.stop();
|
|
131
|
+
audioRecorder?.stop();
|
|
115
132
|
await stopped;
|
|
133
|
+
await audioStopped;
|
|
116
134
|
return new Blob(chunks, { type: mimeType });
|
|
117
135
|
}
|
|
118
136
|
function truncate(s, max) {
|
|
@@ -322,4 +340,4 @@ export {
|
|
|
322
340
|
ctaScene,
|
|
323
341
|
runPromoCapture
|
|
324
342
|
};
|
|
325
|
-
//# sourceMappingURL=chunk-
|
|
343
|
+
//# sourceMappingURL=chunk-PVZXLC23.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 * Record video and audio as TWO SEPARATE MediaRecorders instead of one\n * combined MediaStream. Default false (preserves the original combined\n * behavior for existing callers).\n *\n * Why this exists: combining a canvas `captureStream()` video track with a\n * live Web Audio (`createMediaStreamDestination`) audio track into one\n * MediaStream and feeding THAT to a single MediaRecorder produces an\n * essentially empty (~110-byte, header-only) WebM in the Chromium build\n * this fleet's headless render harness uses (Playwright's bundled Chrome\n * for Testing as of chromium-1228) — reproduced with vp8/vp9/default\n * codecs, with/without bitrate options, with/without a timeslice, headed\n * AND headless. Recording each track with its OWN MediaRecorder (proven\n * to work reliably alone) and muxing the two resulting blobs afterward\n * (see tools/promo/render.mjs's muxIfSeparateAudio) sidesteps it entirely.\n * Opt-in (not the default) so existing callers/routes are unaffected until\n * they're verified against this path too.\n */\n splitAudioTrack?: boolean;\n /** Only meaningful with `splitAudioTrack: true` — called once with the\n * audio-only Blob when its recorder stops (same start/stop window as the\n * returned video-only Blob, so the two are time-aligned for muxing). */\n onAudioBlob?: (blob: Blob) => 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, splitAudioTrack = false, onAudioBlob } = 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\n // See RecordOpts.splitAudioTrack: combining video+audio tracks into one\n // MediaStream can produce an empty recording in this fleet's headless\n // Chromium. When opted in, record video and audio with their OWN\n // MediaRecorders (each combination proven to work alone) instead of one\n // combined recorder.\n const videoOnly = splitAudioTrack;\n const recordedStream = videoOnly\n ? new MediaStream([...videoStream.getVideoTracks()])\n : new MediaStream([...videoStream.getVideoTracks(), ...audioStream.getAudioTracks()]);\n\n const mimeType = pickMimeType();\n const recorder = new MediaRecorder(recordedStream, {\n mimeType,\n videoBitsPerSecond: 4_000_000,\n ...(videoOnly ? {} : { 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 // Audio-only recorder, started/stopped in lockstep with the video recorder\n // so the two blobs cover the SAME real-time window (muxed back together\n // downstream — see tools/promo/render.mjs).\n let audioRecorder: MediaRecorder | null = null;\n let audioStopped: Promise<void> = Promise.resolve();\n if (videoOnly && audioStream.getAudioTracks().length > 0) {\n const audioOnlyStream = new MediaStream([...audioStream.getAudioTracks()]);\n const audioMime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';\n audioRecorder = new MediaRecorder(audioOnlyStream, { mimeType: audioMime, audioBitsPerSecond: 128_000 });\n const audioChunks: Blob[] = [];\n audioRecorder.ondataavailable = (e) => {\n if (e.data.size > 0) audioChunks.push(e.data);\n };\n audioStopped = new Promise<void>((res) => {\n audioRecorder!.onstop = () => {\n onAudioBlob?.(new Blob(audioChunks, { type: audioMime }));\n res();\n };\n });\n }\n\n recorder.start();\n audioRecorder?.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 audioRecorder?.stop();\n await stopped;\n await audioStopped;\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":[]}
|
package/dist/notationPlayer.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
getNotationEngraving,
|
|
3
3
|
notationFactory,
|
|
4
4
|
scrollCursorFactory
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NZFS7ZTK.js";
|
|
6
6
|
import {
|
|
7
7
|
audioPlayheadLine,
|
|
8
8
|
distinctOnsets,
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
measureCount,
|
|
11
11
|
notationLayout,
|
|
12
12
|
vstackAudioPlayheadLine
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-DBFGX6OK.js";
|
|
14
14
|
import {
|
|
15
15
|
safeBox
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-PVZXLC23.js";
|
|
17
17
|
|
|
18
18
|
// src/notationPlayer.ts
|
|
19
19
|
var DEFAULT_THEME = {
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
createFollowController,
|
|
5
5
|
hasReachedProgrammaticTarget,
|
|
6
6
|
isWithinProgrammaticScroll
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HIYU73EB.js";
|
|
8
8
|
import {
|
|
9
9
|
distinctOnsets,
|
|
10
10
|
hitTestMeasureAt,
|
|
11
11
|
vstackAudioPlayheadLine
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-DBFGX6OK.js";
|
|
13
|
+
import "./chunk-PVZXLC23.js";
|
|
14
14
|
|
|
15
15
|
// src/notationPlayerSvg.ts
|
|
16
16
|
function graphicalNotesAtColumn(osmd, col) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
computeReflowScrollDelta,
|
|
3
3
|
createFollowController
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-HIYU73EB.js";
|
|
5
5
|
import {
|
|
6
6
|
distinctOnsets,
|
|
7
7
|
hitTestMeasureAt,
|
|
8
8
|
measureColumnsFromLayout,
|
|
9
9
|
systemIndexOfBox,
|
|
10
10
|
vstackAudioPlayheadLine
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-DBFGX6OK.js";
|
|
12
|
+
import "./chunk-PVZXLC23.js";
|
|
13
13
|
|
|
14
14
|
// src/notationXml.ts
|
|
15
15
|
function stampNoteIds(xml) {
|
package/dist/scene/index.d.ts
CHANGED
|
@@ -360,6 +360,10 @@ interface RecordSceneSpecOpts {
|
|
|
360
360
|
audioNow: () => number;
|
|
361
361
|
lookaheadSec?: number;
|
|
362
362
|
};
|
|
363
|
+
/** Passed straight through to recordScenes — see RecordOpts.splitAudioTrack. */
|
|
364
|
+
splitAudioTrack?: RecordOpts['splitAudioTrack'];
|
|
365
|
+
/** Passed straight through to recordScenes — see RecordOpts.onAudioBlob. */
|
|
366
|
+
onAudioBlob?: RecordOpts['onAudioBlob'];
|
|
363
367
|
}
|
|
364
368
|
/**
|
|
365
369
|
* Capture a built scene to a Blob by wrapping the whole timeline as a SINGLE
|
|
@@ -1785,7 +1789,7 @@ interface HandIndicatorProps {
|
|
|
1785
1789
|
/** How fast a hand fades from lit back to its resting outline after its
|
|
1786
1790
|
* last active note ends, ms. Default 200 (the "quickly" in ~150-250ms). */
|
|
1787
1791
|
fadeMs?: number;
|
|
1788
|
-
/** Icon size, px (roughly the glyph's bounding box). Default height*0.
|
|
1792
|
+
/** Icon size, px (roughly the glyph's bounding box). Default height*0.78. */
|
|
1789
1793
|
size?: number;
|
|
1790
1794
|
/** LH icon x, fraction of safeBox width from its left edge. Default 0.26
|
|
1791
1795
|
* (under the lower/bass register). */
|
package/dist/scene/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
scrollCursorFactory,
|
|
16
16
|
setFollowLayoutProvider,
|
|
17
17
|
setNotationEngraving
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-NZFS7ZTK.js";
|
|
19
19
|
import {
|
|
20
20
|
FOLLOW_BARS,
|
|
21
21
|
FOLLOW_PAD,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
systemBox,
|
|
38
38
|
vstackAudioPlayheadLine,
|
|
39
39
|
vstackFollowBox
|
|
40
|
-
} from "../chunk-
|
|
40
|
+
} from "../chunk-DBFGX6OK.js";
|
|
41
41
|
import {
|
|
42
42
|
ctaScene,
|
|
43
43
|
drawSafeGuides,
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
recordScenes,
|
|
47
47
|
revealScene,
|
|
48
48
|
safeBox
|
|
49
|
-
} from "../chunk-
|
|
49
|
+
} from "../chunk-PVZXLC23.js";
|
|
50
50
|
|
|
51
51
|
// src/scene/score.ts
|
|
52
52
|
var BEATS_PER_WHOLE_NOTE = 4;
|
|
@@ -4652,7 +4652,7 @@ function handIndicatorLayer() {
|
|
|
4652
4652
|
height = props.height ?? 60;
|
|
4653
4653
|
hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
|
|
4654
4654
|
fadeMs = props.fadeMs ?? 200;
|
|
4655
|
-
size = props.size ?? height * 0.
|
|
4655
|
+
size = props.size ?? height * 0.78;
|
|
4656
4656
|
leftXFrac = props.leftXFrac ?? 0.26;
|
|
4657
4657
|
rightXFrac = props.rightXFrac ?? 0.74;
|
|
4658
4658
|
restAlpha = props.restAlpha ?? 0.32;
|
|
@@ -4676,8 +4676,6 @@ function handIndicatorLayer() {
|
|
|
4676
4676
|
if (op > 0.01) {
|
|
4677
4677
|
c.save();
|
|
4678
4678
|
c.globalAlpha = op;
|
|
4679
|
-
c.shadowColor = color;
|
|
4680
|
-
c.shadowBlur = 16 * op;
|
|
4681
4679
|
c.fillStyle = color;
|
|
4682
4680
|
drawHandGlyph(c, cx, cy, size, mirror, "fill");
|
|
4683
4681
|
c.restore();
|
|
@@ -5182,7 +5180,9 @@ async function recordSceneSpec(opts) {
|
|
|
5182
5180
|
height: built.H,
|
|
5183
5181
|
fps: built.fps,
|
|
5184
5182
|
background: opts.background,
|
|
5185
|
-
onProgress: opts.onProgress
|
|
5183
|
+
onProgress: opts.onProgress,
|
|
5184
|
+
splitAudioTrack: opts.splitAudioTrack,
|
|
5185
|
+
onAudioBlob: opts.onAudioBlob
|
|
5186
5186
|
});
|
|
5187
5187
|
}
|
|
5188
5188
|
|