odori 0.0.7 → 0.0.9

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/effects.d.ts CHANGED
@@ -72,13 +72,23 @@ type EffectInstance = {
72
72
  type EffectFactory = (values?: Record<string, UniformValue>) => EffectInstance;
73
73
  declare const defineShaderEffect: (definition: ShaderEffectDefinition) => EffectFactory;
74
74
  /** Prepended to every effect, so a shader author writes only the body. */
75
- declare const SHADER_PRELUDE = "#version 300 es\nprecision highp float;\nuniform sampler2D source;\nuniform vec2 resolution;\nuniform float frame;\nuniform float fps;\nuniform float seconds;\nuniform float progress;\nout vec4 odoriColour;\n// sample is a reserved word in GLSL ES 3.00, so the helper is tex.\nvec4 tex(vec2 uv) { return texture(source, uv); }\n";
75
+ declare const SHADER_PRELUDE = "#version 300 es\nprecision highp float;\nuniform sampler2D source;\nuniform vec2 resolution;\nuniform float frame;\nuniform float fps;\nuniform float seconds;\nuniform float progress;\nout vec4 odoriColour;\n// sample is a reserved word in GLSL ES 3.00, so the helper is tex.\nvec4 tex(vec2 uv) { return texture(source, uv); }\n\n/*\n * Randomness that is the same on every backend.\n *\n * The idiom everyone reaches for is fract(sin(dot(p, k)) * 43758.5453), and it\n * is not portable: sin() of a large argument is implementation defined, so\n * SwiftShader and Metal disagree. Measured on one frame of grain, that idiom\n * scored 0.75 SSIM between the two while this one scores 0.999993. Integer\n * arithmetic in GLSL ES 3.00 is exact, so it is the only randomness a renderer\n * that has to reproduce itself can use.\n *\n * Seed from the frame, never from a clock, and the same frame keeps the same\n * noise however it was rendered.\n */\nuint odoriPcg(uint v) {\n uint state = v * 747796405u + 2891336453u;\n uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n return (word >> 22u) ^ word;\n}\nfloat hash(vec2 p) {\n uvec2 q = uvec2(ivec2(p));\n return float(odoriPcg(q.x ^ odoriPcg(q.y)) & 0xffffffu) / float(0xffffffu);\n}\n";
76
76
  declare class ShaderCompileError extends Error {
77
77
  readonly effect: string;
78
78
  readonly log: string;
79
79
  readonly source: string;
80
80
  constructor(effect: string, log: string, source: string);
81
81
  }
82
+ /**
83
+ * The one shader idiom that quietly breaks reproducibility.
84
+ *
85
+ * `fract(sin(dot(p, k)) * large)` is the hash in every shader tutorial, and it
86
+ * produces different noise on different backends because sin() of a large
87
+ * argument is implementation defined. A video written with it renders one way
88
+ * on a laptop and another in CI, with nothing to say why. The prelude ships a
89
+ * portable `hash(vec2)`, so this is refused rather than warned about.
90
+ */
91
+ declare const assertPortableShader: (name: string, fragmentShader: string) => void;
82
92
  type PipelineClock = {
83
93
  frame: number;
84
94
  fps: number;
@@ -199,4 +209,4 @@ declare const filmGrain: EffectFactory;
199
209
  /** Magnify a circle of the picture, like a loupe held over it. */
200
210
  declare const magnify: EffectFactory;
201
211
 
202
- export { type CaptureMethod, type EffectFactory, type EffectInstance, EffectPipeline, EffectSurface, HtmlInCanvas, type HtmlInCanvasInit, type HtmlInCanvasPaint, SHADER_PRELUDE, ShaderCompileError, type ShaderEffectDefinition, type SurfaceCapabilities, type SurfaceClock, type UniformSpec, type UniformValue, barrelDistortion, captureElement, captureMethod, defineShaderEffect, filmGrain, magnify, numberUniform, pixelate, rgbSplit, scanlines, serializeElement, vec2Uniform, vec3Uniform, vec4Uniform };
212
+ export { type CaptureMethod, type EffectFactory, type EffectInstance, EffectPipeline, EffectSurface, HtmlInCanvas, type HtmlInCanvasInit, type HtmlInCanvasPaint, SHADER_PRELUDE, ShaderCompileError, type ShaderEffectDefinition, type SurfaceCapabilities, type SurfaceClock, type UniformSpec, type UniformValue, assertPortableShader, barrelDistortion, captureElement, captureMethod, defineShaderEffect, filmGrain, magnify, numberUniform, pixelate, rgbSplit, scanlines, serializeElement, vec2Uniform, vec3Uniform, vec4Uniform };
package/dist/effects.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import {
3
+ SceneContext,
3
4
  useFrame,
4
5
  useReadiness,
5
6
  useVideo
@@ -40,6 +41,12 @@ var PAINTED = [
40
41
  "opacity",
41
42
  "overflow",
42
43
  "padding",
44
+ "font-family",
45
+ "font-size",
46
+ "font-stretch",
47
+ "font-style",
48
+ "font-variant",
49
+ "font-weight",
43
50
  "position",
44
51
  "right",
45
52
  "text-align",
@@ -73,9 +80,30 @@ var inlineStyles = (source, clone) => {
73
80
  if (child) inlineStyles(sourceChildren[index], child);
74
81
  }
75
82
  };
83
+ var inlineCanvases = (source, clone) => {
84
+ const sourceCanvases = source.querySelectorAll("canvas");
85
+ const cloneCanvases = clone.querySelectorAll("canvas");
86
+ for (let index = 0; index < sourceCanvases.length; index += 1) {
87
+ const original = sourceCanvases[index];
88
+ const placeholder = cloneCanvases[index];
89
+ if (!placeholder) continue;
90
+ let url = "";
91
+ try {
92
+ url = original.toDataURL("image/png");
93
+ } catch {
94
+ }
95
+ const image = clone.ownerDocument.createElement("img");
96
+ image.setAttribute("src", url);
97
+ image.setAttribute("width", String(original.width));
98
+ image.setAttribute("height", String(original.height));
99
+ image.setAttribute("style", placeholder.getAttribute("style") ?? "");
100
+ placeholder.replaceWith(image);
101
+ }
102
+ };
76
103
  var serializeElement = (element) => {
77
104
  const clone = element.cloneNode(true);
78
105
  inlineStyles(element, clone);
106
+ inlineCanvases(element, clone);
79
107
  clone.style.position = "static";
80
108
  clone.style.left = "auto";
81
109
  clone.style.top = "auto";
@@ -111,6 +139,7 @@ var vec4Uniform = (defaultValue) => ({
111
139
  defaultValue
112
140
  });
113
141
  var defineShaderEffect = (definition) => {
142
+ assertPortableShader(definition.name, definition.fragmentShader);
114
143
  return (values = {}) => ({ definition, values });
115
144
  };
116
145
  var VERTEX_SHADER = `#version 300 es
@@ -129,6 +158,29 @@ uniform float progress;
129
158
  out vec4 odoriColour;
130
159
  // sample is a reserved word in GLSL ES 3.00, so the helper is tex.
131
160
  vec4 tex(vec2 uv) { return texture(source, uv); }
161
+
162
+ /*
163
+ * Randomness that is the same on every backend.
164
+ *
165
+ * The idiom everyone reaches for is fract(sin(dot(p, k)) * 43758.5453), and it
166
+ * is not portable: sin() of a large argument is implementation defined, so
167
+ * SwiftShader and Metal disagree. Measured on one frame of grain, that idiom
168
+ * scored 0.75 SSIM between the two while this one scores 0.999993. Integer
169
+ * arithmetic in GLSL ES 3.00 is exact, so it is the only randomness a renderer
170
+ * that has to reproduce itself can use.
171
+ *
172
+ * Seed from the frame, never from a clock, and the same frame keeps the same
173
+ * noise however it was rendered.
174
+ */
175
+ uint odoriPcg(uint v) {
176
+ uint state = v * 747796405u + 2891336453u;
177
+ uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
178
+ return (word >> 22u) ^ word;
179
+ }
180
+ float hash(vec2 p) {
181
+ uvec2 q = uvec2(ivec2(p));
182
+ return float(odoriPcg(q.x ^ odoriPcg(q.y)) & 0xffffffu) / float(0xffffffu);
183
+ }
132
184
  `;
133
185
  var ShaderCompileError = class extends Error {
134
186
  constructor(effect, log, source) {
@@ -145,6 +197,13 @@ var ShaderCompileError = class extends Error {
145
197
  log;
146
198
  source;
147
199
  };
200
+ var assertPortableShader = (name, fragmentShader) => {
201
+ if (/fract\s*\(\s*sin\s*\(/.test(fragmentShader)) {
202
+ throw new Error(
203
+ `${name}: fract(sin(...)) is not reproducible. sin() of a large argument is implementation defined, so this renders differently on different graphics backends. Use the prelude's hash(vec2), which is exact everywhere.`
204
+ );
205
+ }
206
+ };
148
207
  var compile = (gl, type, source, effect) => {
149
208
  const shader = gl.createShader(type);
150
209
  if (!shader) throw new Error(`${effect}: the browser refused to create a shader.`);
@@ -289,7 +348,7 @@ var EffectPipeline = class {
289
348
  };
290
349
 
291
350
  // src/effects/surface.tsx
292
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
351
+ import { useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
293
352
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
294
353
  var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
295
354
  var hidden = {
@@ -313,6 +372,7 @@ var HtmlInCanvas = ({
313
372
  const video = useVideo();
314
373
  const frame = useFrame();
315
374
  const readiness = useReadiness();
375
+ const scene = useContext(SceneContext);
316
376
  const surfaceWidth = width ?? video.width;
317
377
  const surfaceHeight = height ?? video.height;
318
378
  const source = useRef(null);
@@ -350,8 +410,8 @@ var HtmlInCanvas = ({
350
410
  frame,
351
411
  fps: video.fps,
352
412
  seconds: frame / video.fps,
353
- progress: frame / Math.max(1, video.durationInFrames - 1),
354
- durationInFrames: video.durationInFrames,
413
+ progress: scene ? Math.min(1, Math.max(0, (frame - scene.start) / Math.max(1, scene.durationInFrames - 1))) : frame / Math.max(1, video.durationInFrames - 1),
414
+ durationInFrames: scene ? scene.durationInFrames : video.durationInFrames,
355
415
  width: surfaceWidth,
356
416
  height: surfaceHeight
357
417
  };
@@ -515,7 +575,6 @@ var filmGrain = defineShaderEffect({
515
575
  uniforms: { amount: numberUniform(0.06) },
516
576
  fragmentShader: `
517
577
  uniform float amount;
518
- float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
519
578
  void main() {
520
579
  vec2 uv = gl_FragCoord.xy / resolution;
521
580
  float noise = hash(gl_FragCoord.xy + frame) - 0.5;
@@ -550,6 +609,7 @@ export {
550
609
  HtmlInCanvas,
551
610
  SHADER_PRELUDE,
552
611
  ShaderCompileError,
612
+ assertPortableShader,
553
613
  barrelDistortion,
554
614
  captureElement,
555
615
  captureMethod,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/effects/capture.ts","../src/effects/shader.ts","../src/effects/surface.tsx","../src/effects/library.ts"],"sourcesContent":["/**\n * Turn a live DOM subtree into an image, deterministically.\n *\n * Chromium can draw an SVG onto a canvas, and an SVG `foreignObject` can hold\n * ordinary markup, which is the whole trick. The reason it needs care is that\n * the foreignObject is a sealed document: nothing outside it reaches in, so a\n * subtree that looks right on the page rasterizes unstyled unless every rule\n * that applied to it is carried across as an inline declaration.\n *\n * There is a native alternative, `CanvasRenderingContext2D.drawElement`, which\n * would make all of this unnecessary. It is not in the pinned render browser,\n * measured rather than assumed, so this is the only path that produces the\n * same pixels in Studio and in an export. `captureMethod()` reports which one\n * is in use so Studio never has to guess.\n */\n\n/** Which capture strategy this browser can actually offer. */\nexport type CaptureMethod = \"native\" | \"foreign-object\";\n\ntype DrawElementContext = CanvasRenderingContext2D & {drawElement?: unknown};\n\nexport const captureMethod = (): CaptureMethod => {\n if (typeof document === \"undefined\") return \"foreign-object\";\n const probe = document.createElement(\"canvas\").getContext(\"2d\") as DrawElementContext | null;\n return probe && typeof probe.drawElement === \"function\" ? \"native\" : \"foreign-object\";\n};\n\n/*\n * Properties worth carrying, rather than everything getComputedStyle returns.\n *\n * A computed style has around 340 entries, and writing all of them onto every\n * node produces a document megabytes wide that the SVG parser then has to\n * read, once per frame. This is the set that changes how a box is painted;\n * anything absent is either inherited from a parent that also carries it or\n * has no effect on a static rasterization.\n */\nconst PAINTED = [\n \"align-items\", \"background\", \"border\", \"border-radius\", \"box-shadow\", \"box-sizing\", \"clip-path\",\n \"color\", \"display\", \"filter\", \"flex\", \"flex-direction\", \"flex-wrap\", \"font\", \"gap\", \"grid\",\n \"grid-template-columns\", \"grid-template-rows\", \"height\", \"justify-content\", \"left\",\n \"letter-spacing\", \"line-height\", \"margin\", \"mix-blend-mode\", \"opacity\", \"overflow\", \"padding\",\n \"position\", \"right\", \"text-align\", \"text-decoration\", \"text-shadow\", \"text-transform\", \"top\",\n \"transform\", \"transform-origin\", \"white-space\", \"width\", \"word-break\", \"writing-mode\",\n \"z-index\",\n] as const;\n\n/**\n * Copy the painted style of every node onto the node itself.\n *\n * The clone is walked beside the original because a clone has no computed\n * style of its own: it is not in the document, so the cascade never ran for\n * it. The two trees are the same shape, so one index walks both.\n */\nconst inlineStyles = (source: Element, clone: Element) => {\n const computed = window.getComputedStyle(source);\n const declarations: string[] = [];\n for (const property of PAINTED) {\n const value = computed.getPropertyValue(property);\n if (value && value !== \"none\" && value !== \"normal\" && value !== \"auto\") {\n declarations.push(`${property}:${value}`);\n }\n }\n clone.setAttribute(\"style\", declarations.join(\";\"));\n clone.removeAttribute(\"class\");\n\n const sourceChildren = source.children;\n const cloneChildren = clone.children;\n for (let index = 0; index < sourceChildren.length; index += 1) {\n const child = cloneChildren[index];\n if (child) inlineStyles(sourceChildren[index], child);\n }\n};\n\n/** The subtree as standalone XHTML, carrying everything it needs to paint. */\nexport const serializeElement = (element: HTMLElement): string => {\n const clone = element.cloneNode(true) as HTMLElement;\n inlineStyles(element, clone);\n\n /*\n * The root is placed by whatever was hiding it, and that placement must not\n * travel into the picture.\n *\n * A surface keeps the subtree it captures off screen, at `position: fixed;\n * left: -99999px`. Copying the computed style faithfully carried those two\n * declarations into the SVG, where they are not \"off screen\" but \"outside\n * the frame\", and every capture came back empty. Inside the foreignObject\n * the root is simply the whole picture, so it says so.\n */\n clone.style.position = \"static\";\n clone.style.left = \"auto\";\n clone.style.top = \"auto\";\n clone.style.right = \"auto\";\n clone.style.bottom = \"auto\";\n clone.style.transform = \"none\";\n clone.style.pointerEvents = \"auto\";\n\n // XHTML, because an SVG foreignObject is parsed as XML and unclosed tags\n // that a browser forgives in HTML are a parse error here.\n return new XMLSerializer().serializeToString(clone);\n};\n\n/**\n * Rasterize a subtree at a fixed size.\n *\n * Fonts first: a face that has not loaded when the SVG is parsed falls back,\n * and the fallback is what gets baked into the pixels. The decode is awaited\n * rather than assumed, so a caller holding the frame releases it only when\n * there is something to capture.\n */\nexport const captureElement = async (\n element: HTMLElement,\n options: {width: number; height: number},\n): Promise<HTMLImageElement> => {\n const {width, height} = options;\n if (document.fonts?.ready) await document.fonts.ready;\n\n const markup = serializeElement(element);\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\">`,\n markup,\n `</div></foreignObject></svg>`,\n ].join(\"\");\n\n const image = new Image(width, height);\n // A data URL rather than a blob URL: a blob is revoked on a timer somebody\n // has to own, and the frame budget is not the place for that bookkeeping.\n image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n await image.decode();\n return image;\n};\n","/**\n * A fragment shader, and everything around it that nobody should have to\n * write twice.\n *\n * An effect is one fragment shader and its named uniforms. The context, the\n * program, the full-screen triangle, the source texture, the framebuffers a\n * multi-pass chain ping-pongs between, and the teardown are all owned here,\n * because every video that reached for a shader was otherwise rebuilding the\n * same two hundred lines and getting the determinism details wrong.\n *\n * Time is not one of the things a shader may ask the browser for. Every\n * built-in uniform derives from the frame, so frame 90 is the same picture\n * whether it was rendered on its own or after eighty-nine others.\n */\n\nexport type UniformValue = number | readonly number[];\n\nexport type UniformSpec = {\n /** How many floats: 1, 2, 3, or 4. Decides which uniform call is made. */\n size: 1 | 2 | 3 | 4;\n defaultValue: UniformValue;\n};\n\nexport const numberUniform = (defaultValue: number): UniformSpec => ({size: 1, defaultValue});\nexport const vec2Uniform = (defaultValue: readonly [number, number]): UniformSpec => ({size: 2, defaultValue});\nexport const vec3Uniform = (defaultValue: readonly [number, number, number]): UniformSpec => ({size: 3, defaultValue});\nexport const vec4Uniform = (defaultValue: readonly [number, number, number, number]): UniformSpec => ({\n size: 4,\n defaultValue,\n});\n\nexport type ShaderEffectDefinition = {\n name: string;\n fragmentShader: string;\n uniforms: Record<string, UniformSpec>;\n};\n\n/** One configured use of an effect, which is what a surface is handed. */\nexport type EffectInstance = {\n definition: ShaderEffectDefinition;\n values: Record<string, UniformValue>;\n};\n\n/** A factory: call it with overrides to get something to put in `effects`. */\nexport type EffectFactory = (values?: Record<string, UniformValue>) => EffectInstance;\n\nexport const defineShaderEffect = (definition: ShaderEffectDefinition): EffectFactory => {\n return (values = {}) => ({definition, values});\n};\n\n/*\n * One triangle that covers the screen, not two that make a quad.\n *\n * A quad's diagonal is a seam the rasterizer visits twice, and any effect\n * reading its neighbourhood can show it. The triangle is bigger than the\n * viewport and clipped, which costs nothing and has no interior edge.\n */\nconst VERTEX_SHADER = `#version 300 es\nvoid main() {\n vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n gl_Position = vec4(corners[gl_VertexID], 0.0, 1.0);\n}`;\n\n/** Prepended to every effect, so a shader author writes only the body. */\nexport const SHADER_PRELUDE = `#version 300 es\nprecision highp float;\nuniform sampler2D source;\nuniform vec2 resolution;\nuniform float frame;\nuniform float fps;\nuniform float seconds;\nuniform float progress;\nout vec4 odoriColour;\n// sample is a reserved word in GLSL ES 3.00, so the helper is tex.\nvec4 tex(vec2 uv) { return texture(source, uv); }\n`;\n\nexport class ShaderCompileError extends Error {\n constructor(\n readonly effect: string,\n readonly log: string,\n readonly source: string,\n ) {\n /* The log names a line in the assembled source, and the assembled source\n is not what the author wrote, so the offending line is quoted here\n rather than left as a number to count to. */\n const line = Number(/(\\d+):(\\d+)/.exec(log)?.[2] ?? 0);\n const quoted = source.split(\"\\n\")[line - 1];\n super(`${effect}: ${log.trim()}${quoted ? `\\n ${line} | ${quoted.trim()}` : \"\"}`);\n this.name = \"ShaderCompileError\";\n }\n}\n\nconst compile = (gl: WebGL2RenderingContext, type: number, source: string, effect: string) => {\n const shader = gl.createShader(type);\n if (!shader) throw new Error(`${effect}: the browser refused to create a shader.`);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader) ?? \"compilation failed\";\n gl.deleteShader(shader);\n throw new ShaderCompileError(effect, log, source);\n }\n return shader;\n};\n\ntype Pass = {\n program: WebGLProgram;\n locations: Map<string, WebGLUniformLocation | null>;\n definition: ShaderEffectDefinition;\n};\n\n/** A texture and the framebuffer that draws into it. */\ntype Target = {texture: WebGLTexture; framebuffer: WebGLFramebuffer};\n\nconst createTarget = (gl: WebGL2RenderingContext, width: number, height: number): Target => {\n const texture = gl.createTexture()!;\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n // Clamped and linear: an effect that reaches past an edge should find the\n // edge repeated rather than the opposite side of the picture.\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n\n const framebuffer = gl.createFramebuffer()!;\n gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);\n gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n return {texture, framebuffer};\n};\n\nexport type PipelineClock = {frame: number; fps: number; durationInFrames: number};\n\n/**\n * The compiled chain for one surface. Built once, painted every frame, and\n * disposed when the surface unmounts.\n */\nexport class EffectPipeline {\n private readonly gl: WebGL2RenderingContext;\n private readonly passes: Pass[] = [];\n private readonly sourceTexture: WebGLTexture;\n private targets: [Target, Target] | null = null;\n private width = 0;\n private height = 0;\n\n constructor(canvas: HTMLCanvasElement, effects: readonly EffectInstance[]) {\n const gl = canvas.getContext(\"webgl2\", {\n // The buffer is read back by odori test and by the screenshot, and a\n // composited buffer is cleared unless this is asked for.\n preserveDrawingBuffer: true,\n premultipliedAlpha: false,\n antialias: false,\n });\n if (!gl) {\n throw new Error(\n \"WebGL2 is unavailable. Studio needs hardware or software GL; the render worker enables ANGLE's software backend itself.\",\n );\n }\n this.gl = gl;\n this.sourceTexture = gl.createTexture()!;\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n\n const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER, \"odori\");\n for (const {definition} of effects) {\n const source = `${SHADER_PRELUDE}${definition.fragmentShader}`;\n const program = gl.createProgram()!;\n gl.attachShader(program, vertex);\n gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, source, definition.name));\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(`${definition.name}: ${gl.getProgramInfoLog(program) ?? \"link failed\"}`);\n }\n const locations = new Map<string, WebGLUniformLocation | null>();\n for (const name of [\"source\", \"resolution\", \"frame\", \"fps\", \"seconds\", \"progress\", ...Object.keys(definition.uniforms)]) {\n locations.set(name, gl.getUniformLocation(program, name));\n }\n this.passes.push({program, locations, definition});\n }\n }\n\n /** The renderer as Studio reports it, for the capability panel. */\n get renderer(): string {\n const debug = this.gl.getExtension(\"WEBGL_debug_renderer_info\");\n return String(this.gl.getParameter(debug ? debug.UNMASKED_RENDERER_WEBGL : this.gl.RENDERER));\n }\n\n private resize(width: number, height: number) {\n if (this.width === width && this.height === height && this.targets) return;\n this.disposeTargets();\n this.width = width;\n this.height = height;\n // Only needed when more than one pass has to hand its output on.\n this.targets = this.passes.length > 1 ? [createTarget(this.gl, width, height), createTarget(this.gl, width, height)] : null;\n }\n\n private setUniforms(pass: Pass, effect: EffectInstance, clock: PipelineClock) {\n const {gl} = this;\n const at = (name: string) => pass.locations.get(name) ?? null;\n gl.uniform1i(at(\"source\"), 0);\n gl.uniform2f(at(\"resolution\"), this.width, this.height);\n gl.uniform1f(at(\"frame\"), clock.frame);\n gl.uniform1f(at(\"fps\"), clock.fps);\n gl.uniform1f(at(\"seconds\"), clock.frame / clock.fps);\n gl.uniform1f(at(\"progress\"), clock.frame / Math.max(1, clock.durationInFrames - 1));\n\n for (const [name, spec] of Object.entries(pass.definition.uniforms)) {\n const value = effect.values[name] ?? spec.defaultValue;\n const location = at(name);\n if (location === null) continue;\n const numbers = typeof value === \"number\" ? [value] : [...value];\n if (spec.size === 1) gl.uniform1f(location, numbers[0]);\n else if (spec.size === 2) gl.uniform2f(location, numbers[0], numbers[1]);\n else if (spec.size === 3) gl.uniform3f(location, numbers[0], numbers[1], numbers[2]);\n else gl.uniform4f(location, numbers[0], numbers[1], numbers[2], numbers[3]);\n }\n }\n\n /** Upload the captured picture and run every pass over it. */\n paint(source: TexImageSource, effects: readonly EffectInstance[], clock: PipelineClock) {\n const {gl} = this;\n const canvas = gl.canvas as HTMLCanvasElement;\n this.resize(canvas.width, canvas.height);\n gl.viewport(0, 0, this.width, this.height);\n\n // Uploaded once per frame however many passes read it.\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);\n\n let input = this.sourceTexture;\n this.passes.forEach((pass, index) => {\n const last = index === this.passes.length - 1;\n const target = last || !this.targets ? null : this.targets[index % 2];\n gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.framebuffer : null);\n gl.useProgram(pass.program);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, input);\n this.setUniforms(pass, effects[index], clock);\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n if (target) input = target.texture;\n });\n\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n // The screenshot must not see a queued frame.\n gl.finish();\n }\n\n private disposeTargets() {\n if (!this.targets) return;\n for (const target of this.targets) {\n this.gl.deleteFramebuffer(target.framebuffer);\n this.gl.deleteTexture(target.texture);\n }\n this.targets = null;\n }\n\n dispose() {\n this.disposeTargets();\n this.gl.deleteTexture(this.sourceTexture);\n for (const pass of this.passes) this.gl.deleteProgram(pass.program);\n this.passes.length = 0;\n }\n}\n","\"use client\";\n\nimport {useEffect, useLayoutEffect, useRef, useState, type ReactNode} from \"react\";\nimport {useFrame, useReadiness, useVideo} from \"../context\";\nimport {captureElement, captureMethod, type CaptureMethod} from \"./capture\";\nimport {EffectPipeline, ShaderCompileError, type EffectInstance} from \"./shader\";\n\n/** `useLayoutEffect` warns during server rendering, where there is no DOM. */\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport type SurfaceClock = {\n frame: number;\n fps: number;\n seconds: number;\n /** 0 to 1 across the composition. */\n progress: number;\n durationInFrames: number;\n width: number;\n height: number;\n};\n\nexport type HtmlInCanvasPaint<Renderer> = (\n args: SurfaceClock & {renderer: Renderer; source: CanvasImageSource; canvas: HTMLCanvasElement},\n) => void;\n\nexport type HtmlInCanvasInit<Renderer> = (args: {canvas: HTMLCanvasElement; width: number; height: number}) => Renderer;\n\n/**\n * What Studio shows about a surface, so a difference between preview and\n * export is diagnosable without opening devtools.\n */\nexport type SurfaceCapabilities = {\n capture: CaptureMethod;\n backend: \"canvas-2d\" | \"webgl2\";\n renderer?: string;\n error?: string;\n};\n\nconst hidden: React.CSSProperties = {\n // Laid out at full size and painted, because an element with `display:none`\n // has no computed style worth copying, but kept off screen and out of the\n // way of hit testing.\n left: \"-99999px\",\n pointerEvents: \"none\",\n position: \"fixed\",\n top: 0,\n};\n\n/**\n * Capture a React subtree once per frame and hand the picture to a painter.\n *\n * The children are ordinary React, laid out by the browser, which is the whole\n * point: typography and product UI stay in the language they are written in\n * and only the compositing is different. They are rendered off screen rather\n * than into the canvas, captured, and then drawn.\n *\n * Determinism is the contract. The frame is held from before the capture\n * starts until after the paint has finished, so an export never screenshots a\n * surface that is halfway through, and every value a painter is given comes\n * from the frame rather than from a clock.\n */\nexport const HtmlInCanvas = <Renderer,>({\n children,\n width,\n height,\n onInit,\n onPaint,\n onDispose,\n onCapabilities,\n}: {\n children: ReactNode;\n /** Defaults to the composition's own size. */\n width?: number;\n height?: number;\n onInit?: HtmlInCanvasInit<Renderer>;\n onPaint?: HtmlInCanvasPaint<Renderer>;\n onDispose?: (renderer: Renderer) => void;\n onCapabilities?: (capabilities: SurfaceCapabilities) => void;\n}) => {\n const video = useVideo();\n const frame = useFrame();\n const readiness = useReadiness();\n const surfaceWidth = width ?? video.width;\n const surfaceHeight = height ?? video.height;\n\n const source = useRef<HTMLDivElement>(null);\n const canvas = useRef<HTMLCanvasElement>(null);\n const renderer = useRef<Renderer | null>(null);\n const [failure, setFailure] = useState<string | null>(null);\n\n /* Kept in refs so a new inline callback does not re-run the paint effect,\n which would repaint on every render rather than every frame. */\n const latest = useRef({onInit, onPaint, onDispose, onCapabilities});\n latest.current = {onInit, onPaint, onDispose, onCapabilities};\n\n useIsomorphicLayoutEffect(() => {\n const element = canvas.current;\n if (!element || !latest.current.onInit) return undefined;\n try {\n renderer.current = latest.current.onInit({canvas: element, width: surfaceWidth, height: surfaceHeight});\n } catch (error) {\n setFailure(error instanceof Error ? error.message : String(error));\n }\n return () => {\n if (renderer.current !== null) latest.current.onDispose?.(renderer.current);\n renderer.current = null;\n };\n }, [surfaceWidth, surfaceHeight]);\n\n useIsomorphicLayoutEffect(() => {\n const host = source.current;\n const element = canvas.current;\n if (!host || !element) return undefined;\n\n /*\n * A composition is mounted more than once.\n *\n * The runtime renders it a second time inside its audio pass, hidden, to\n * collect every cue without seeking the timeline. That copy is real React\n * with real refs, so this effect runs there too, and it held the frame\n * while trying to photograph an element with no layout. The hold never\n * came back and the export waited for a readiness that could not arrive.\n *\n * offsetWidth is layout, not paint, so it is zero exactly when there is\n * nothing to capture and unaffected by the scale Studio fits the stage\n * with. No layout, no picture, no hold.\n */\n if (host.offsetWidth === 0 || host.offsetHeight === 0) return undefined;\n\n const release = readiness.hold();\n let cancelled = false;\n\n void (async () => {\n try {\n const picture = await captureElement(host, {width: surfaceWidth, height: surfaceHeight});\n if (cancelled) return;\n const paint = latest.current.onPaint;\n const clock: SurfaceClock = {\n frame,\n fps: video.fps,\n seconds: frame / video.fps,\n progress: frame / Math.max(1, video.durationInFrames - 1),\n durationInFrames: video.durationInFrames,\n width: surfaceWidth,\n height: surfaceHeight,\n };\n if (paint) {\n paint({...clock, renderer: renderer.current as Renderer, source: picture, canvas: element});\n } else {\n // No painter: the surface is just a deterministic rasterization.\n const context = element.getContext(\"2d\");\n context?.clearRect(0, 0, element.width, element.height);\n context?.drawImage(picture, 0, 0, element.width, element.height);\n }\n } catch (error) {\n if (!cancelled) setFailure(error instanceof Error ? error.message : String(error));\n } finally {\n release();\n }\n })();\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [frame, surfaceWidth, surfaceHeight]);\n\n useEffect(() => {\n latest.current.onCapabilities?.({\n capture: captureMethod(),\n backend: latest.current.onInit ? \"webgl2\" : \"canvas-2d\",\n ...(failure ? {error: failure} : {}),\n });\n }, [failure]);\n\n return (\n <>\n {/* The subtree being captured. Off screen, not hidden: a display:none\n element has no layout and nothing to photograph. */}\n <div\n ref={source}\n /* Marked so the checks know this is the thing being photographed and\n not part of the picture. Without it, every word in the subtree is\n reported as escaping the canvas, which it does, on purpose. */\n data-odori-capture=\"\"\n style={{...hidden, height: surfaceHeight, width: surfaceWidth}}\n aria-hidden=\"true\"\n >\n {children}\n </div>\n <canvas\n ref={canvas}\n width={surfaceWidth}\n height={surfaceHeight}\n style={{display: \"block\", height: \"100%\", width: \"100%\"}}\n />\n {failure ? (\n <pre\n style={{\n background: \"#1a0b0b\",\n color: \"#ff9d9d\",\n font: \"13px ui-monospace, monospace\",\n inset: 0,\n margin: 0,\n padding: 24,\n position: \"absolute\",\n whiteSpace: \"pre-wrap\",\n }}\n >\n {failure}\n </pre>\n ) : null}\n </>\n );\n};\n\n/**\n * The same capture, with a chain of shaders over it.\n *\n * One surface runs every effect in a single pipeline. Nesting two surfaces\n * would capture and upload the DOM twice for one picture, which is why the\n * composition is a list here rather than something to wrap repeatedly.\n */\nexport const EffectSurface = ({\n children,\n effects,\n width,\n height,\n onCapabilities,\n}: {\n children: ReactNode;\n effects: readonly EffectInstance[];\n width?: number;\n height?: number;\n onCapabilities?: (capabilities: SurfaceCapabilities) => void;\n}) => {\n const pipeline = useRef<EffectPipeline | null>(null);\n const list = useRef(effects);\n list.current = effects;\n\n return (\n <HtmlInCanvas<EffectPipeline | null>\n width={width}\n height={height}\n onInit={({canvas}) => {\n try {\n pipeline.current = new EffectPipeline(canvas, list.current);\n } catch (error) {\n pipeline.current = null;\n throw error instanceof ShaderCompileError ? error : new Error(String(error));\n }\n return pipeline.current;\n }}\n onPaint={({renderer, source, frame, fps, durationInFrames}) => {\n if (!renderer) return;\n renderer.paint(source as TexImageSource, list.current, {frame, fps, durationInFrames});\n }}\n onDispose={(renderer) => renderer?.dispose()}\n onCapabilities={onCapabilities}\n >\n {children}\n </HtmlInCanvas>\n );\n};\n","import {defineShaderEffect, numberUniform, vec2Uniform} from \"./shader\";\n\n/**\n * The effects Odori ships, written the way a project would write its own.\n *\n * Each is one fragment shader against `SHADER_PRELUDE`, so `tex(uv)` reads\n * the captured picture and `resolution`, `frame`, `fps`, `seconds` and\n * `progress` are already in scope. Nothing here reaches for a clock, and\n * nothing carries state between frames, which is what lets a worker render\n * frame 200 without having rendered frame 199.\n */\n\n/** Pull the picture out through a lens, or push it in. */\nexport const barrelDistortion = defineShaderEffect({\n name: \"barrel-distortion\",\n uniforms: {\n amount: numberUniform(0.3),\n radius: numberUniform(0.6),\n centre: vec2Uniform([0.5, 0.5]),\n },\n fragmentShader: `\nuniform float amount;\nuniform float radius;\nuniform vec2 centre;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n // Corrected for the frame's shape, or a wide composition bulges into an\n // oval instead of a circle.\n vec2 aspect = vec2(resolution.x / resolution.y, 1.0);\n vec2 offset = (uv - centre) * aspect;\n float distance = length(offset) / max(radius, 0.0001);\n float scale = 1.0 + amount * (1.0 - clamp(distance, 0.0, 1.0));\n odoriColour = tex(centre + offset / scale / aspect);\n}`,\n});\n\n/** Separate the channels, the way a cheap lens does at its edges. */\nexport const rgbSplit = defineShaderEffect({\n name: \"rgb-split\",\n uniforms: {amount: numberUniform(4.0), angle: numberUniform(0.0)},\n fragmentShader: `\nuniform float amount;\nuniform float angle;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n vec2 shift = vec2(cos(angle), sin(angle)) * amount / resolution;\n odoriColour = vec4(tex(uv + shift).r, tex(uv).g, tex(uv - shift).b, tex(uv).a);\n}`,\n});\n\n/** Horizontal lines, restrained enough to read as a screen and not a costume. */\nexport const scanlines = defineShaderEffect({\n name: \"scanlines\",\n uniforms: {opacity: numberUniform(0.08), spacing: numberUniform(3.0)},\n fragmentShader: `\nuniform float opacity;\nuniform float spacing;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n float line = step(0.5, fract(gl_FragCoord.y / max(spacing, 1.0)));\n odoriColour = tex(uv) * (1.0 - opacity * line);\n}`,\n});\n\n/** Quantise to blocks. Sizes in output pixels, so it reads the same at any scale. */\nexport const pixelate = defineShaderEffect({\n name: \"pixelate\",\n uniforms: {size: numberUniform(8.0)},\n fragmentShader: `\nuniform float size;\nvoid main() {\n float block = max(size, 1.0);\n vec2 snapped = (floor(gl_FragCoord.xy / block) + 0.5) * block;\n odoriColour = tex(snapped / resolution);\n}`,\n});\n\n/**\n * Grain. Seeded from the frame rather than from a random number, so the same\n * frame has the same grain every time it is rendered.\n */\nexport const filmGrain = defineShaderEffect({\n name: \"film-grain\",\n uniforms: {amount: numberUniform(0.06)},\n fragmentShader: `\nuniform float amount;\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n float noise = hash(gl_FragCoord.xy + frame) - 0.5;\n odoriColour = tex(uv) + vec4(vec3(noise * amount), 0.0);\n}`,\n});\n\n/** Magnify a circle of the picture, like a loupe held over it. */\nexport const magnify = defineShaderEffect({\n name: \"magnify\",\n uniforms: {\n amount: numberUniform(1.6),\n radius: numberUniform(0.22),\n centre: vec2Uniform([0.5, 0.5]),\n softness: numberUniform(0.06),\n },\n fragmentShader: `\nuniform float amount;\nuniform float radius;\nuniform vec2 centre;\nuniform float softness;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n vec2 aspect = vec2(resolution.x / resolution.y, 1.0);\n vec2 offset = (uv - centre) * aspect;\n float inside = 1.0 - smoothstep(radius - softness, radius + softness, length(offset));\n float scale = mix(1.0, max(amount, 0.0001), inside);\n odoriColour = tex(centre + offset / scale / aspect);\n}`,\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/effects/capture.ts","../src/effects/shader.ts","../src/effects/surface.tsx","../src/effects/library.ts"],"sourcesContent":["/**\n * Turn a live DOM subtree into an image, deterministically.\n *\n * Chromium can draw an SVG onto a canvas, and an SVG `foreignObject` can hold\n * ordinary markup, which is the whole trick. The reason it needs care is that\n * the foreignObject is a sealed document: nothing outside it reaches in, so a\n * subtree that looks right on the page rasterizes unstyled unless every rule\n * that applied to it is carried across as an inline declaration.\n *\n * There is a native alternative, `CanvasRenderingContext2D.drawElement`, which\n * would make all of this unnecessary. It is not in the pinned render browser,\n * measured rather than assumed, so this is the only path that produces the\n * same pixels in Studio and in an export. `captureMethod()` reports which one\n * is in use so Studio never has to guess.\n */\n\n/** Which capture strategy this browser can actually offer. */\nexport type CaptureMethod = \"native\" | \"foreign-object\";\n\ntype DrawElementContext = CanvasRenderingContext2D & {drawElement?: unknown};\n\nexport const captureMethod = (): CaptureMethod => {\n if (typeof document === \"undefined\") return \"foreign-object\";\n const probe = document.createElement(\"canvas\").getContext(\"2d\") as DrawElementContext | null;\n return probe && typeof probe.drawElement === \"function\" ? \"native\" : \"foreign-object\";\n};\n\n/*\n * Properties worth carrying, rather than everything getComputedStyle returns.\n *\n * A computed style has around 340 entries, and writing all of them onto every\n * node produces a document megabytes wide that the SVG parser then has to\n * read, once per frame. This is the set that changes how a box is painted;\n * anything absent is either inherited from a parent that also carries it or\n * has no effect on a static rasterization.\n */\n/*\n * The `font` shorthand is in here and is not enough on its own: Chrome returns\n * an empty string for it from a computed style, so a subtree captured with only\n * the shorthand loses its type entirely and rasterizes at the SVG default of\n * 16px serif. The longhands are what actually carry it.\n */\nconst PAINTED = [\n \"align-items\", \"background\", \"border\", \"border-radius\", \"box-shadow\", \"box-sizing\", \"clip-path\",\n \"color\", \"display\", \"filter\", \"flex\", \"flex-direction\", \"flex-wrap\", \"font\", \"gap\", \"grid\",\n \"grid-template-columns\", \"grid-template-rows\", \"height\", \"justify-content\", \"left\",\n \"letter-spacing\", \"line-height\", \"margin\", \"mix-blend-mode\", \"opacity\", \"overflow\", \"padding\",\n \"font-family\", \"font-size\", \"font-stretch\", \"font-style\", \"font-variant\", \"font-weight\",\n \"position\", \"right\", \"text-align\", \"text-decoration\", \"text-shadow\", \"text-transform\", \"top\",\n \"transform\", \"transform-origin\", \"white-space\", \"width\", \"word-break\", \"writing-mode\",\n \"z-index\",\n] as const;\n\n/**\n * Copy the painted style of every node onto the node itself.\n *\n * The clone is walked beside the original because a clone has no computed\n * style of its own: it is not in the document, so the cascade never ran for\n * it. The two trees are the same shape, so one index walks both.\n */\nconst inlineStyles = (source: Element, clone: Element) => {\n const computed = window.getComputedStyle(source);\n const declarations: string[] = [];\n for (const property of PAINTED) {\n const value = computed.getPropertyValue(property);\n if (value && value !== \"none\" && value !== \"normal\" && value !== \"auto\") {\n declarations.push(`${property}:${value}`);\n }\n }\n clone.setAttribute(\"style\", declarations.join(\";\"));\n clone.removeAttribute(\"class\");\n\n const sourceChildren = source.children;\n const cloneChildren = clone.children;\n for (let index = 0; index < sourceChildren.length; index += 1) {\n const child = cloneChildren[index];\n if (child) inlineStyles(sourceChildren[index], child);\n }\n};\n\n/**\n * Replace every canvas with a picture of itself.\n *\n * A canvas serializes to an empty element: its pixels live in a drawing buffer\n * that no amount of markup describes. So a chart, or another effect surface,\n * or a three.js scene sitting inside the subtree came out as a hole. Reading\n * each one back as a data URL and swapping in an `img` keeps them, and it is\n * what makes a surface composable with everything else that draws.\n *\n * A GL canvas only reads back if it was asked for with preserveDrawingBuffer,\n * which the pipeline here does. One that was not comes back blank rather than\n * throwing, so a foreign canvas degrades to a hole instead of a failure.\n */\nconst inlineCanvases = (source: Element, clone: Element) => {\n const sourceCanvases = source.querySelectorAll(\"canvas\");\n const cloneCanvases = clone.querySelectorAll(\"canvas\");\n for (let index = 0; index < sourceCanvases.length; index += 1) {\n const original = sourceCanvases[index];\n const placeholder = cloneCanvases[index];\n if (!placeholder) continue;\n let url = \"\";\n try {\n url = original.toDataURL(\"image/png\");\n } catch {\n /* Tainted by a cross-origin draw: nothing to be done, and losing one\n canvas is better than losing the frame. */\n }\n const image = clone.ownerDocument.createElement(\"img\");\n image.setAttribute(\"src\", url);\n image.setAttribute(\"width\", String(original.width));\n image.setAttribute(\"height\", String(original.height));\n image.setAttribute(\"style\", placeholder.getAttribute(\"style\") ?? \"\");\n placeholder.replaceWith(image);\n }\n};\n\n/** The subtree as standalone XHTML, carrying everything it needs to paint. */\nexport const serializeElement = (element: HTMLElement): string => {\n const clone = element.cloneNode(true) as HTMLElement;\n inlineStyles(element, clone);\n // After the styles, so the img inherits the box the canvas was laid out in.\n inlineCanvases(element, clone);\n\n /*\n * The root is placed by whatever was hiding it, and that placement must not\n * travel into the picture.\n *\n * A surface keeps the subtree it captures off screen, at `position: fixed;\n * left: -99999px`. Copying the computed style faithfully carried those two\n * declarations into the SVG, where they are not \"off screen\" but \"outside\n * the frame\", and every capture came back empty. Inside the foreignObject\n * the root is simply the whole picture, so it says so.\n */\n clone.style.position = \"static\";\n clone.style.left = \"auto\";\n clone.style.top = \"auto\";\n clone.style.right = \"auto\";\n clone.style.bottom = \"auto\";\n clone.style.transform = \"none\";\n clone.style.pointerEvents = \"auto\";\n\n // XHTML, because an SVG foreignObject is parsed as XML and unclosed tags\n // that a browser forgives in HTML are a parse error here.\n return new XMLSerializer().serializeToString(clone);\n};\n\n/**\n * Rasterize a subtree at a fixed size.\n *\n * Fonts first: a face that has not loaded when the SVG is parsed falls back,\n * and the fallback is what gets baked into the pixels. The decode is awaited\n * rather than assumed, so a caller holding the frame releases it only when\n * there is something to capture.\n */\nexport const captureElement = async (\n element: HTMLElement,\n options: {width: number; height: number},\n): Promise<HTMLImageElement> => {\n const {width, height} = options;\n if (document.fonts?.ready) await document.fonts.ready;\n\n const markup = serializeElement(element);\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\">`,\n markup,\n `</div></foreignObject></svg>`,\n ].join(\"\");\n\n const image = new Image(width, height);\n // A data URL rather than a blob URL: a blob is revoked on a timer somebody\n // has to own, and the frame budget is not the place for that bookkeeping.\n image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n await image.decode();\n return image;\n};\n","/**\n * A fragment shader, and everything around it that nobody should have to\n * write twice.\n *\n * An effect is one fragment shader and its named uniforms. The context, the\n * program, the full-screen triangle, the source texture, the framebuffers a\n * multi-pass chain ping-pongs between, and the teardown are all owned here,\n * because every video that reached for a shader was otherwise rebuilding the\n * same two hundred lines and getting the determinism details wrong.\n *\n * Time is not one of the things a shader may ask the browser for. Every\n * built-in uniform derives from the frame, so frame 90 is the same picture\n * whether it was rendered on its own or after eighty-nine others.\n */\n\nexport type UniformValue = number | readonly number[];\n\nexport type UniformSpec = {\n /** How many floats: 1, 2, 3, or 4. Decides which uniform call is made. */\n size: 1 | 2 | 3 | 4;\n defaultValue: UniformValue;\n};\n\nexport const numberUniform = (defaultValue: number): UniformSpec => ({size: 1, defaultValue});\nexport const vec2Uniform = (defaultValue: readonly [number, number]): UniformSpec => ({size: 2, defaultValue});\nexport const vec3Uniform = (defaultValue: readonly [number, number, number]): UniformSpec => ({size: 3, defaultValue});\nexport const vec4Uniform = (defaultValue: readonly [number, number, number, number]): UniformSpec => ({\n size: 4,\n defaultValue,\n});\n\nexport type ShaderEffectDefinition = {\n name: string;\n fragmentShader: string;\n uniforms: Record<string, UniformSpec>;\n};\n\n/** One configured use of an effect, which is what a surface is handed. */\nexport type EffectInstance = {\n definition: ShaderEffectDefinition;\n values: Record<string, UniformValue>;\n};\n\n/** A factory: call it with overrides to get something to put in `effects`. */\nexport type EffectFactory = (values?: Record<string, UniformValue>) => EffectInstance;\n\nexport const defineShaderEffect = (definition: ShaderEffectDefinition): EffectFactory => {\n // At definition time, so it fails when the module loads rather than on the\n // one frame somebody happens to render on the other backend.\n assertPortableShader(definition.name, definition.fragmentShader);\n return (values = {}) => ({definition, values});\n};\n\n/*\n * One triangle that covers the screen, not two that make a quad.\n *\n * A quad's diagonal is a seam the rasterizer visits twice, and any effect\n * reading its neighbourhood can show it. The triangle is bigger than the\n * viewport and clipped, which costs nothing and has no interior edge.\n */\nconst VERTEX_SHADER = `#version 300 es\nvoid main() {\n vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n gl_Position = vec4(corners[gl_VertexID], 0.0, 1.0);\n}`;\n\n/** Prepended to every effect, so a shader author writes only the body. */\nexport const SHADER_PRELUDE = `#version 300 es\nprecision highp float;\nuniform sampler2D source;\nuniform vec2 resolution;\nuniform float frame;\nuniform float fps;\nuniform float seconds;\nuniform float progress;\nout vec4 odoriColour;\n// sample is a reserved word in GLSL ES 3.00, so the helper is tex.\nvec4 tex(vec2 uv) { return texture(source, uv); }\n\n/*\n * Randomness that is the same on every backend.\n *\n * The idiom everyone reaches for is fract(sin(dot(p, k)) * 43758.5453), and it\n * is not portable: sin() of a large argument is implementation defined, so\n * SwiftShader and Metal disagree. Measured on one frame of grain, that idiom\n * scored 0.75 SSIM between the two while this one scores 0.999993. Integer\n * arithmetic in GLSL ES 3.00 is exact, so it is the only randomness a renderer\n * that has to reproduce itself can use.\n *\n * Seed from the frame, never from a clock, and the same frame keeps the same\n * noise however it was rendered.\n */\nuint odoriPcg(uint v) {\n uint state = v * 747796405u + 2891336453u;\n uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n return (word >> 22u) ^ word;\n}\nfloat hash(vec2 p) {\n uvec2 q = uvec2(ivec2(p));\n return float(odoriPcg(q.x ^ odoriPcg(q.y)) & 0xffffffu) / float(0xffffffu);\n}\n`;\n\nexport class ShaderCompileError extends Error {\n constructor(\n readonly effect: string,\n readonly log: string,\n readonly source: string,\n ) {\n /* The log names a line in the assembled source, and the assembled source\n is not what the author wrote, so the offending line is quoted here\n rather than left as a number to count to. */\n const line = Number(/(\\d+):(\\d+)/.exec(log)?.[2] ?? 0);\n const quoted = source.split(\"\\n\")[line - 1];\n super(`${effect}: ${log.trim()}${quoted ? `\\n ${line} | ${quoted.trim()}` : \"\"}`);\n this.name = \"ShaderCompileError\";\n }\n}\n\n/**\n * The one shader idiom that quietly breaks reproducibility.\n *\n * `fract(sin(dot(p, k)) * large)` is the hash in every shader tutorial, and it\n * produces different noise on different backends because sin() of a large\n * argument is implementation defined. A video written with it renders one way\n * on a laptop and another in CI, with nothing to say why. The prelude ships a\n * portable `hash(vec2)`, so this is refused rather than warned about.\n */\nexport const assertPortableShader = (name: string, fragmentShader: string) => {\n if (/fract\\s*\\(\\s*sin\\s*\\(/.test(fragmentShader)) {\n throw new Error(\n `${name}: fract(sin(...)) is not reproducible. sin() of a large argument is implementation defined, so this renders differently on different graphics backends. Use the prelude's hash(vec2), which is exact everywhere.`,\n );\n }\n};\n\nconst compile = (gl: WebGL2RenderingContext, type: number, source: string, effect: string) => {\n const shader = gl.createShader(type);\n if (!shader) throw new Error(`${effect}: the browser refused to create a shader.`);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader) ?? \"compilation failed\";\n gl.deleteShader(shader);\n throw new ShaderCompileError(effect, log, source);\n }\n return shader;\n};\n\ntype Pass = {\n program: WebGLProgram;\n locations: Map<string, WebGLUniformLocation | null>;\n definition: ShaderEffectDefinition;\n};\n\n/** A texture and the framebuffer that draws into it. */\ntype Target = {texture: WebGLTexture; framebuffer: WebGLFramebuffer};\n\nconst createTarget = (gl: WebGL2RenderingContext, width: number, height: number): Target => {\n const texture = gl.createTexture()!;\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n // Clamped and linear: an effect that reaches past an edge should find the\n // edge repeated rather than the opposite side of the picture.\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n\n const framebuffer = gl.createFramebuffer()!;\n gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);\n gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n return {texture, framebuffer};\n};\n\nexport type PipelineClock = {frame: number; fps: number; durationInFrames: number};\n\n/**\n * The compiled chain for one surface. Built once, painted every frame, and\n * disposed when the surface unmounts.\n */\nexport class EffectPipeline {\n private readonly gl: WebGL2RenderingContext;\n private readonly passes: Pass[] = [];\n private readonly sourceTexture: WebGLTexture;\n private targets: [Target, Target] | null = null;\n private width = 0;\n private height = 0;\n\n constructor(canvas: HTMLCanvasElement, effects: readonly EffectInstance[]) {\n const gl = canvas.getContext(\"webgl2\", {\n // The buffer is read back by odori test and by the screenshot, and a\n // composited buffer is cleared unless this is asked for.\n preserveDrawingBuffer: true,\n premultipliedAlpha: false,\n antialias: false,\n });\n if (!gl) {\n throw new Error(\n \"WebGL2 is unavailable. Studio needs hardware or software GL; the render worker enables ANGLE's software backend itself.\",\n );\n }\n this.gl = gl;\n this.sourceTexture = gl.createTexture()!;\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n\n const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER, \"odori\");\n for (const {definition} of effects) {\n const source = `${SHADER_PRELUDE}${definition.fragmentShader}`;\n const program = gl.createProgram()!;\n gl.attachShader(program, vertex);\n gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, source, definition.name));\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(`${definition.name}: ${gl.getProgramInfoLog(program) ?? \"link failed\"}`);\n }\n const locations = new Map<string, WebGLUniformLocation | null>();\n for (const name of [\"source\", \"resolution\", \"frame\", \"fps\", \"seconds\", \"progress\", ...Object.keys(definition.uniforms)]) {\n locations.set(name, gl.getUniformLocation(program, name));\n }\n this.passes.push({program, locations, definition});\n }\n }\n\n /** The renderer as Studio reports it, for the capability panel. */\n get renderer(): string {\n const debug = this.gl.getExtension(\"WEBGL_debug_renderer_info\");\n return String(this.gl.getParameter(debug ? debug.UNMASKED_RENDERER_WEBGL : this.gl.RENDERER));\n }\n\n private resize(width: number, height: number) {\n if (this.width === width && this.height === height && this.targets) return;\n this.disposeTargets();\n this.width = width;\n this.height = height;\n // Only needed when more than one pass has to hand its output on.\n this.targets = this.passes.length > 1 ? [createTarget(this.gl, width, height), createTarget(this.gl, width, height)] : null;\n }\n\n private setUniforms(pass: Pass, effect: EffectInstance, clock: PipelineClock) {\n const {gl} = this;\n const at = (name: string) => pass.locations.get(name) ?? null;\n gl.uniform1i(at(\"source\"), 0);\n gl.uniform2f(at(\"resolution\"), this.width, this.height);\n gl.uniform1f(at(\"frame\"), clock.frame);\n gl.uniform1f(at(\"fps\"), clock.fps);\n gl.uniform1f(at(\"seconds\"), clock.frame / clock.fps);\n gl.uniform1f(at(\"progress\"), clock.frame / Math.max(1, clock.durationInFrames - 1));\n\n for (const [name, spec] of Object.entries(pass.definition.uniforms)) {\n const value = effect.values[name] ?? spec.defaultValue;\n const location = at(name);\n if (location === null) continue;\n const numbers = typeof value === \"number\" ? [value] : [...value];\n if (spec.size === 1) gl.uniform1f(location, numbers[0]);\n else if (spec.size === 2) gl.uniform2f(location, numbers[0], numbers[1]);\n else if (spec.size === 3) gl.uniform3f(location, numbers[0], numbers[1], numbers[2]);\n else gl.uniform4f(location, numbers[0], numbers[1], numbers[2], numbers[3]);\n }\n }\n\n /** Upload the captured picture and run every pass over it. */\n paint(source: TexImageSource, effects: readonly EffectInstance[], clock: PipelineClock) {\n const {gl} = this;\n const canvas = gl.canvas as HTMLCanvasElement;\n this.resize(canvas.width, canvas.height);\n gl.viewport(0, 0, this.width, this.height);\n\n // Uploaded once per frame however many passes read it.\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);\n\n let input = this.sourceTexture;\n this.passes.forEach((pass, index) => {\n const last = index === this.passes.length - 1;\n const target = last || !this.targets ? null : this.targets[index % 2];\n gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.framebuffer : null);\n gl.useProgram(pass.program);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, input);\n this.setUniforms(pass, effects[index], clock);\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n if (target) input = target.texture;\n });\n\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n // The screenshot must not see a queued frame.\n gl.finish();\n }\n\n private disposeTargets() {\n if (!this.targets) return;\n for (const target of this.targets) {\n this.gl.deleteFramebuffer(target.framebuffer);\n this.gl.deleteTexture(target.texture);\n }\n this.targets = null;\n }\n\n dispose() {\n this.disposeTargets();\n this.gl.deleteTexture(this.sourceTexture);\n for (const pass of this.passes) this.gl.deleteProgram(pass.program);\n this.passes.length = 0;\n }\n}\n","\"use client\";\n\nimport {useContext, useEffect, useLayoutEffect, useRef, useState, type ReactNode} from \"react\";\nimport {SceneContext, useFrame, useReadiness, useVideo} from \"../context\";\nimport {captureElement, captureMethod, type CaptureMethod} from \"./capture\";\nimport {EffectPipeline, ShaderCompileError, type EffectInstance} from \"./shader\";\n\n/** `useLayoutEffect` warns during server rendering, where there is no DOM. */\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport type SurfaceClock = {\n frame: number;\n fps: number;\n seconds: number;\n /** 0 to 1 across the composition. */\n progress: number;\n durationInFrames: number;\n width: number;\n height: number;\n};\n\nexport type HtmlInCanvasPaint<Renderer> = (\n args: SurfaceClock & {renderer: Renderer; source: CanvasImageSource; canvas: HTMLCanvasElement},\n) => void;\n\nexport type HtmlInCanvasInit<Renderer> = (args: {canvas: HTMLCanvasElement; width: number; height: number}) => Renderer;\n\n/**\n * What Studio shows about a surface, so a difference between preview and\n * export is diagnosable without opening devtools.\n */\nexport type SurfaceCapabilities = {\n capture: CaptureMethod;\n backend: \"canvas-2d\" | \"webgl2\";\n renderer?: string;\n error?: string;\n};\n\nconst hidden: React.CSSProperties = {\n // Laid out at full size and painted, because an element with `display:none`\n // has no computed style worth copying, but kept off screen and out of the\n // way of hit testing.\n left: \"-99999px\",\n pointerEvents: \"none\",\n position: \"fixed\",\n top: 0,\n};\n\n/**\n * Capture a React subtree once per frame and hand the picture to a painter.\n *\n * The children are ordinary React, laid out by the browser, which is the whole\n * point: typography and product UI stay in the language they are written in\n * and only the compositing is different. They are rendered off screen rather\n * than into the canvas, captured, and then drawn.\n *\n * Determinism is the contract. The frame is held from before the capture\n * starts until after the paint has finished, so an export never screenshots a\n * surface that is halfway through, and every value a painter is given comes\n * from the frame rather than from a clock.\n */\nexport const HtmlInCanvas = <Renderer,>({\n children,\n width,\n height,\n onInit,\n onPaint,\n onDispose,\n onCapabilities,\n}: {\n children: ReactNode;\n /** Defaults to the composition's own size. */\n width?: number;\n height?: number;\n onInit?: HtmlInCanvasInit<Renderer>;\n onPaint?: HtmlInCanvasPaint<Renderer>;\n onDispose?: (renderer: Renderer) => void;\n onCapabilities?: (capabilities: SurfaceCapabilities) => void;\n}) => {\n const video = useVideo();\n const frame = useFrame();\n const readiness = useReadiness();\n /*\n * Progress is through the scene, not through the video.\n *\n * A component placed in a scene means its motion to finish when that scene\n * does. Measuring against the whole cut instead made a three second reveal\n * inside a twelve second video arrive already over, which is not a setting\n * anybody would choose. Read from context rather than useScene() so a\n * surface used outside a scene still works, falling back to the video.\n */\n const scene = useContext(SceneContext);\n const surfaceWidth = width ?? video.width;\n const surfaceHeight = height ?? video.height;\n\n const source = useRef<HTMLDivElement>(null);\n const canvas = useRef<HTMLCanvasElement>(null);\n const renderer = useRef<Renderer | null>(null);\n const [failure, setFailure] = useState<string | null>(null);\n\n /* Kept in refs so a new inline callback does not re-run the paint effect,\n which would repaint on every render rather than every frame. */\n const latest = useRef({onInit, onPaint, onDispose, onCapabilities});\n latest.current = {onInit, onPaint, onDispose, onCapabilities};\n\n useIsomorphicLayoutEffect(() => {\n const element = canvas.current;\n if (!element || !latest.current.onInit) return undefined;\n try {\n renderer.current = latest.current.onInit({canvas: element, width: surfaceWidth, height: surfaceHeight});\n } catch (error) {\n setFailure(error instanceof Error ? error.message : String(error));\n }\n return () => {\n if (renderer.current !== null) latest.current.onDispose?.(renderer.current);\n renderer.current = null;\n };\n }, [surfaceWidth, surfaceHeight]);\n\n useIsomorphicLayoutEffect(() => {\n const host = source.current;\n const element = canvas.current;\n if (!host || !element) return undefined;\n\n /*\n * A composition is mounted more than once.\n *\n * The runtime renders it a second time inside its audio pass, hidden, to\n * collect every cue without seeking the timeline. That copy is real React\n * with real refs, so this effect runs there too, and it held the frame\n * while trying to photograph an element with no layout. The hold never\n * came back and the export waited for a readiness that could not arrive.\n *\n * offsetWidth is layout, not paint, so it is zero exactly when there is\n * nothing to capture and unaffected by the scale Studio fits the stage\n * with. No layout, no picture, no hold.\n */\n if (host.offsetWidth === 0 || host.offsetHeight === 0) return undefined;\n\n const release = readiness.hold();\n let cancelled = false;\n\n void (async () => {\n try {\n const picture = await captureElement(host, {width: surfaceWidth, height: surfaceHeight});\n if (cancelled) return;\n const paint = latest.current.onPaint;\n const clock: SurfaceClock = {\n frame,\n fps: video.fps,\n seconds: frame / video.fps,\n progress: scene\n ? Math.min(1, Math.max(0, (frame - scene.start) / Math.max(1, scene.durationInFrames - 1)))\n : frame / Math.max(1, video.durationInFrames - 1),\n durationInFrames: scene ? scene.durationInFrames : video.durationInFrames,\n width: surfaceWidth,\n height: surfaceHeight,\n };\n if (paint) {\n paint({...clock, renderer: renderer.current as Renderer, source: picture, canvas: element});\n } else {\n // No painter: the surface is just a deterministic rasterization.\n const context = element.getContext(\"2d\");\n context?.clearRect(0, 0, element.width, element.height);\n context?.drawImage(picture, 0, 0, element.width, element.height);\n }\n } catch (error) {\n if (!cancelled) setFailure(error instanceof Error ? error.message : String(error));\n } finally {\n release();\n }\n })();\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [frame, surfaceWidth, surfaceHeight]);\n\n useEffect(() => {\n latest.current.onCapabilities?.({\n capture: captureMethod(),\n backend: latest.current.onInit ? \"webgl2\" : \"canvas-2d\",\n ...(failure ? {error: failure} : {}),\n });\n }, [failure]);\n\n return (\n <>\n {/* The subtree being captured. Off screen, not hidden: a display:none\n element has no layout and nothing to photograph. */}\n <div\n ref={source}\n /* Marked so the checks know this is the thing being photographed and\n not part of the picture. Without it, every word in the subtree is\n reported as escaping the canvas, which it does, on purpose. */\n data-odori-capture=\"\"\n style={{...hidden, height: surfaceHeight, width: surfaceWidth}}\n aria-hidden=\"true\"\n >\n {children}\n </div>\n <canvas\n ref={canvas}\n width={surfaceWidth}\n height={surfaceHeight}\n style={{display: \"block\", height: \"100%\", width: \"100%\"}}\n />\n {failure ? (\n <pre\n style={{\n background: \"#1a0b0b\",\n color: \"#ff9d9d\",\n font: \"13px ui-monospace, monospace\",\n inset: 0,\n margin: 0,\n padding: 24,\n position: \"absolute\",\n whiteSpace: \"pre-wrap\",\n }}\n >\n {failure}\n </pre>\n ) : null}\n </>\n );\n};\n\n/**\n * The same capture, with a chain of shaders over it.\n *\n * One surface runs every effect in a single pipeline. Nesting two surfaces\n * would capture and upload the DOM twice for one picture, which is why the\n * composition is a list here rather than something to wrap repeatedly.\n */\nexport const EffectSurface = ({\n children,\n effects,\n width,\n height,\n onCapabilities,\n}: {\n children: ReactNode;\n effects: readonly EffectInstance[];\n width?: number;\n height?: number;\n onCapabilities?: (capabilities: SurfaceCapabilities) => void;\n}) => {\n const pipeline = useRef<EffectPipeline | null>(null);\n const list = useRef(effects);\n list.current = effects;\n\n return (\n <HtmlInCanvas<EffectPipeline | null>\n width={width}\n height={height}\n onInit={({canvas}) => {\n try {\n pipeline.current = new EffectPipeline(canvas, list.current);\n } catch (error) {\n pipeline.current = null;\n throw error instanceof ShaderCompileError ? error : new Error(String(error));\n }\n return pipeline.current;\n }}\n onPaint={({renderer, source, frame, fps, durationInFrames}) => {\n if (!renderer) return;\n renderer.paint(source as TexImageSource, list.current, {frame, fps, durationInFrames});\n }}\n onDispose={(renderer) => renderer?.dispose()}\n onCapabilities={onCapabilities}\n >\n {children}\n </HtmlInCanvas>\n );\n};\n","import {defineShaderEffect, numberUniform, vec2Uniform} from \"./shader\";\n\n/**\n * The effects Odori ships, written the way a project would write its own.\n *\n * Each is one fragment shader against `SHADER_PRELUDE`, so `tex(uv)` reads\n * the captured picture and `resolution`, `frame`, `fps`, `seconds` and\n * `progress` are already in scope. Nothing here reaches for a clock, and\n * nothing carries state between frames, which is what lets a worker render\n * frame 200 without having rendered frame 199.\n */\n\n/** Pull the picture out through a lens, or push it in. */\nexport const barrelDistortion = defineShaderEffect({\n name: \"barrel-distortion\",\n uniforms: {\n amount: numberUniform(0.3),\n radius: numberUniform(0.6),\n centre: vec2Uniform([0.5, 0.5]),\n },\n fragmentShader: `\nuniform float amount;\nuniform float radius;\nuniform vec2 centre;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n // Corrected for the frame's shape, or a wide composition bulges into an\n // oval instead of a circle.\n vec2 aspect = vec2(resolution.x / resolution.y, 1.0);\n vec2 offset = (uv - centre) * aspect;\n float distance = length(offset) / max(radius, 0.0001);\n float scale = 1.0 + amount * (1.0 - clamp(distance, 0.0, 1.0));\n odoriColour = tex(centre + offset / scale / aspect);\n}`,\n});\n\n/** Separate the channels, the way a cheap lens does at its edges. */\nexport const rgbSplit = defineShaderEffect({\n name: \"rgb-split\",\n uniforms: {amount: numberUniform(4.0), angle: numberUniform(0.0)},\n fragmentShader: `\nuniform float amount;\nuniform float angle;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n vec2 shift = vec2(cos(angle), sin(angle)) * amount / resolution;\n odoriColour = vec4(tex(uv + shift).r, tex(uv).g, tex(uv - shift).b, tex(uv).a);\n}`,\n});\n\n/** Horizontal lines, restrained enough to read as a screen and not a costume. */\nexport const scanlines = defineShaderEffect({\n name: \"scanlines\",\n uniforms: {opacity: numberUniform(0.08), spacing: numberUniform(3.0)},\n fragmentShader: `\nuniform float opacity;\nuniform float spacing;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n float line = step(0.5, fract(gl_FragCoord.y / max(spacing, 1.0)));\n odoriColour = tex(uv) * (1.0 - opacity * line);\n}`,\n});\n\n/** Quantise to blocks. Sizes in output pixels, so it reads the same at any scale. */\nexport const pixelate = defineShaderEffect({\n name: \"pixelate\",\n uniforms: {size: numberUniform(8.0)},\n fragmentShader: `\nuniform float size;\nvoid main() {\n float block = max(size, 1.0);\n vec2 snapped = (floor(gl_FragCoord.xy / block) + 0.5) * block;\n odoriColour = tex(snapped / resolution);\n}`,\n});\n\n/**\n * Grain. Seeded from the frame rather than from a random number, so the same\n * frame has the same grain every time it is rendered.\n */\nexport const filmGrain = defineShaderEffect({\n name: \"film-grain\",\n uniforms: {amount: numberUniform(0.06)},\n fragmentShader: `\nuniform float amount;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n float noise = hash(gl_FragCoord.xy + frame) - 0.5;\n odoriColour = tex(uv) + vec4(vec3(noise * amount), 0.0);\n}`,\n});\n\n/** Magnify a circle of the picture, like a loupe held over it. */\nexport const magnify = defineShaderEffect({\n name: \"magnify\",\n uniforms: {\n amount: numberUniform(1.6),\n radius: numberUniform(0.22),\n centre: vec2Uniform([0.5, 0.5]),\n softness: numberUniform(0.06),\n },\n fragmentShader: `\nuniform float amount;\nuniform float radius;\nuniform vec2 centre;\nuniform float softness;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution;\n vec2 aspect = vec2(resolution.x / resolution.y, 1.0);\n vec2 offset = (uv - centre) * aspect;\n float inside = 1.0 - smoothstep(radius - softness, radius + softness, length(offset));\n float scale = mix(1.0, max(amount, 0.0001), inside);\n odoriColour = tex(centre + offset / scale / aspect);\n}`,\n});\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -102,6 +102,85 @@ type AudioPlaybackOptions = {
102
102
  */
103
103
  declare const useAudioPlayback: ({ track, frame, fps, playing, muted, masterGain, soloCue, scrubbing, rate, onBlocked, onFailed, }: AudioPlaybackOptions) => void;
104
104
 
105
+ /**
106
+ * Narration as source.
107
+ *
108
+ * A recorded voice is the one part of a video that usually lives outside the
109
+ * project: a file somebody exported once, with captions transcribed after the
110
+ * fact and timed by hand. Odori keeps it in the repository instead. The
111
+ * artifact is a small JSON document holding the script, the voice, and the
112
+ * time every word starts and ends, written by `odori narrate` in the same
113
+ * call that writes the audio.
114
+ *
115
+ * Everything downstream derives from that document. Captions are computed
116
+ * from the word timings at the project's fps, so they cannot drift from the
117
+ * recording; a scene can hold until a sentence lands, because the sentence's
118
+ * end is data; re-recording is one command, and the diff shows exactly which
119
+ * words moved. The render never talks to a provider, because by then the
120
+ * narration is just a file and some numbers.
121
+ */
122
+ /** One spoken word and when it happens, in seconds from the start. */
123
+ type NarrationWord = {
124
+ word: string;
125
+ startSeconds: number;
126
+ endSeconds: number;
127
+ };
128
+ type Narration = {
129
+ /** The script as given, so a re-record can be diffed against it. */
130
+ script: string;
131
+ /** Provider and voice that read it, so a re-record sounds the same. */
132
+ provider: string;
133
+ voice: string;
134
+ /** The audio file, as a path under public/ or a brand cue role. */
135
+ audio: string;
136
+ words: NarrationWord[];
137
+ };
138
+ /**
139
+ * Character-level timing, the shape speech APIs return: three parallel
140
+ * arrays, one entry per character of the spoken text.
141
+ */
142
+ type CharacterAlignment = {
143
+ characters: string[];
144
+ startSeconds: number[];
145
+ endSeconds: number[];
146
+ };
147
+ /**
148
+ * Fold character timings into word timings.
149
+ *
150
+ * A word runs from its first character's start to its last character's end.
151
+ * Whitespace separates; punctuation stays attached to the word it follows,
152
+ * because captions read better with it than without.
153
+ */
154
+ declare const wordsFromCharacters: (alignment: CharacterAlignment) => NarrationWord[];
155
+ type CaptionCueTiming = {
156
+ text: string;
157
+ fromFrame: number;
158
+ durationInFrames: number;
159
+ };
160
+ type CaptionOptions = {
161
+ /** Upper bound per caption. Fewer arrive when punctuation or a pause ends one. */
162
+ maxWords?: number;
163
+ /** A silence at least this long ends the caption, because the speaker did. */
164
+ breakOnGapSeconds?: number;
165
+ /** Hold the last caption of a group this long after its final word. */
166
+ hangSeconds?: number;
167
+ };
168
+ /**
169
+ * Group word timings into caption cues at a given fps.
170
+ *
171
+ * Deterministic on purpose: the same narration and fps produce the same cues
172
+ * on every machine, so captions are computed where they are used rather than
173
+ * stored, and a change of fps or grouping is a re-render rather than a
174
+ * re-transcription.
175
+ *
176
+ * A caption ends at a sentence mark, at a real pause, or at `maxWords`,
177
+ * whichever comes first. It stays on screen until the next one starts or its
178
+ * hang runs out, so text never vanishes mid-phrase.
179
+ */
180
+ declare const captionCues: (narration: Pick<Narration, "words">, fps: number, options?: CaptionOptions) => CaptionCueTiming[];
181
+ /** The narration's last spoken moment, for sizing a scene to it. */
182
+ declare const narrationEndSeconds: (narration: Pick<Narration, "words">) => number;
183
+
105
184
  type PlaybackOptions = {
106
185
  fps: number;
107
186
  durationInFrames: number;
@@ -516,4 +595,4 @@ declare const canonicalJson: (value: unknown) => string;
516
595
  declare const hashString: (input: string) => string;
517
596
  declare const hashValue: (value: unknown) => string;
518
597
 
519
- export { type AssetRegistry, type AudioPlaybackOptions, AudioTrack, type Biquad, Brand, type CanvasDraw, CompiledTimeline, type CursorState, type CursorStop, Duration, Easing, type EasingFunction, type FrameState, type InterpolateOptions, ParsableSchema, type PlaceholderOptions, type Playback, type PlaybackOptions, type PrepareContext, type PrepareFunction, type Readiness, RenderSurface, type SceneState, Signal, type SpringOptions, type TypingOptions, type TypingState, VideoEntry, VideoLayout, type VideoMetadata, type VideoMetadataInput, Viewer, type ViewerProps, advanceFrames, canonicalJson, createAssetRegistry, cursorAt, cursorDuration, definePrepare, defineVideoMetadata, drawHtml, encodeWav, hashString, hashValue, integratedLufs, interpolate, isValidVideoId, placeholderFrame, placeholderImage, placeholderSvg, random, randomBetween, randomOrder, randomPick, resolveVideoId, spring, typedAt, typingFrames, useAssets, useAudioPlayback, useBrand, useCanvas, useDesignScale, useFrame, useLayout, usePlayback, useReadiness, useScene, useTyping, useVideo };
598
+ export { type AssetRegistry, type AudioPlaybackOptions, AudioTrack, type Biquad, Brand, type CanvasDraw, type CaptionCueTiming, type CaptionOptions, type CharacterAlignment, CompiledTimeline, type CursorState, type CursorStop, Duration, Easing, type EasingFunction, type FrameState, type InterpolateOptions, type Narration, type NarrationWord, ParsableSchema, type PlaceholderOptions, type Playback, type PlaybackOptions, type PrepareContext, type PrepareFunction, type Readiness, RenderSurface, type SceneState, Signal, type SpringOptions, type TypingOptions, type TypingState, VideoEntry, VideoLayout, type VideoMetadata, type VideoMetadataInput, Viewer, type ViewerProps, advanceFrames, canonicalJson, captionCues, createAssetRegistry, cursorAt, cursorDuration, definePrepare, defineVideoMetadata, drawHtml, encodeWav, hashString, hashValue, integratedLufs, interpolate, isValidVideoId, narrationEndSeconds, placeholderFrame, placeholderImage, placeholderSvg, random, randomBetween, randomOrder, randomPick, resolveVideoId, spring, typedAt, typingFrames, useAssets, useAudioPlayback, useBrand, useCanvas, useDesignScale, useFrame, useLayout, usePlayback, useReadiness, useScene, useTyping, useVideo, wordsFromCharacters };
package/dist/index.js CHANGED
@@ -444,6 +444,55 @@ var Viewer = ({
444
444
  ] });
445
445
  };
446
446
 
447
+ // src/narration.ts
448
+ var wordsFromCharacters = (alignment) => {
449
+ const words = [];
450
+ let current = "";
451
+ let start = 0;
452
+ let end = 0;
453
+ alignment.characters.forEach((character, index) => {
454
+ if (/\s/.test(character)) {
455
+ if (current) words.push({ word: current, startSeconds: start, endSeconds: end });
456
+ current = "";
457
+ return;
458
+ }
459
+ if (!current) start = alignment.startSeconds[index];
460
+ current += character;
461
+ end = alignment.endSeconds[index];
462
+ });
463
+ if (current) words.push({ word: current, startSeconds: start, endSeconds: end });
464
+ return words;
465
+ };
466
+ var captionCues = (narration, fps, options = {}) => {
467
+ const { maxWords = 7, breakOnGapSeconds = 0.6, hangSeconds = 0.8 } = options;
468
+ const groups = [];
469
+ let group = [];
470
+ narration.words.forEach((word, index) => {
471
+ group.push(word);
472
+ const next = narration.words[index + 1];
473
+ const sentence = /[.!?]$/.test(word.word);
474
+ const clause = /[,;:]$/.test(word.word) && group.length >= Math.ceil(maxWords / 2);
475
+ const pause = next !== void 0 && next.startSeconds - word.endSeconds >= breakOnGapSeconds;
476
+ if (sentence || clause || pause || group.length >= maxWords || next === void 0) {
477
+ groups.push(group);
478
+ group = [];
479
+ }
480
+ });
481
+ return groups.map((words, index) => {
482
+ const start = words[0].startSeconds;
483
+ const spoken = words[words.length - 1].endSeconds;
484
+ const nextStart = groups[index + 1]?.[0].startSeconds;
485
+ const end = nextStart !== void 0 ? Math.min(spoken + hangSeconds, nextStart) : spoken + hangSeconds;
486
+ const fromFrame = Math.round(start * fps);
487
+ return {
488
+ text: words.map((entry) => entry.word).join(" "),
489
+ fromFrame,
490
+ durationInFrames: Math.max(1, Math.round(end * fps) - fromFrame)
491
+ };
492
+ });
493
+ };
494
+ var narrationEndSeconds = (narration) => narration.words.length > 0 ? narration.words[narration.words.length - 1].endSeconds : 0;
495
+
447
496
  // src/render-surface.tsx
448
497
  import { useEffect as useEffect3, useState as useState3 } from "react";
449
498
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -831,6 +880,7 @@ export {
831
880
  advanceFrames,
832
881
  brandCssVariables,
833
882
  canonicalJson,
883
+ captionCues,
834
884
  chord,
835
885
  createAssetRegistry,
836
886
  createRenderManifest,
@@ -870,6 +920,7 @@ export {
870
920
  loop,
871
921
  lowPass,
872
922
  mix,
923
+ narrationEndSeconds,
873
924
  noise,
874
925
  normalize,
875
926
  note,
@@ -914,6 +965,7 @@ export {
914
965
  useScene,
915
966
  useSceneTransition,
916
967
  useTyping,
917
- useVideo
968
+ useVideo,
969
+ wordsFromCharacters
918
970
  };
919
971
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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\n/**\n * The longest wall-clock gap the clock will believe in one tick.\n *\n * requestAnimationFrame stops firing when the tab is hidden, the window goes\n * to the background, or an embedded webview loses the cursor, and it resumes\n * with a single delta covering the entire gap. Handed to advanceFrames that is\n * a jump of hundreds of frames: a video that does not loop lands on its last\n * frame and stops, which is what a hang looks like from the outside, and one\n * that loops wraps to somewhere arbitrary.\n *\n * A gap this long is a suspended clock, not a slow frame, so it is capped\n * rather than trusted, and playback picks up a few frames on from where it\n * stopped. The cost is that a renderer slower than four frames a second falls\n * behind wall clock. That is the right way round for a preview: this clock\n * never touches an export, which the encoder drives frame by frame.\n */\nexport const MAX_TICK_MS = 250;\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, Math.min(now - previous, MAX_TICK_MS), 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 /**\n * True while the playhead is being moved by hand. Cues go quiet: auditioning\n * under the cursor sounds like a stuck record, because every frame reseeks\n * the element and you hear the same few milliseconds over and over.\n */\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 /*\n * A hidden tab is silent, and this is the check that makes it so. The\n * visibility listener below used to pause every element on its own, but\n * a hidden tab still ticks its throttled clock, so the very next frame\n * ran this pass, found the cue audible, and called play() again. What\n * came out was a cue restarted and reseeked once or twice a second for\n * as long as the tab stayed in the background, which is the glitching.\n * Deciding it here means the pause cannot be undone by the next tick.\n */\n const audible = playing && !scrubbing && !document.hidden;\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 // Visibility is not something React re-renders for, so the change is what\n // re-runs the pass. Which way it went does not matter: run() reads\n // document.hidden itself and either holds every cue or resyncs them to the\n // frame the timeline is actually showing.\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n const onVisibility = () => sync.current();\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
+ {"version":3,"sources":["../src/loudness.ts","../src/viewer.tsx","../src/playback.ts","../src/audio-playback.ts","../src/narration.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\n/**\n * The longest wall-clock gap the clock will believe in one tick.\n *\n * requestAnimationFrame stops firing when the tab is hidden, the window goes\n * to the background, or an embedded webview loses the cursor, and it resumes\n * with a single delta covering the entire gap. Handed to advanceFrames that is\n * a jump of hundreds of frames: a video that does not loop lands on its last\n * frame and stops, which is what a hang looks like from the outside, and one\n * that loops wraps to somewhere arbitrary.\n *\n * A gap this long is a suspended clock, not a slow frame, so it is capped\n * rather than trusted, and playback picks up a few frames on from where it\n * stopped. The cost is that a renderer slower than four frames a second falls\n * behind wall clock. That is the right way round for a preview: this clock\n * never touches an export, which the encoder drives frame by frame.\n */\nexport const MAX_TICK_MS = 250;\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, Math.min(now - previous, MAX_TICK_MS), 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 /**\n * True while the playhead is being moved by hand. Cues go quiet: auditioning\n * under the cursor sounds like a stuck record, because every frame reseeks\n * the element and you hear the same few milliseconds over and over.\n */\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 /*\n * A hidden tab is silent, and this is the check that makes it so. The\n * visibility listener below used to pause every element on its own, but\n * a hidden tab still ticks its throttled clock, so the very next frame\n * ran this pass, found the cue audible, and called play() again. What\n * came out was a cue restarted and reseeked once or twice a second for\n * as long as the tab stayed in the background, which is the glitching.\n * Deciding it here means the pause cannot be undone by the next tick.\n */\n const audible = playing && !scrubbing && !document.hidden;\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 // Visibility is not something React re-renders for, so the change is what\n // re-runs the pass. Which way it went does not matter: run() reads\n // document.hidden itself and either holds every cue or resyncs them to the\n // frame the timeline is actually showing.\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n const onVisibility = () => sync.current();\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => document.removeEventListener(\"visibilitychange\", onVisibility);\n }, []);\n};\n","/**\n * Narration as source.\n *\n * A recorded voice is the one part of a video that usually lives outside the\n * project: a file somebody exported once, with captions transcribed after the\n * fact and timed by hand. Odori keeps it in the repository instead. The\n * artifact is a small JSON document holding the script, the voice, and the\n * time every word starts and ends, written by `odori narrate` in the same\n * call that writes the audio.\n *\n * Everything downstream derives from that document. Captions are computed\n * from the word timings at the project's fps, so they cannot drift from the\n * recording; a scene can hold until a sentence lands, because the sentence's\n * end is data; re-recording is one command, and the diff shows exactly which\n * words moved. The render never talks to a provider, because by then the\n * narration is just a file and some numbers.\n */\n\n/** One spoken word and when it happens, in seconds from the start. */\nexport type NarrationWord = {\n word: string;\n startSeconds: number;\n endSeconds: number;\n};\n\nexport type Narration = {\n /** The script as given, so a re-record can be diffed against it. */\n script: string;\n /** Provider and voice that read it, so a re-record sounds the same. */\n provider: string;\n voice: string;\n /** The audio file, as a path under public/ or a brand cue role. */\n audio: string;\n words: NarrationWord[];\n};\n\n/**\n * Character-level timing, the shape speech APIs return: three parallel\n * arrays, one entry per character of the spoken text.\n */\nexport type CharacterAlignment = {\n characters: string[];\n startSeconds: number[];\n endSeconds: number[];\n};\n\n/**\n * Fold character timings into word timings.\n *\n * A word runs from its first character's start to its last character's end.\n * Whitespace separates; punctuation stays attached to the word it follows,\n * because captions read better with it than without.\n */\nexport const wordsFromCharacters = (alignment: CharacterAlignment): NarrationWord[] => {\n const words: NarrationWord[] = [];\n let current = \"\";\n let start = 0;\n let end = 0;\n\n alignment.characters.forEach((character, index) => {\n if (/\\s/.test(character)) {\n if (current) words.push({word: current, startSeconds: start, endSeconds: end});\n current = \"\";\n return;\n }\n if (!current) start = alignment.startSeconds[index];\n current += character;\n end = alignment.endSeconds[index];\n });\n if (current) words.push({word: current, startSeconds: start, endSeconds: end});\n return words;\n};\n\nexport type CaptionCueTiming = {\n text: string;\n fromFrame: number;\n durationInFrames: number;\n};\n\nexport type CaptionOptions = {\n /** Upper bound per caption. Fewer arrive when punctuation or a pause ends one. */\n maxWords?: number;\n /** A silence at least this long ends the caption, because the speaker did. */\n breakOnGapSeconds?: number;\n /** Hold the last caption of a group this long after its final word. */\n hangSeconds?: number;\n};\n\n/**\n * Group word timings into caption cues at a given fps.\n *\n * Deterministic on purpose: the same narration and fps produce the same cues\n * on every machine, so captions are computed where they are used rather than\n * stored, and a change of fps or grouping is a re-render rather than a\n * re-transcription.\n *\n * A caption ends at a sentence mark, at a real pause, or at `maxWords`,\n * whichever comes first. It stays on screen until the next one starts or its\n * hang runs out, so text never vanishes mid-phrase.\n */\nexport const captionCues = (\n narration: Pick<Narration, \"words\">,\n fps: number,\n options: CaptionOptions = {},\n): CaptionCueTiming[] => {\n const {maxWords = 7, breakOnGapSeconds = 0.6, hangSeconds = 0.8} = options;\n const groups: NarrationWord[][] = [];\n let group: NarrationWord[] = [];\n\n narration.words.forEach((word, index) => {\n group.push(word);\n const next = narration.words[index + 1];\n const sentence = /[.!?]$/.test(word.word);\n const clause = /[,;:]$/.test(word.word) && group.length >= Math.ceil(maxWords / 2);\n const pause = next !== undefined && next.startSeconds - word.endSeconds >= breakOnGapSeconds;\n if (sentence || clause || pause || group.length >= maxWords || next === undefined) {\n groups.push(group);\n group = [];\n }\n });\n\n return groups.map((words, index) => {\n const start = words[0].startSeconds;\n const spoken = words[words.length - 1].endSeconds;\n const nextStart = groups[index + 1]?.[0].startSeconds;\n // Held to the next caption or the hang, but never past the next caption.\n const end = nextStart !== undefined ? Math.min(spoken + hangSeconds, nextStart) : spoken + hangSeconds;\n const fromFrame = Math.round(start * fps);\n return {\n text: words.map((entry) => entry.word).join(\" \"),\n fromFrame,\n durationInFrames: Math.max(1, Math.round(end * fps) - fromFrame),\n };\n });\n};\n\n/** The narration's last spoken moment, for sizing a scene to it. */\nexport const narrationEndSeconds = (narration: Pick<Narration, \"words\">): number =>\n narration.words.length > 0 ? narration.words[narration.words.length - 1].endSeconds : 0;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;AAkBjC,IAAM,cAAc;AAEpB,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,KAAK,IAAI,MAAM,UAAU,WAAW,GAAG,KAAK,IAAI;AACvG,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;;;ACvIA,SAAQ,aAAAC,YAAW,UAAAC,eAAa;AA0CzB,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;AAUhB,YAAM,UAAU,WAAW,CAAC,aAAa,CAAC,SAAS;AAEnD,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;AAMjG,EAAAD,WAAU,MAAM;AACd,QAAI,OAAO,aAAa,YAAa;AACrC,UAAM,eAAe,MAAM,KAAK,QAAQ;AACxC,aAAS,iBAAiB,oBAAoB,YAAY;AAC1D,WAAO,MAAM,SAAS,oBAAoB,oBAAoB,YAAY;AAAA,EAC5E,GAAG,CAAC,CAAC;AACP;;;AFpEQ,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;;;AG9FO,IAAM,sBAAsB,CAAC,cAAmD;AACrF,QAAM,QAAyB,CAAC;AAChC,MAAI,UAAU;AACd,MAAI,QAAQ;AACZ,MAAI,MAAM;AAEV,YAAU,WAAW,QAAQ,CAAC,WAAW,UAAU;AACjD,QAAI,KAAK,KAAK,SAAS,GAAG;AACxB,UAAI,QAAS,OAAM,KAAK,EAAC,MAAM,SAAS,cAAc,OAAO,YAAY,IAAG,CAAC;AAC7E,gBAAU;AACV;AAAA,IACF;AACA,QAAI,CAAC,QAAS,SAAQ,UAAU,aAAa,KAAK;AAClD,eAAW;AACX,UAAM,UAAU,WAAW,KAAK;AAAA,EAClC,CAAC;AACD,MAAI,QAAS,OAAM,KAAK,EAAC,MAAM,SAAS,cAAc,OAAO,YAAY,IAAG,CAAC;AAC7E,SAAO;AACT;AA6BO,IAAM,cAAc,CACzB,WACA,KACA,UAA0B,CAAC,MACJ;AACvB,QAAM,EAAC,WAAW,GAAG,oBAAoB,KAAK,cAAc,IAAG,IAAI;AACnE,QAAM,SAA4B,CAAC;AACnC,MAAI,QAAyB,CAAC;AAE9B,YAAU,MAAM,QAAQ,CAAC,MAAM,UAAU;AACvC,UAAM,KAAK,IAAI;AACf,UAAM,OAAO,UAAU,MAAM,QAAQ,CAAC;AACtC,UAAM,WAAW,SAAS,KAAK,KAAK,IAAI;AACxC,UAAM,SAAS,SAAS,KAAK,KAAK,IAAI,KAAK,MAAM,UAAU,KAAK,KAAK,WAAW,CAAC;AACjF,UAAM,QAAQ,SAAS,UAAa,KAAK,eAAe,KAAK,cAAc;AAC3E,QAAI,YAAY,UAAU,SAAS,MAAM,UAAU,YAAY,SAAS,QAAW;AACjF,aAAO,KAAK,KAAK;AACjB,cAAQ,CAAC;AAAA,IACX;AAAA,EACF,CAAC;AAED,SAAO,OAAO,IAAI,CAAC,OAAO,UAAU;AAClC,UAAM,QAAQ,MAAM,CAAC,EAAE;AACvB,UAAM,SAAS,MAAM,MAAM,SAAS,CAAC,EAAE;AACvC,UAAM,YAAY,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE;AAEzC,UAAM,MAAM,cAAc,SAAY,KAAK,IAAI,SAAS,aAAa,SAAS,IAAI,SAAS;AAC3F,UAAM,YAAY,KAAK,MAAM,QAAQ,GAAG;AACxC,WAAO;AAAA,MACL,MAAM,MAAM,IAAI,CAAC,UAAU,MAAM,IAAI,EAAE,KAAK,GAAG;AAAA,MAC/C;AAAA,MACA,kBAAkB,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,GAAG,IAAI,SAAS;AAAA,IACjE;AAAA,EACF,CAAC;AACH;AAGO,IAAM,sBAAsB,CAAC,cAClC,UAAU,MAAM,SAAS,IAAI,UAAU,MAAM,UAAU,MAAM,SAAS,CAAC,EAAE,aAAa;;;ACxIxF,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;;;AC9DA,IAAM,WAAW,EAAC,MAAM,GAAG,qBAAqB,IAAI,OAAO,GAAG,QAAQ,GAAE;AAGjE,IAAM,eAAe,CAAC,MAAc,UAAyB,CAAC,GAAG,MAAM,OAAe;AAC3F,QAAM,EAAC,qBAAqB,MAAK,IAAI,EAAC,GAAG,UAAU,GAAG,QAAO;AAC7D,QAAM,QAAQ,KAAK,KAAK,KAAK,SAAS,KAAK,IAAI,GAAG,KAAK,CAAC;AACxD,SAAO,KAAK,KAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,MAAO,KAAK,IAAI,GAAG,mBAAmB,CAAC;AACxF;AAUO,IAAM,UAAU,CAAC,MAAc,OAAe,UAAyB,CAAC,GAAG,MAAM,OAAoB;AAC1G,QAAM,EAAC,MAAM,qBAAqB,OAAO,OAAM,IAAI,EAAC,GAAG,UAAU,GAAG,QAAO;AAC3E,QAAMC,QAAO,KAAK,IAAI,GAAG,KAAK;AAC9B,QAAM,UAAU,QAAQ;AACxB,QAAM,WAAW,KAAK,MAAO,UAAU,MAAO,mBAAmB;AACjE,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM,WAAWA,KAAI,IAAIA,KAAI,CAAC;AACpF,QAAM,OAAO,WAAW,KAAK,UAAU,KAAK;AAC5C,QAAM,aAAa,OAAO,aAAa,MAAM,SAAS,GAAG;AAGzD,QAAM,QAAQ,CAAC,QAAQ,QAAQ,aAAa,SAAS,WAAW,KAAK,QAAQ,aAAa,UAAU,MAAM,MAAM;AAEhH,SAAO;AAAA,IACL,MAAM,KAAK,MAAM,GAAG,MAAM;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,KAAK,WAAW,IAAI,IAAI,SAAS,KAAK;AAAA,EAClD;AACF;AAGO,IAAM,YAAY,CAAC,MAAc,UAAyB,CAAC,MAChE,QAAQ,MAAM,SAAS,GAAG,OAAO;;;ACtDnC,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","step","useEffect","useRef","useEffect","useRef","seconds"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odori",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "An independent React video framework with a first-class videos source root, deterministic frame runtime, timeline compiler, player, and render manifest.",
6
6
  "license": "Apache-2.0",