odori 0.0.2 → 0.0.4

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/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
  import {
3
3
  Audio,
4
+ Clip,
4
5
  DUCK_GAIN,
5
6
  DUCK_RAMP_FRAMES,
6
7
  Easing,
7
8
  Fill,
8
- Freeze,
9
- Loop,
9
+ Hold,
10
10
  OdoriRuntime,
11
+ Repeat,
11
12
  SAMPLE_RATE,
12
13
  SafeArea,
13
14
  Scene,
@@ -66,18 +67,19 @@ import {
66
67
  useScene,
67
68
  useSceneTransition,
68
69
  useVideo
69
- } from "./chunk-2SROZSZM.js";
70
+ } from "./chunk-XN6DA264.js";
70
71
  import {
71
72
  createRenderManifest
72
- } from "./chunk-VQLDJUK4.js";
73
+ } from "./chunk-7NPIGLFR.js";
73
74
  import {
74
75
  canonicalJson,
75
76
  formatTimecode,
76
77
  framesFromDuration,
78
+ framesFromOffset,
77
79
  hashString,
78
80
  hashValue,
79
81
  secondsFromFrames
80
- } from "./chunk-PXG45HYV.js";
82
+ } from "./chunk-V34N2I4A.js";
81
83
 
82
84
  // src/loudness.ts
83
85
  var SHELF = { frequency: 1681.974450955533, gainDb: 3.999843853973347, q: 0.7071752369554196 };
@@ -160,7 +162,7 @@ var integratedLufs = (channels, sampleRate) => {
160
162
  return loudnessOf(integrated);
161
163
  };
162
164
 
163
- // src/player.tsx
165
+ // src/viewer.tsx
164
166
  import { useCallback as useCallback2, useMemo, useState as useState2 } from "react";
165
167
 
166
168
  // src/playback.ts
@@ -339,9 +341,9 @@ var useAudioPlayback = ({
339
341
  }, []);
340
342
  };
341
343
 
342
- // src/player.tsx
344
+ // src/viewer.tsx
343
345
  import { jsx, jsxs } from "react/jsx-runtime";
