odori 0.0.6 → 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/{chunk-XN6DA264.js → chunk-EVICSOWU.js} +27 -74
- package/dist/chunk-EVICSOWU.js.map +1 -0
- package/dist/chunk-UMNWCL6I.js +74 -0
- package/dist/chunk-UMNWCL6I.js.map +1 -0
- package/dist/effects.d.ts +212 -0
- package/dist/effects.js +628 -0
- package/dist/effects.js.map +1 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/preview.js +4 -2
- package/dist/preview.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunk-XN6DA264.js.map +0 -1
package/dist/effects.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
SceneContext,
|
|
4
|
+
useFrame,
|
|
5
|
+
useReadiness,
|
|
6
|
+
useVideo
|
|
7
|
+
} from "./chunk-UMNWCL6I.js";
|
|
8
|
+
|
|
9
|
+
// src/effects/capture.ts
|
|
10
|
+
var captureMethod = () => {
|
|
11
|
+
if (typeof document === "undefined") return "foreign-object";
|
|
12
|
+
const probe = document.createElement("canvas").getContext("2d");
|
|
13
|
+
return probe && typeof probe.drawElement === "function" ? "native" : "foreign-object";
|
|
14
|
+
};
|
|
15
|
+
var PAINTED = [
|
|
16
|
+
"align-items",
|
|
17
|
+
"background",
|
|
18
|
+
"border",
|
|
19
|
+
"border-radius",
|
|
20
|
+
"box-shadow",
|
|
21
|
+
"box-sizing",
|
|
22
|
+
"clip-path",
|
|
23
|
+
"color",
|
|
24
|
+
"display",
|
|
25
|
+
"filter",
|
|
26
|
+
"flex",
|
|
27
|
+
"flex-direction",
|
|
28
|
+
"flex-wrap",
|
|
29
|
+
"font",
|
|
30
|
+
"gap",
|
|
31
|
+
"grid",
|
|
32
|
+
"grid-template-columns",
|
|
33
|
+
"grid-template-rows",
|
|
34
|
+
"height",
|
|
35
|
+
"justify-content",
|
|
36
|
+
"left",
|
|
37
|
+
"letter-spacing",
|
|
38
|
+
"line-height",
|
|
39
|
+
"margin",
|
|
40
|
+
"mix-blend-mode",
|
|
41
|
+
"opacity",
|
|
42
|
+
"overflow",
|
|
43
|
+
"padding",
|
|
44
|
+
"font-family",
|
|
45
|
+
"font-size",
|
|
46
|
+
"font-stretch",
|
|
47
|
+
"font-style",
|
|
48
|
+
"font-variant",
|
|
49
|
+
"font-weight",
|
|
50
|
+
"position",
|
|
51
|
+
"right",
|
|
52
|
+
"text-align",
|
|
53
|
+
"text-decoration",
|
|
54
|
+
"text-shadow",
|
|
55
|
+
"text-transform",
|
|
56
|
+
"top",
|
|
57
|
+
"transform",
|
|
58
|
+
"transform-origin",
|
|
59
|
+
"white-space",
|
|
60
|
+
"width",
|
|
61
|
+
"word-break",
|
|
62
|
+
"writing-mode",
|
|
63
|
+
"z-index"
|
|
64
|
+
];
|
|
65
|
+
var inlineStyles = (source, clone) => {
|
|
66
|
+
const computed = window.getComputedStyle(source);
|
|
67
|
+
const declarations = [];
|
|
68
|
+
for (const property of PAINTED) {
|
|
69
|
+
const value = computed.getPropertyValue(property);
|
|
70
|
+
if (value && value !== "none" && value !== "normal" && value !== "auto") {
|
|
71
|
+
declarations.push(`${property}:${value}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
clone.setAttribute("style", declarations.join(";"));
|
|
75
|
+
clone.removeAttribute("class");
|
|
76
|
+
const sourceChildren = source.children;
|
|
77
|
+
const cloneChildren = clone.children;
|
|
78
|
+
for (let index = 0; index < sourceChildren.length; index += 1) {
|
|
79
|
+
const child = cloneChildren[index];
|
|
80
|
+
if (child) inlineStyles(sourceChildren[index], child);
|
|
81
|
+
}
|
|
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
|
+
};
|
|
103
|
+
var serializeElement = (element) => {
|
|
104
|
+
const clone = element.cloneNode(true);
|
|
105
|
+
inlineStyles(element, clone);
|
|
106
|
+
inlineCanvases(element, clone);
|
|
107
|
+
clone.style.position = "static";
|
|
108
|
+
clone.style.left = "auto";
|
|
109
|
+
clone.style.top = "auto";
|
|
110
|
+
clone.style.right = "auto";
|
|
111
|
+
clone.style.bottom = "auto";
|
|
112
|
+
clone.style.transform = "none";
|
|
113
|
+
clone.style.pointerEvents = "auto";
|
|
114
|
+
return new XMLSerializer().serializeToString(clone);
|
|
115
|
+
};
|
|
116
|
+
var captureElement = async (element, options) => {
|
|
117
|
+
const { width, height } = options;
|
|
118
|
+
if (document.fonts?.ready) await document.fonts.ready;
|
|
119
|
+
const markup = serializeElement(element);
|
|
120
|
+
const svg = [
|
|
121
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">`,
|
|
122
|
+
`<foreignObject width="100%" height="100%">`,
|
|
123
|
+
`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${width}px;height:${height}px">`,
|
|
124
|
+
markup,
|
|
125
|
+
`</div></foreignObject></svg>`
|
|
126
|
+
].join("");
|
|
127
|
+
const image = new Image(width, height);
|
|
128
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
129
|
+
await image.decode();
|
|
130
|
+
return image;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/effects/shader.ts
|
|
134
|
+
var numberUniform = (defaultValue) => ({ size: 1, defaultValue });
|
|
135
|
+
var vec2Uniform = (defaultValue) => ({ size: 2, defaultValue });
|
|
136
|
+
var vec3Uniform = (defaultValue) => ({ size: 3, defaultValue });
|
|
137
|
+
var vec4Uniform = (defaultValue) => ({
|
|
138
|
+
size: 4,
|
|
139
|
+
defaultValue
|
|
140
|
+
});
|
|
141
|
+
var defineShaderEffect = (definition) => {
|
|
142
|
+
assertPortableShader(definition.name, definition.fragmentShader);
|
|
143
|
+
return (values = {}) => ({ definition, values });
|
|
144
|
+
};
|
|
145
|
+
var VERTEX_SHADER = `#version 300 es
|
|
146
|
+
void main() {
|
|
147
|
+
vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
|
148
|
+
gl_Position = vec4(corners[gl_VertexID], 0.0, 1.0);
|
|
149
|
+
}`;
|
|
150
|
+
var SHADER_PRELUDE = `#version 300 es
|
|
151
|
+
precision highp float;
|
|
152
|
+
uniform sampler2D source;
|
|
153
|
+
uniform vec2 resolution;
|
|
154
|
+
uniform float frame;
|
|
155
|
+
uniform float fps;
|
|
156
|
+
uniform float seconds;
|
|
157
|
+
uniform float progress;
|
|
158
|
+
out vec4 odoriColour;
|
|
159
|
+
// sample is a reserved word in GLSL ES 3.00, so the helper is tex.
|
|
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
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
var ShaderCompileError = class extends Error {
|
|
186
|
+
constructor(effect, log, source) {
|
|
187
|
+
const line = Number(/(\d+):(\d+)/.exec(log)?.[2] ?? 0);
|
|
188
|
+
const quoted = source.split("\n")[line - 1];
|
|
189
|
+
super(`${effect}: ${log.trim()}${quoted ? `
|
|
190
|
+
${line} | ${quoted.trim()}` : ""}`);
|
|
191
|
+
this.effect = effect;
|
|
192
|
+
this.log = log;
|
|
193
|
+
this.source = source;
|
|
194
|
+
this.name = "ShaderCompileError";
|
|
195
|
+
}
|
|
196
|
+
effect;
|
|
197
|
+
log;
|
|
198
|
+
source;
|
|
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
|
+
};
|
|
207
|
+
var compile = (gl, type, source, effect) => {
|
|
208
|
+
const shader = gl.createShader(type);
|
|
209
|
+
if (!shader) throw new Error(`${effect}: the browser refused to create a shader.`);
|
|
210
|
+
gl.shaderSource(shader, source);
|
|
211
|
+
gl.compileShader(shader);
|
|
212
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
213
|
+
const log = gl.getShaderInfoLog(shader) ?? "compilation failed";
|
|
214
|
+
gl.deleteShader(shader);
|
|
215
|
+
throw new ShaderCompileError(effect, log, source);
|
|
216
|
+
}
|
|
217
|
+
return shader;
|
|
218
|
+
};
|
|
219
|
+
var createTarget = (gl, width, height) => {
|
|
220
|
+
const texture = gl.createTexture();
|
|
221
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
222
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
223
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
224
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
225
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
226
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
227
|
+
const framebuffer = gl.createFramebuffer();
|
|
228
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
229
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
230
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
231
|
+
return { texture, framebuffer };
|
|
232
|
+
};
|
|
233
|
+
var EffectPipeline = class {
|
|
234
|
+
gl;
|
|
235
|
+
passes = [];
|
|
236
|
+
sourceTexture;
|
|
237
|
+
targets = null;
|
|
238
|
+
width = 0;
|
|
239
|
+
height = 0;
|
|
240
|
+
constructor(canvas, effects) {
|
|
241
|
+
const gl = canvas.getContext("webgl2", {
|
|
242
|
+
// The buffer is read back by odori test and by the screenshot, and a
|
|
243
|
+
// composited buffer is cleared unless this is asked for.
|
|
244
|
+
preserveDrawingBuffer: true,
|
|
245
|
+
premultipliedAlpha: false,
|
|
246
|
+
antialias: false
|
|
247
|
+
});
|
|
248
|
+
if (!gl) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"WebGL2 is unavailable. Studio needs hardware or software GL; the render worker enables ANGLE's software backend itself."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
this.gl = gl;
|
|
254
|
+
this.sourceTexture = gl.createTexture();
|
|
255
|
+
gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);
|
|
256
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
257
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
258
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
259
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
260
|
+
const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER, "odori");
|
|
261
|
+
for (const { definition } of effects) {
|
|
262
|
+
const source = `${SHADER_PRELUDE}${definition.fragmentShader}`;
|
|
263
|
+
const program = gl.createProgram();
|
|
264
|
+
gl.attachShader(program, vertex);
|
|
265
|
+
gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, source, definition.name));
|
|
266
|
+
gl.linkProgram(program);
|
|
267
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
268
|
+
throw new Error(`${definition.name}: ${gl.getProgramInfoLog(program) ?? "link failed"}`);
|
|
269
|
+
}
|
|
270
|
+
const locations = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const name of ["source", "resolution", "frame", "fps", "seconds", "progress", ...Object.keys(definition.uniforms)]) {
|
|
272
|
+
locations.set(name, gl.getUniformLocation(program, name));
|
|
273
|
+
}
|
|
274
|
+
this.passes.push({ program, locations, definition });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** The renderer as Studio reports it, for the capability panel. */
|
|
278
|
+
get renderer() {
|
|
279
|
+
const debug = this.gl.getExtension("WEBGL_debug_renderer_info");
|
|
280
|
+
return String(this.gl.getParameter(debug ? debug.UNMASKED_RENDERER_WEBGL : this.gl.RENDERER));
|
|
281
|
+
}
|
|
282
|
+
resize(width, height) {
|
|
283
|
+
if (this.width === width && this.height === height && this.targets) return;
|
|
284
|
+
this.disposeTargets();
|
|
285
|
+
this.width = width;
|
|
286
|
+
this.height = height;
|
|
287
|
+
this.targets = this.passes.length > 1 ? [createTarget(this.gl, width, height), createTarget(this.gl, width, height)] : null;
|
|
288
|
+
}
|
|
289
|
+
setUniforms(pass, effect, clock) {
|
|
290
|
+
const { gl } = this;
|
|
291
|
+
const at = (name) => pass.locations.get(name) ?? null;
|
|
292
|
+
gl.uniform1i(at("source"), 0);
|
|
293
|
+
gl.uniform2f(at("resolution"), this.width, this.height);
|
|
294
|
+
gl.uniform1f(at("frame"), clock.frame);
|
|
295
|
+
gl.uniform1f(at("fps"), clock.fps);
|
|
296
|
+
gl.uniform1f(at("seconds"), clock.frame / clock.fps);
|
|
297
|
+
gl.uniform1f(at("progress"), clock.frame / Math.max(1, clock.durationInFrames - 1));
|
|
298
|
+
for (const [name, spec] of Object.entries(pass.definition.uniforms)) {
|
|
299
|
+
const value = effect.values[name] ?? spec.defaultValue;
|
|
300
|
+
const location = at(name);
|
|
301
|
+
if (location === null) continue;
|
|
302
|
+
const numbers = typeof value === "number" ? [value] : [...value];
|
|
303
|
+
if (spec.size === 1) gl.uniform1f(location, numbers[0]);
|
|
304
|
+
else if (spec.size === 2) gl.uniform2f(location, numbers[0], numbers[1]);
|
|
305
|
+
else if (spec.size === 3) gl.uniform3f(location, numbers[0], numbers[1], numbers[2]);
|
|
306
|
+
else gl.uniform4f(location, numbers[0], numbers[1], numbers[2], numbers[3]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** Upload the captured picture and run every pass over it. */
|
|
310
|
+
paint(source, effects, clock) {
|
|
311
|
+
const { gl } = this;
|
|
312
|
+
const canvas = gl.canvas;
|
|
313
|
+
this.resize(canvas.width, canvas.height);
|
|
314
|
+
gl.viewport(0, 0, this.width, this.height);
|
|
315
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
316
|
+
gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);
|
|
317
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
318
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
319
|
+
let input = this.sourceTexture;
|
|
320
|
+
this.passes.forEach((pass, index) => {
|
|
321
|
+
const last = index === this.passes.length - 1;
|
|
322
|
+
const target = last || !this.targets ? null : this.targets[index % 2];
|
|
323
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.framebuffer : null);
|
|
324
|
+
gl.useProgram(pass.program);
|
|
325
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
326
|
+
gl.bindTexture(gl.TEXTURE_2D, input);
|
|
327
|
+
this.setUniforms(pass, effects[index], clock);
|
|
328
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
329
|
+
if (target) input = target.texture;
|
|
330
|
+
});
|
|
331
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
332
|
+
gl.finish();
|
|
333
|
+
}
|
|
334
|
+
disposeTargets() {
|
|
335
|
+
if (!this.targets) return;
|
|
336
|
+
for (const target of this.targets) {
|
|
337
|
+
this.gl.deleteFramebuffer(target.framebuffer);
|
|
338
|
+
this.gl.deleteTexture(target.texture);
|
|
339
|
+
}
|
|
340
|
+
this.targets = null;
|
|
341
|
+
}
|
|
342
|
+
dispose() {
|
|
343
|
+
this.disposeTargets();
|
|
344
|
+
this.gl.deleteTexture(this.sourceTexture);
|
|
345
|
+
for (const pass of this.passes) this.gl.deleteProgram(pass.program);
|
|
346
|
+
this.passes.length = 0;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// src/effects/surface.tsx
|
|
351
|
+
import { useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
352
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
353
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
354
|
+
var hidden = {
|
|
355
|
+
// Laid out at full size and painted, because an element with `display:none`
|
|
356
|
+
// has no computed style worth copying, but kept off screen and out of the
|
|
357
|
+
// way of hit testing.
|
|
358
|
+
left: "-99999px",
|
|
359
|
+
pointerEvents: "none",
|
|
360
|
+
position: "fixed",
|
|
361
|
+
top: 0
|
|
362
|
+
};
|
|
363
|
+
var HtmlInCanvas = ({
|
|
364
|
+
children,
|
|
365
|
+
width,
|
|
366
|
+
height,
|
|
367
|
+
onInit,
|
|
368
|
+
onPaint,
|
|
369
|
+
onDispose,
|
|
370
|
+
onCapabilities
|
|
371
|
+
}) => {
|
|
372
|
+
const video = useVideo();
|
|
373
|
+
const frame = useFrame();
|
|
374
|
+
const readiness = useReadiness();
|
|
375
|
+
const scene = useContext(SceneContext);
|
|
376
|
+
const surfaceWidth = width ?? video.width;
|
|
377
|
+
const surfaceHeight = height ?? video.height;
|
|
378
|
+
const source = useRef(null);
|
|
379
|
+
const canvas = useRef(null);
|
|
380
|
+
const renderer = useRef(null);
|
|
381
|
+
const [failure, setFailure] = useState(null);
|
|
382
|
+
const latest = useRef({ onInit, onPaint, onDispose, onCapabilities });
|
|
383
|
+
latest.current = { onInit, onPaint, onDispose, onCapabilities };
|
|
384
|
+
useIsomorphicLayoutEffect(() => {
|
|
385
|
+
const element = canvas.current;
|
|
386
|
+
if (!element || !latest.current.onInit) return void 0;
|
|
387
|
+
try {
|
|
388
|
+
renderer.current = latest.current.onInit({ canvas: element, width: surfaceWidth, height: surfaceHeight });
|
|
389
|
+
} catch (error) {
|
|
390
|
+
setFailure(error instanceof Error ? error.message : String(error));
|
|
391
|
+
}
|
|
392
|
+
return () => {
|
|
393
|
+
if (renderer.current !== null) latest.current.onDispose?.(renderer.current);
|
|
394
|
+
renderer.current = null;
|
|
395
|
+
};
|
|
396
|
+
}, [surfaceWidth, surfaceHeight]);
|
|
397
|
+
useIsomorphicLayoutEffect(() => {
|
|
398
|
+
const host = source.current;
|
|
399
|
+
const element = canvas.current;
|
|
400
|
+
if (!host || !element) return void 0;
|
|
401
|
+
if (host.offsetWidth === 0 || host.offsetHeight === 0) return void 0;
|
|
402
|
+
const release = readiness.hold();
|
|
403
|
+
let cancelled = false;
|
|
404
|
+
void (async () => {
|
|
405
|
+
try {
|
|
406
|
+
const picture = await captureElement(host, { width: surfaceWidth, height: surfaceHeight });
|
|
407
|
+
if (cancelled) return;
|
|
408
|
+
const paint = latest.current.onPaint;
|
|
409
|
+
const clock = {
|
|
410
|
+
frame,
|
|
411
|
+
fps: video.fps,
|
|
412
|
+
seconds: frame / video.fps,
|
|
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,
|
|
415
|
+
width: surfaceWidth,
|
|
416
|
+
height: surfaceHeight
|
|
417
|
+
};
|
|
418
|
+
if (paint) {
|
|
419
|
+
paint({ ...clock, renderer: renderer.current, source: picture, canvas: element });
|
|
420
|
+
} else {
|
|
421
|
+
const context = element.getContext("2d");
|
|
422
|
+
context?.clearRect(0, 0, element.width, element.height);
|
|
423
|
+
context?.drawImage(picture, 0, 0, element.width, element.height);
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (!cancelled) setFailure(error instanceof Error ? error.message : String(error));
|
|
427
|
+
} finally {
|
|
428
|
+
release();
|
|
429
|
+
}
|
|
430
|
+
})();
|
|
431
|
+
return () => {
|
|
432
|
+
cancelled = true;
|
|
433
|
+
};
|
|
434
|
+
}, [frame, surfaceWidth, surfaceHeight]);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
latest.current.onCapabilities?.({
|
|
437
|
+
capture: captureMethod(),
|
|
438
|
+
backend: latest.current.onInit ? "webgl2" : "canvas-2d",
|
|
439
|
+
...failure ? { error: failure } : {}
|
|
440
|
+
});
|
|
441
|
+
}, [failure]);
|
|
442
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
443
|
+
/* @__PURE__ */ jsx(
|
|
444
|
+
"div",
|
|
445
|
+
{
|
|
446
|
+
ref: source,
|
|
447
|
+
"data-odori-capture": "",
|
|
448
|
+
style: { ...hidden, height: surfaceHeight, width: surfaceWidth },
|
|
449
|
+
"aria-hidden": "true",
|
|
450
|
+
children
|
|
451
|
+
}
|
|
452
|
+
),
|
|
453
|
+
/* @__PURE__ */ jsx(
|
|
454
|
+
"canvas",
|
|
455
|
+
{
|
|
456
|
+
ref: canvas,
|
|
457
|
+
width: surfaceWidth,
|
|
458
|
+
height: surfaceHeight,
|
|
459
|
+
style: { display: "block", height: "100%", width: "100%" }
|
|
460
|
+
}
|
|
461
|
+
),
|
|
462
|
+
failure ? /* @__PURE__ */ jsx(
|
|
463
|
+
"pre",
|
|
464
|
+
{
|
|
465
|
+
style: {
|
|
466
|
+
background: "#1a0b0b",
|
|
467
|
+
color: "#ff9d9d",
|
|
468
|
+
font: "13px ui-monospace, monospace",
|
|
469
|
+
inset: 0,
|
|
470
|
+
margin: 0,
|
|
471
|
+
padding: 24,
|
|
472
|
+
position: "absolute",
|
|
473
|
+
whiteSpace: "pre-wrap"
|
|
474
|
+
},
|
|
475
|
+
children: failure
|
|
476
|
+
}
|
|
477
|
+
) : null
|
|
478
|
+
] });
|
|
479
|
+
};
|
|
480
|
+
var EffectSurface = ({
|
|
481
|
+
children,
|
|
482
|
+
effects,
|
|
483
|
+
width,
|
|
484
|
+
height,
|
|
485
|
+
onCapabilities
|
|
486
|
+
}) => {
|
|
487
|
+
const pipeline = useRef(null);
|
|
488
|
+
const list = useRef(effects);
|
|
489
|
+
list.current = effects;
|
|
490
|
+
return /* @__PURE__ */ jsx(
|
|
491
|
+
HtmlInCanvas,
|
|
492
|
+
{
|
|
493
|
+
width,
|
|
494
|
+
height,
|
|
495
|
+
onInit: ({ canvas }) => {
|
|
496
|
+
try {
|
|
497
|
+
pipeline.current = new EffectPipeline(canvas, list.current);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
pipeline.current = null;
|
|
500
|
+
throw error instanceof ShaderCompileError ? error : new Error(String(error));
|
|
501
|
+
}
|
|
502
|
+
return pipeline.current;
|
|
503
|
+
},
|
|
504
|
+
onPaint: ({ renderer, source, frame, fps, durationInFrames }) => {
|
|
505
|
+
if (!renderer) return;
|
|
506
|
+
renderer.paint(source, list.current, { frame, fps, durationInFrames });
|
|
507
|
+
},
|
|
508
|
+
onDispose: (renderer) => renderer?.dispose(),
|
|
509
|
+
onCapabilities,
|
|
510
|
+
children
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// src/effects/library.ts
|
|
516
|
+
var barrelDistortion = defineShaderEffect({
|
|
517
|
+
name: "barrel-distortion",
|
|
518
|
+
uniforms: {
|
|
519
|
+
amount: numberUniform(0.3),
|
|
520
|
+
radius: numberUniform(0.6),
|
|
521
|
+
centre: vec2Uniform([0.5, 0.5])
|
|
522
|
+
},
|
|
523
|
+
fragmentShader: `
|
|
524
|
+
uniform float amount;
|
|
525
|
+
uniform float radius;
|
|
526
|
+
uniform vec2 centre;
|
|
527
|
+
void main() {
|
|
528
|
+
vec2 uv = gl_FragCoord.xy / resolution;
|
|
529
|
+
// Corrected for the frame's shape, or a wide composition bulges into an
|
|
530
|
+
// oval instead of a circle.
|
|
531
|
+
vec2 aspect = vec2(resolution.x / resolution.y, 1.0);
|
|
532
|
+
vec2 offset = (uv - centre) * aspect;
|
|
533
|
+
float distance = length(offset) / max(radius, 0.0001);
|
|
534
|
+
float scale = 1.0 + amount * (1.0 - clamp(distance, 0.0, 1.0));
|
|
535
|
+
odoriColour = tex(centre + offset / scale / aspect);
|
|
536
|
+
}`
|
|
537
|
+
});
|
|
538
|
+
var rgbSplit = defineShaderEffect({
|
|
539
|
+
name: "rgb-split",
|
|
540
|
+
uniforms: { amount: numberUniform(4), angle: numberUniform(0) },
|
|
541
|
+
fragmentShader: `
|
|
542
|
+
uniform float amount;
|
|
543
|
+
uniform float angle;
|
|
544
|
+
void main() {
|
|
545
|
+
vec2 uv = gl_FragCoord.xy / resolution;
|
|
546
|
+
vec2 shift = vec2(cos(angle), sin(angle)) * amount / resolution;
|
|
547
|
+
odoriColour = vec4(tex(uv + shift).r, tex(uv).g, tex(uv - shift).b, tex(uv).a);
|
|
548
|
+
}`
|
|
549
|
+
});
|
|
550
|
+
var scanlines = defineShaderEffect({
|
|
551
|
+
name: "scanlines",
|
|
552
|
+
uniforms: { opacity: numberUniform(0.08), spacing: numberUniform(3) },
|
|
553
|
+
fragmentShader: `
|
|
554
|
+
uniform float opacity;
|
|
555
|
+
uniform float spacing;
|
|
556
|
+
void main() {
|
|
557
|
+
vec2 uv = gl_FragCoord.xy / resolution;
|
|
558
|
+
float line = step(0.5, fract(gl_FragCoord.y / max(spacing, 1.0)));
|
|
559
|
+
odoriColour = tex(uv) * (1.0 - opacity * line);
|
|
560
|
+
}`
|
|
561
|
+
});
|
|
562
|
+
var pixelate = defineShaderEffect({
|
|
563
|
+
name: "pixelate",
|
|
564
|
+
uniforms: { size: numberUniform(8) },
|
|
565
|
+
fragmentShader: `
|
|
566
|
+
uniform float size;
|
|
567
|
+
void main() {
|
|
568
|
+
float block = max(size, 1.0);
|
|
569
|
+
vec2 snapped = (floor(gl_FragCoord.xy / block) + 0.5) * block;
|
|
570
|
+
odoriColour = tex(snapped / resolution);
|
|
571
|
+
}`
|
|
572
|
+
});
|
|
573
|
+
var filmGrain = defineShaderEffect({
|
|
574
|
+
name: "film-grain",
|
|
575
|
+
uniforms: { amount: numberUniform(0.06) },
|
|
576
|
+
fragmentShader: `
|
|
577
|
+
uniform float amount;
|
|
578
|
+
void main() {
|
|
579
|
+
vec2 uv = gl_FragCoord.xy / resolution;
|
|
580
|
+
float noise = hash(gl_FragCoord.xy + frame) - 0.5;
|
|
581
|
+
odoriColour = tex(uv) + vec4(vec3(noise * amount), 0.0);
|
|
582
|
+
}`
|
|
583
|
+
});
|
|
584
|
+
var magnify = defineShaderEffect({
|
|
585
|
+
name: "magnify",
|
|
586
|
+
uniforms: {
|
|
587
|
+
amount: numberUniform(1.6),
|
|
588
|
+
radius: numberUniform(0.22),
|
|
589
|
+
centre: vec2Uniform([0.5, 0.5]),
|
|
590
|
+
softness: numberUniform(0.06)
|
|
591
|
+
},
|
|
592
|
+
fragmentShader: `
|
|
593
|
+
uniform float amount;
|
|
594
|
+
uniform float radius;
|
|
595
|
+
uniform vec2 centre;
|
|
596
|
+
uniform float softness;
|
|
597
|
+
void main() {
|
|
598
|
+
vec2 uv = gl_FragCoord.xy / resolution;
|
|
599
|
+
vec2 aspect = vec2(resolution.x / resolution.y, 1.0);
|
|
600
|
+
vec2 offset = (uv - centre) * aspect;
|
|
601
|
+
float inside = 1.0 - smoothstep(radius - softness, radius + softness, length(offset));
|
|
602
|
+
float scale = mix(1.0, max(amount, 0.0001), inside);
|
|
603
|
+
odoriColour = tex(centre + offset / scale / aspect);
|
|
604
|
+
}`
|
|
605
|
+
});
|
|
606
|
+
export {
|
|
607
|
+
EffectPipeline,
|
|
608
|
+
EffectSurface,
|
|
609
|
+
HtmlInCanvas,
|
|
610
|
+
SHADER_PRELUDE,
|
|
611
|
+
ShaderCompileError,
|
|
612
|
+
assertPortableShader,
|
|
613
|
+
barrelDistortion,
|
|
614
|
+
captureElement,
|
|
615
|
+
captureMethod,
|
|
616
|
+
defineShaderEffect,
|
|
617
|
+
filmGrain,
|
|
618
|
+
magnify,
|
|
619
|
+
numberUniform,
|
|
620
|
+
pixelate,
|
|
621
|
+
rgbSplit,
|
|
622
|
+
scanlines,
|
|
623
|
+
serializeElement,
|
|
624
|
+
vec2Uniform,
|
|
625
|
+
vec3Uniform,
|
|
626
|
+
vec4Uniform
|
|
627
|
+
};
|
|
628
|
+
//# sourceMappingURL=effects.js.map
|