@supersoniks/concorde 4.9.98-visual-stack.0 → 4.9.98-visual-stack.1
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/build-infos.json +1 -1
- package/concorde-3d.bundle.js +201 -152
- package/concorde-3d.es.js +14687 -13905
- package/concorde-core.bundle.js +1 -1
- package/concorde-core.es.js +1 -1
- package/concorde-hugging-face-infer.bundle.js +1 -1
- package/concorde-hugging-face-infer.es.js +1 -1
- package/concorde-shader.bundle.js +31 -20
- package/concorde-shader.es.js +2773 -2547
- package/concorde-webgpu.bundle.js +43 -32
- package/concorde-webgpu.es.js +1819 -1553
- package/dist/concorde-3d.bundle.js +201 -152
- package/dist/concorde-3d.es.js +14687 -13905
- package/dist/concorde-core.bundle.js +1 -1
- package/dist/concorde-core.es.js +1 -1
- package/dist/concorde-hugging-face-infer.bundle.js +1 -1
- package/dist/concorde-hugging-face-infer.es.js +1 -1
- package/dist/concorde-shader.bundle.js +31 -20
- package/dist/concorde-shader.es.js +2773 -2547
- package/dist/concorde-webgpu.bundle.js +43 -32
- package/dist/concorde-webgpu.es.js +1819 -1553
- package/dist/js/core/components/functional/3d/3d.js +90 -2
- package/dist/js/core/components/functional/3d/3d.js.map +1 -1
- package/dist/js/core/components/functional/3d/constants.js +1 -0
- package/dist/js/core/components/functional/3d/constants.js.map +1 -1
- package/dist/js/core/components/functional/3d/ensure.js +6 -3
- package/dist/js/core/components/functional/3d/ensure.js.map +1 -1
- package/dist/js/core/components/functional/3d/form-params.js +2 -0
- package/dist/js/core/components/functional/3d/form-params.js.map +1 -1
- package/dist/js/core/components/functional/3d/life.js +2 -1
- package/dist/js/core/components/functional/3d/life.js.map +1 -1
- package/dist/js/core/components/functional/3d/out-publish.js +3 -2
- package/dist/js/core/components/functional/3d/out-publish.js.map +1 -1
- package/dist/js/core/components/functional/3d/playback.js +6 -1
- package/dist/js/core/components/functional/3d/playback.js.map +1 -1
- package/dist/js/core/components/functional/3d/post-bind.js +207 -0
- package/dist/js/core/components/functional/3d/post-bind.js.map +1 -0
- package/dist/js/core/components/functional/3d/post-channels.js +39 -0
- package/dist/js/core/components/functional/3d/post-channels.js.map +1 -0
- package/dist/js/core/components/functional/3d/post-fx.js +495 -0
- package/dist/js/core/components/functional/3d/post-fx.js.map +1 -0
- package/dist/js/core/components/functional/3d/props.js +142 -0
- package/dist/js/core/components/functional/3d/props.js.map +1 -1
- package/dist/js/core/components/functional/3d/reload.js +2 -0
- package/dist/js/core/components/functional/3d/reload.js.map +1 -1
- package/dist/js/core/components/functional/3d/runtime.js +80 -3
- package/dist/js/core/components/functional/3d/runtime.js.map +1 -1
- package/dist/js/core/components/functional/3d/scene-runtime.js +1 -1
- package/dist/js/core/components/functional/3d/scene-runtime.js.map +1 -1
- package/dist/js/core/components/functional/3d/tune.js +21 -0
- package/dist/js/core/components/functional/3d/tune.js.map +1 -1
- package/dist/js/core/components/functional/3d/updated.js +13 -1
- package/dist/js/core/components/functional/3d/updated.js.map +1 -1
- package/dist/js/core/components/functional/shader/gl-runtime.js +128 -14
- package/dist/js/core/components/functional/shader/gl-runtime.js.map +1 -1
- package/dist/js/core/components/functional/shader/shader.js +154 -57
- package/dist/js/core/components/functional/shader/shader.js.map +1 -1
- package/dist/js/core/components/functional/shader/shadertoy-prelude.js +39 -5
- package/dist/js/core/components/functional/shader/shadertoy-prelude.js.map +1 -1
- package/dist/js/core/components/functional/shader/types.js +58 -7
- package/dist/js/core/components/functional/shader/types.js.map +1 -1
- package/dist/js/core/components/functional/webgpu/gl-fallback.js +27 -6
- package/dist/js/core/components/functional/webgpu/gl-fallback.js.map +1 -1
- package/dist/js/core/components/functional/webgpu/gpu-runtime.js +115 -13
- package/dist/js/core/components/functional/webgpu/gpu-runtime.js.map +1 -1
- package/dist/js/core/components/functional/webgpu/helpers.js +8 -2
- package/dist/js/core/components/functional/webgpu/helpers.js.map +1 -1
- package/dist/js/core/components/functional/webgpu/webgpu.js +34 -1
- package/dist/js/core/components/functional/webgpu/webgpu.js.map +1 -1
- package/dist/js/core/utils/mediaRef.js +37 -0
- package/dist/js/core/utils/mediaRef.js.map +1 -1
- package/dist/types/core/components/functional/3d/3d.d.ts +26 -3
- package/dist/types/core/components/functional/3d/3d.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/constants.d.ts +1 -1
- package/dist/types/core/components/functional/3d/constants.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/ensure.d.ts +3 -0
- package/dist/types/core/components/functional/3d/ensure.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/form-params.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/life.d.ts +2 -0
- package/dist/types/core/components/functional/3d/life.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/out-publish.d.ts +1 -0
- package/dist/types/core/components/functional/3d/out-publish.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/playback.d.ts +9 -0
- package/dist/types/core/components/functional/3d/playback.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/post-bind.d.ts +52 -0
- package/dist/types/core/components/functional/3d/post-bind.d.ts.map +1 -0
- package/dist/types/core/components/functional/3d/post-channels.d.ts +40 -0
- package/dist/types/core/components/functional/3d/post-channels.d.ts.map +1 -0
- package/dist/types/core/components/functional/3d/post-fx.d.ts +73 -0
- package/dist/types/core/components/functional/3d/post-fx.d.ts.map +1 -0
- package/dist/types/core/components/functional/3d/props.d.ts +42 -0
- package/dist/types/core/components/functional/3d/props.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/reload.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/runtime.d.ts +16 -1
- package/dist/types/core/components/functional/3d/runtime.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/scene-runtime.d.ts +1 -1
- package/dist/types/core/components/functional/3d/scene-runtime.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/tune.d.ts +5 -0
- package/dist/types/core/components/functional/3d/tune.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/types.d.ts +9 -0
- package/dist/types/core/components/functional/3d/types.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/updated.d.ts.map +1 -1
- package/dist/types/core/components/functional/shader/gl-runtime.d.ts +33 -1
- package/dist/types/core/components/functional/shader/gl-runtime.d.ts.map +1 -1
- package/dist/types/core/components/functional/shader/shader.d.ts +28 -7
- package/dist/types/core/components/functional/shader/shader.d.ts.map +1 -1
- package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts +16 -2
- package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts.map +1 -1
- package/dist/types/core/components/functional/shader/types.d.ts +29 -5
- package/dist/types/core/components/functional/shader/types.d.ts.map +1 -1
- package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts +4 -0
- package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts.map +1 -1
- package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts +14 -0
- package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts.map +1 -1
- package/dist/types/core/components/functional/webgpu/helpers.d.ts +1 -0
- package/dist/types/core/components/functional/webgpu/helpers.d.ts.map +1 -1
- package/dist/types/core/components/functional/webgpu/webgpu.d.ts +7 -1
- package/dist/types/core/components/functional/webgpu/webgpu.d.ts.map +1 -1
- package/dist/types/core/utils/mediaRef.d.ts +15 -0
- package/dist/types/core/utils/mediaRef.d.ts.map +1 -1
- package/package.json +37 -1
- package/src/core/components/functional/3d/3d.md +34 -1
- package/src/core/components/functional/3d/3d.spec.ts +42 -0
- package/src/core/components/functional/3d/3d.ts +101 -3
- package/src/core/components/functional/3d/constants.ts +1 -0
- package/src/core/components/functional/3d/ensure.ts +11 -3
- package/src/core/components/functional/3d/form-params.ts +2 -0
- package/src/core/components/functional/3d/life.ts +4 -1
- package/src/core/components/functional/3d/out-publish.ts +4 -3
- package/src/core/components/functional/3d/playback.ts +13 -1
- package/src/core/components/functional/3d/post-bind.ts +262 -0
- package/src/core/components/functional/3d/post-channels.spec.ts +36 -0
- package/src/core/components/functional/3d/post-channels.ts +69 -0
- package/src/core/components/functional/3d/post-fx.ts +594 -0
- package/src/core/components/functional/3d/props.ts +45 -0
- package/src/core/components/functional/3d/reload.ts +2 -0
- package/src/core/components/functional/3d/runtime.ts +93 -4
- package/src/core/components/functional/3d/scene-runtime.ts +3 -0
- package/src/core/components/functional/3d/tune.ts +21 -0
- package/src/core/components/functional/3d/types.ts +9 -0
- package/src/core/components/functional/3d/updated.ts +17 -1
- package/src/core/components/functional/shader/gl-runtime.ts +169 -12
- package/src/core/components/functional/shader/shader.md +27 -4
- package/src/core/components/functional/shader/shader.spec.ts +267 -0
- package/src/core/components/functional/shader/shader.ts +177 -55
- package/src/core/components/functional/shader/shadertoy-prelude.ts +49 -5
- package/src/core/components/functional/shader/types.ts +89 -12
- package/src/core/components/functional/webgpu/gl-fallback.ts +28 -4
- package/src/core/components/functional/webgpu/gpu-runtime.ts +154 -18
- package/src/core/components/functional/webgpu/helpers.ts +8 -2
- package/src/core/components/functional/webgpu/webgpu.md +13 -2
- package/src/core/components/functional/webgpu/webgpu.spec.ts +37 -0
- package/src/core/components/functional/webgpu/webgpu.ts +39 -1
- package/src/core/utils/mediaRef.spec.ts +25 -0
- package/src/core/utils/mediaRef.ts +45 -0
- package/src/docs/_misc/visual-stack.md +19 -1
- package/src/docs/components/docs-demo-sources.ts +28 -0
- package/src/docs/docs.ts +2 -0
- package/src/docs/example/docs-3d-post-depth-demo.ts +68 -0
- package/src/docs/example/docs-3d-webgpu-live-demo.ts +83 -0
- package/src/docs/example/docs-harris-webgpu-demo.ts +53 -55
- package/src/docs/search/docs-search.json +252 -2
- package/src/tsconfig.json +27 -0
- package/src/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ShaderToy-compatible GLSL ES 3.0 prelude (fragment body, sans `#version`).
|
|
3
|
+
* - `sonic-shader` préfixe `#version 300 es` via `buildFragmentSource`.
|
|
4
|
+
* - `sonic-3d` post passe le corps à Three `RawShaderMaterial` (GLSL3).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const GLSL_VERSION_300 = `#version 300 es
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
/** Corps des uniforms / out (sans version). */
|
|
11
|
+
export const SHADERTOY_FRAGMENT_BODY = `precision highp float;
|
|
4
12
|
precision highp int;
|
|
5
13
|
precision mediump sampler2D;
|
|
6
14
|
|
|
@@ -22,11 +30,26 @@ uniform float uParam0;
|
|
|
22
30
|
uniform float uParam1;
|
|
23
31
|
uniform float uParam2;
|
|
24
32
|
uniform float uParam3;
|
|
33
|
+
/** Near / far caméra (0 dans sonic-shader standalone). */
|
|
34
|
+
uniform float uNear;
|
|
35
|
+
uniform float uFar;
|
|
25
36
|
|
|
26
37
|
out vec4 sonicFragColor;
|
|
27
38
|
|
|
39
|
+
/** Profondeur linéaire monde depuis une DepthTexture (0–1). */
|
|
40
|
+
float sonicLinearDepth(float d) {
|
|
41
|
+
float z = d * 2.0 - 1.0;
|
|
42
|
+
float n = max(uNear, 1e-4);
|
|
43
|
+
float f = max(uFar, n + 1e-4);
|
|
44
|
+
return (2.0 * n * f) / (f + n - z * (f - n));
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
`;
|
|
29
48
|
|
|
49
|
+
/** @deprecated Préférer SHADERTOY_FRAGMENT_BODY + buildFragmentSource. */
|
|
50
|
+
export const SHADERTOY_FRAGMENT_PRELUDE =
|
|
51
|
+
GLSL_VERSION_300 + SHADERTOY_FRAGMENT_BODY;
|
|
52
|
+
|
|
30
53
|
export const SHADERTOY_FRAGMENT_EPILOGUE = `
|
|
31
54
|
void main() {
|
|
32
55
|
mainImage(sonicFragColor, gl_FragCoord.xy);
|
|
@@ -41,12 +64,33 @@ void main() {
|
|
|
41
64
|
}
|
|
42
65
|
`;
|
|
43
66
|
|
|
44
|
-
|
|
67
|
+
/** Vertex Three RawShaderMaterial GLSL3 (fullscreen triangle via PlaneGeometry 2×2). */
|
|
68
|
+
export const THREE_FULLSCREEN_VERTEX = `precision highp float;
|
|
69
|
+
in vec3 position;
|
|
70
|
+
void main() {
|
|
71
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
export function buildFragmentBody(common: string, userCode: string): string {
|
|
45
76
|
return (
|
|
46
|
-
|
|
77
|
+
SHADERTOY_FRAGMENT_BODY +
|
|
47
78
|
(common ? common + "\n" : "") +
|
|
48
79
|
userCode +
|
|
49
80
|
"\n" +
|
|
50
81
|
SHADERTOY_FRAGMENT_EPILOGUE
|
|
51
82
|
);
|
|
52
83
|
}
|
|
84
|
+
|
|
85
|
+
/** Source fragment WebGL2 native (`sonic-shader`). */
|
|
86
|
+
export function buildFragmentSource(common: string, userCode: string): string {
|
|
87
|
+
return GLSL_VERSION_300 + buildFragmentBody(common, userCode);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Source fragment pour Three RawShaderMaterial (GLSL3, sans #version). */
|
|
91
|
+
export function buildThreeFragmentSource(
|
|
92
|
+
common: string,
|
|
93
|
+
userCode: string,
|
|
94
|
+
): string {
|
|
95
|
+
return buildFragmentBody(common, userCode);
|
|
96
|
+
}
|
|
@@ -5,6 +5,57 @@ export type ShaderPassId =
|
|
|
5
5
|
| "bufferC"
|
|
6
6
|
| "bufferD";
|
|
7
7
|
|
|
8
|
+
/** Format interne des buffers A–D (Image = toujours le canvas RGBA8). */
|
|
9
|
+
export type ShaderBufferFormat = "rgba8" | "rgba16f" | "rgba32f";
|
|
10
|
+
|
|
11
|
+
export function parseBufferFormat(
|
|
12
|
+
value: string | null | undefined,
|
|
13
|
+
): ShaderBufferFormat {
|
|
14
|
+
const s = (value ?? "").trim().toLowerCase();
|
|
15
|
+
if (s === "rgba16f" || s === "rgba16" || s === "half") return "rgba16f";
|
|
16
|
+
if (s === "rgba32f" || s === "rgba32" || s === "float") return "rgba32f";
|
|
17
|
+
return "rgba8";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hors viewport : libérer textures/FBO ?
|
|
22
|
+
* - `true` / `false` — forcé
|
|
23
|
+
* - `auto` (défaut) — libère sauf si une passe lit `self` (état conservé)
|
|
24
|
+
*/
|
|
25
|
+
export type ReleaseOffscreen = boolean | "auto";
|
|
26
|
+
|
|
27
|
+
export function parseReleaseOffscreen(
|
|
28
|
+
value: string | null | undefined,
|
|
29
|
+
): ReleaseOffscreen {
|
|
30
|
+
if (value == null) return "auto";
|
|
31
|
+
const s = value.trim().toLowerCase();
|
|
32
|
+
if (s === "auto") return "auto";
|
|
33
|
+
if (s === "false" || s === "0") return false;
|
|
34
|
+
// attribut booléen présent (`""`) ou `"true"`
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Au moins un canal `self` (feedback frame précédente). */
|
|
39
|
+
export function channelsUseSelf(
|
|
40
|
+
channels: Iterable<readonly ChannelSource[]>,
|
|
41
|
+
): boolean {
|
|
42
|
+
for (const chs of channels) {
|
|
43
|
+
for (const c of chs) {
|
|
44
|
+
if (c.kind === "self") return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function shouldReleaseWhenOffscreen(
|
|
51
|
+
mode: ReleaseOffscreen,
|
|
52
|
+
usesSelf: boolean,
|
|
53
|
+
): boolean {
|
|
54
|
+
if (mode === false) return false;
|
|
55
|
+
if (mode === true) return true;
|
|
56
|
+
return !usesSelf;
|
|
57
|
+
}
|
|
58
|
+
|
|
8
59
|
export type ChannelSource =
|
|
9
60
|
| { kind: "url"; url: string }
|
|
10
61
|
| { kind: "buffer"; buffer: Exclude<ShaderPassId, "image"> }
|
|
@@ -161,10 +212,18 @@ export type SonicShaderConfig = {
|
|
|
161
212
|
dprMax?: number;
|
|
162
213
|
|
|
163
214
|
/**
|
|
164
|
-
*
|
|
215
|
+
* Format des buffers A–D : `rgba8` (défaut) \| `rgba16f` \| `rgba32f`.
|
|
216
|
+
* Requis pour des valeurs signées / hors [0,1] (ex. tenseur Harris).
|
|
217
|
+
* Repli auto en `rgba8` si `EXT_color_buffer_float` manque.
|
|
218
|
+
*/
|
|
219
|
+
bufferFormat?: ShaderBufferFormat;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Hors viewport : libère textures/FBO, sans loseContext.
|
|
223
|
+
* `auto` (défaut) : ne libère pas si une passe lit `self`.
|
|
165
224
|
* Le slot navigateur est libéré au disconnect.
|
|
166
225
|
*/
|
|
167
|
-
releaseOffscreen?:
|
|
226
|
+
releaseOffscreen?: ReleaseOffscreen;
|
|
168
227
|
};
|
|
169
228
|
|
|
170
229
|
/** Parse `hover-param` → slot ou `null` si désactivé / invalide. */
|
|
@@ -274,7 +333,8 @@ export type ReadRectResult = {
|
|
|
274
333
|
y: number;
|
|
275
334
|
width: number;
|
|
276
335
|
height: number;
|
|
277
|
-
|
|
336
|
+
/** `Uint8Array` (RGBA8) ou `Float32Array` (buffers flottants). */
|
|
337
|
+
data: Uint8Array | Float32Array;
|
|
278
338
|
};
|
|
279
339
|
|
|
280
340
|
/** Parse `extract-source` / `extract-mask` → pass id ou null. */
|
|
@@ -295,9 +355,10 @@ export function parseExtractPass(
|
|
|
295
355
|
/**
|
|
296
356
|
* NMS simple sur canal R (RGBA row-major, origine bas-gauche comme WebGL).
|
|
297
357
|
* Retourne peaks triés par score décroissant.
|
|
358
|
+
* Accepte Uint8 (0–255) ou Float32 (scores déjà dans ~[0,1]).
|
|
298
359
|
*/
|
|
299
360
|
export function findPeaks(
|
|
300
|
-
data: Uint8Array,
|
|
361
|
+
data: Uint8Array | Float32Array,
|
|
301
362
|
width: number,
|
|
302
363
|
height: number,
|
|
303
364
|
options: FindPeaksOptions = {},
|
|
@@ -308,13 +369,15 @@ export function findPeaks(
|
|
|
308
369
|
if (maxCount === 0 || width < 1 || height < 1 || data.length < width * height * 4) {
|
|
309
370
|
return [];
|
|
310
371
|
}
|
|
311
|
-
const
|
|
372
|
+
const isFloat = data instanceof Float32Array;
|
|
373
|
+
const thr = isFloat ? threshold : threshold * 255;
|
|
374
|
+
const toScore = (v: number) => (isFloat ? v : v / 255);
|
|
312
375
|
const candidates: ShaderPeak[] = [];
|
|
313
376
|
for (let y = 1; y < height - 1; y++) {
|
|
314
377
|
for (let x = 1; x < width - 1; x++) {
|
|
315
378
|
const i = (y * width + x) * 4;
|
|
316
379
|
const v = data[i];
|
|
317
|
-
if (v <
|
|
380
|
+
if (v < thr) continue;
|
|
318
381
|
// Local max 3×3
|
|
319
382
|
let isMax = true;
|
|
320
383
|
for (let dy = -1; dy <= 1 && isMax; dy++) {
|
|
@@ -327,7 +390,7 @@ export function findPeaks(
|
|
|
327
390
|
}
|
|
328
391
|
}
|
|
329
392
|
}
|
|
330
|
-
if (isMax) candidates.push({ x, y, score: v
|
|
393
|
+
if (isMax) candidates.push({ x, y, score: toScore(v) });
|
|
331
394
|
}
|
|
332
395
|
}
|
|
333
396
|
candidates.sort((a, b) => b.score - a.score);
|
|
@@ -403,7 +466,7 @@ export function roiToPixels(
|
|
|
403
466
|
return { x: px, y: py, w: pw, h: ph };
|
|
404
467
|
}
|
|
405
468
|
|
|
406
|
-
export function meanRgba(data: Uint8Array): {
|
|
469
|
+
export function meanRgba(data: Uint8Array | Float32Array): {
|
|
407
470
|
r: number;
|
|
408
471
|
g: number;
|
|
409
472
|
b: number;
|
|
@@ -412,6 +475,7 @@ export function meanRgba(data: Uint8Array): {
|
|
|
412
475
|
} {
|
|
413
476
|
const n = Math.floor(data.length / 4);
|
|
414
477
|
if (n < 1) return { r: 0, g: 0, b: 0, a: 0, luma: 0 };
|
|
478
|
+
const isFloat = data instanceof Float32Array;
|
|
415
479
|
let r = 0;
|
|
416
480
|
let g = 0;
|
|
417
481
|
let b = 0;
|
|
@@ -423,10 +487,11 @@ export function meanRgba(data: Uint8Array): {
|
|
|
423
487
|
b += data[o + 2];
|
|
424
488
|
a += data[o + 3];
|
|
425
489
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
490
|
+
const scale = isFloat ? n : n * 255;
|
|
491
|
+
r /= scale;
|
|
492
|
+
g /= scale;
|
|
493
|
+
b /= scale;
|
|
494
|
+
a /= scale;
|
|
430
495
|
return {
|
|
431
496
|
r,
|
|
432
497
|
g,
|
|
@@ -436,6 +501,18 @@ export function meanRgba(data: Uint8Array): {
|
|
|
436
501
|
};
|
|
437
502
|
}
|
|
438
503
|
|
|
504
|
+
/** Convertit un buffer RGBA (byte ou float 0–1) en Uint8 pour canvas / blob. */
|
|
505
|
+
export function rgbaToUint8(
|
|
506
|
+
data: Uint8Array | Float32Array,
|
|
507
|
+
): Uint8Array {
|
|
508
|
+
if (data instanceof Uint8Array) return data;
|
|
509
|
+
const out = new Uint8Array(data.length);
|
|
510
|
+
for (let i = 0; i < data.length; i++) {
|
|
511
|
+
out[i] = Math.max(0, Math.min(255, Math.round(data[i] * 255)));
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
|
|
439
516
|
export class ShaderCompileError extends Error {
|
|
440
517
|
constructor(
|
|
441
518
|
message: string,
|
|
@@ -45,6 +45,7 @@ export class WebGl2FallbackRuntime {
|
|
|
45
45
|
private lastTime = 0;
|
|
46
46
|
private disposed = false;
|
|
47
47
|
private channelQueue: Promise<void> = Promise.resolve();
|
|
48
|
+
private elementResolver: ((key: string) => Element | null) | null = null;
|
|
48
49
|
|
|
49
50
|
private constructor(canvas: HTMLCanvasElement, toy: ShaderToyRuntime) {
|
|
50
51
|
this.canvas = canvas;
|
|
@@ -57,6 +58,11 @@ export class WebGl2FallbackRuntime {
|
|
|
57
58
|
return new WebGl2FallbackRuntime(canvas, toy);
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
setElementResolver(fn: ((key: string) => Element | null) | null) {
|
|
62
|
+
this.elementResolver = fn;
|
|
63
|
+
this.toy.setElementResolver(fn);
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
/** WGSL ignoré — effet GLSL fixe (canaux / params OK). */
|
|
61
67
|
async setShader(_userBody: string): Promise<void> {
|
|
62
68
|
this.assertAlive();
|
|
@@ -77,6 +83,10 @@ export class WebGl2FallbackRuntime {
|
|
|
77
83
|
this.assertAlive();
|
|
78
84
|
const trimmed = url.trim();
|
|
79
85
|
if (this.channelUrls[index] === trimmed) {
|
|
86
|
+
if (trimmed.startsWith("#")) {
|
|
87
|
+
this.toy.ensureElementTexture(trimmed);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
80
90
|
// Même URL : s’assurer que la texture est bien chargée
|
|
81
91
|
if (trimmed && !this.toy.getTextureSize(trimmed)) {
|
|
82
92
|
await this.toy.ensureTextures([trimmed]);
|
|
@@ -85,8 +95,7 @@ export class WebGl2FallbackRuntime {
|
|
|
85
95
|
}
|
|
86
96
|
this.channelUrls[index] = trimmed;
|
|
87
97
|
this.applySources();
|
|
88
|
-
|
|
89
|
-
if (urls.length) await this.toy.ensureTextures(urls);
|
|
98
|
+
await this.ensureChannelAssets();
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
/** Applique les 4 canaux d’un coup (préféré au boot). */
|
|
@@ -100,8 +109,7 @@ export class WebGl2FallbackRuntime {
|
|
|
100
109
|
urls[3].trim(),
|
|
101
110
|
];
|
|
102
111
|
this.applySources();
|
|
103
|
-
|
|
104
|
-
if (list.length) await this.toy.ensureTextures(list);
|
|
112
|
+
await this.ensureChannelAssets();
|
|
105
113
|
});
|
|
106
114
|
return this.channelQueue;
|
|
107
115
|
}
|
|
@@ -115,9 +123,17 @@ export class WebGl2FallbackRuntime {
|
|
|
115
123
|
this.toy.resize(width, height);
|
|
116
124
|
}
|
|
117
125
|
|
|
126
|
+
updateLiveChannels(resolveEl: (key: string) => Element | null) {
|
|
127
|
+
this.assertAlive();
|
|
128
|
+
this.toy.updateElementTextures(resolveEl);
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
draw(state: WebGpuFrameState) {
|
|
119
132
|
this.assertAlive();
|
|
120
133
|
if (this.toy.isLost) return;
|
|
134
|
+
if (this.elementResolver) {
|
|
135
|
+
this.toy.updateElementTextures(this.elementResolver);
|
|
136
|
+
}
|
|
121
137
|
const timeDelta =
|
|
122
138
|
this.lastTime > 0 ? Math.max(0, state.time - this.lastTime) : 0;
|
|
123
139
|
this.lastTime = state.time;
|
|
@@ -137,6 +153,14 @@ export class WebGl2FallbackRuntime {
|
|
|
137
153
|
this.toy.dispose(loseContext);
|
|
138
154
|
}
|
|
139
155
|
|
|
156
|
+
private async ensureChannelAssets() {
|
|
157
|
+
const urls = this.channelUrls.filter((u) => u && !u.startsWith("#"));
|
|
158
|
+
if (urls.length) await this.toy.ensureTextures(urls);
|
|
159
|
+
for (const key of this.channelUrls) {
|
|
160
|
+
if (key.startsWith("#")) this.toy.ensureElementTexture(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
private applySources() {
|
|
141
165
|
const channels = new Map([
|
|
142
166
|
[
|
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
/// <reference types="@webgpu/types" />
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
isFrameSource,
|
|
5
|
+
type SonicFrameSource,
|
|
6
|
+
} from "@supersoniks/concorde/core/utils/mediaRef";
|
|
7
|
+
import { shouldUploadElementFrame } from "../shader/gl-runtime";
|
|
3
8
|
import { WebGpuCompileError } from "./types";
|
|
4
9
|
import { buildWgslSource } from "./wgsl-prelude";
|
|
5
10
|
|
|
6
11
|
const UNIFORM_BYTES = 64; // resolution+time+frame+mouse+params + pad
|
|
7
12
|
const CHANNEL_COUNT = 4;
|
|
8
13
|
|
|
14
|
+
/** `true` si la texture existante peut être réécrite (même taille). */
|
|
15
|
+
export function channelTextureNeedsRecreate(
|
|
16
|
+
current: { width: number; height: number } | null | undefined,
|
|
17
|
+
nextW: number,
|
|
18
|
+
nextH: number,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (!current) return true;
|
|
21
|
+
return current.width !== nextW || current.height !== nextH;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function externalSourceSize(
|
|
25
|
+
src: TexImageSource,
|
|
26
|
+
): { width: number; height: number } {
|
|
27
|
+
if (
|
|
28
|
+
typeof HTMLVideoElement !== "undefined" &&
|
|
29
|
+
src instanceof HTMLVideoElement
|
|
30
|
+
) {
|
|
31
|
+
return { width: src.videoWidth || 1, height: src.videoHeight || 1 };
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
typeof HTMLImageElement !== "undefined" &&
|
|
35
|
+
src instanceof HTMLImageElement
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
width: src.naturalWidth || src.width || 1,
|
|
39
|
+
height: src.naturalHeight || src.height || 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if ("width" in src && "height" in src) {
|
|
43
|
+
return {
|
|
44
|
+
width: Math.max(1, Number(src.width) || 1),
|
|
45
|
+
height: Math.max(1, Number(src.height) || 1),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { width: 1, height: 1 };
|
|
49
|
+
}
|
|
50
|
+
|
|
9
51
|
/** Adapter WebGPU : high-performance → low-power → défaut. */
|
|
10
52
|
export async function requestBestAdapter(
|
|
11
53
|
gpu: GPU = navigator.gpu!,
|
|
@@ -77,6 +119,13 @@ export class WebGpuRuntime {
|
|
|
77
119
|
null,
|
|
78
120
|
];
|
|
79
121
|
private channelUrls: [string, string, string, string] = ["", "", "", ""];
|
|
122
|
+
private channelLastSeq = [0, 0, 0, 0];
|
|
123
|
+
private channelLastSrc: (string | undefined)[] = [
|
|
124
|
+
undefined,
|
|
125
|
+
undefined,
|
|
126
|
+
undefined,
|
|
127
|
+
undefined,
|
|
128
|
+
];
|
|
80
129
|
private placeholder: GPUTexture | null = null;
|
|
81
130
|
private placeholderView: GPUTextureView | null = null;
|
|
82
131
|
private bindGroup: GPUBindGroup | null = null;
|
|
@@ -185,10 +234,16 @@ export class WebGpuRuntime {
|
|
|
185
234
|
async setChannel(index: 0 | 1 | 2 | 3, url: string): Promise<void> {
|
|
186
235
|
this.assertAlive();
|
|
187
236
|
const trimmed = url.trim();
|
|
237
|
+
if (trimmed.startsWith("#")) {
|
|
238
|
+
this.bindElementChannel(index, trimmed);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
188
241
|
if (this.channelUrls[index] === trimmed && this.channelTextures[index]) {
|
|
189
242
|
return;
|
|
190
243
|
}
|
|
191
244
|
this.channelUrls[index] = trimmed;
|
|
245
|
+
this.channelLastSeq[index] = 0;
|
|
246
|
+
this.channelLastSrc[index] = undefined;
|
|
192
247
|
const token = ++this.channelLoadTokens[index];
|
|
193
248
|
if (!trimmed) {
|
|
194
249
|
this.destroyChannel(index);
|
|
@@ -197,24 +252,35 @@ export class WebGpuRuntime {
|
|
|
197
252
|
}
|
|
198
253
|
const img = await loadImage(trimmed);
|
|
199
254
|
if (token !== this.channelLoadTokens[index] || this.disposed) return;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
255
|
+
this.uploadExternal(index, img, /* flipY */ false);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Upload live des canaux `#id` / FrameSource / video / canvas.
|
|
260
|
+
* Recrée la texture seulement si la taille change (pas de createTexture chaque frame).
|
|
261
|
+
*/
|
|
262
|
+
updateLiveChannels(resolveEl: (key: string) => Element | null) {
|
|
263
|
+
this.assertAlive();
|
|
264
|
+
for (let i = 0; i < CHANNEL_COUNT; i++) {
|
|
265
|
+
const key = this.channelUrls[i];
|
|
266
|
+
if (!key.startsWith("#")) continue;
|
|
267
|
+
const el = resolveEl(key);
|
|
268
|
+
const decision = shouldUploadElementFrame(
|
|
269
|
+
el,
|
|
270
|
+
this.channelLastSeq[i],
|
|
271
|
+
this.channelLastSrc[i],
|
|
272
|
+
);
|
|
273
|
+
if (!decision.upload || !decision.texSource) continue;
|
|
274
|
+
this.uploadExternal(i, decision.texSource, /* flipY */ true);
|
|
275
|
+
if (isFrameSource(el)) {
|
|
276
|
+
this.channelLastSeq[i] = (el as SonicFrameSource).frameSeq;
|
|
277
|
+
} else {
|
|
278
|
+
this.channelLastSeq[i] = (this.channelLastSeq[i] || 0) + 1;
|
|
279
|
+
}
|
|
280
|
+
if (decision.nextSrc !== undefined) {
|
|
281
|
+
this.channelLastSrc[i] = decision.nextSrc;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
218
284
|
}
|
|
219
285
|
|
|
220
286
|
getTextureSize(url: string): { width: number; height: number } | null {
|
|
@@ -311,6 +377,76 @@ export class WebGpuRuntime {
|
|
|
311
377
|
}
|
|
312
378
|
}
|
|
313
379
|
|
|
380
|
+
private bindElementChannel(index: 0 | 1 | 2 | 3, key: string) {
|
|
381
|
+
if (this.channelUrls[index] === key && this.channelTextures[index]) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
this.channelUrls[index] = key;
|
|
385
|
+
this.channelLastSeq[index] = 0;
|
|
386
|
+
this.channelLastSrc[index] = undefined;
|
|
387
|
+
this.channelLoadTokens[index]++;
|
|
388
|
+
if (!this.channelTextures[index]) {
|
|
389
|
+
const texture = this.device.createTexture({
|
|
390
|
+
size: [1, 1],
|
|
391
|
+
format: "rgba8unorm",
|
|
392
|
+
usage:
|
|
393
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
394
|
+
GPUTextureUsage.COPY_DST |
|
|
395
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
396
|
+
});
|
|
397
|
+
this.device.queue.writeTexture(
|
|
398
|
+
{ texture },
|
|
399
|
+
new Uint8Array([0, 0, 0, 255]),
|
|
400
|
+
{ bytesPerRow: 4 },
|
|
401
|
+
[1, 1],
|
|
402
|
+
);
|
|
403
|
+
this.channelTextures[index] = texture;
|
|
404
|
+
this.channelViews[index] = texture.createView();
|
|
405
|
+
this.channelSizes[index] = { width: 1, height: 1 };
|
|
406
|
+
this.bindGroup = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private uploadExternal(
|
|
411
|
+
index: number,
|
|
412
|
+
source: TexImageSource,
|
|
413
|
+
flipY: boolean,
|
|
414
|
+
) {
|
|
415
|
+
const { width, height } = externalSourceSize(source);
|
|
416
|
+
const w = Math.max(1, width);
|
|
417
|
+
const h = Math.max(1, height);
|
|
418
|
+
const existing = this.channelTextures[index];
|
|
419
|
+
if (
|
|
420
|
+
existing &&
|
|
421
|
+
!channelTextureNeedsRecreate(this.channelSizes[index], w, h)
|
|
422
|
+
) {
|
|
423
|
+
this.device.queue.copyExternalImageToTexture(
|
|
424
|
+
{ source, flipY },
|
|
425
|
+
{ texture: existing },
|
|
426
|
+
[w, h],
|
|
427
|
+
);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const texture = this.device.createTexture({
|
|
431
|
+
size: [w, h],
|
|
432
|
+
format: "rgba8unorm",
|
|
433
|
+
usage:
|
|
434
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
435
|
+
GPUTextureUsage.COPY_DST |
|
|
436
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
437
|
+
});
|
|
438
|
+
this.device.queue.copyExternalImageToTexture(
|
|
439
|
+
{ source, flipY },
|
|
440
|
+
{ texture },
|
|
441
|
+
[w, h],
|
|
442
|
+
);
|
|
443
|
+
this.destroyChannel(index);
|
|
444
|
+
this.channelTextures[index] = texture;
|
|
445
|
+
this.channelViews[index] = texture.createView();
|
|
446
|
+
this.channelSizes[index] = { width: w, height: h };
|
|
447
|
+
this.bindGroup = null;
|
|
448
|
+
}
|
|
449
|
+
|
|
314
450
|
private writeUniforms(state: WebGpuFrameState) {
|
|
315
451
|
const data = new ArrayBuffer(UNIFORM_BYTES);
|
|
316
452
|
const f32 = new Float32Array(data);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toMediaUrl } from "@supersoniks/concorde/core/utils/mediaRef";
|
|
1
|
+
import { toMediaUrl, mediaElementKey } from "@supersoniks/concorde/core/utils/mediaRef";
|
|
2
2
|
|
|
3
3
|
export const LOG_PREFIX = "[sonic-webgpu]";
|
|
4
4
|
|
|
@@ -37,11 +37,17 @@ export function toNum(v: unknown, fallback: number): number {
|
|
|
37
37
|
return fallback;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** string (url / #id) | MediaRef → url | { element }. */
|
|
40
41
|
export function channelFormValue(value: unknown): string | undefined {
|
|
41
42
|
if (value === null || value === "") return "";
|
|
42
43
|
if (typeof value === "string") return value.trim();
|
|
43
44
|
const url = toMediaUrl(value);
|
|
44
45
|
if (url) return url;
|
|
45
|
-
if (value && typeof value === "object")
|
|
46
|
+
if (value && typeof value === "object") {
|
|
47
|
+
const el = (value as { element?: unknown }).element;
|
|
48
|
+
const key = mediaElementKey(el);
|
|
49
|
+
if (key) return key;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
46
52
|
return "";
|
|
47
53
|
}
|
|
@@ -41,11 +41,22 @@ Simulation: `param0` = speed, `param1` = mouse attraction. WebGPU compute when a
|
|
|
41
41
|
| `active-backend` | reflect |
|
|
42
42
|
| `param0`…`param3` | floats (particles: speed / attraction) |
|
|
43
43
|
| `targetX` / `targetY` | attractor 0–1 (DP, top-left origin) |
|
|
44
|
-
| `channel0`…`channel3` | textures (image kernel) |
|
|
44
|
+
| `channel0`…`channel3` | textures (image kernel) : URL, `#id` live (`SonicFrameSource`), ou MediaRef `{ element }` |
|
|
45
45
|
| `dataProvider` | params / channels / `targetX`·`Y` |
|
|
46
46
|
| `frame-out` | `frameUrl` + `snapshot` |
|
|
47
47
|
| `play` / `mouse` | loop / pointer |
|
|
48
48
|
|
|
49
|
+
## Live bridge (`#id`)
|
|
50
|
+
|
|
51
|
+
Comme `sonic-shader` : `channel0="#plateau"` copie le canvas source chaque `frameSeq` (`copyExternalImageToTexture`). La texture GPU est **réutilisée** tant que la taille ne change pas (pas de `createTexture` / rebuild bind group chaque frame).
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<sonic-3d id="plateau" play …></sonic-3d>
|
|
55
|
+
<sonic-webgpu channel0="#plateau" kernel="image" play shader="…"></sonic-webgpu>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Demo: [Visual stack — 3D → WebGPU live](#docs/_misc/visual-stack.md/visual-stack).
|
|
59
|
+
|
|
49
60
|
## Markup particles
|
|
50
61
|
|
|
51
62
|
<sonic-code language="html">
|
|
@@ -61,7 +72,7 @@ Simulation: `param0` = speed, `param1` = mouse attraction. WebGPU compute when a
|
|
|
61
72
|
|
|
62
73
|
## Chaining
|
|
63
74
|
|
|
64
|
-
Same MediaRef / DP bridge as the rest of the stack.
|
|
75
|
+
Same MediaRef / DP bridge as the rest of the stack. Live textures entre composants : pont `#id` / `{ element }` (copie canvas), pas de contexte WebGPU partagé avec `sonic-shader`.
|
|
65
76
|
|
|
66
77
|
## CDN
|
|
67
78
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { expect, describe, it, vi, beforeAll } from "vitest";
|
|
2
2
|
import TestUtils from "@supersoniks/concorde/test-utils/TestUtils";
|
|
3
3
|
import { buildWgslSource, DEFAULT_MAIN_IMAGE } from "./wgsl-prelude";
|
|
4
|
+
import { channelTextureNeedsRecreate } from "./gpu-runtime";
|
|
5
|
+
import { channelFormValue } from "./helpers";
|
|
4
6
|
import "./webgpu";
|
|
5
7
|
import { SonicWebGpu } from "./webgpu";
|
|
6
8
|
|
|
@@ -28,6 +30,30 @@ describe("buildWgslSource", () => {
|
|
|
28
30
|
});
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
describe("channelTextureNeedsRecreate", () => {
|
|
34
|
+
it("recrée si absent ou taille différente", () => {
|
|
35
|
+
expect(channelTextureNeedsRecreate(null, 64, 64)).toBe(true);
|
|
36
|
+
expect(channelTextureNeedsRecreate({ width: 32, height: 64 }, 64, 64)).toBe(
|
|
37
|
+
true,
|
|
38
|
+
);
|
|
39
|
+
expect(channelTextureNeedsRecreate({ width: 64, height: 64 }, 64, 64)).toBe(
|
|
40
|
+
false,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("channelFormValue", () => {
|
|
46
|
+
it("accepte url / #id / { element }", () => {
|
|
47
|
+
expect(channelFormValue("https://x/a.png")).toBe("https://x/a.png");
|
|
48
|
+
expect(channelFormValue("#plateau")).toBe("#plateau");
|
|
49
|
+
expect(channelFormValue({ element: "#plateau" })).toBe("#plateau");
|
|
50
|
+
expect(channelFormValue({ element: "scene" })).toBe("#scene");
|
|
51
|
+
expect(channelFormValue({ url: "https://x/a.png" })).toBe(
|
|
52
|
+
"https://x/a.png",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
31
57
|
describe("sonic-webgpu", () => {
|
|
32
58
|
beforeAll(() => {
|
|
33
59
|
if (typeof globalThis.ResizeObserver === "undefined") {
|
|
@@ -67,6 +93,7 @@ describe("sonic-webgpu", () => {
|
|
|
67
93
|
param1: 0.22,
|
|
68
94
|
channel0: { url: "https://example.test/a.png" },
|
|
69
95
|
channel1: "https://example.test/b.png",
|
|
96
|
+
channel2: { element: "#plateau" },
|
|
70
97
|
});
|
|
71
98
|
const el = TestUtils.bootstrap(
|
|
72
99
|
`<sonic-webgpu dataProvider="${id}"></sonic-webgpu>`,
|
|
@@ -78,6 +105,7 @@ describe("sonic-webgpu", () => {
|
|
|
78
105
|
expect(el.param1).toBe(0.22);
|
|
79
106
|
expect(el.channel0).toBe("https://example.test/a.png");
|
|
80
107
|
expect(el.channel1).toBe("https://example.test/b.png");
|
|
108
|
+
expect(el.channel2).toBe("#plateau");
|
|
81
109
|
|
|
82
110
|
pub.param0.set(0.99);
|
|
83
111
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -138,4 +166,13 @@ describe("sonic-webgpu", () => {
|
|
|
138
166
|
expect(parseBackendPrefer("webgl2")).toBe("webgl2");
|
|
139
167
|
expect(parseBackendPrefer("nope")).toBe("auto");
|
|
140
168
|
});
|
|
169
|
+
|
|
170
|
+
it("expose getFrameCanvas + frameSeq (SonicFrameSource)", () => {
|
|
171
|
+
const el = TestUtils.bootstrap(
|
|
172
|
+
`<sonic-webgpu></sonic-webgpu>`,
|
|
173
|
+
true,
|
|
174
|
+
)[0] as SonicWebGpu;
|
|
175
|
+
expect(typeof el.getFrameCanvas).toBe("function");
|
|
176
|
+
expect(typeof el.frameSeq).toBe("number");
|
|
177
|
+
});
|
|
141
178
|
});
|