344
- var Player = ({
346
+ var Viewer = ({
345
347
  entry,
346
348
  input,
347
349
  prepared,
@@ -384,11 +386,11 @@ var Player = ({
384
386
  const activeScene = timeline?.scenes.find(
385
387
  (scene) => frame >= scene.start && frame < scene.start + scene.durationInFrames
386
388
  );
387
- return /* @__PURE__ */ jsxs("div", { className: "odori-player", style: { display: "grid", gap: 12, width: "100%", ...style }, children: [
389
+ return /* @__PURE__ */ jsxs("div", { className: "odori-viewer", style: { display: "grid", gap: 12, width: "100%", ...style }, children: [
388
390
  /* @__PURE__ */ jsx(
389
391
  "div",
390
392
  {
391
- "data-odori-player": true,
393
+ "data-odori-viewer": true,
392
394
  style: {
393
395
  aspectRatio,
394
396
  background: resolvedLayout.brand.colors.background,
@@ -412,7 +414,7 @@ var Player = ({
412
414
  )
413
415
  }
414
416
  ),
415
- controls ? /* @__PURE__ */ jsxs("div", { className: "odori-player-controls", style: { alignItems: "center", display: "flex", gap: 10 }, children: [
417
+ controls ? /* @__PURE__ */ jsxs("div", { className: "odori-viewer-controls", style: { alignItems: "center", display: "flex", gap: 10 }, children: [
416
418
  /* @__PURE__ */ jsx("button", { type: "button", onClick: playback.toggle, children: playback.playing ? "Pause" : "Play" }),
417
419
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => playback.step(-1), "aria-label": "Previous frame", children: "\u2039" }),
418
420
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => playback.step(1), "aria-label": "Next frame", children: "\u203A" }),
@@ -566,6 +568,32 @@ var cursorDuration = (stops) => {
566
568
  return (stops[stops.length - 1]?.frame ?? 0) + hold + 20;
567
569
  };
568
570
 
571
+ // src/typing.ts
572
+ var DEFAULTS = { from: 0, charactersPerSecond: 22, chunk: 1, settle: 12 };
573
+ var typingFrames = (text, options = {}, fps = 30) => {
574
+ const { charactersPerSecond, chunk } = { ...DEFAULTS, ...options };
575
+ const steps = Math.ceil(text.length / Math.max(1, chunk));
576
+ return Math.ceil(steps * Math.max(1, chunk) * fps / Math.max(1, charactersPerSecond));
577
+ };
578
+ var typedAt = (text, frame, options = {}, fps = 30) => {
579
+ const { from, charactersPerSecond, chunk, settle } = { ...DEFAULTS, ...options };
580
+ const step2 = Math.max(1, chunk);
581
+ const elapsed = frame - from;
582
+ const revealed = Math.floor(elapsed / fps * charactersPerSecond);
583
+ const length = Math.max(0, Math.min(text.length, Math.floor(revealed / step2) * step2));
584
+ const done = elapsed >= 0 && length >= text.length;
585
+ const finishedAt = from + typingFrames(text, options, fps);
586
+ const caret = !done || frame < finishedAt + settle ? elapsed >= 0 : (frame - finishedAt - settle) % fps < fps / 2;
587
+ return {
588
+ text: text.slice(0, length),
589
+ length,
590
+ done,
591
+ caret,
592
+ progress: text.length === 0 ? 1 : length / text.length
593
+ };
594
+ };
595
+ var useTyping = (text, options = {}) => typedAt(text, useFrame(), options);
596
+
569
597
  // src/random.ts
570
598
  var hashSeed = (value) => {
571
599
  let hash = 2166136261;
@@ -788,20 +816,21 @@ var isOdoriSchema = (schema) => typeof schema === "object" && schema !== null &&
788
816
  var parseWithSchema = (schema, input) => schema ? schema.parse(input) : input ?? {};
789
817
  export {
790
818
  Audio,
819
+ Clip,
791
820
  DUCK_GAIN,
792
821
  DUCK_RAMP_FRAMES,
793
822
  Easing,
794
823
  Fill,
795
- Freeze,
796
- Loop,
824
+ Hold,
797
825
  OdoriRuntime,
798
- Player,
799
826
  RenderSurface,
827
+ Repeat,
800
828
  SAMPLE_RATE,
801
829
  SafeArea,
802
830
  Scene,
803
831
  Stagger,
804
832
  Video,
833
+ Viewer,
805
834
  advanceFrames,
806
835
  brandCssVariables,
807
836
  canonicalJson,
@@ -829,6 +858,7 @@ export {
829
858
  envelopeAtFrame,
830
859
  formatTimecode,
831
860
  framesFromDuration,
861
+ framesFromOffset,
832
862
  gain,
833
863
  gainAtFrame,
834
864
  hashString,
@@ -873,6 +903,8 @@ export {
873
903
  trackDuration,
874
904
  trackGainAtFrame,
875
905
  triangle,
906
+ typedAt,
907
+ typingFrames,
876
908
  useAssets,
877
909
  useAudioPlayback,
878
910
  useBrand,
@@ -884,6 +916,7 @@ export {
884
916
  useReadiness,
885
917
  useScene,
886
918
  useSceneTransition,
919
+ useTyping,
887
920
  useVideo
888
921
  };
889
922
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/loudness.ts","../src/player.tsx","../src/playback.ts","../src/audio-playback.ts","../src/render-surface.tsx","../src/wav.ts","../src/cursor.ts","../src/random.ts","../src/canvas.ts","../src/placeholder.ts","../src/metadata.ts","../src/schema.ts"],"sourcesContent":["/**\n * Integrated loudness, ITU-R BS.1770-4.\n *\n * The number a mix is judged by is not peak or RMS: it is K-weighted, gated\n * loudness. Studio measures what it is about to hand the encoder so the\n * brand's target is something you can mix toward rather than discover after an\n * export.\n */\n\nexport type Biquad = {b0: number; b1: number; b2: number; a1: number; a2: number};\n\n/** Stage 1: the head shelf, and stage 2: the high pass, from the spec's filter table. */\nconst SHELF = {frequency: 1681.974450955533, gainDb: 3.999843853973347, q: 0.7071752369554196};\nconst HIGH_PASS = {frequency: 38.13547087602444, q: 0.5003270373238773};\n\n/**\n * The spec tabulates coefficients at 48 kHz. Deriving them per rate keeps a\n * 44.1 kHz source from being measured with the wrong filter.\n */\nexport const shelfCoefficients = (sampleRate: number): Biquad => {\n const amplitude = 10 ** (SHELF.gainDb / 40);\n const omega = (2 * Math.PI * SHELF.frequency) / sampleRate;\n const alpha = Math.sin(omega) / (2 * SHELF.q);\n const cos = Math.cos(omega);\n const shared = 2 * Math.sqrt(amplitude) * alpha;\n const a0 = amplitude + 1 - (amplitude - 1) * cos + shared;\n return {\n b0: (amplitude * (amplitude + 1 + (amplitude - 1) * cos + shared)) / a0,\n b1: (-2 * amplitude * (amplitude - 1 + (amplitude + 1) * cos)) / a0,\n b2: (amplitude * (amplitude + 1 + (amplitude - 1) * cos - shared)) / a0,\n a1: (2 * (amplitude - 1 - (amplitude + 1) * cos)) / a0,\n a2: (amplitude + 1 - (amplitude - 1) * cos - shared) / a0,\n };\n};\n\nexport const highPassCoefficients = (sampleRate: number): Biquad => {\n const omega = (2 * Math.PI * HIGH_PASS.frequency) / sampleRate;\n const alpha = Math.sin(omega) / (2 * HIGH_PASS.q);\n const cos = Math.cos(omega);\n const a0 = 1 + alpha;\n return {\n b0: (1 + cos) / 2 / a0,\n b1: (-(1 + cos)) / a0,\n b2: (1 + cos) / 2 / a0,\n a1: (-2 * cos) / a0,\n a2: (1 - alpha) / a0,\n };\n};\n\nconst filter = (samples: Float32Array, {b0, b1, b2, a1, a2}: Biquad): Float32Array => {\n const output = new Float32Array(samples.length);\n let x1 = 0;\n let x2 = 0;\n let y1 = 0;\n let y2 = 0;\n for (let index = 0; index < samples.length; index += 1) {\n const x0 = samples[index];\n const y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;\n output[index] = y0;\n x2 = x1;\n x1 = x0;\n y2 = y1;\n y1 = y0;\n }\n return output;\n};\n\nconst BLOCK_SECONDS = 0.4;\n/** Blocks overlap by 75%, so a short transient cannot hide between them. */\nconst STEP = 0.25;\nconst ABSOLUTE_GATE = -70;\nconst RELATIVE_GATE = -10;\nconst OFFSET = -0.691;\n\nconst loudnessOf = (meanSquares: number[]) =>\n OFFSET + 10 * Math.log10(meanSquares.reduce((total, value) => total + value, 0) || Number.MIN_VALUE);\n\n/**\n * Channel weights for stereo. Surround weights the surround channels higher;\n * a video mix is stereo, so both channels count equally.\n */\nexport const integratedLufs = (channels: Float32Array[], sampleRate: number): number | null => {\n if (channels.length === 0 || channels[0].length === 0) return null;\n const weighted = channels.map((channel) => filter(filter(channel, shelfCoefficients(sampleRate)), highPassCoefficients(sampleRate)));\n\n const blockSize = Math.round(BLOCK_SECONDS * sampleRate);\n const hop = Math.round(BLOCK_SECONDS * STEP * sampleRate);\n if (weighted[0].length < blockSize) return null;\n\n // One mean square per channel per block, kept apart so gating can sum them.\n const blocks: number[][] = [];\n for (let start = 0; start + blockSize <= weighted[0].length; start += hop) {\n blocks.push(\n weighted.map((channel) => {\n let sum = 0;\n for (let index = start; index < start + blockSize; index += 1) sum += channel[index] * channel[index];\n return sum / blockSize;\n }),\n );\n }\n if (blocks.length === 0) return null;\n\n const above = blocks.filter((block) => loudnessOf(block) > ABSOLUTE_GATE);\n if (above.length === 0) return null;\n\n // The relative gate is measured against the ungated mean of what survived.\n const mean = above[0].map((_, channel) => above.reduce((total, block) => total + block[channel], 0) / above.length);\n const threshold = loudnessOf(mean) + RELATIVE_GATE;\n const gated = above.filter((block) => loudnessOf(block) > threshold);\n if (gated.length === 0) return null;\n\n const integrated = gated[0].map((_, channel) => gated.reduce((total, block) => total + block[channel], 0) / gated.length);\n return loudnessOf(integrated);\n};\n","\"use client\";\n\nimport {useCallback, useMemo, useState, type CSSProperties} from \"react\";\nimport {OdoriRuntime, entryDurationInFrames, resolveEntryLayout, type CompiledTimeline, type VideoEntry} from \"./runtime\";\nimport {type VideoLayout} from \"./layout\";\nimport {formatTimecode} from \"./time\";\nimport {usePlayback} from \"./playback\";\nimport {useAudioPlayback} from \"./audio-playback\";\nimport {type AudioTrack} from \"./audio\";\n\nexport type PlayerProps = {\n entry: VideoEntry;\n input?: Record<string, unknown>;\n prepared?: unknown;\n assets?: Array<{reference: string; url: string}>;\n layout?: VideoLayout;\n initialFrame?: number;\n autoPlay?: boolean;\n loop?: boolean;\n controls?: boolean;\n style?: CSSProperties;\n muted?: boolean;\n onFrame?: (frame: number) => void;\n onTimeline?: (timeline: CompiledTimeline) => void;\n onAudio?: (track: AudioTrack) => void;\n};\n\n/**\n * The seekable frame clock. Playback advances a fractional frame counter from\n * wall-clock deltas, but React only ever sees an integer frame, so a paused\n * player and a render worker produce identical output.\n */\nexport const Player = ({\n entry,\n input,\n prepared,\n assets,\n layout,\n initialFrame = 0,\n autoPlay = false,\n loop = true,\n controls = true,\n muted = false,\n style,\n onFrame,\n onTimeline,\n onAudio,\n}: PlayerProps) => {\n const resolvedLayout = resolveEntryLayout(entry, layout);\n const {fps, width, height} = resolvedLayout.format;\n const [timeline, setTimeline] = useState<CompiledTimeline | null>(null);\n const [track, setTrack] = useState<AudioTrack | null>(null);\n const declared = entryDurationInFrames(entry, resolvedLayout);\n const durationInFrames = Math.max(1, declared || timeline?.durationInFrames || fps);\n\n const playback = usePlayback({fps, durationInFrames, initialFrame, autoPlay, loop, onFrame});\n const {frame} = playback;\n\n useAudioPlayback({track, frame: playback.frame, fps, playing: playback.playing, muted});\n\n const handleAudio = useCallback(\n (next: AudioTrack) => {\n setTrack(next);\n onAudio?.(next);\n },\n [onAudio],\n );\n\n const handleTimeline = useCallback(\n (next: CompiledTimeline) => {\n // Comparing the count and total would hold a stale timeline when two\n // scenes trade frames between them: same length, same total, different\n // boundaries.\n setTimeline((current) => (JSON.stringify(current) === JSON.stringify(next) ? current : next));\n onTimeline?.(next);\n },\n [onTimeline],\n );\n\n const aspectRatio = useMemo(() => `${width} / ${height}`, [height, width]);\n const activeScene = timeline?.scenes.find(\n (scene) => frame >= scene.start && frame < scene.start + scene.durationInFrames,\n );\n\n return (\n <div className=\"odori-player\" style={{display: \"grid\", gap: 12, width: \"100%\", ...style}}>\n <div\n data-odori-player\n style={{\n aspectRatio,\n background: resolvedLayout.brand.colors.background,\n borderRadius: 10,\n overflow: \"hidden\",\n position: \"relative\",\n width: \"100%\",\n }}\n >\n <OdoriRuntime\n entry={entry}\n frame={frame}\n input={input}\n prepared={prepared}\n assets={assets}\n layout={layout}\n onTimeline={handleTimeline}\n onAudio={handleAudio}\n />\n </div>\n {controls ? (\n <div className=\"odori-player-controls\" style={{alignItems: \"center\", display: \"flex\", gap: 10}}>\n <button type=\"button\" onClick={playback.toggle}>\n {playback.playing ? \"Pause\" : \"Play\"}\n </button>\n <button type=\"button\" onClick={() => playback.step(-1)} aria-label=\"Previous frame\">\n {\"\\u2039\"}\n </button>\n <button type=\"button\" onClick={() => playback.step(1)} aria-label=\"Next frame\">\n {\"\\u203a\"}\n </button>\n <input\n aria-label=\"Timeline\"\n type=\"range\"\n min={0}\n max={durationInFrames - 1}\n value={frame}\n onChange={(event) => {\n playback.pause();\n playback.seek(Number(event.currentTarget.value));\n }}\n style={{flex: 1}}\n />\n <output style={{fontVariantNumeric: \"tabular-nums\", minWidth: 132, textAlign: \"right\"}}>\n {formatTimecode(frame, fps)} {\"·\"} {frame}/{durationInFrames - 1}\n {activeScene ? ` · ${activeScene.name ?? activeScene.id}` : \"\"}\n </output>\n </div>\n ) : null}\n </div>\n );\n};\n","\"use client\";\n\nimport {useCallback, useEffect, useRef, useState} from \"react\";\n\nexport type PlaybackOptions = {\n fps: number;\n durationInFrames: number;\n initialFrame?: number;\n autoPlay?: boolean;\n loop?: boolean;\n /** Wall-clock multiplier. The frame clock keeps its rate; only time moves. */\n rate?: number;\n onFrame?: (frame: number) => void;\n};\n\nexport type Playback = {\n frame: number;\n playing: boolean;\n rate: number;\n play(): void;\n pause(): void;\n toggle(): void;\n seek(frame: number): void;\n step(delta: number): void;\n restart(): void;\n};\n\n/**\n * The seekable frame clock. Playback advances a fractional counter from\n * wall-clock deltas, but React only ever sees an integer frame, so a paused\n * player, a still, and the export worker agree by construction.\n */\n/**\n * How far the clock moves for a wall-clock delta.\n *\n * Rate scales elapsed time, never the frame index, so frame 90 is the same\n * image at 0.25x, 1x, and 4x, and an export ignores rate entirely.\n */\nexport const advanceFrames = (fractional: number, deltaMs: number, fps: number, rate = 1): number =>\n fractional + (deltaMs / 1000) * fps * rate;\n\nexport const usePlayback = ({\n fps,\n durationInFrames,\n initialFrame = 0,\n autoPlay = false,\n loop = true,\n rate = 1,\n onFrame,\n}: PlaybackOptions): Playback => {\n const [frame, setFrame] = useState(initialFrame);\n const [playing, setPlaying] = useState(autoPlay);\n const animation = useRef<number | null>(null);\n const previousTime = useRef<number | null>(null);\n const fractional = useRef(initialFrame);\n const frameRef = useRef(initialFrame);\n\n const commit = useCallback(\n (next: number) => {\n const clamped = Math.max(0, Math.min(Math.round(next), Math.max(0, durationInFrames - 1)));\n frameRef.current = clamped;\n setFrame(clamped);\n onFrame?.(clamped);\n },\n [durationInFrames, onFrame],\n );\n\n useEffect(() => {\n if (!playing) {\n previousTime.current = null;\n fractional.current = frameRef.current;\n return;\n }\n const tick = (now: number) => {\n const previous = previousTime.current ?? now;\n previousTime.current = now;\n fractional.current = advanceFrames(fractional.current, now - previous, fps, rate);\n if (fractional.current >= durationInFrames) {\n if (!loop) {\n commit(durationInFrames - 1);\n setPlaying(false);\n return;\n }\n fractional.current %= durationInFrames;\n }\n commit(Math.floor(fractional.current));\n animation.current = requestAnimationFrame(tick);\n };\n animation.current = requestAnimationFrame(tick);\n return () => {\n if (animation.current !== null) cancelAnimationFrame(animation.current);\n };\n }, [commit, durationInFrames, fps, loop, playing, rate]);\n\n const seek = useCallback(\n (next: number) => {\n fractional.current = next;\n commit(next);\n },\n [commit],\n );\n\n return {\n frame,\n playing,\n rate,\n play: () => setPlaying(true),\n pause: () => setPlaying(false),\n toggle: () => setPlaying((value) => !value),\n seek,\n step: (delta: number) => {\n setPlaying(false);\n seek(frameRef.current + delta);\n },\n restart: () => {\n seek(0);\n setPlaying(true);\n },\n };\n};\n","\"use client\";\n\nimport {useEffect, useRef} from \"react\";\nimport {trackGainAtFrame, type AudioTrack} from \"./audio\";\n\nexport type AudioPlaybackOptions = {\n track: AudioTrack | null;\n frame: number;\n fps: number;\n playing: boolean;\n muted?: boolean;\n masterGain?: number;\n /** Cue id to hear alone, or null for the whole mix. */\n soloCue?: string | null;\n /** True while the playhead is being dragged, which auditions under the cursor. */\n scrubbing?: boolean;\n /** Playback rate, so cues stay with the frame clock when it is sped up. */\n rate?: number;\n /**\n * Called when the browser refuses to start a cue without a user gesture, and\n * again when it relents. Autoplay policy is the difference between a silent\n * preview and a broken one, so it is reported rather than swallowed.\n */\n onBlocked?: (blocked: boolean) => void;\n /**\n * Called with the cues whose files failed to load or decode. A cue pointing\n * at a missing or wrong file is silence with no other symptom, so it is\n * reported rather than swallowed.\n */\n onFailed?: (sources: string[]) => void;\n};\n\n/**\n * Drives one HTMLAudioElement per cue from the frame clock.\n *\n * Audio is the one thing that cannot be derived from a frame index, so preview\n * playback resyncs whenever the element drifts more than a frame from where the\n * timeline says it should be. The exported mix is built separately by the\n * encoder from the same cues, which keeps the file frame accurate.\n */\nexport const useAudioPlayback = ({\n track,\n frame,\n fps,\n playing,\n muted = false,\n masterGain = 1,\n soloCue = null,\n scrubbing = false,\n rate = 1,\n onBlocked,\n onFailed,\n}: AudioPlaybackOptions) => {\n const elements = useRef(new Map<string, HTMLAudioElement>());\n // The sync pass, reachable from listeners that fire when the frame has not\n // changed: the clock can stall (a hidden tab throttles rAF, a slow render\n // drops frames) while an audio element keeps running at its own pace.\n const sync = useRef<() => void>(() => {});\n const failed = useRef(new Set<string>());\n\n useEffect(() => {\n const table = elements.current;\n const live = new Set((track?.cues ?? []).map((cue) => cue.id));\n for (const [id, element] of table) {\n if (live.has(id)) continue;\n element.pause();\n table.delete(id);\n }\n return () => {\n for (const element of table.values()) element.pause();\n };\n }, [track]);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const table = elements.current;\n\n const onTimeUpdate = () => run();\n\n const run = () => {\n // Scrubbing is playback the user is driving by hand, so cues sound under\n // the cursor instead of going silent the moment the transport pauses.\n const audible = playing || scrubbing;\n\n for (const cue of track?.cues ?? []) {\n let element = table.get(cue.id);\n if (!element) {\n element = new window.Audio(cue.src);\n element.preload = \"auto\";\n element.loop = cue.loop;\n // An element that drifts while the clock is stalled corrects itself\n // on its own time updates, so audio can never run away from a\n // frozen picture.\n element.addEventListener(\"timeupdate\", onTimeUpdate);\n element.addEventListener(\"error\", () => {\n failed.current.add(cue.src);\n onFailed?.([...failed.current]);\n });\n table.set(cue.id, element);\n }\n\n const local = frame - cue.fromFrame;\n const inside = local >= 0 && local < cue.durationInFrames;\n /**\n * Where in the file this frame sounds. A looping cue's window is\n * longer than its file, so the position wraps: seeking straight to\n * `local / fps` would land past the end and the browser would clamp\n * it, leaving the bed stuck on its final sample. The wrap needs the\n * file's real length, which only exists once metadata has loaded.\n */\n const span = element.duration;\n const elapsed = local / fps;\n const position =\n cue.loop && Number.isFinite(span) && span > 0 ? elapsed % span : elapsed;\n // Set every pass, not only at creation: an edit that turns looping on\n // does not change the cue's id, so the element it reuses would keep\n // the old behaviour.\n if (element.loop !== cue.loop) element.loop = cue.loop;\n element.volume = Math.max(0, Math.min(1, trackGainAtFrame(cue, track?.cues ?? [], frame) * masterGain));\n element.muted = muted || (soloCue !== null && soloCue !== cue.id);\n\n if (!inside || !audible) {\n if (!element.paused) element.pause();\n if (inside && !audible) {\n const target = cue.trimStartSeconds + position;\n if (Math.abs(element.currentTime - target) > 1 / fps) element.currentTime = target;\n }\n continue;\n }\n\n const target = cue.trimStartSeconds + position;\n // A rate change is a new playbackRate, not a reseek: the element keeps\n // playing and the drift check below catches it if it falls behind.\n if (element.playbackRate !== rate) element.playbackRate = rate;\n if (Math.abs(element.currentTime - target) > (2 / fps) * Math.max(1, rate)) element.currentTime = target;\n if (element.paused) {\n void element.play().then(\n () => onBlocked?.(false),\n (error: unknown) => onBlocked?.((error as Error)?.name === \"NotAllowedError\"),\n );\n }\n }\n };\n\n sync.current = run;\n run();\n\n return () => {\n for (const element of table.values()) element.removeEventListener(\"timeupdate\", onTimeUpdate);\n };\n }, [fps, frame, masterGain, muted, onBlocked, onFailed, playing, rate, scrubbing, soloCue, track]);\n\n // A hidden tab throttles the frame clock, so hold every cue until the tab is\n // visible again and then resync from the frame the timeline actually shows.\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n const onVisibility = () => {\n if (document.hidden) {\n for (const element of elements.current.values()) element.pause();\n return;\n }\n sync.current();\n };\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => document.removeEventListener(\"visibilitychange\", onVisibility);\n }, []);\n};\n","\"use client\";\n\nimport {useEffect, useState} from \"react\";\nimport {OdoriRuntime, type CompiledTimeline, type VideoEntry} from \"./runtime\";\nimport {type AudioTrack} from \"./audio\";\nimport {type VideoLayout} from \"./layout\";\n\ndeclare global {\n interface Window {\n __ODORI_SET_FRAME__?: (frame: number) => void;\n __ODORI_TIMELINE__?: CompiledTimeline;\n __ODORI_AUDIO__?: AudioTrack;\n __ODORI_READY__?: boolean;\n }\n}\n\n/**\n * The surface the render worker drives. It exposes an explicit frame setter\n * and a readiness handshake instead of relying on timing heuristics.\n */\nexport const RenderSurface = ({\n entry,\n initialFrame = 0,\n input,\n prepared,\n assets,\n layout,\n}: {\n entry: VideoEntry;\n initialFrame?: number;\n input?: Record<string, unknown>;\n prepared?: unknown;\n assets?: Array<{reference: string; url: string}>;\n layout?: VideoLayout;\n}) => {\n const [frame, setFrame] = useState(initialFrame);\n\n useEffect(() => {\n window.__ODORI_SET_FRAME__ = setFrame;\n window.__ODORI_READY__ = true;\n return () => {\n delete window.__ODORI_SET_FRAME__;\n delete window.__ODORI_READY__;\n };\n }, []);\n\n return (\n <OdoriRuntime\n entry={entry}\n frame={frame}\n input={input}\n prepared={prepared}\n assets={assets}\n layout={layout}\n onTimeline={(timeline) => {\n window.__ODORI_TIMELINE__ = timeline;\n }}\n onAudio={(track) => {\n window.__ODORI_AUDIO__ = track;\n }}\n />\n );\n};\n","import {type Signal} from \"./synth\";\n\n/**\n * Signal to a 16 bit PCM WAV. Small, lossless, and readable by FFmpeg without\n * a decoder, which is all the mix needs from a generated cue.\n *\n * Encoding lives in the runtime rather than the CLI so preview and export\n * share it: the browser can hand the same bytes to an AudioContext that the\n * render worker writes to disk.\n */\nexport const encodeWav = (signal: Signal): Uint8Array => {\n const channels = signal.channels.length || 1;\n const frames = signal.channels[0]?.length ?? 0;\n const bytesPerSample = 2;\n const dataBytes = frames * channels * bytesPerSample;\n const buffer = new ArrayBuffer(44 + dataBytes);\n const view = new DataView(buffer);\n\n const ascii = (offset: number, text: string) => {\n for (let index = 0; index < text.length; index += 1) view.setUint8(offset + index, text.charCodeAt(index));\n };\n\n ascii(0, \"RIFF\");\n view.setUint32(4, 36 + dataBytes, true);\n ascii(8, \"WAVE\");\n ascii(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true); // PCM\n view.setUint16(22, channels, true);\n view.setUint32(24, signal.sampleRate, true);\n view.setUint32(28, signal.sampleRate * channels * bytesPerSample, true);\n view.setUint16(32, channels * bytesPerSample, true);\n view.setUint16(34, 8 * bytesPerSample, true);\n ascii(36, \"data\");\n view.setUint32(40, dataBytes, true);\n\n let offset = 44;\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n const sample = signal.channels[channel]?.[frame] ?? 0;\n // Clamp before quantizing, so a hot score distorts predictably instead\n // of wrapping into noise.\n const clamped = Math.max(-1, Math.min(1, sample));\n view.setInt16(offset, Math.round(clamped * 32767), true);\n offset += bytesPerSample;\n }\n }\n\n return new Uint8Array(buffer);\n};\n","import {Easing, interpolate} from \"./easing\";\n\nexport type CursorStop = {\n /** Frame this stop is reached, from the start of the enclosing scene. */\n frame: number;\n /** Canvas coordinates, in the composition's own pixels. */\n x: number;\n y: number;\n /**\n * A click landing on this stop. The press is drawn at the stop's frame and\n * decays over a few frames, so the pointer visibly does the thing the UI is\n * about to react to.\n */\n click?: boolean;\n /** Hold here until this many frames have passed before moving on. */\n hold?: number;\n};\n\nexport type CursorState = {\n x: number;\n y: number;\n /** 0 before the path starts and after it ends, 1 while it is on screen. */\n visible: number;\n /** 1 at the instant of a click, decaying to 0. Drives the press ring. */\n pressed: number;\n /** True while a click is within its press window, for a UI to react to. */\n clicking: boolean;\n};\n\n/** Frames a press ring takes to expand and fade. */\nconst PRESS_FRAMES = 9;\n\n/**\n * Where an authored pointer is at this frame.\n *\n * Recording a real cursor would make a video that cannot be re-rendered: the\n * path would live in a file, not in the composition, and a change of copy or\n * canvas would leave it pointing at nothing. An authored path is source — it\n * diffs, it survives a reflow, and it produces the same pixels every run.\n *\n * Movement eases between stops rather than running linearly, because a pointer\n * that travels at constant speed reads as a machine. A `hold` keeps the\n * pointer still without needing a duplicate stop at the same coordinates.\n */\nexport const cursorAt = (stops: CursorStop[], frame: number): CursorState => {\n if (stops.length === 0) return {x: 0, y: 0, visible: 0, pressed: 0, clicking: false};\n\n // A hold extends the stop it is on, which shifts everything after it.\n const timed: CursorStop[] = [];\n let shift = 0;\n for (const stop of stops) {\n const start = stop.frame + shift;\n timed.push({...stop, frame: start});\n if (stop.hold) {\n timed.push({...stop, frame: start + stop.hold, click: false});\n shift += stop.hold;\n }\n }\n\n const first = timed[0];\n const last = timed[timed.length - 1];\n if (frame <= first.frame) return {x: first.x, y: first.y, visible: 0, pressed: 0, clicking: false};\n\n const frames = timed.map((stop) => stop.frame);\n const x = interpolate(frame, frames, timed.map((stop) => stop.x), {easing: Easing.standard});\n const y = interpolate(frame, frames, timed.map((stop) => stop.y), {easing: Easing.standard});\n\n // The most recent click at or before this frame owns the press ring.\n let pressed = 0;\n for (const stop of timed) {\n if (!stop.click || stop.frame > frame) continue;\n const age = frame - stop.frame;\n if (age <= PRESS_FRAMES) pressed = Math.max(pressed, 1 - age / PRESS_FRAMES);\n }\n\n return {\n x,\n y,\n // Fade in as it arrives and out after the last stop, so a pointer never\n // pops onto a frame it was not part of.\n visible: interpolate(\n frame,\n [first.frame, first.frame + 6, last.frame + 12, last.frame + 20],\n [0, 1, 1, 0],\n {easing: Easing.standard},\n ),\n pressed,\n clicking: pressed > 0,\n };\n};\n\n/** The last frame an authored path is still on screen, for sizing a scene. */\nexport const cursorDuration = (stops: CursorStop[]): number => {\n const hold = stops.reduce((total, stop) => total + (stop.hold ?? 0), 0);\n return (stops[stops.length - 1]?.frame ?? 0) + hold + 20;\n};\n","/**\n * Randomness that survives a re-render.\n *\n * A frame is a pure function of its number. `Math.random()` breaks that in the\n * quietest possible way: the preview looks fine, every export looks fine, and\n * the two are different — and so are two chunks of the same export, because a\n * render is parallel and each worker rolls its own numbers. Fifty particles\n * that jump between chunk boundaries is the usual symptom, found late.\n *\n * So a composition asks for a number by name instead. The same seed always\n * gives the same value, on every machine and in every worker, and a seed that\n * includes the frame gives motion that is random-looking and reproducible.\n */\n\n/**\n * A 32-bit hash of a string, so a seed can be written as a readable name\n * rather than a magic integer. FNV-1a: small, well distributed for short keys,\n * and stable across engines, which matters because two workers must agree.\n */\nconst hashSeed = (value: string): number => {\n let hash = 0x811c9dc5;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return hash >>> 0;\n};\n\nconst toSeed = (seed: number | string): number =>\n typeof seed === \"number\" ? Math.floor(seed) >>> 0 : hashSeed(seed);\n\n/**\n * A number in `[0, 1)` for a seed. The same seed always returns the same\n * number, which is the whole point.\n *\n * ```tsx\n * const drift = random(`particle-${index}`) * 40;\n * const jitter = random([frame, index]) - 0.5;\n * ```\n *\n * An array seed is joined, which is the convenient way to say \"this thing, on\n * this frame\" without building the string by hand.\n */\nexport const random = (seed: number | string | Array<number | string>): number => {\n const key = Array.isArray(seed) ? seed.join(\":\") : seed;\n // Mulberry32, the same generator the audio synthesis uses, so a project has\n // one notion of \"seeded\" rather than two that disagree.\n let state = (toSeed(key) + 0x6d2b79f5) >>> 0;\n let t = Math.imul(state ^ (state >>> 15), 1 | state);\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n};\n\n/** A number in `[min, max)`, for a seed. */\nexport const randomBetween = (seed: number | string | Array<number | string>, min: number, max: number): number =>\n min + random(seed) * (max - min);\n\n/** One item from a list, for a seed. Empty lists return undefined. */\nexport const randomPick = <T,>(seed: number | string | Array<number | string>, items: readonly T[]): T | undefined =>\n items.length === 0 ? undefined : items[Math.floor(random(seed) * items.length)];\n\n/**\n * A shuffled copy, for a seed. Fisher-Yates driven by the same generator, so\n * the order is arbitrary but fixed — a list that reshuffles every frame is an\n * animation nobody asked for.\n */\nexport const randomOrder = <T,>(seed: number | string | Array<number | string>, items: readonly T[]): T[] => {\n const key = Array.isArray(seed) ? seed.join(\":\") : String(seed);\n const out = [...items];\n for (let index = out.length - 1; index > 0; index -= 1) {\n const swap = Math.floor(random(`${key}:${index}`) * (index + 1));\n [out[index], out[swap]] = [out[swap], out[index]];\n }\n return out;\n};\n","\"use client\";\n\nimport {useEffect, useLayoutEffect, useRef, type RefObject} from \"react\";\nimport {useFrame, useReadiness, useVideo} from \"./context\";\n\nexport type CanvasDraw = (context: CanvasRenderingContext2D, state: {frame: number; width: number; height: number}) => void;\n\n/** `useLayoutEffect` warns during server rendering, where there is no canvas. */\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\n/**\n * Draw to a canvas from the frame clock.\n *\n * The contract a video runs on is that frame N produces the same pixels every\n * time. A canvas is where that is easiest to lose: the obvious way to animate\n * one is `requestAnimationFrame`, which is wall time, and wall time means the\n * export samples wherever the loop happened to be. Two workers rendering\n * neighbouring chunks then disagree, and the seam shows.\n *\n * So the draw is a pure function of the frame, called synchronously before the\n * browser paints, and the frame is held until it has run. The capture waits on\n * the same readiness handshake an image decode uses, which is what makes the\n * screenshot see finished pixels rather than an empty buffer.\n */\nexport const useCanvas = (draw: CanvasDraw, dependencies: readonly unknown[] = []): RefObject<HTMLCanvasElement | null> => {\n const canvas = useRef<HTMLCanvasElement | null>(null);\n const frame = useFrame();\n const {width, height} = useVideo();\n const readiness = useReadiness();\n // The draw is called with the current closure but must not re-run the effect\n // when an inline function identity changes, or every render would repaint.\n const latest = useRef(draw);\n latest.current = draw;\n\n useIsomorphicLayoutEffect(() => {\n const element = canvas.current;\n if (!element) return;\n\n // Held across the draw, so a frame is never captured mid-paint.\n const release = readiness.hold();\n try {\n const context = element.getContext(\"2d\", {alpha: true});\n if (!context) return;\n\n // Reset rather than accumulate: a frame is drawn from nothing, so\n // scrubbing backwards produces the same image as playing forwards.\n context.setTransform(1, 0, 0, 1, 0, 0);\n context.clearRect(0, 0, element.width, element.height);\n latest.current(context, {frame, width: element.width, height: element.height});\n } finally {\n release();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [frame, width, height, ...dependencies]);\n\n return canvas;\n};\n\n/**\n * Rasterize HTML into a canvas, deterministically.\n *\n * The browser will draw an SVG containing a `foreignObject` onto a canvas, and\n * a `foreignObject` can hold ordinary markup. That is the whole trick, and the\n * reason it needs care: the image decode is asynchronous, so the frame has to\n * be held until it lands, and the markup has to carry its own styles because\n * nothing outside the SVG reaches into it.\n *\n * Fonts are the sharp edge. A face that is not loaded when this runs will fall\n * back, and the fallback is what gets baked into the pixels — which is why the\n * caller waits on `document.fonts.ready` before drawing.\n */\nexport const drawHtml = async (\n context: CanvasRenderingContext2D,\n html: string,\n options: {width: number; height: number; style?: string},\n): Promise<void> => {\n const {width, height, style = \"\"} = options;\n if (typeof document !== \"undefined\" && document.fonts?.ready) await document.fonts.ready;\n\n const svg = [\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">`,\n `<foreignObject width=\"100%\" height=\"100%\">`,\n `<div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"width:${width}px;height:${height}px;${style}\">`,\n html,\n `</div></foreignObject></svg>`,\n ].join(\"\");\n\n // A data URL rather than a blob URL: a blob URL has to be revoked, and a\n // leak here is a leak once per frame for the length of the video.\n const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n\n await new Promise<void>((done, fail) => {\n const image = new Image();\n image.onload = () => {\n context.drawImage(image, 0, 0, width, height);\n done();\n };\n image.onerror = () =>\n fail(\n new Error(\n \"The HTML could not be rasterized. Every element inside must carry inline styles, and images must be data URLs: an SVG foreignObject cannot reach outside itself.\",\n ),\n );\n image.src = encoded;\n });\n};\n","import {random} from \"./random\";\n\nexport type PlaceholderOptions = {\n width?: number;\n height?: number;\n /** Drawn across the middle, so a fixture says what it is standing in for. */\n label?: string;\n /** Two colours the gradient runs between. */\n from?: string;\n to?: string;\n /** Seed for the scatter, so two placeholders differ without differing runs. */\n seed?: string;\n};\n\n/**\n * A picture that ships as code.\n *\n * A component that shows media needs media to show, and a fixture that ships a\n * JPEG cannot be reviewed in a diff, cannot be recoloured by a brand, and adds\n * a binary to a repository forever. Generating an SVG instead keeps the\n * registry's rule intact — install copies source — and makes the picture do\n * something a file cannot: describe itself.\n *\n * It is deliberately obviously a placeholder. A fixture that looks like real\n * photography invites someone to ship it.\n */\nexport const placeholderSvg = ({\n width = 1600,\n height = 900,\n label,\n from = \"#1a1a1a\",\n to = \"#0a0a0a\",\n seed = \"placeholder\",\n}: PlaceholderOptions = {}): string => {\n const shapes = Array.from({length: 14}, (_, index) => {\n const x = random([seed, \"x\", index]) * width;\n const y = random([seed, \"y\", index]) * height;\n const radius = (random([seed, \"r\", index]) * 0.16 + 0.03) * Math.min(width, height);\n const opacity = (random([seed, \"o\", index]) * 0.06 + 0.02).toFixed(3);\n return `<circle cx=\"${x.toFixed(1)}\" cy=\"${y.toFixed(1)}\" r=\"${radius.toFixed(1)}\" fill=\"#ffffff\" opacity=\"${opacity}\"/>`;\n }).join(\"\");\n\n const caption = label\n ? `<text x=\"50%\" y=\"50%\" fill=\"#ffffff\" fill-opacity=\"0.42\" font-family=\"ui-monospace, monospace\" font-size=\"${Math.round(\n Math.min(width, height) * 0.06,\n )}\" text-anchor=\"middle\" dominant-baseline=\"middle\">${label.replace(/[<>&]/g, \"\")}</text>`\n : \"\";\n\n return [\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">`,\n `<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">`,\n `<stop offset=\"0\" stop-color=\"${from}\"/><stop offset=\"1\" stop-color=\"${to}\"/>`,\n `</linearGradient></defs>`,\n `<rect width=\"${width}\" height=\"${height}\" fill=\"url(#g)\"/>`,\n shapes,\n `<rect x=\"1\" y=\"1\" width=\"${width - 2}\" height=\"${height - 2}\" fill=\"none\" stroke=\"#ffffff\" stroke-opacity=\"0.08\"/>`,\n caption,\n `</svg>`,\n ].join(\"\");\n};\n\n/**\n * The same picture as a data URL, which is what an `<img>` or a canvas draw\n * wants. Inline rather than fetched: a fixture that needs the network is a\n * fixture that fails on a plane, in CI, and in a sandboxed render.\n */\nexport const placeholderImage = (options: PlaceholderOptions = {}): string =>\n `data:image/svg+xml;charset=utf-8,${encodeURIComponent(placeholderSvg(options))}`;\n\n/**\n * A frame of a placeholder \"clip\": the same picture with a moving marker and a\n * timecode, so a component that plays media has something to play that visibly\n * advances and is still a pure function of the frame.\n */\nexport const placeholderFrame = (frame: number, options: PlaceholderOptions & {fps?: number} = {}): string => {\n const {width = 1600, height = 900, fps = 30, ...rest} = options;\n const seconds = frame / fps;\n const timecode = `${String(Math.floor(seconds / 60)).padStart(2, \"0\")}:${String(Math.floor(seconds % 60)).padStart(2, \"0\")}:${String(\n frame % fps,\n ).padStart(2, \"0\")}`;\n\n const base = placeholderSvg({...rest, width, height, label: undefined});\n const progress = (frame % (fps * 4)) / (fps * 4);\n const marker = [\n `<rect x=\"0\" y=\"${height - 12}\" width=\"${(width * progress).toFixed(1)}\" height=\"12\" fill=\"#ffffff\" fill-opacity=\"0.5\"/>`,\n `<text x=\"${width / 2}\" y=\"${height / 2}\" fill=\"#ffffff\" fill-opacity=\"0.5\" font-family=\"ui-monospace, monospace\" font-size=\"${Math.round(\n Math.min(width, height) * 0.08,\n )}\" text-anchor=\"middle\" dominant-baseline=\"middle\">${timecode}</text>`,\n ].join(\"\");\n\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(base.replace(\"</svg>\", `${marker}</svg>`))}`;\n};\n","import {type Duration} from \"./time\";\nimport {type VideoLayout} from \"./layout\";\nimport {type ParsableSchema} from \"./schema\";\n\nexport type VideoMetadata<Input = Record<string, unknown>> = {\n readonly kind: \"odori-video-metadata\";\n id: string;\n title: string;\n description?: string;\n duration?: Duration;\n layout?: VideoLayout;\n schema?: ParsableSchema<Input>;\n defaultProps?: Partial<Input>;\n tags?: string[];\n thumbnailFrame?: number;\n};\n\nexport type VideoMetadataInput<Input = Record<string, unknown>> = Omit<VideoMetadata<Input>, \"kind\" | \"id\"> & {\n /**\n * Defaults to the entry's path under `videos/`, so the directory names a\n * video the way a route names a page. Set it to keep an id stable across a\n * directory move.\n */\n id?: string;\n};\n\n/** A segment of an id: alphanumeric with dashes, the way a directory is named. */\nconst SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;\n\nexport const isValidVideoId = (id: string): boolean =>\n id.length > 0 && id.split(\"/\").every((segment) => SEGMENT.test(segment));\n\n/**\n * An id left unset is resolved from the filesystem by discovery. The empty\n * string is the unresolved state: no entry reaches a manifest, a render, or\n * Studio without an id stamped in.\n */\nexport const resolveVideoId = (id: string | undefined, pathId: string): string => id || pathId;\n\nexport const defineVideoMetadata = <Input = Record<string, unknown>>(\n metadata: VideoMetadataInput<Input>,\n): VideoMetadata<Input> => {\n if (metadata.id !== undefined && !isValidVideoId(metadata.id)) {\n throw new Error(`Video id must be alphanumeric path segments with dashes: ${metadata.id}`);\n }\n return {kind: \"odori-video-metadata\", ...metadata, id: metadata.id ?? \"\"};\n};\n\nexport type PrepareContext<Input> = {\n input: Input;\n assets: {resolve(reference: string): Promise<string>};\n cache: {getOrSet<Value>(key: string, factory: () => Promise<Value>): Promise<Value>};\n signal?: AbortSignal;\n};\n\nexport type PrepareFunction<Input = Record<string, unknown>, Prepared = unknown> = {\n readonly kind: \"odori-prepare\";\n version: string;\n run(context: PrepareContext<Input>): Promise<Prepared>;\n};\n\nexport const definePrepare = <Input = Record<string, unknown>, Prepared = unknown>(\n run: (context: PrepareContext<Input>) => Promise<Prepared>,\n options: {version?: string} = {},\n): PrepareFunction<Input, Prepared> => ({\n kind: \"odori-prepare\",\n version: options.version ?? \"1\",\n run,\n});\n","/**\n * A tiny serializable input contract.\n *\n * Odori needs three things from a schema: validation with defaults, a JSON\n * description Studio can turn into controls, and zero runtime dependencies.\n * Any zod-compatible object with `parse()` is also accepted.\n */\nexport type FieldDescriptor =\n | {type: \"text\"; defaultValue: string; maxLength?: number; multiline?: boolean}\n | {type: \"number\"; defaultValue: number; min?: number; max?: number; step?: number}\n | {type: \"boolean\"; defaultValue: boolean}\n | {type: \"select\"; defaultValue: string; options: string[]}\n | {type: \"color\"; defaultValue: string}\n | {type: \"json\"; defaultValue: unknown};\n\nexport type InputSchema<Value = Record<string, unknown>> = {\n readonly kind: \"odori-schema\";\n readonly fields: Record<string, FieldDescriptor>;\n parse(input: unknown): Value;\n safeParse(input: unknown): {success: true; data: Value} | {success: false; issues: string[]};\n defaults(): Value;\n describe(): Record<string, FieldDescriptor>;\n};\n\nexport type ParsableSchema<Value = unknown> = InputSchema<Value> | {parse(input: unknown): Value};\n\nconst validateField = (name: string, field: FieldDescriptor, value: unknown, issues: string[]): unknown => {\n if (value === undefined) return field.defaultValue;\n switch (field.type) {\n case \"text\":\n case \"color\": {\n if (typeof value !== \"string\") {\n issues.push(`${name} must be a string`);\n return field.defaultValue;\n }\n if (field.type === \"text\" && field.maxLength !== undefined && value.length > field.maxLength) {\n issues.push(`${name} exceeds ${field.maxLength} characters`);\n }\n return value;\n }\n case \"number\": {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n issues.push(`${name} must be a finite number`);\n return field.defaultValue;\n }\n if (field.min !== undefined && value < field.min) issues.push(`${name} is below ${field.min}`);\n if (field.max !== undefined && value > field.max) issues.push(`${name} is above ${field.max}`);\n return value;\n }\n case \"boolean\": {\n if (typeof value !== \"boolean\") {\n issues.push(`${name} must be a boolean`);\n return field.defaultValue;\n }\n return value;\n }\n case \"select\": {\n if (typeof value !== \"string\" || !field.options.includes(value)) {\n issues.push(`${name} must be one of ${field.options.join(\", \")}`);\n return field.defaultValue;\n }\n return value;\n }\n default:\n return value;\n }\n};\n\nexport const defineInputSchema = <Fields extends Record<string, FieldDescriptor>>(\n fields: Fields,\n): InputSchema<Record<string, unknown>> => {\n const defaults = () =>\n Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.defaultValue]));\n\n const safeParse = (input: unknown) => {\n if (input !== undefined && input !== null && typeof input !== \"object\") {\n return {success: false as const, issues: [\"input must be an object\"]};\n }\n const source = (input ?? {}) as Record<string, unknown>;\n const issues: string[] = [];\n const data: Record<string, unknown> = {};\n for (const [name, field] of Object.entries(fields)) {\n data[name] = validateField(name, field, source[name], issues);\n }\n for (const key of Object.keys(source)) {\n if (!(key in fields)) data[key] = source[key];\n }\n return issues.length > 0\n ? {success: false as const, issues}\n : {success: true as const, data};\n };\n\n return {\n kind: \"odori-schema\",\n fields,\n defaults,\n describe: () => fields,\n safeParse,\n parse(input) {\n const result = safeParse(input);\n if (!result.success) throw new Error(`Invalid video input: ${result.issues.join(\"; \")}`);\n return result.data;\n },\n };\n};\n\nexport const isOdoriSchema = (schema: unknown): schema is InputSchema =>\n typeof schema === \"object\" && schema !== null && (schema as {kind?: string}).kind === \"odori-schema\";\n\nexport const parseWithSchema = <Value>(schema: ParsableSchema<Value> | undefined, input: unknown): Value =>\n schema ? schema.parse(input) : ((input ?? {}) as Value);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,IAAM,QAAQ,EAAC,WAAW,mBAAmB,QAAQ,mBAAmB,GAAG,mBAAkB;AAC7F,IAAM,YAAY,EAAC,WAAW,mBAAmB,GAAG,mBAAkB;AAM/D,IAAM,oBAAoB,CAAC,eAA+B;AAC/D,QAAM,YAAY,OAAO,MAAM,SAAS;AACxC,QAAM,QAAS,IAAI,KAAK,KAAK,MAAM,YAAa;AAChD,QAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,IAAI,MAAM;AAC3C,QAAM,MAAM,KAAK,IAAI,KAAK;AAC1B,QAAM,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI;AAC1C,QAAM,KAAK,YAAY,KAAK,YAAY,KAAK,MAAM;AACnD,SAAO;AAAA,IACL,IAAK,aAAa,YAAY,KAAK,YAAY,KAAK,MAAM,UAAW;AAAA,IACrE,IAAK,KAAK,aAAa,YAAY,KAAK,YAAY,KAAK,OAAQ;AAAA,IACjE,IAAK,aAAa,YAAY,KAAK,YAAY,KAAK,MAAM,UAAW;AAAA,IACrE,IAAK,KAAK,YAAY,KAAK,YAAY,KAAK,OAAQ;AAAA,IACpD,KAAK,YAAY,KAAK,YAAY,KAAK,MAAM,UAAU;AAAA,EACzD;AACF;AAEO,IAAM,uBAAuB,CAAC,eAA+B;AAClE,QAAM,QAAS,IAAI,KAAK,KAAK,UAAU,YAAa;AACpD,QAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,IAAI,UAAU;AAC/C,QAAM,MAAM,KAAK,IAAI,KAAK;AAC1B,QAAM,KAAK,IAAI;AACf,SAAO;AAAA,IACL,KAAK,IAAI,OAAO,IAAI;AAAA,IACpB,IAAK,EAAE,IAAI,OAAQ;AAAA,IACnB,KAAK,IAAI,OAAO,IAAI;AAAA,IACpB,IAAK,KAAK,MAAO;AAAA,IACjB,KAAK,IAAI,SAAS;AAAA,EACpB;AACF;AAEA,IAAM,SAAS,CAAC,SAAuB,EAAC,IAAI,IAAI,IAAI,IAAI,GAAE,MAA4B;AACpF,QAAM,SAAS,IAAI,aAAa,QAAQ,MAAM;AAC9C,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,WAAS,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;AACtD,UAAM,KAAK,QAAQ,KAAK;AACxB,UAAM,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK;AACxD,WAAO,KAAK,IAAI;AAChB,SAAK;AACL,SAAK;AACL,SAAK;AACL,SAAK;AAAA,EACP;AACA,SAAO;AACT;AAEA,IAAM,gBAAgB;AAEtB,IAAM,OAAO;AACb,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AACtB,IAAM,SAAS;AAEf,IAAM,aAAa,CAAC,gBAClB,SAAS,KAAK,KAAK,MAAM,YAAY,OAAO,CAAC,OAAO,UAAU,QAAQ,OAAO,CAAC,KAAK,OAAO,SAAS;AAM9F,IAAM,iBAAiB,CAAC,UAA0B,eAAsC;AAC7F,MAAI,SAAS,WAAW,KAAK,SAAS,CAAC,EAAE,WAAW,EAAG,QAAO;AAC9D,QAAM,WAAW,SAAS,IAAI,CAAC,YAAY,OAAO,OAAO,SAAS,kBAAkB,UAAU,CAAC,GAAG,qBAAqB,UAAU,CAAC,CAAC;AAEnI,QAAM,YAAY,KAAK,MAAM,gBAAgB,UAAU;AACvD,QAAM,MAAM,KAAK,MAAM,gBAAgB,OAAO,UAAU;AACxD,MAAI,SAAS,CAAC,EAAE,SAAS,UAAW,QAAO;AAG3C,QAAM,SAAqB,CAAC;AAC5B,WAAS,QAAQ,GAAG,QAAQ,aAAa,SAAS,CAAC,EAAE,QAAQ,SAAS,KAAK;AACzE,WAAO;AAAA,MACL,SAAS,IAAI,CAAC,YAAY;AACxB,YAAI,MAAM;AACV,iBAAS,QAAQ,OAAO,QAAQ,QAAQ,WAAW,SAAS,EAAG,QAAO,QAAQ,KAAK,IAAI,QAAQ,KAAK;AACpG,eAAO,MAAM;AAAA,MACf,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,QAAQ,OAAO,OAAO,CAAC,UAAU,WAAW,KAAK,IAAI,aAAa;AACxE,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,OAAO,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,YAAY,MAAM,OAAO,CAAC,OAAO,UAAU,QAAQ,MAAM,OAAO,GAAG,CAAC,IAAI,MAAM,MAAM;AAClH,QAAM,YAAY,WAAW,IAAI,IAAI;AACrC,QAAM,QAAQ,MAAM,OAAO,CAAC,UAAU,WAAW,KAAK,IAAI,SAAS;AACnE,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,aAAa,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,YAAY,MAAM,OAAO,CAAC,OAAO,UAAU,QAAQ,MAAM,OAAO,GAAG,CAAC,IAAI,MAAM,MAAM;AACxH,SAAO,WAAW,UAAU;AAC9B;;;AC/GA,SAAQ,eAAAA,cAAa,SAAS,YAAAC,iBAAmC;;;ACAjE,SAAQ,aAAa,WAAW,QAAQ,gBAAe;AAoChD,IAAM,gBAAgB,CAAC,YAAoB,SAAiB,KAAa,OAAO,MACrF,aAAc,UAAU,MAAQ,MAAM;AAEjC,IAAM,cAAc,CAAC;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf,WAAW;AAAA,EACX,MAAAC,QAAO;AAAA,EACP,OAAO;AAAA,EACP;AACF,MAAiC;AAC/B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,YAAY;AAC/C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,QAAQ;AAC/C,QAAM,YAAY,OAAsB,IAAI;AAC5C,QAAM,eAAe,OAAsB,IAAI;AAC/C,QAAM,aAAa,OAAO,YAAY;AACtC,QAAM,WAAW,OAAO,YAAY;AAEpC,QAAM,SAAS;AAAA,IACb,CAAC,SAAiB;AAChB,YAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,KAAK,IAAI,GAAG,mBAAmB,CAAC,CAAC,CAAC;AACzF,eAAS,UAAU;AACnB,eAAS,OAAO;AAChB,gBAAU,OAAO;AAAA,IACnB;AAAA,IACA,CAAC,kBAAkB,OAAO;AAAA,EAC5B;AAEA,YAAU,MAAM;AACd,QAAI,CAAC,SAAS;AACZ,mBAAa,UAAU;AACvB,iBAAW,UAAU,SAAS;AAC9B;AAAA,IACF;AACA,UAAM,OAAO,CAAC,QAAgB;AAC5B,YAAM,WAAW,aAAa,WAAW;AACzC,mBAAa,UAAU;AACvB,iBAAW,UAAU,cAAc,WAAW,SAAS,MAAM,UAAU,KAAK,IAAI;AAChF,UAAI,WAAW,WAAW,kBAAkB;AAC1C,YAAI,CAACA,OAAM;AACT,iBAAO,mBAAmB,CAAC;AAC3B,qBAAW,KAAK;AAChB;AAAA,QACF;AACA,mBAAW,WAAW;AAAA,MACxB;AACA,aAAO,KAAK,MAAM,WAAW,OAAO,CAAC;AACrC,gBAAU,UAAU,sBAAsB,IAAI;AAAA,IAChD;AACA,cAAU,UAAU,sBAAsB,IAAI;AAC9C,WAAO,MAAM;AACX,UAAI,UAAU,YAAY,KAAM,sBAAqB,UAAU,OAAO;AAAA,IACxE;AAAA,EACF,GAAG,CAAC,QAAQ,kBAAkB,KAAKA,OAAM,SAAS,IAAI,CAAC;AAEvD,QAAM,OAAO;AAAA,IACX,CAAC,SAAiB;AAChB,iBAAW,UAAU;AACrB,aAAO,IAAI;AAAA,IACb;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,MAAM,WAAW,IAAI;AAAA,IAC3B,OAAO,MAAM,WAAW,KAAK;AAAA,IAC7B,QAAQ,MAAM,WAAW,CAAC,UAAU,CAAC,KAAK;AAAA,IAC1C;AAAA,IACA,MAAM,CAAC,UAAkB;AACvB,iBAAW,KAAK;AAChB,WAAK,SAAS,UAAU,KAAK;AAAA,IAC/B;AAAA,IACA,SAAS,MAAM;AACb,WAAK,CAAC;AACN,iBAAW,IAAI;AAAA,IACjB;AAAA,EACF;AACF;;;ACrHA,SAAQ,aAAAC,YAAW,UAAAC,eAAa;AAsCzB,IAAM,mBAAmB,CAAC;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,OAAO;AAAA,EACP;AAAA,EACA;AACF,MAA4B;AAC1B,QAAM,WAAWC,QAAO,oBAAI,IAA8B,CAAC;AAI3D,QAAM,OAAOA,QAAmB,MAAM;AAAA,EAAC,CAAC;AACxC,QAAM,SAASA,QAAO,oBAAI,IAAY,CAAC;AAEvC,EAAAC,WAAU,MAAM;AACd,UAAM,QAAQ,SAAS;AACvB,UAAM,OAAO,IAAI,KAAK,OAAO,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;AAC7D,eAAW,CAAC,IAAI,OAAO,KAAK,OAAO;AACjC,UAAI,KAAK,IAAI,EAAE,EAAG;AAClB,cAAQ,MAAM;AACd,YAAM,OAAO,EAAE;AAAA,IACjB;AACA,WAAO,MAAM;AACX,iBAAW,WAAW,MAAM,OAAO,EAAG,SAAQ,MAAM;AAAA,IACtD;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AAEV,EAAAA,WAAU,MAAM;AACd,QAAI,OAAO,WAAW,YAAa;AACnC,UAAM,QAAQ,SAAS;AAEvB,UAAM,eAAe,MAAM,IAAI;AAE/B,UAAM,MAAM,MAAM;AAGhB,YAAM,UAAU,WAAW;AAE3B,iBAAW,OAAO,OAAO,QAAQ,CAAC,GAAG;AACnC,YAAI,UAAU,MAAM,IAAI,IAAI,EAAE;AAC9B,YAAI,CAAC,SAAS;AACZ,oBAAU,IAAI,OAAO,MAAM,IAAI,GAAG;AAClC,kBAAQ,UAAU;AAClB,kBAAQ,OAAO,IAAI;AAInB,kBAAQ,iBAAiB,cAAc,YAAY;AACnD,kBAAQ,iBAAiB,SAAS,MAAM;AACtC,mBAAO,QAAQ,IAAI,IAAI,GAAG;AAC1B,uBAAW,CAAC,GAAG,OAAO,OAAO,CAAC;AAAA,UAChC,CAAC;AACD,gBAAM,IAAI,IAAI,IAAI,OAAO;AAAA,QAC3B;AAEA,cAAM,QAAQ,QAAQ,IAAI;AAC1B,cAAM,SAAS,SAAS,KAAK,QAAQ,IAAI;AAQzC,cAAM,OAAO,QAAQ;AACrB,cAAM,UAAU,QAAQ;AACxB,cAAM,WACJ,IAAI,QAAQ,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI,UAAU,OAAO;AAInE,YAAI,QAAQ,SAAS,IAAI,KAAM,SAAQ,OAAO,IAAI;AAClD,gBAAQ,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,iBAAiB,KAAK,OAAO,QAAQ,CAAC,GAAG,KAAK,IAAI,UAAU,CAAC;AACtG,gBAAQ,QAAQ,SAAU,YAAY,QAAQ,YAAY,IAAI;AAE9D,YAAI,CAAC,UAAU,CAAC,SAAS;AACvB,cAAI,CAAC,QAAQ,OAAQ,SAAQ,MAAM;AACnC,cAAI,UAAU,CAAC,SAAS;AACtB,kBAAMC,UAAS,IAAI,mBAAmB;AACtC,gBAAI,KAAK,IAAI,QAAQ,cAAcA,OAAM,IAAI,IAAI,IAAK,SAAQ,cAAcA;AAAA,UAC9E;AACA;AAAA,QACF;AAEA,cAAM,SAAS,IAAI,mBAAmB;AAGtC,YAAI,QAAQ,iBAAiB,KAAM,SAAQ,eAAe;AAC1D,YAAI,KAAK,IAAI,QAAQ,cAAc,MAAM,IAAK,IAAI,MAAO,KAAK,IAAI,GAAG,IAAI,EAAG,SAAQ,cAAc;AAClG,YAAI,QAAQ,QAAQ;AAClB,eAAK,QAAQ,KAAK,EAAE;AAAA,YAClB,MAAM,YAAY,KAAK;AAAA,YACvB,CAAC,UAAmB,YAAa,OAAiB,SAAS,iBAAiB;AAAA,UAC9E;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,SAAK,UAAU;AACf,QAAI;AAEJ,WAAO,MAAM;AACX,iBAAW,WAAW,MAAM,OAAO,EAAG,SAAQ,oBAAoB,cAAc,YAAY;AAAA,IAC9F;AAAA,EACF,GAAG,CAAC,KAAK,OAAO,YAAY,OAAO,WAAW,UAAU,SAAS,MAAM,WAAW,SAAS,KAAK,CAAC;AAIjG,EAAAD,WAAU,MAAM;AACd,QAAI,OAAO,aAAa,YAAa;AACrC,UAAM,eAAe,MAAM;AACzB,UAAI,SAAS,QAAQ;AACnB,mBAAW,WAAW,SAAS,QAAQ,OAAO,EAAG,SAAQ,MAAM;AAC/D;AAAA,MACF;AACA,WAAK,QAAQ;AAAA,IACf;AACA,aAAS,iBAAiB,oBAAoB,YAAY;AAC1D,WAAO,MAAM,SAAS,oBAAoB,oBAAoB,YAAY;AAAA,EAC5E,GAAG,CAAC,CAAC;AACP;;;AFrEQ,cAkCE,YAlCF;AAjED,IAAM,SAAS,CAAC;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf,WAAW;AAAA,EACX,MAAAE,QAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAmB;AACjB,QAAM,iBAAiB,mBAAmB,OAAO,MAAM;AACvD,QAAM,EAAC,KAAK,OAAO,OAAM,IAAI,eAAe;AAC5C,QAAM,CAAC,UAAU,WAAW,IAAIC,UAAkC,IAAI;AACtE,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAA4B,IAAI;AAC1D,QAAM,WAAW,sBAAsB,OAAO,cAAc;AAC5D,QAAM,mBAAmB,KAAK,IAAI,GAAG,YAAY,UAAU,oBAAoB,GAAG;AAElF,QAAM,WAAW,YAAY,EAAC,KAAK,kBAAkB,cAAc,UAAU,MAAAD,OAAM,QAAO,CAAC;AAC3F,QAAM,EAAC,MAAK,IAAI;AAEhB,mBAAiB,EAAC,OAAO,OAAO,SAAS,OAAO,KAAK,SAAS,SAAS,SAAS,MAAK,CAAC;AAEtF,QAAM,cAAcE;AAAA,IAClB,CAAC,SAAqB;AACpB,eAAS,IAAI;AACb,gBAAU,IAAI;AAAA,IAChB;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAEA,QAAM,iBAAiBA;AAAA,IACrB,CAAC,SAA2B;AAI1B,kBAAY,CAAC,YAAa,KAAK,UAAU,OAAO,MAAM,KAAK,UAAU,IAAI,IAAI,UAAU,IAAK;AAC5F,mBAAa,IAAI;AAAA,IACnB;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,cAAc,QAAQ,MAAM,GAAG,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC;AACzE,QAAM,cAAc,UAAU,OAAO;AAAA,IACnC,CAAC,UAAU,SAAS,MAAM,SAAS,QAAQ,MAAM,QAAQ,MAAM;AAAA,EACjE;AAEA,SACE,qBAAC,SAAI,WAAU,gBAAe,OAAO,EAAC,SAAS,QAAQ,KAAK,IAAI,OAAO,QAAQ,GAAG,MAAK,GACrF;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,qBAAiB;AAAA,QACjB,OAAO;AAAA,UACL;AAAA,UACA,YAAY,eAAe,MAAM,OAAO;AAAA,UACxC,cAAc;AAAA,UACd,UAAU;AAAA,UACV,UAAU;AAAA,UACV,OAAO;AAAA,QACT;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,YAAY;AAAA,YACZ,SAAS;AAAA;AAAA,QACX;AAAA;AAAA,IACF;AAAA,IACC,WACC,qBAAC,SAAI,WAAU,yBAAwB,OAAO,EAAC,YAAY,UAAU,SAAS,QAAQ,KAAK,GAAE,GAC3F;AAAA,0BAAC,YAAO,MAAK,UAAS,SAAS,SAAS,QACrC,mBAAS,UAAU,UAAU,QAChC;AAAA,MACA,oBAAC,YAAO,MAAK,UAAS,SAAS,MAAM,SAAS,KAAK,EAAE,GAAG,cAAW,kBAChE,oBACH;AAAA,MACA,oBAAC,YAAO,MAAK,UAAS,SAAS,MAAM,SAAS,KAAK,CAAC,GAAG,cAAW,cAC/D,oBACH;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,cAAW;AAAA,UACX,MAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK,mBAAmB;AAAA,UACxB,OAAO;AAAA,UACP,UAAU,CAAC,UAAU;AACnB,qBAAS,MAAM;AACf,qBAAS,KAAK,OAAO,MAAM,cAAc,KAAK,CAAC;AAAA,UACjD;AAAA,UACA,OAAO,EAAC,MAAM,EAAC;AAAA;AAAA,MACjB;AAAA,MACA,qBAAC,YAAO,OAAO,EAAC,oBAAoB,gBAAgB,UAAU,KAAK,WAAW,QAAO,GAClF;AAAA,uBAAe,OAAO,GAAG;AAAA,QAAE;AAAA,QAAE;AAAA,QAAI;AAAA,QAAE;AAAA,QAAM;AAAA,QAAE,mBAAmB;AAAA,QAC9D,cAAc,SAAM,YAAY,QAAQ,YAAY,EAAE,KAAK;AAAA,SAC9D;AAAA,OACF,IACE;AAAA,KACN;AAEJ;;;AGzIA,SAAQ,aAAAC,YAAW,YAAAC,iBAAe;AA6C9B,gBAAAC,YAAA;AA3BG,IAAM,gBAAgB,CAAC;AAAA,EAC5B;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAOM;AACJ,QAAM,CAAC,OAAO,QAAQ,IAAIC,UAAS,YAAY;AAE/C,EAAAC,WAAU,MAAM;AACd,WAAO,sBAAsB;AAC7B,WAAO,kBAAkB;AACzB,WAAO,MAAM;AACX,aAAO,OAAO;AACd,aAAO,OAAO;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SACE,gBAAAF;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAY,CAAC,aAAa;AACxB,eAAO,qBAAqB;AAAA,MAC9B;AAAA,MACA,SAAS,CAAC,UAAU;AAClB,eAAO,kBAAkB;AAAA,MAC3B;AAAA;AAAA,EACF;AAEJ;;;ACpDO,IAAM,YAAY,CAAC,WAA+B;AACvD,QAAM,WAAW,OAAO,SAAS,UAAU;AAC3C,QAAM,SAAS,OAAO,SAAS,CAAC,GAAG,UAAU;AAC7C,QAAM,iBAAiB;AACvB,QAAM,YAAY,SAAS,WAAW;AACtC,QAAM,SAAS,IAAI,YAAY,KAAK,SAAS;AAC7C,QAAM,OAAO,IAAI,SAAS,MAAM;AAEhC,QAAM,QAAQ,CAACG,SAAgB,SAAiB;AAC9C,aAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,EAAG,MAAK,SAASA,UAAS,OAAO,KAAK,WAAW,KAAK,CAAC;AAAA,EAC3G;AAEA,QAAM,GAAG,MAAM;AACf,OAAK,UAAU,GAAG,KAAK,WAAW,IAAI;AACtC,QAAM,GAAG,MAAM;AACf,QAAM,IAAI,MAAM;AAChB,OAAK,UAAU,IAAI,IAAI,IAAI;AAC3B,OAAK,UAAU,IAAI,GAAG,IAAI;AAC1B,OAAK,UAAU,IAAI,UAAU,IAAI;AACjC,OAAK,UAAU,IAAI,OAAO,YAAY,IAAI;AAC1C,OAAK,UAAU,IAAI,OAAO,aAAa,WAAW,gBAAgB,IAAI;AACtE,OAAK,UAAU,IAAI,WAAW,gBAAgB,IAAI;AAClD,OAAK,UAAU,IAAI,IAAI,gBAAgB,IAAI;AAC3C,QAAM,IAAI,MAAM;AAChB,OAAK,UAAU,IAAI,WAAW,IAAI;AAElC,MAAI,SAAS;AACb,WAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS,GAAG;AAC9C,aAAS,UAAU,GAAG,UAAU,UAAU,WAAW,GAAG;AACtD,YAAM,SAAS,OAAO,SAAS,OAAO,IAAI,KAAK,KAAK;AAGpD,YAAM,UAAU,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,MAAM,CAAC;AAChD,WAAK,SAAS,QAAQ,KAAK,MAAM,UAAU,KAAK,GAAG,IAAI;AACvD,gBAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO,IAAI,WAAW,MAAM;AAC9B;;;ACnBA,IAAM,eAAe;AAcd,IAAM,WAAW,CAAC,OAAqB,UAA+B;AAC3E,MAAI,MAAM,WAAW,EAAG,QAAO,EAAC,GAAG,GAAG,GAAG,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,MAAK;AAGnF,QAAM,QAAsB,CAAC;AAC7B,MAAI,QAAQ;AACZ,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,KAAK,QAAQ;AAC3B,UAAM,KAAK,EAAC,GAAG,MAAM,OAAO,MAAK,CAAC;AAClC,QAAI,KAAK,MAAM;AACb,YAAM,KAAK,EAAC,GAAG,MAAM,OAAO,QAAQ,KAAK,MAAM,OAAO,MAAK,CAAC;AAC5D,eAAS,KAAK;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,CAAC;AACrB,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,MAAI,SAAS,MAAM,MAAO,QAAO,EAAC,GAAG,MAAM,GAAG,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,MAAK;AAEjG,QAAM,SAAS,MAAM,IAAI,CAAC,SAAS,KAAK,KAAK;AAC7C,QAAM,IAAI,YAAY,OAAO,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,CAAC,GAAG,EAAC,QAAQ,OAAO,SAAQ,CAAC;AAC3F,QAAM,IAAI,YAAY,OAAO,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,CAAC,GAAG,EAAC,QAAQ,OAAO,SAAQ,CAAC;AAG3F,MAAI,UAAU;AACd,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,SAAS,KAAK,QAAQ,MAAO;AACvC,UAAM,MAAM,QAAQ,KAAK;AACzB,QAAI,OAAO,aAAc,WAAU,KAAK,IAAI,SAAS,IAAI,MAAM,YAAY;AAAA,EAC7E;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,SAAS;AAAA,MACP;AAAA,MACA,CAAC,MAAM,OAAO,MAAM,QAAQ,GAAG,KAAK,QAAQ,IAAI,KAAK,QAAQ,EAAE;AAAA,MAC/D,CAAC,GAAG,GAAG,GAAG,CAAC;AAAA,MACX,EAAC,QAAQ,OAAO,SAAQ;AAAA,IAC1B;AAAA,IACA;AAAA,IACA,UAAU,UAAU;AAAA,EACtB;AACF;AAGO,IAAM,iBAAiB,CAAC,UAAgC;AAC7D,QAAM,OAAO,MAAM,OAAO,CAAC,OAAO,SAAS,SAAS,KAAK,QAAQ,IAAI,CAAC;AACtE,UAAQ,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,KAAK,OAAO;AACxD;;;AC5EA,IAAM,WAAW,CAAC,UAA0B;AAC1C,MAAI,OAAO;AACX,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG;AACpD,YAAQ,MAAM,WAAW,KAAK;AAC9B,WAAO,KAAK,KAAK,MAAM,QAAU;AAAA,EACnC;AACA,SAAO,SAAS;AAClB;AAEA,IAAM,SAAS,CAAC,SACd,OAAO,SAAS,WAAW,KAAK,MAAM,IAAI,MAAM,IAAI,SAAS,IAAI;AAc5D,IAAM,SAAS,CAAC,SAA2D;AAChF,QAAM,MAAM,MAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,GAAG,IAAI;AAGnD,MAAI,QAAS,OAAO,GAAG,IAAI,eAAgB;AAC3C,MAAI,IAAI,KAAK,KAAK,QAAS,UAAU,IAAK,IAAI,KAAK;AACnD,MAAK,IAAI,KAAK,KAAK,IAAK,MAAM,GAAI,KAAK,CAAC,IAAK;AAC7C,WAAS,IAAK,MAAM,QAAS,KAAK;AACpC;AAGO,IAAM,gBAAgB,CAAC,MAAgD,KAAa,QACzF,MAAM,OAAO,IAAI,KAAK,MAAM;AAGvB,IAAM,aAAa,CAAK,MAAgD,UAC7E,MAAM,WAAW,IAAI,SAAY,MAAM,KAAK,MAAM,OAAO,IAAI,IAAI,MAAM,MAAM,CAAC;AAOzE,IAAM,cAAc,CAAK,MAAgD,UAA6B;AAC3G,QAAM,MAAM,MAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,GAAG,IAAI,OAAO,IAAI;AAC9D,QAAM,MAAM,CAAC,GAAG,KAAK;AACrB,WAAS,QAAQ,IAAI,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG;AACtD,UAAM,OAAO,KAAK,MAAM,OAAO,GAAG,GAAG,IAAI,KAAK,EAAE,KAAK,QAAQ,EAAE;AAC/D,KAAC,IAAI,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,KAAK,CAAC;AAAA,EAClD;AACA,SAAO;AACT;;;ACxEA,SAAQ,aAAAC,YAAW,iBAAiB,UAAAC,eAA6B;AAMjE,IAAM,4BAA4B,OAAO,WAAW,cAAcC,aAAY;AAgBvE,IAAM,YAAY,CAAC,MAAkB,eAAmC,CAAC,MAA2C;AACzH,QAAM,SAASC,QAAiC,IAAI;AACpD,QAAM,QAAQ,SAAS;AACvB,QAAM,EAAC,OAAO,OAAM,IAAI,SAAS;AACjC,QAAM,YAAY,aAAa;AAG/B,QAAM,SAASA,QAAO,IAAI;AAC1B,SAAO,UAAU;AAEjB,4BAA0B,MAAM;AAC9B,UAAM,UAAU,OAAO;AACvB,QAAI,CAAC,QAAS;AAGd,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI;AACF,YAAM,UAAU,QAAQ,WAAW,MAAM,EAAC,OAAO,KAAI,CAAC;AACtD,UAAI,CAAC,QAAS;AAId,cAAQ,aAAa,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AACrC,cAAQ,UAAU,GAAG,GAAG,QAAQ,OAAO,QAAQ,MAAM;AACrD,aAAO,QAAQ,SAAS,EAAC,OAAO,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAM,CAAC;AAAA,IAC/E,UAAE;AACA,cAAQ;AAAA,IACV;AAAA,EAEF,GAAG,CAAC,OAAO,OAAO,QAAQ,GAAG,YAAY,CAAC;AAE1C,SAAO;AACT;AAeO,IAAM,WAAW,OACtB,SACA,MACA,YACkB;AAClB,QAAM,EAAC,OAAO,QAAQ,QAAQ,GAAE,IAAI;AACpC,MAAI,OAAO,aAAa,eAAe,SAAS,OAAO,MAAO,OAAM,SAAS,MAAM;AAEnF,QAAM,MAAM;AAAA,IACV,kDAAkD,KAAK,aAAa,MAAM;AAAA,IAC1E;AAAA,IACA,0DAA0D,KAAK,aAAa,MAAM,MAAM,KAAK;AAAA,IAC7F;AAAA,IACA;AAAA,EACF,EAAE,KAAK,EAAE;AAIT,QAAM,UAAU,oCAAoC,mBAAmB,GAAG,CAAC;AAE3E,QAAM,IAAI,QAAc,CAAC,MAAM,SAAS;AACtC,UAAM,QAAQ,IAAI,MAAM;AACxB,UAAM,SAAS,MAAM;AACnB,cAAQ,UAAU,OAAO,GAAG,GAAG,OAAO,MAAM;AAC5C,WAAK;AAAA,IACP;AACA,UAAM,UAAU,MACd;AAAA,MACE,IAAI;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACF,UAAM,MAAM;AAAA,EACd,CAAC;AACH;;;AC/EO,IAAM,iBAAiB,CAAC;AAAA,EAC7B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT;AAAA,EACA,OAAO;AAAA,EACP,KAAK;AAAA,EACL,OAAO;AACT,IAAwB,CAAC,MAAc;AACrC,QAAM,SAAS,MAAM,KAAK,EAAC,QAAQ,GAAE,GAAG,CAAC,GAAG,UAAU;AACpD,UAAM,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI;AACvC,UAAM,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI;AACvC,UAAM,UAAU,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI,OAAO,QAAQ,KAAK,IAAI,OAAO,MAAM;AAClF,UAAM,WAAW,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI,OAAO,MAAM,QAAQ,CAAC;AACpE,WAAO,eAAe,EAAE,QAAQ,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,QAAQ,OAAO,QAAQ,CAAC,CAAC,6BAA6B,OAAO;AAAA,EACtH,CAAC,EAAE,KAAK,EAAE;AAEV,QAAM,UAAU,QACZ,6GAA6G,KAAK;AAAA,IAChH,KAAK,IAAI,OAAO,MAAM,IAAI;AAAA,EAC5B,CAAC,qDAAqD,MAAM,QAAQ,UAAU,EAAE,CAAC,YACjF;AAEJ,SAAO;AAAA,IACL,kDAAkD,KAAK,aAAa,MAAM,kBAAkB,KAAK,IAAI,MAAM;AAAA,IAC3G;AAAA,IACA,gCAAgC,IAAI,mCAAmC,EAAE;AAAA,IACzE;AAAA,IACA,gBAAgB,KAAK,aAAa,MAAM;AAAA,IACxC;AAAA,IACA,4BAA4B,QAAQ,CAAC,aAAa,SAAS,CAAC;AAAA,IAC5D;AAAA,IACA;AAAA,EACF,EAAE,KAAK,EAAE;AACX;AAOO,IAAM,mBAAmB,CAAC,UAA8B,CAAC,MAC9D,oCAAoC,mBAAmB,eAAe,OAAO,CAAC,CAAC;AAO1E,IAAM,mBAAmB,CAAC,OAAe,UAA+C,CAAC,MAAc;AAC5G,QAAM,EAAC,QAAQ,MAAM,SAAS,KAAK,MAAM,IAAI,GAAG,KAAI,IAAI;AACxD,QAAMC,WAAU,QAAQ;AACxB,QAAM,WAAW,GAAG,OAAO,KAAK,MAAMA,WAAU,EAAE,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,KAAK,MAAMA,WAAU,EAAE,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI;AAAA,IAC5H,QAAQ;AAAA,EACV,EAAE,SAAS,GAAG,GAAG,CAAC;AAElB,QAAM,OAAO,eAAe,EAAC,GAAG,MAAM,OAAO,QAAQ,OAAO,OAAS,CAAC;AACtE,QAAM,WAAY,SAAS,MAAM,MAAO,MAAM;AAC9C,QAAM,SAAS;AAAA,IACb,kBAAkB,SAAS,EAAE,aAAa,QAAQ,UAAU,QAAQ,CAAC,CAAC;AAAA,IACtE,YAAY,QAAQ,CAAC,QAAQ,SAAS,CAAC,wFAAwF,KAAK;AAAA,MAClI,KAAK,IAAI,OAAO,MAAM,IAAI;AAAA,IAC5B,CAAC,qDAAqD,QAAQ;AAAA,EAChE,EAAE,KAAK,EAAE;AAET,SAAO,oCAAoC,mBAAmB,KAAK,QAAQ,UAAU,GAAG,MAAM,QAAQ,CAAC,CAAC;AAC1G;;;AChEA,IAAM,UAAU;AAET,IAAM,iBAAiB,CAAC,OAC7B,GAAG,SAAS,KAAK,GAAG,MAAM,GAAG,EAAE,MAAM,CAAC,YAAY,QAAQ,KAAK,OAAO,CAAC;AAOlE,IAAM,iBAAiB,CAAC,IAAwB,WAA2B,MAAM;AAEjF,IAAM,sBAAsB,CACjC,aACyB;AACzB,MAAI,SAAS,OAAO,UAAa,CAAC,eAAe,SAAS,EAAE,GAAG;AAC7D,UAAM,IAAI,MAAM,4DAA4D,SAAS,EAAE,EAAE;AAAA,EAC3F;AACA,SAAO,EAAC,MAAM,wBAAwB,GAAG,UAAU,IAAI,SAAS,MAAM,GAAE;AAC1E;AAeO,IAAM,gBAAgB,CAC3B,KACA,UAA8B,CAAC,OACO;AAAA,EACtC,MAAM;AAAA,EACN,SAAS,QAAQ,WAAW;AAAA,EAC5B;AACF;;;AC1CA,IAAM,gBAAgB,CAAC,MAAc,OAAwB,OAAgB,WAA8B;AACzG,MAAI,UAAU,OAAW,QAAO,MAAM;AACtC,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AAAA,IACL,KAAK,SAAS;AACZ,UAAI,OAAO,UAAU,UAAU;AAC7B,eAAO,KAAK,GAAG,IAAI,mBAAmB;AACtC,eAAO,MAAM;AAAA,MACf;AACA,UAAI,MAAM,SAAS,UAAU,MAAM,cAAc,UAAa,MAAM,SAAS,MAAM,WAAW;AAC5F,eAAO,KAAK,GAAG,IAAI,YAAY,MAAM,SAAS,aAAa;AAAA,MAC7D;AACA,aAAO;AAAA,IACT;AAAA,IACA,KAAK,UAAU;AACb,UAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GAAG;AACxD,eAAO,KAAK,GAAG,IAAI,0BAA0B;AAC7C,eAAO,MAAM;AAAA,MACf;AACA,UAAI,MAAM,QAAQ,UAAa,QAAQ,MAAM,IAAK,QAAO,KAAK,GAAG,IAAI,aAAa,MAAM,GAAG,EAAE;AAC7F,UAAI,MAAM,QAAQ,UAAa,QAAQ,MAAM,IAAK,QAAO,KAAK,GAAG,IAAI,aAAa,MAAM,GAAG,EAAE;AAC7F,aAAO;AAAA,IACT;AAAA,IACA,KAAK,WAAW;AACd,UAAI,OAAO,UAAU,WAAW;AAC9B,eAAO,KAAK,GAAG,IAAI,oBAAoB;AACvC,eAAO,MAAM;AAAA,MACf;AACA,aAAO;AAAA,IACT;AAAA,IACA,KAAK,UAAU;AACb,UAAI,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,SAAS,KAAK,GAAG;AAC/D,eAAO,KAAK,GAAG,IAAI,mBAAmB,MAAM,QAAQ,KAAK,IAAI,CAAC,EAAE;AAChE,eAAO,MAAM;AAAA,MACf;AACA,aAAO;AAAA,IACT;AAAA,IACA;AACE,aAAO;AAAA,EACX;AACF;AAEO,IAAM,oBAAoB,CAC/B,WACyC;AACzC,QAAM,WAAW,MACf,OAAO,YAAY,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,MAAM,YAAY,CAAC,CAAC;AAE9F,QAAM,YAAY,CAAC,UAAmB;AACpC,QAAI,UAAU,UAAa,UAAU,QAAQ,OAAO,UAAU,UAAU;AACtE,aAAO,EAAC,SAAS,OAAgB,QAAQ,CAAC,yBAAyB,EAAC;AAAA,IACtE;AACA,UAAM,SAAU,SAAS,CAAC;AAC1B,UAAM,SAAmB,CAAC;AAC1B,UAAM,OAAgC,CAAC;AACvC,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAClD,WAAK,IAAI,IAAI,cAAc,MAAM,OAAO,OAAO,IAAI,GAAG,MAAM;AAAA,IAC9D;AACA,eAAW,OAAO,OAAO,KAAK,MAAM,GAAG;AACrC,UAAI,EAAE,OAAO,QAAS,MAAK,GAAG,IAAI,OAAO,GAAG;AAAA,IAC9C;AACA,WAAO,OAAO,SAAS,IACnB,EAAC,SAAS,OAAgB,OAAM,IAChC,EAAC,SAAS,MAAe,KAAI;AAAA,EACnC;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,UAAU,MAAM;AAAA,IAChB;AAAA,IACA,MAAM,OAAO;AACX,YAAM,SAAS,UAAU,KAAK;AAC9B,UAAI,CAAC,OAAO,QAAS,OAAM,IAAI,MAAM,wBAAwB,OAAO,OAAO,KAAK,IAAI,CAAC,EAAE;AACvF,aAAO,OAAO;AAAA,IAChB;AAAA,EACF;AACF;AAEO,IAAM,gBAAgB,CAAC,WAC5B,OAAO,WAAW,YAAY,WAAW,QAAS,OAA2B,SAAS;AAEjF,IAAM,kBAAkB,CAAQ,QAA2C,UAChF,SAAS,OAAO,MAAM,KAAK,IAAM,SAAS,CAAC;","names":["useCallback","useState","loop","useEffect","useRef","useRef","useEffect","target","loop","useState","useCallback","useEffect","useState","jsx","useState","useEffect","offset","useEffect","useRef","useEffect","useRef","seconds"]}
1
+ {"version":3,"sources":["../src/loudness.ts","../src/viewer.tsx","../src/playback.ts","../src/audio-playback.ts","../src/render-surface.tsx","../src/wav.ts","../src/cursor.ts","../src/typing.ts","../src/random.ts","../src/canvas.ts","../src/placeholder.ts","../src/metadata.ts","../src/schema.ts"],"sourcesContent":["/**\n * Integrated loudness, ITU-R BS.1770-4.\n *\n * The number a mix is judged by is not peak or RMS: it is K-weighted, gated\n * loudness. Studio measures what it is about to hand the encoder so the\n * brand's target is something you can mix toward rather than discover after an\n * export.\n */\n\nexport type Biquad = {b0: number; b1: number; b2: number; a1: number; a2: number};\n\n/** Stage 1: the head shelf, and stage 2: the high pass, from the spec's filter table. */\nconst SHELF = {frequency: 1681.974450955533, gainDb: 3.999843853973347, q: 0.7071752369554196};\nconst HIGH_PASS = {frequency: 38.13547087602444, q: 0.5003270373238773};\n\n/**\n * The spec tabulates coefficients at 48 kHz. Deriving them per rate keeps a\n * 44.1 kHz source from being measured with the wrong filter.\n */\nexport const shelfCoefficients = (sampleRate: number): Biquad => {\n const amplitude = 10 ** (SHELF.gainDb / 40);\n const omega = (2 * Math.PI * SHELF.frequency) / sampleRate;\n const alpha = Math.sin(omega) / (2 * SHELF.q);\n const cos = Math.cos(omega);\n const shared = 2 * Math.sqrt(amplitude) * alpha;\n const a0 = amplitude + 1 - (amplitude - 1) * cos + shared;\n return {\n b0: (amplitude * (amplitude + 1 + (amplitude - 1) * cos + shared)) / a0,\n b1: (-2 * amplitude * (amplitude - 1 + (amplitude + 1) * cos)) / a0,\n b2: (amplitude * (amplitude + 1 + (amplitude - 1) * cos - shared)) / a0,\n a1: (2 * (amplitude - 1 - (amplitude + 1) * cos)) / a0,\n a2: (amplitude + 1 - (amplitude - 1) * cos - shared) / a0,\n };\n};\n\nexport const highPassCoefficients = (sampleRate: number): Biquad => {\n const omega = (2 * Math.PI * HIGH_PASS.frequency) / sampleRate;\n const alpha = Math.sin(omega) / (2 * HIGH_PASS.q);\n const cos = Math.cos(omega);\n const a0 = 1 + alpha;\n return {\n b0: (1 + cos) / 2 / a0,\n b1: (-(1 + cos)) / a0,\n b2: (1 + cos) / 2 / a0,\n a1: (-2 * cos) / a0,\n a2: (1 - alpha) / a0,\n };\n};\n\nconst filter = (samples: Float32Array, {b0, b1, b2, a1, a2}: Biquad): Float32Array => {\n const output = new Float32Array(samples.length);\n let x1 = 0;\n let x2 = 0;\n let y1 = 0;\n let y2 = 0;\n for (let index = 0; index < samples.length; index += 1) {\n const x0 = samples[index];\n const y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;\n output[index] = y0;\n x2 = x1;\n x1 = x0;\n y2 = y1;\n y1 = y0;\n }\n return output;\n};\n\nconst BLOCK_SECONDS = 0.4;\n/** Blocks overlap by 75%, so a short transient cannot hide between them. */\nconst STEP = 0.25;\nconst ABSOLUTE_GATE = -70;\nconst RELATIVE_GATE = -10;\nconst OFFSET = -0.691;\n\nconst loudnessOf = (meanSquares: number[]) =>\n OFFSET + 10 * Math.log10(meanSquares.reduce((total, value) => total + value, 0) || Number.MIN_VALUE);\n\n/**\n * Channel weights for stereo. Surround weights the surround channels higher;\n * a video mix is stereo, so both channels count equally.\n */\nexport const integratedLufs = (channels: Float32Array[], sampleRate: number): number | null => {\n if (channels.length === 0 || channels[0].length === 0) return null;\n const weighted = channels.map((channel) => filter(filter(channel, shelfCoefficients(sampleRate)), highPassCoefficients(sampleRate)));\n\n const blockSize = Math.round(BLOCK_SECONDS * sampleRate);\n const hop = Math.round(BLOCK_SECONDS * STEP * sampleRate);\n if (weighted[0].length < blockSize) return null;\n\n // One mean square per channel per block, kept apart so gating can sum them.\n const blocks: number[][] = [];\n for (let start = 0; start + blockSize <= weighted[0].length; start += hop) {\n blocks.push(\n weighted.map((channel) => {\n let sum = 0;\n for (let index = start; index < start + blockSize; index += 1) sum += channel[index] * channel[index];\n return sum / blockSize;\n }),\n );\n }\n if (blocks.length === 0) return null;\n\n const above = blocks.filter((block) => loudnessOf(block) > ABSOLUTE_GATE);\n if (above.length === 0) return null;\n\n // The relative gate is measured against the ungated mean of what survived.\n const mean = above[0].map((_, channel) => above.reduce((total, block) => total + block[channel], 0) / above.length);\n const threshold = loudnessOf(mean) + RELATIVE_GATE;\n const gated = above.filter((block) => loudnessOf(block) > threshold);\n if (gated.length === 0) return null;\n\n const integrated = gated[0].map((_, channel) => gated.reduce((total, block) => total + block[channel], 0) / gated.length);\n return loudnessOf(integrated);\n};\n","\"use client\";\n\nimport {useCallback, useMemo, useState, type CSSProperties} from \"react\";\nimport {OdoriRuntime, entryDurationInFrames, resolveEntryLayout, type CompiledTimeline, type VideoEntry} from \"./runtime\";\nimport {type VideoLayout} from \"./layout\";\nimport {formatTimecode} from \"./time\";\nimport {usePlayback} from \"./playback\";\nimport {useAudioPlayback} from \"./audio-playback\";\nimport {type AudioTrack} from \"./audio\";\n\nexport type ViewerProps = {\n entry: VideoEntry;\n input?: Record<string, unknown>;\n prepared?: unknown;\n assets?: Array<{reference: string; url: string}>;\n layout?: VideoLayout;\n initialFrame?: number;\n autoPlay?: boolean;\n loop?: boolean;\n controls?: boolean;\n style?: CSSProperties;\n muted?: boolean;\n onFrame?: (frame: number) => void;\n onTimeline?: (timeline: CompiledTimeline) => void;\n onAudio?: (track: AudioTrack) => void;\n};\n\n/**\n * A composition, embeddable and seekable, for a product surface rather than\n * for Studio.\n *\n * Playback advances a fractional frame counter from wall-clock deltas, but\n * React only ever sees an integer frame, so a paused viewer and a render\n * worker produce identical output: what somebody watches in your app is the\n * file you would export.\n *\n * The controls here are the plain ones. A surface that wants its own transport\n * imports `usePlayback` instead and keeps this out of it, which is what Studio\n * and the documentation site both do.\n */\nexport const Viewer = ({\n entry,\n input,\n prepared,\n assets,\n layout,\n initialFrame = 0,\n autoPlay = false,\n loop = true,\n controls = true,\n muted = false,\n style,\n onFrame,\n onTimeline,\n onAudio,\n}: ViewerProps) => {\n const resolvedLayout = resolveEntryLayout(entry, layout);\n const {fps, width, height} = resolvedLayout.format;\n const [timeline, setTimeline] = useState<CompiledTimeline | null>(null);\n const [track, setTrack] = useState<AudioTrack | null>(null);\n const declared = entryDurationInFrames(entry, resolvedLayout);\n const durationInFrames = Math.max(1, declared || timeline?.durationInFrames || fps);\n\n const playback = usePlayback({fps, durationInFrames, initialFrame, autoPlay, loop, onFrame});\n const {frame} = playback;\n\n useAudioPlayback({track, frame: playback.frame, fps, playing: playback.playing, muted});\n\n const handleAudio = useCallback(\n (next: AudioTrack) => {\n setTrack(next);\n onAudio?.(next);\n },\n [onAudio],\n );\n\n const handleTimeline = useCallback(\n (next: CompiledTimeline) => {\n // Comparing the count and total would hold a stale timeline when two\n // scenes trade frames between them: same length, same total, different\n // boundaries.\n setTimeline((current) => (JSON.stringify(current) === JSON.stringify(next) ? current : next));\n onTimeline?.(next);\n },\n [onTimeline],\n );\n\n const aspectRatio = useMemo(() => `${width} / ${height}`, [height, width]);\n const activeScene = timeline?.scenes.find(\n (scene) => frame >= scene.start && frame < scene.start + scene.durationInFrames,\n );\n\n return (\n <div className=\"odori-viewer\" style={{display: \"grid\", gap: 12, width: \"100%\", ...style}}>\n <div\n data-odori-viewer\n style={{\n aspectRatio,\n background: resolvedLayout.brand.colors.background,\n borderRadius: 10,\n overflow: \"hidden\",\n position: \"relative\",\n width: \"100%\",\n }}\n >\n <OdoriRuntime\n entry={entry}\n frame={frame}\n input={input}\n prepared={prepared}\n assets={assets}\n layout={layout}\n onTimeline={handleTimeline}\n onAudio={handleAudio}\n />\n </div>\n {controls ? (\n <div className=\"odori-viewer-controls\" style={{alignItems: \"center\", display: \"flex\", gap: 10}}>\n <button type=\"button\" onClick={playback.toggle}>\n {playback.playing ? \"Pause\" : \"Play\"}\n </button>\n <button type=\"button\" onClick={() => playback.step(-1)} aria-label=\"Previous frame\">\n {\"\\u2039\"}\n </button>\n <button type=\"button\" onClick={() => playback.step(1)} aria-label=\"Next frame\">\n {\"\\u203a\"}\n </button>\n <input\n aria-label=\"Timeline\"\n type=\"range\"\n min={0}\n max={durationInFrames - 1}\n value={frame}\n onChange={(event) => {\n playback.pause();\n playback.seek(Number(event.currentTarget.value));\n }}\n style={{flex: 1}}\n />\n <output style={{fontVariantNumeric: \"tabular-nums\", minWidth: 132, textAlign: \"right\"}}>\n {formatTimecode(frame, fps)} {\"·\"} {frame}/{durationInFrames - 1}\n {activeScene ? ` · ${activeScene.name ?? activeScene.id}` : \"\"}\n </output>\n </div>\n ) : null}\n </div>\n );\n};\n","\"use client\";\n\nimport {useCallback, useEffect, useRef, useState} from \"react\";\n\nexport type PlaybackOptions = {\n fps: number;\n durationInFrames: number;\n initialFrame?: number;\n autoPlay?: boolean;\n loop?: boolean;\n /** Wall-clock multiplier. The frame clock keeps its rate; only time moves. */\n rate?: number;\n onFrame?: (frame: number) => void;\n};\n\nexport type Playback = {\n frame: number;\n playing: boolean;\n rate: number;\n play(): void;\n pause(): void;\n toggle(): void;\n seek(frame: number): void;\n step(delta: number): void;\n restart(): void;\n};\n\n/**\n * The seekable frame clock. Playback advances a fractional counter from\n * wall-clock deltas, but React only ever sees an integer frame, so a paused\n * player, a still, and the export worker agree by construction.\n */\n/**\n * How far the clock moves for a wall-clock delta.\n *\n * Rate scales elapsed time, never the frame index, so frame 90 is the same\n * image at 0.25x, 1x, and 4x, and an export ignores rate entirely.\n */\nexport const advanceFrames = (fractional: number, deltaMs: number, fps: number, rate = 1): number =>\n fractional + (deltaMs / 1000) * fps * rate;\n\nexport const usePlayback = ({\n fps,\n durationInFrames,\n initialFrame = 0,\n autoPlay = false,\n loop = true,\n rate = 1,\n onFrame,\n}: PlaybackOptions): Playback => {\n const [frame, setFrame] = useState(initialFrame);\n const [playing, setPlaying] = useState(autoPlay);\n const animation = useRef<number | null>(null);\n const previousTime = useRef<number | null>(null);\n const fractional = useRef(initialFrame);\n const frameRef = useRef(initialFrame);\n\n const commit = useCallback(\n (next: number) => {\n const clamped = Math.max(0, Math.min(Math.round(next), Math.max(0, durationInFrames - 1)));\n frameRef.current = clamped;\n setFrame(clamped);\n onFrame?.(clamped);\n },\n [durationInFrames, onFrame],\n );\n\n useEffect(() => {\n if (!playing) {\n previousTime.current = null;\n fractional.current = frameRef.current;\n return;\n }\n const tick = (now: number) => {\n const previous = previousTime.current ?? now;\n previousTime.current = now;\n fractional.current = advanceFrames(fractional.current, now - previous, fps, rate);\n if (fractional.current >= durationInFrames) {\n if (!loop) {\n commit(durationInFrames - 1);\n setPlaying(false);\n return;\n }\n fractional.current %= durationInFrames;\n }\n commit(Math.floor(fractional.current));\n animation.current = requestAnimationFrame(tick);\n };\n animation.current = requestAnimationFrame(tick);\n return () => {\n if (animation.current !== null) cancelAnimationFrame(animation.current);\n };\n }, [commit, durationInFrames, fps, loop, playing, rate]);\n\n const seek = useCallback(\n (next: number) => {\n fractional.current = next;\n commit(next);\n },\n [commit],\n );\n\n return {\n frame,\n playing,\n rate,\n play: () => setPlaying(true),\n pause: () => setPlaying(false),\n toggle: () => setPlaying((value) => !value),\n seek,\n step: (delta: number) => {\n setPlaying(false);\n seek(frameRef.current + delta);\n },\n restart: () => {\n seek(0);\n setPlaying(true);\n },\n };\n};\n","\"use client\";\n\nimport {useEffect, useRef} from \"react\";\nimport {trackGainAtFrame, type AudioTrack} from \"./audio\";\n\nexport type AudioPlaybackOptions = {\n track: AudioTrack | null;\n frame: number;\n fps: number;\n playing: boolean;\n muted?: boolean;\n masterGain?: number;\n /** Cue id to hear alone, or null for the whole mix. */\n soloCue?: string | null;\n /** True while the playhead is being dragged, which auditions under the cursor. */\n scrubbing?: boolean;\n /** Playback rate, so cues stay with the frame clock when it is sped up. */\n rate?: number;\n /**\n * Called when the browser refuses to start a cue without a user gesture, and\n * again when it relents. Autoplay policy is the difference between a silent\n * preview and a broken one, so it is reported rather than swallowed.\n */\n onBlocked?: (blocked: boolean) => void;\n /**\n * Called with the cues whose files failed to load or decode. A cue pointing\n * at a missing or wrong file is silence with no other symptom, so it is\n * reported rather than swallowed.\n */\n onFailed?: (sources: string[]) => void;\n};\n\n/**\n * Drives one HTMLAudioElement per cue from the frame clock.\n *\n * Audio is the one thing that cannot be derived from a frame index, so preview\n * playback resyncs whenever the element drifts more than a frame from where the\n * timeline says it should be. The exported mix is built separately by the\n * encoder from the same cues, which keeps the file frame accurate.\n */\nexport const useAudioPlayback = ({\n track,\n frame,\n fps,\n playing,\n muted = false,\n masterGain = 1,\n soloCue = null,\n scrubbing = false,\n rate = 1,\n onBlocked,\n onFailed,\n}: AudioPlaybackOptions) => {\n const elements = useRef(new Map<string, HTMLAudioElement>());\n // The sync pass, reachable from listeners that fire when the frame has not\n // changed: the clock can stall (a hidden tab throttles rAF, a slow render\n // drops frames) while an audio element keeps running at its own pace.\n const sync = useRef<() => void>(() => {});\n const failed = useRef(new Set<string>());\n\n useEffect(() => {\n const table = elements.current;\n const live = new Set((track?.cues ?? []).map((cue) => cue.id));\n for (const [id, element] of table) {\n if (live.has(id)) continue;\n element.pause();\n table.delete(id);\n }\n return () => {\n for (const element of table.values()) element.pause();\n };\n }, [track]);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const table = elements.current;\n\n const onTimeUpdate = () => run();\n\n const run = () => {\n // Scrubbing is playback the user is driving by hand, so cues sound under\n // the cursor instead of going silent the moment the transport pauses.\n const audible = playing || scrubbing;\n\n for (const cue of track?.cues ?? []) {\n let element = table.get(cue.id);\n if (!element) {\n element = new window.Audio(cue.src);\n element.preload = \"auto\";\n element.loop = cue.loop;\n // An element that drifts while the clock is stalled corrects itself\n // on its own time updates, so audio can never run away from a\n // frozen picture.\n element.addEventListener(\"timeupdate\", onTimeUpdate);\n element.addEventListener(\"error\", () => {\n failed.current.add(cue.src);\n onFailed?.([...failed.current]);\n });\n table.set(cue.id, element);\n }\n\n const local = frame - cue.fromFrame;\n const inside = local >= 0 && local < cue.durationInFrames;\n /**\n * Where in the file this frame sounds. A looping cue's window is\n * longer than its file, so the position wraps: seeking straight to\n * `local / fps` would land past the end and the browser would clamp\n * it, leaving the bed stuck on its final sample. The wrap needs the\n * file's real length, which only exists once metadata has loaded.\n */\n const span = element.duration;\n const elapsed = local / fps;\n const position =\n cue.loop && Number.isFinite(span) && span > 0 ? elapsed % span : elapsed;\n // Set every pass, not only at creation: an edit that turns looping on\n // does not change the cue's id, so the element it reuses would keep\n // the old behaviour.\n if (element.loop !== cue.loop) element.loop = cue.loop;\n element.volume = Math.max(0, Math.min(1, trackGainAtFrame(cue, track?.cues ?? [], frame) * masterGain));\n element.muted = muted || (soloCue !== null && soloCue !== cue.id);\n\n if (!inside || !audible) {\n if (!element.paused) element.pause();\n if (inside && !audible) {\n const target = cue.trimStartSeconds + position;\n if (Math.abs(element.currentTime - target) > 1 / fps) element.currentTime = target;\n }\n continue;\n }\n\n const target = cue.trimStartSeconds + position;\n // A rate change is a new playbackRate, not a reseek: the element keeps\n // playing and the drift check below catches it if it falls behind.\n if (element.playbackRate !== rate) element.playbackRate = rate;\n if (Math.abs(element.currentTime - target) > (2 / fps) * Math.max(1, rate)) element.currentTime = target;\n if (element.paused) {\n void element.play().then(\n () => onBlocked?.(false),\n (error: unknown) => onBlocked?.((error as Error)?.name === \"NotAllowedError\"),\n );\n }\n }\n };\n\n sync.current = run;\n run();\n\n return () => {\n for (const element of table.values()) element.removeEventListener(\"timeupdate\", onTimeUpdate);\n };\n }, [fps, frame, masterGain, muted, onBlocked, onFailed, playing, rate, scrubbing, soloCue, track]);\n\n // A hidden tab throttles the frame clock, so hold every cue until the tab is\n // visible again and then resync from the frame the timeline actually shows.\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n const onVisibility = () => {\n if (document.hidden) {\n for (const element of elements.current.values()) element.pause();\n return;\n }\n sync.current();\n };\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => document.removeEventListener(\"visibilitychange\", onVisibility);\n }, []);\n};\n","\"use client\";\n\nimport {useEffect, useState} from \"react\";\nimport {OdoriRuntime, type CompiledTimeline, type VideoEntry} from \"./runtime\";\nimport {type AudioTrack} from \"./audio\";\nimport {type VideoLayout} from \"./layout\";\n\ndeclare global {\n interface Window {\n __ODORI_SET_FRAME__?: (frame: number) => void;\n __ODORI_TIMELINE__?: CompiledTimeline;\n __ODORI_AUDIO__?: AudioTrack;\n __ODORI_READY__?: boolean;\n }\n}\n\n/**\n * The surface the render worker drives. It exposes an explicit frame setter\n * and a readiness handshake instead of relying on timing heuristics.\n */\nexport const RenderSurface = ({\n entry,\n initialFrame = 0,\n input,\n prepared,\n assets,\n layout,\n}: {\n entry: VideoEntry;\n initialFrame?: number;\n input?: Record<string, unknown>;\n prepared?: unknown;\n assets?: Array<{reference: string; url: string}>;\n layout?: VideoLayout;\n}) => {\n const [frame, setFrame] = useState(initialFrame);\n\n useEffect(() => {\n window.__ODORI_SET_FRAME__ = setFrame;\n window.__ODORI_READY__ = true;\n return () => {\n delete window.__ODORI_SET_FRAME__;\n delete window.__ODORI_READY__;\n };\n }, []);\n\n return (\n <OdoriRuntime\n entry={entry}\n frame={frame}\n input={input}\n prepared={prepared}\n assets={assets}\n layout={layout}\n onTimeline={(timeline) => {\n window.__ODORI_TIMELINE__ = timeline;\n }}\n onAudio={(track) => {\n window.__ODORI_AUDIO__ = track;\n }}\n />\n );\n};\n","import {type Signal} from \"./synth\";\n\n/**\n * Signal to a 16 bit PCM WAV. Small, lossless, and readable by FFmpeg without\n * a decoder, which is all the mix needs from a generated cue.\n *\n * Encoding lives in the runtime rather than the CLI so preview and export\n * share it: the browser can hand the same bytes to an AudioContext that the\n * render worker writes to disk.\n */\nexport const encodeWav = (signal: Signal): Uint8Array => {\n const channels = signal.channels.length || 1;\n const frames = signal.channels[0]?.length ?? 0;\n const bytesPerSample = 2;\n const dataBytes = frames * channels * bytesPerSample;\n const buffer = new ArrayBuffer(44 + dataBytes);\n const view = new DataView(buffer);\n\n const ascii = (offset: number, text: string) => {\n for (let index = 0; index < text.length; index += 1) view.setUint8(offset + index, text.charCodeAt(index));\n };\n\n ascii(0, \"RIFF\");\n view.setUint32(4, 36 + dataBytes, true);\n ascii(8, \"WAVE\");\n ascii(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true); // PCM\n view.setUint16(22, channels, true);\n view.setUint32(24, signal.sampleRate, true);\n view.setUint32(28, signal.sampleRate * channels * bytesPerSample, true);\n view.setUint16(32, channels * bytesPerSample, true);\n view.setUint16(34, 8 * bytesPerSample, true);\n ascii(36, \"data\");\n view.setUint32(40, dataBytes, true);\n\n let offset = 44;\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n const sample = signal.channels[channel]?.[frame] ?? 0;\n // Clamp before quantizing, so a hot score distorts predictably instead\n // of wrapping into noise.\n const clamped = Math.max(-1, Math.min(1, sample));\n view.setInt16(offset, Math.round(clamped * 32767), true);\n offset += bytesPerSample;\n }\n }\n\n return new Uint8Array(buffer);\n};\n","import {Easing, interpolate} from \"./easing\";\n\nexport type CursorStop = {\n /** Frame this stop is reached, from the start of the enclosing scene. */\n frame: number;\n /** Canvas coordinates, in the composition's own pixels. */\n x: number;\n y: number;\n /**\n * A click landing on this stop. The press is drawn at the stop's frame and\n * decays over a few frames, so the pointer visibly does the thing the UI is\n * about to react to.\n */\n click?: boolean;\n /** Hold here until this many frames have passed before moving on. */\n hold?: number;\n};\n\nexport type CursorState = {\n x: number;\n y: number;\n /** 0 before the path starts and after it ends, 1 while it is on screen. */\n visible: number;\n /** 1 at the instant of a click, decaying to 0. Drives the press ring. */\n pressed: number;\n /** True while a click is within its press window, for a UI to react to. */\n clicking: boolean;\n};\n\n/** Frames a press ring takes to expand and fade. */\nconst PRESS_FRAMES = 9;\n\n/**\n * Where an authored pointer is at this frame.\n *\n * Recording a real cursor would make a video that cannot be re-rendered: the\n * path would live in a file, not in the composition, and a change of copy or\n * canvas would leave it pointing at nothing. An authored path is source — it\n * diffs, it survives a reflow, and it produces the same pixels every run.\n *\n * Movement eases between stops rather than running linearly, because a pointer\n * that travels at constant speed reads as a machine. A `hold` keeps the\n * pointer still without needing a duplicate stop at the same coordinates.\n */\nexport const cursorAt = (stops: CursorStop[], frame: number): CursorState => {\n if (stops.length === 0) return {x: 0, y: 0, visible: 0, pressed: 0, clicking: false};\n\n // A hold extends the stop it is on, which shifts everything after it.\n const timed: CursorStop[] = [];\n let shift = 0;\n for (const stop of stops) {\n const start = stop.frame + shift;\n timed.push({...stop, frame: start});\n if (stop.hold) {\n timed.push({...stop, frame: start + stop.hold, click: false});\n shift += stop.hold;\n }\n }\n\n const first = timed[0];\n const last = timed[timed.length - 1];\n if (frame <= first.frame) return {x: first.x, y: first.y, visible: 0, pressed: 0, clicking: false};\n\n const frames = timed.map((stop) => stop.frame);\n const x = interpolate(frame, frames, timed.map((stop) => stop.x), {easing: Easing.standard});\n const y = interpolate(frame, frames, timed.map((stop) => stop.y), {easing: Easing.standard});\n\n // The most recent click at or before this frame owns the press ring.\n let pressed = 0;\n for (const stop of timed) {\n if (!stop.click || stop.frame > frame) continue;\n const age = frame - stop.frame;\n if (age <= PRESS_FRAMES) pressed = Math.max(pressed, 1 - age / PRESS_FRAMES);\n }\n\n return {\n x,\n y,\n // Fade in as it arrives and out after the last stop, so a pointer never\n // pops onto a frame it was not part of.\n visible: interpolate(\n frame,\n [first.frame, first.frame + 6, last.frame + 12, last.frame + 20],\n [0, 1, 1, 0],\n {easing: Easing.standard},\n ),\n pressed,\n clicking: pressed > 0,\n };\n};\n\n/** The last frame an authored path is still on screen, for sizing a scene. */\nexport const cursorDuration = (stops: CursorStop[]): number => {\n const hold = stops.reduce((total, stop) => total + (stop.hold ?? 0), 0);\n return (stops[stops.length - 1]?.frame ?? 0) + hold + 20;\n};\n","import {useFrame} from \"./context\";\n\nexport type TypingOptions = {\n /** Frame the first character lands on. */\n from?: number;\n /** Characters revealed per second. */\n charactersPerSecond?: number;\n /**\n * Characters revealed per step. Typing one character at a time reads as a\n * machine at high speeds; two or three at a time reads as hands, because\n * that is roughly what a fast typist does between glances at the screen.\n */\n chunk?: number;\n /** Frames the caret stays solid after the last character before it blinks. */\n settle?: number;\n};\n\nexport type TypingState = {\n /** What is on screen at this frame. */\n text: string;\n /** How many characters of the source are revealed. */\n length: number;\n /** True once every character is on screen. */\n done: boolean;\n /**\n * Whether the caret is drawn this frame: solid while typing and for a beat\n * after, blinking once the line is finished, the way a terminal waits.\n */\n caret: boolean;\n /** 0 before the first character, 1 at the last. */\n progress: number;\n};\n\nconst DEFAULTS = {from: 0, charactersPerSecond: 22, chunk: 1, settle: 12};\n\n/** Frames the typing itself occupies, for laying out what comes after it. */\nexport const typingFrames = (text: string, options: TypingOptions = {}, fps = 30): number => {\n const {charactersPerSecond, chunk} = {...DEFAULTS, ...options};\n const steps = Math.ceil(text.length / Math.max(1, chunk));\n return Math.ceil((steps * Math.max(1, chunk) * fps) / Math.max(1, charactersPerSecond));\n};\n\n/**\n * What a line of typed text looks like at one frame.\n *\n * A pure function of the frame, so scrubbing backwards untypes the line\n * exactly and two render workers on either side of a chunk boundary agree\n * character for character. The caret is part of the state rather than a\n * separate blink timer for the same reason.\n */\nexport const typedAt = (text: string, frame: number, options: TypingOptions = {}, fps = 30): TypingState => {\n const {from, charactersPerSecond, chunk, settle} = {...DEFAULTS, ...options};\n const step = Math.max(1, chunk);\n const elapsed = frame - from;\n const revealed = Math.floor((elapsed / fps) * charactersPerSecond);\n const length = Math.max(0, Math.min(text.length, Math.floor(revealed / step) * step));\n const done = elapsed >= 0 && length >= text.length;\n const finishedAt = from + typingFrames(text, options, fps);\n // Solid while there is more to type and through the settle, then a one\n // second blink: on for the first half of each cycle.\n const caret = !done || frame < finishedAt + settle ? elapsed >= 0 : (frame - finishedAt - settle) % fps < fps / 2;\n\n return {\n text: text.slice(0, length),\n length,\n done,\n caret,\n progress: text.length === 0 ? 1 : length / text.length,\n };\n};\n\n/** `typedAt` bound to the current frame. */\nexport const useTyping = (text: string, options: TypingOptions = {}): TypingState =>\n typedAt(text, useFrame(), options);\n","/**\n * Randomness that survives a re-render.\n *\n * A frame is a pure function of its number. `Math.random()` breaks that in the\n * quietest possible way: the preview looks fine, every export looks fine, and\n * the two are different — and so are two chunks of the same export, because a\n * render is parallel and each worker rolls its own numbers. Fifty particles\n * that jump between chunk boundaries is the usual symptom, found late.\n *\n * So a composition asks for a number by name instead. The same seed always\n * gives the same value, on every machine and in every worker, and a seed that\n * includes the frame gives motion that is random-looking and reproducible.\n */\n\n/**\n * A 32-bit hash of a string, so a seed can be written as a readable name\n * rather than a magic integer. FNV-1a: small, well distributed for short keys,\n * and stable across engines, which matters because two workers must agree.\n */\nconst hashSeed = (value: string): number => {\n let hash = 0x811c9dc5;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return hash >>> 0;\n};\n\nconst toSeed = (seed: number | string): number =>\n typeof seed === \"number\" ? Math.floor(seed) >>> 0 : hashSeed(seed);\n\n/**\n * A number in `[0, 1)` for a seed. The same seed always returns the same\n * number, which is the whole point.\n *\n * ```tsx\n * const drift = random(`particle-${index}`) * 40;\n * const jitter = random([frame, index]) - 0.5;\n * ```\n *\n * An array seed is joined, which is the convenient way to say \"this thing, on\n * this frame\" without building the string by hand.\n */\nexport const random = (seed: number | string | Array<number | string>): number => {\n const key = Array.isArray(seed) ? seed.join(\":\") : seed;\n // Mulberry32, the same generator the audio synthesis uses, so a project has\n // one notion of \"seeded\" rather than two that disagree.\n let state = (toSeed(key) + 0x6d2b79f5) >>> 0;\n let t = Math.imul(state ^ (state >>> 15), 1 | state);\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n};\n\n/** A number in `[min, max)`, for a seed. */\nexport const randomBetween = (seed: number | string | Array<number | string>, min: number, max: number): number =>\n min + random(seed) * (max - min);\n\n/** One item from a list, for a seed. Empty lists return undefined. */\nexport const randomPick = <T,>(seed: number | string | Array<number | string>, items: readonly T[]): T | undefined =>\n items.length === 0 ? undefined : items[Math.floor(random(seed) * items.length)];\n\n/**\n * A shuffled copy, for a seed. Fisher-Yates driven by the same generator, so\n * the order is arbitrary but fixed — a list that reshuffles every frame is an\n * animation nobody asked for.\n */\nexport const randomOrder = <T,>(seed: number | string | Array<number | string>, items: readonly T[]): T[] => {\n const key = Array.isArray(seed) ? seed.join(\":\") : String(seed);\n const out = [...items];\n for (let index = out.length - 1; index > 0; index -= 1) {\n const swap = Math.floor(random(`${key}:${index}`) * (index + 1));\n [out[index], out[swap]] = [out[swap], out[index]];\n }\n return out;\n};\n","\"use client\";\n\nimport {useEffect, useLayoutEffect, useRef, type RefObject} from \"react\";\nimport {useFrame, useReadiness, useVideo} from \"./context\";\n\nexport type CanvasDraw = (context: CanvasRenderingContext2D, state: {frame: number; width: number; height: number}) => void;\n\n/** `useLayoutEffect` warns during server rendering, where there is no canvas. */\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\n/**\n * Draw to a canvas from the frame clock.\n *\n * The contract a video runs on is that frame N produces the same pixels every\n * time. A canvas is where that is easiest to lose: the obvious way to animate\n * one is `requestAnimationFrame`, which is wall time, and wall time means the\n * export samples wherever the loop happened to be. Two workers rendering\n * neighbouring chunks then disagree, and the seam shows.\n *\n * So the draw is a pure function of the frame, called synchronously before the\n * browser paints, and the frame is held until it has run. The capture waits on\n * the same readiness handshake an image decode uses, which is what makes the\n * screenshot see finished pixels rather than an empty buffer.\n */\nexport const useCanvas = (draw: CanvasDraw, dependencies: readonly unknown[] = []): RefObject<HTMLCanvasElement | null> => {\n const canvas = useRef<HTMLCanvasElement | null>(null);\n const frame = useFrame();\n const {width, height} = useVideo();\n const readiness = useReadiness();\n // The draw is called with the current closure but must not re-run the effect\n // when an inline function identity changes, or every render would repaint.\n const latest = useRef(draw);\n latest.current = draw;\n\n useIsomorphicLayoutEffect(() => {\n const element = canvas.current;\n if (!element) return;\n\n // Held across the draw, so a frame is never captured mid-paint.\n const release = readiness.hold();\n try {\n const context = element.getContext(\"2d\", {alpha: true});\n if (!context) return;\n\n // Reset rather than accumulate: a frame is drawn from nothing, so\n // scrubbing backwards produces the same image as playing forwards.\n context.setTransform(1, 0, 0, 1, 0, 0);\n context.clearRect(0, 0, element.width, element.height);\n latest.current(context, {frame, width: element.width, height: element.height});\n } finally {\n release();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [frame, width, height, ...dependencies]);\n\n return canvas;\n};\n\n/**\n * Rasterize HTML into a canvas, deterministically.\n *\n * The browser will draw an SVG containing a `foreignObject` onto a canvas, and\n * a `foreignObject` can hold ordinary markup. That is the whole trick, and the\n * reason it needs care: the image decode is asynchronous, so the frame has to\n * be held until it lands, and the markup has to carry its own styles because\n * nothing outside the SVG reaches into it.\n *\n * Fonts are the sharp edge. A face that is not loaded when this runs will fall\n * back, and the fallback is what gets baked into the pixels — which is why the\n * caller waits on `document.fonts.ready` before drawing.\n */\nexport const drawHtml = async (\n context: CanvasRenderingContext2D,\n html: string,\n options: {width: number; height: number; style?: string},\n): Promise<void> => {\n const {width, height, style = \"\"} = options;\n if (typeof document !== \"undefined\" && document.fonts?.ready) await document.fonts.ready;\n\n const svg = [\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">`,\n `<foreignObject width=\"100%\" height=\"100%\">`,\n `<div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"width:${width}px;height:${height}px;${style}\">`,\n html,\n `</div></foreignObject></svg>`,\n ].join(\"\");\n\n // A data URL rather than a blob URL: a blob URL has to be revoked, and a\n // leak here is a leak once per frame for the length of the video.\n const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n\n await new Promise<void>((done, fail) => {\n const image = new Image();\n image.onload = () => {\n context.drawImage(image, 0, 0, width, height);\n done();\n };\n image.onerror = () =>\n fail(\n new Error(\n \"The HTML could not be rasterized. Every element inside must carry inline styles, and images must be data URLs: an SVG foreignObject cannot reach outside itself.\",\n ),\n );\n image.src = encoded;\n });\n};\n","import {random} from \"./random\";\n\nexport type PlaceholderOptions = {\n width?: number;\n height?: number;\n /** Drawn across the middle, so a fixture says what it is standing in for. */\n label?: string;\n /** Two colours the gradient runs between. */\n from?: string;\n to?: string;\n /** Seed for the scatter, so two placeholders differ without differing runs. */\n seed?: string;\n};\n\n/**\n * A picture that ships as code.\n *\n * A component that shows media needs media to show, and a fixture that ships a\n * JPEG cannot be reviewed in a diff, cannot be recoloured by a brand, and adds\n * a binary to a repository forever. Generating an SVG instead keeps the\n * registry's rule intact — install copies source — and makes the picture do\n * something a file cannot: describe itself.\n *\n * It is deliberately obviously a placeholder. A fixture that looks like real\n * photography invites someone to ship it.\n */\nexport const placeholderSvg = ({\n width = 1600,\n height = 900,\n label,\n from = \"#1a1a1a\",\n to = \"#0a0a0a\",\n seed = \"placeholder\",\n}: PlaceholderOptions = {}): string => {\n const shapes = Array.from({length: 14}, (_, index) => {\n const x = random([seed, \"x\", index]) * width;\n const y = random([seed, \"y\", index]) * height;\n const radius = (random([seed, \"r\", index]) * 0.16 + 0.03) * Math.min(width, height);\n const opacity = (random([seed, \"o\", index]) * 0.06 + 0.02).toFixed(3);\n return `<circle cx=\"${x.toFixed(1)}\" cy=\"${y.toFixed(1)}\" r=\"${radius.toFixed(1)}\" fill=\"#ffffff\" opacity=\"${opacity}\"/>`;\n }).join(\"\");\n\n const caption = label\n ? `<text x=\"50%\" y=\"50%\" fill=\"#ffffff\" fill-opacity=\"0.42\" font-family=\"ui-monospace, monospace\" font-size=\"${Math.round(\n Math.min(width, height) * 0.06,\n )}\" text-anchor=\"middle\" dominant-baseline=\"middle\">${label.replace(/[<>&]/g, \"\")}</text>`\n : \"\";\n\n return [\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">`,\n `<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">`,\n `<stop offset=\"0\" stop-color=\"${from}\"/><stop offset=\"1\" stop-color=\"${to}\"/>`,\n `</linearGradient></defs>`,\n `<rect width=\"${width}\" height=\"${height}\" fill=\"url(#g)\"/>`,\n shapes,\n `<rect x=\"1\" y=\"1\" width=\"${width - 2}\" height=\"${height - 2}\" fill=\"none\" stroke=\"#ffffff\" stroke-opacity=\"0.08\"/>`,\n caption,\n `</svg>`,\n ].join(\"\");\n};\n\n/**\n * The same picture as a data URL, which is what an `<img>` or a canvas draw\n * wants. Inline rather than fetched: a fixture that needs the network is a\n * fixture that fails on a plane, in CI, and in a sandboxed render.\n */\nexport const placeholderImage = (options: PlaceholderOptions = {}): string =>\n `data:image/svg+xml;charset=utf-8,${encodeURIComponent(placeholderSvg(options))}`;\n\n/**\n * A frame of a placeholder \"clip\": the same picture with a moving marker and a\n * timecode, so a component that plays media has something to play that visibly\n * advances and is still a pure function of the frame.\n */\nexport const placeholderFrame = (frame: number, options: PlaceholderOptions & {fps?: number} = {}): string => {\n const {width = 1600, height = 900, fps = 30, ...rest} = options;\n const seconds = frame / fps;\n const timecode = `${String(Math.floor(seconds / 60)).padStart(2, \"0\")}:${String(Math.floor(seconds % 60)).padStart(2, \"0\")}:${String(\n frame % fps,\n ).padStart(2, \"0\")}`;\n\n const base = placeholderSvg({...rest, width, height, label: undefined});\n const progress = (frame % (fps * 4)) / (fps * 4);\n const marker = [\n `<rect x=\"0\" y=\"${height - 12}\" width=\"${(width * progress).toFixed(1)}\" height=\"12\" fill=\"#ffffff\" fill-opacity=\"0.5\"/>`,\n `<text x=\"${width / 2}\" y=\"${height / 2}\" fill=\"#ffffff\" fill-opacity=\"0.5\" font-family=\"ui-monospace, monospace\" font-size=\"${Math.round(\n Math.min(width, height) * 0.08,\n )}\" text-anchor=\"middle\" dominant-baseline=\"middle\">${timecode}</text>`,\n ].join(\"\");\n\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(base.replace(\"</svg>\", `${marker}</svg>`))}`;\n};\n","import {type Duration} from \"./time\";\nimport {type VideoLayout} from \"./layout\";\nimport {type ParsableSchema} from \"./schema\";\n\nexport type VideoMetadata<Input = Record<string, unknown>> = {\n readonly kind: \"odori-video-metadata\";\n id: string;\n title: string;\n description?: string;\n duration?: Duration;\n layout?: VideoLayout;\n schema?: ParsableSchema<Input>;\n defaultProps?: Partial<Input>;\n tags?: string[];\n thumbnailFrame?: number;\n};\n\nexport type VideoMetadataInput<Input = Record<string, unknown>> = Omit<VideoMetadata<Input>, \"kind\" | \"id\"> & {\n /**\n * Defaults to the entry's path under `videos/`, so the directory names a\n * video the way a route names a page. Set it to keep an id stable across a\n * directory move.\n */\n id?: string;\n};\n\n/** A segment of an id: alphanumeric with dashes, the way a directory is named. */\nconst SEGMENT = /^[a-z0-9][a-z0-9-]*$/i;\n\nexport const isValidVideoId = (id: string): boolean =>\n id.length > 0 && id.split(\"/\").every((segment) => SEGMENT.test(segment));\n\n/**\n * An id left unset is resolved from the filesystem by discovery. The empty\n * string is the unresolved state: no entry reaches a manifest, a render, or\n * Studio without an id stamped in.\n */\nexport const resolveVideoId = (id: string | undefined, pathId: string): string => id || pathId;\n\nexport const defineVideoMetadata = <Input = Record<string, unknown>>(\n metadata: VideoMetadataInput<Input>,\n): VideoMetadata<Input> => {\n if (metadata.id !== undefined && !isValidVideoId(metadata.id)) {\n throw new Error(`Video id must be alphanumeric path segments with dashes: ${metadata.id}`);\n }\n return {kind: \"odori-video-metadata\", ...metadata, id: metadata.id ?? \"\"};\n};\n\nexport type PrepareContext<Input> = {\n input: Input;\n assets: {resolve(reference: string): Promise<string>};\n cache: {getOrSet<Value>(key: string, factory: () => Promise<Value>): Promise<Value>};\n signal?: AbortSignal;\n};\n\nexport type PrepareFunction<Input = Record<string, unknown>, Prepared = unknown> = {\n readonly kind: \"odori-prepare\";\n version: string;\n run(context: PrepareContext<Input>): Promise<Prepared>;\n};\n\nexport const definePrepare = <Input = Record<string, unknown>, Prepared = unknown>(\n run: (context: PrepareContext<Input>) => Promise<Prepared>,\n options: {version?: string} = {},\n): PrepareFunction<Input, Prepared> => ({\n kind: \"odori-prepare\",\n version: options.version ?? \"1\",\n run,\n});\n","/**\n * A tiny serializable input contract.\n *\n * Odori needs three things from a schema: validation with defaults, a JSON\n * description Studio can turn into controls, and zero runtime dependencies.\n * Any zod-compatible object with `parse()` is also accepted.\n */\nexport type FieldDescriptor =\n | {type: \"text\"; defaultValue: string; maxLength?: number; multiline?: boolean}\n | {type: \"number\"; defaultValue: number; min?: number; max?: number; step?: number}\n | {type: \"boolean\"; defaultValue: boolean}\n | {type: \"select\"; defaultValue: string; options: string[]}\n | {type: \"color\"; defaultValue: string}\n | {type: \"json\"; defaultValue: unknown};\n\nexport type InputSchema<Value = Record<string, unknown>> = {\n readonly kind: \"odori-schema\";\n readonly fields: Record<string, FieldDescriptor>;\n parse(input: unknown): Value;\n safeParse(input: unknown): {success: true; data: Value} | {success: false; issues: string[]};\n defaults(): Value;\n describe(): Record<string, FieldDescriptor>;\n};\n\nexport type ParsableSchema<Value = unknown> = InputSchema<Value> | {parse(input: unknown): Value};\n\nconst validateField = (name: string, field: FieldDescriptor, value: unknown, issues: string[]): unknown => {\n if (value === undefined) return field.defaultValue;\n switch (field.type) {\n case \"text\":\n case \"color\": {\n if (typeof value !== \"string\") {\n issues.push(`${name} must be a string`);\n return field.defaultValue;\n }\n if (field.type === \"text\" && field.maxLength !== undefined && value.length > field.maxLength) {\n issues.push(`${name} exceeds ${field.maxLength} characters`);\n }\n return value;\n }\n case \"number\": {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n issues.push(`${name} must be a finite number`);\n return field.defaultValue;\n }\n if (field.min !== undefined && value < field.min) issues.push(`${name} is below ${field.min}`);\n if (field.max !== undefined && value > field.max) issues.push(`${name} is above ${field.max}`);\n return value;\n }\n case \"boolean\": {\n if (typeof value !== \"boolean\") {\n issues.push(`${name} must be a boolean`);\n return field.defaultValue;\n }\n return value;\n }\n case \"select\": {\n if (typeof value !== \"string\" || !field.options.includes(value)) {\n issues.push(`${name} must be one of ${field.options.join(\", \")}`);\n return field.defaultValue;\n }\n return value;\n }\n default:\n return value;\n }\n};\n\nexport const defineInputSchema = <Fields extends Record<string, FieldDescriptor>>(\n fields: Fields,\n): InputSchema<Record<string, unknown>> => {\n const defaults = () =>\n Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.defaultValue]));\n\n const safeParse = (input: unknown) => {\n if (input !== undefined && input !== null && typeof input !== \"object\") {\n return {success: false as const, issues: [\"input must be an object\"]};\n }\n const source = (input ?? {}) as Record<string, unknown>;\n const issues: string[] = [];\n const data: Record<string, unknown> = {};\n for (const [name, field] of Object.entries(fields)) {\n data[name] = validateField(name, field, source[name], issues);\n }\n for (const key of Object.keys(source)) {\n if (!(key in fields)) data[key] = source[key];\n }\n return issues.length > 0\n ? {success: false as const, issues}\n : {success: true as const, data};\n };\n\n return {\n kind: \"odori-schema\",\n fields,\n defaults,\n describe: () => fields,\n safeParse,\n parse(input) {\n const result = safeParse(input);\n if (!result.success) throw new Error(`Invalid video input: ${result.issues.join(\"; \")}`);\n return result.data;\n },\n };\n};\n\nexport const isOdoriSchema = (schema: unknown): schema is InputSchema =>\n typeof schema === \"object\" && schema !== null && (schema as {kind?: string}).kind === \"odori-schema\";\n\nexport const parseWithSchema = <Value>(schema: ParsableSchema<Value> | undefined, input: unknown): Value =>\n schema ? schema.parse(input) : ((input ?? {}) as Value);\n"],"mappings":"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","names":["useCallback","useState","loop","useEffect","useRef","useRef","useEffect","target","loop","useState","useCallback","useEffect","useState","jsx","useState","useEffect","offset","step","useEffect","useRef","useEffect","useRef","seconds"]}
@@ -1,4 +1,4 @@
1
- import { D as Duration, B as Brand } from './brand-D71KbhAe.js';
1
+ import { D as Duration, B as Brand } from './brand-B-Mv4S_Z.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { ReactNode, ComponentPropsWithoutRef, CSSProperties } from 'react';
4
4
 
@@ -149,8 +149,20 @@ type CompiledScene = {
149
149
  /** Frames this scene starts before the previous one ends. Zero is a cut. */
150
150
  overlap: number;
151
151
  };
152
+ /**
153
+ * A clip on the video's own clock. Clips placed inside a scene belong to that
154
+ * scene's internals and are not listed here: this is the video timeline.
155
+ */
156
+ type CompiledClip = {
157
+ id: string;
158
+ index: number;
159
+ start: number;
160
+ durationInFrames: number;
161
+ };
152
162
  type CompiledTimeline = {
153
163
  scenes: CompiledScene[];
164
+ /** Absolutely placed children of `<Video>`, in source order. */
165
+ clips: CompiledClip[];
154
166
  durationInFrames: number;
155
167
  };
156
168
  declare const Fill: ({ style, ...props }: ComponentPropsWithoutRef<"div">) => react_jsx_runtime.JSX.Element;
@@ -170,6 +182,13 @@ type SceneProps = {
170
182
  * by the same amount: an overlap is a join, not an extra beat.
171
183
  */
172
184
  overlap?: Duration;
185
+ /**
186
+ * How this scene meets its neighbours. `fade` dissolves it in and out, which
187
+ * is forgiving when a scene has no entrance of its own. `cut` shows it whole
188
+ * from its first frame, which is what most film actually does and the only
189
+ * way to reproduce a reference that cuts.
190
+ */
191
+ transition?: "fade" | "cut";
173
192
  /** Injected by <Video>. Authors never pass these. */
174
193
  __start?: number;
175
194
  __index?: number;
@@ -204,7 +223,7 @@ declare const useSceneTransition: () => SceneTransition;
204
223
  * A timeline segment with a local frame clock starting at zero.
205
224
  */
206
225
  declare const Scene: {
207
- ({ children, id, name, __start, __index, __durationInFrames, __enterOverlap, __exitOverlap, __videoFrame, }: SceneProps): react_jsx_runtime.JSX.Element | null;
226
+ ({ children, id, name, transition: treatment, __start, __index, __durationInFrames, __enterOverlap, __exitOverlap, __videoFrame, }: SceneProps): react_jsx_runtime.JSX.Element | null;
208
227
  __odoriScene: true;
209
228
  };
210
229
  /** An explicit offset window inside a scene, for staggered layers. */
@@ -223,13 +242,45 @@ declare const Stagger: ({ children, from, duration }: {
223
242
  * why scrubbing backwards into a loop lands exactly where playing forwards
224
243
  * did.
225
244
  */
226
- declare const Loop: ({ children, duration, times, }: {
245
+ declare const Repeat: ({ children, duration, times, }: {
227
246
  children: ReactNode;
228
247
  /** Length of one pass. */
229
248
  duration: Duration;
230
249
  /** How many passes before it stops. Unlimited by default. */
231
250
  times?: number;
232
251
  }) => react_jsx_runtime.JSX.Element | null;
252
+ /**
253
+ * Content placed at a time, rather than after the thing before it.
254
+ *
255
+ * A scene is a length in a sequence: it starts where the previous one ended,
256
+ * so its position is a consequence of its siblings. That is the right model
257
+ * for the spine of a cut, and the wrong one for anything that has to appear
258
+ * over a scene already running — a lower third, a callout, a caption, a mark
259
+ * that outlives the shot it started in. A clip is placed instead: it names its
260
+ * own start, takes no space in the sequence, and moves nothing when its length
261
+ * changes.
262
+ *
263
+ * It reads its clock from wherever it sits, which is what makes it compose. A
264
+ * direct child of `<Video>` is placed against the whole cut; the same clip
265
+ * inside a `<Scene>` is placed against that scene and disappears with it. Its
266
+ * children see a clock starting at zero either way, so a component written for
267
+ * a scene behaves identically inside a clip.
268
+ *
269
+ * Without a `duration` it runs to the end of whatever it is placed in, which
270
+ * is what a watermark wants.
271
+ */
272
+ declare const Clip: {
273
+ ({ children, id, from, duration, }: {
274
+ children: ReactNode;
275
+ /** Names it on the timeline. Defaults to its position among the clips. */
276
+ id?: string;
277
+ /** When it starts, on the clock of whatever contains it. */
278
+ from?: Duration;
279
+ /** How long it lasts. To the end of the container by default. */
280
+ duration?: Duration;
281
+ }): react_jsx_runtime.JSX.Element | null;
282
+ __odoriClip: true;
283
+ };
233
284
  /**
234
285
  * Hold children at one frame.
235
286
  *
@@ -238,7 +289,7 @@ declare const Loop: ({ children, duration, times, }: {
238
289
  * clock that stops, not the render: children still mount and lay out
239
290
  * normally, they simply always see the same number.
240
291
  */
241
- declare const Freeze: ({ children, at }: {
292
+ declare const Hold: ({ children, at }: {
242
293
  children: ReactNode;
243
294
  at?: Duration;
244
295
  }) => react_jsx_runtime.JSX.Element;
@@ -395,4 +446,4 @@ type ExportJob = {
395
446
  updatedAt: string;
396
447
  };
397
448
 
398
- export { type AudioTrack as A, entryDurationInFrames as B, type CompiledTimeline as C, DUCK_GAIN as D, type EnvelopePoint as E, Fill as F, envelopeAtFrame as G, gainAtFrame as H, isAssetReference as I, resolveEntryLayout as J, sampleGainCurve as K, Loop as L, type ManifestAsset as M, sortCues as N, OdoriRuntime as O, trackDuration as P, trackGainAtFrame as Q, type RenderManifest as R, SafeArea as S, useSceneTransition as T, type ManifestToolchain as U, type VideoEntry as V, type VideoLayout as a, Audio as b, type AudioCue as c, type AudioPolicy as d, type AudioProps as e, type CompiledScene as f, type CreateManifestOptions as g, DUCK_RAMP_FRAMES as h, type ExportJob as i, Freeze as j, type ManifestAudioCue as k, type ManifestFont as l, type MotionPolicy as m, type OdoriRuntimeProps as n, type SafeArea$1 as o, Scene as p, type SceneTransition as q, Stagger as r, Video as s, type VideoFormat as t, type VideoLayoutInput as u, createRenderManifest as v, cueId as w, defaultLayout as x, defineVideoLayout as y, duckEnvelope as z };
449
+ export { type AudioTrack as A, duckEnvelope as B, type CompiledTimeline as C, DUCK_GAIN as D, type EnvelopePoint as E, Fill as F, entryDurationInFrames as G, Hold as H, envelopeAtFrame as I, gainAtFrame as J, isAssetReference as K, resolveEntryLayout as L, type ManifestAsset as M, sampleGainCurve as N, OdoriRuntime as O, sortCues as P, trackDuration as Q, type RenderManifest as R, SafeArea as S, trackGainAtFrame as T, useSceneTransition as U, type VideoEntry as V, type ManifestToolchain as W, type VideoLayout as a, Audio as b, type AudioCue as c, type AudioPolicy as d, type AudioProps as e, Clip as f, type CompiledScene as g, type CreateManifestOptions as h, DUCK_RAMP_FRAMES as i, type ExportJob as j, type ManifestAudioCue as k, type ManifestFont as l, type MotionPolicy as m, type OdoriRuntimeProps as n, Repeat as o, type SafeArea$1 as p, Scene as q, type SceneTransition as r, Stagger as s, Video as t, type VideoFormat as u, type VideoLayoutInput as v, createRenderManifest as w, cueId as x, defaultLayout as y, defineVideoLayout as z };
@@ -1,4 +1,4 @@
1
- export { g as CreateManifestOptions, i as ExportJob, M as ManifestAsset, k as ManifestAudioCue, l as ManifestFont, U as ManifestToolchain, R as RenderManifest, v as createRenderManifest } from './manifest-CEuFXG0U.js';
2
- import './brand-D71KbhAe.js';
1
+ export { h as CreateManifestOptions, j as ExportJob, M as ManifestAsset, k as ManifestAudioCue, l as ManifestFont, W as ManifestToolchain, R as RenderManifest, w as createRenderManifest } from './manifest-d7-HWIFj.js';
2
+ import './brand-B-Mv4S_Z.js';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
package/dist/manifest.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import {
3
3
  createRenderManifest
4
- } from "./chunk-VQLDJUK4.js";
5
- import "./chunk-PXG45HYV.js";
4
+ } from "./chunk-7NPIGLFR.js";
5
+ import "./chunk-V34N2I4A.js";
6
6
  export {
7
7
  createRenderManifest
8
8
  };