odori 0.0.7 → 0.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odori",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
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",