@supersoniks/concorde 4.9.97-visual-stack.0 → 4.9.98-visual-stack.0
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 +143 -143
- package/concorde-3d.es.js +1393 -1377
- 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 +19 -19
- package/concorde-shader.es.js +2317 -2172
- package/concorde-webgpu.bundle.js +30 -30
- package/concorde-webgpu.es.js +2439 -2346
- package/dist/concorde-3d.bundle.js +143 -143
- package/dist/concorde-3d.es.js +1393 -1377
- 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 +19 -19
- package/dist/concorde-shader.es.js +2317 -2172
- package/dist/concorde-webgpu.bundle.js +30 -30
- package/dist/concorde-webgpu.es.js +2439 -2346
- package/dist/js/core/components/functional/3d/3d.js +11 -1
- package/dist/js/core/components/functional/3d/3d.js.map +1 -1
- package/dist/js/core/components/functional/3d/constants.js +2 -0
- package/dist/js/core/components/functional/3d/constants.js.map +1 -1
- package/dist/js/core/components/functional/3d/frame-source.js +22 -0
- package/dist/js/core/components/functional/3d/frame-source.js.map +1 -0
- package/dist/js/core/components/functional/3d/reload.js +3 -0
- package/dist/js/core/components/functional/3d/reload.js.map +1 -1
- package/dist/js/core/components/functional/shader/gl-runtime.js +152 -17
- package/dist/js/core/components/functional/shader/gl-runtime.js.map +1 -1
- package/dist/js/core/components/functional/shader/shader.js +55 -6
- package/dist/js/core/components/functional/shader/shader.js.map +1 -1
- package/dist/js/core/components/functional/shader/types.js.map +1 -1
- package/dist/js/core/components/functional/webgpu/webgpu.js +9 -0
- package/dist/js/core/components/functional/webgpu/webgpu.js.map +1 -1
- package/dist/js/core/utils/mediaRef.js +49 -0
- package/dist/js/core/utils/mediaRef.js.map +1 -1
- package/dist/types/core/components/functional/3d/3d.d.ts +7 -2
- 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/frame-source.d.ts +23 -0
- package/dist/types/core/components/functional/3d/frame-source.d.ts.map +1 -0
- package/dist/types/core/components/functional/3d/reload.d.ts +3 -0
- package/dist/types/core/components/functional/3d/reload.d.ts.map +1 -1
- package/dist/types/core/components/functional/3d/types.d.ts +7 -0
- package/dist/types/core/components/functional/3d/types.d.ts.map +1 -1
- package/dist/types/core/components/functional/shader/gl-runtime.d.ts +20 -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 +14 -3
- package/dist/types/core/components/functional/shader/shader.d.ts.map +1 -1
- package/dist/types/core/components/functional/shader/types.d.ts +6 -0
- package/dist/types/core/components/functional/shader/types.d.ts.map +1 -1
- package/dist/types/core/components/functional/webgpu/webgpu.d.ts +4 -0
- package/dist/types/core/components/functional/webgpu/webgpu.d.ts.map +1 -1
- package/dist/types/core/utils/mediaRef.d.ts +19 -0
- package/dist/types/core/utils/mediaRef.d.ts.map +1 -1
- package/package.json +13 -1
- package/src/core/components/functional/3d/3d.md +12 -2
- package/src/core/components/functional/3d/3d.ts +12 -1
- package/src/core/components/functional/3d/constants.ts +2 -0
- package/src/core/components/functional/3d/frame-source.ts +30 -0
- package/src/core/components/functional/3d/reload.ts +6 -0
- package/src/core/components/functional/3d/types.ts +5 -0
- package/src/core/components/functional/shader/gl-runtime.ts +205 -24
- package/src/core/components/functional/shader/shader.md +10 -8
- package/src/core/components/functional/shader/shader.spec.ts +83 -3
- package/src/core/components/functional/shader/shader.ts +61 -6
- package/src/core/components/functional/shader/types.ts +3 -1
- package/src/core/components/functional/webgpu/webgpu.ts +9 -0
- package/src/core/utils/mediaRef.spec.ts +36 -0
- package/src/core/utils/mediaRef.ts +63 -0
- package/src/docs/_misc/visual-stack.md +18 -2
- package/src/docs/components/docs-demo-sources.ts +14 -0
- package/src/docs/docs.ts +1 -0
- package/src/docs/example/docs-3d-shader-live-demo.ts +98 -0
- package/src/docs/search/docs-search.json +2347 -877
- package/src/tsconfig.json +9 -0
- package/src/tsconfig.tsbuildinfo +1 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isFrameSource,
|
|
3
|
+
type SonicFrameSource,
|
|
4
|
+
} from "@supersoniks/concorde/core/utils/mediaRef";
|
|
1
5
|
import { buildFragmentSource, FULLSCREEN_VERTEX } from "./shadertoy-prelude";
|
|
2
6
|
import {
|
|
3
7
|
ChannelSource,
|
|
@@ -55,6 +59,10 @@ type ExternalTexture = {
|
|
|
55
59
|
height: number;
|
|
56
60
|
url: string;
|
|
57
61
|
video?: HTMLVideoElement;
|
|
62
|
+
/** Source live (`#élément`) — image / vidéo / canvas / SonicFrameSource. */
|
|
63
|
+
source?: TexImageSource | SonicFrameSource | null;
|
|
64
|
+
lastSeq: number;
|
|
65
|
+
lastSrc?: string;
|
|
58
66
|
};
|
|
59
67
|
|
|
60
68
|
export type FrameUniforms = {
|
|
@@ -65,10 +73,91 @@ export type FrameUniforms = {
|
|
|
65
73
|
params: [number, number, number, number];
|
|
66
74
|
};
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Décide si un slot élément doit être re-uploadé cette frame.
|
|
78
|
+
* Exporté pour les tests unitaires (stratégie par type).
|
|
79
|
+
*/
|
|
80
|
+
export function shouldUploadElementFrame(
|
|
81
|
+
source: unknown,
|
|
82
|
+
lastSeq: number,
|
|
83
|
+
lastSrc?: string,
|
|
84
|
+
): { upload: boolean; texSource: TexImageSource | null; nextSrc?: string } {
|
|
85
|
+
if (!source) return { upload: false, texSource: null };
|
|
86
|
+
if (
|
|
87
|
+
typeof HTMLImageElement !== "undefined" &&
|
|
88
|
+
source instanceof HTMLImageElement
|
|
89
|
+
) {
|
|
90
|
+
const src = source.currentSrc || source.src || "";
|
|
91
|
+
if (!src || !source.complete || source.naturalWidth < 1) {
|
|
92
|
+
return { upload: false, texSource: null };
|
|
93
|
+
}
|
|
94
|
+
if (lastSrc === src && lastSeq > 0) {
|
|
95
|
+
return { upload: false, texSource: null, nextSrc: src };
|
|
96
|
+
}
|
|
97
|
+
return { upload: true, texSource: source, nextSrc: src };
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
typeof HTMLVideoElement !== "undefined" &&
|
|
101
|
+
source instanceof HTMLVideoElement
|
|
102
|
+
) {
|
|
103
|
+
if (source.readyState < 2 || source.videoWidth < 1) {
|
|
104
|
+
return { upload: false, texSource: null };
|
|
105
|
+
}
|
|
106
|
+
return { upload: true, texSource: source };
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
typeof HTMLCanvasElement !== "undefined" &&
|
|
110
|
+
source instanceof HTMLCanvasElement
|
|
111
|
+
) {
|
|
112
|
+
if (source.width < 1 || source.height < 1) {
|
|
113
|
+
return { upload: false, texSource: null };
|
|
114
|
+
}
|
|
115
|
+
return { upload: true, texSource: source };
|
|
116
|
+
}
|
|
117
|
+
if (isFrameSource(source)) {
|
|
118
|
+
if (source.frameSeq === lastSeq) {
|
|
119
|
+
return { upload: false, texSource: null };
|
|
120
|
+
}
|
|
121
|
+
const canvas = source.getFrameCanvas();
|
|
122
|
+
if (!canvas || canvas.width < 1 || canvas.height < 1) {
|
|
123
|
+
return { upload: false, texSource: null };
|
|
124
|
+
}
|
|
125
|
+
return { upload: true, texSource: canvas };
|
|
126
|
+
}
|
|
127
|
+
return { upload: false, texSource: null };
|
|
128
|
+
}
|
|
129
|
+
|
|
68
130
|
function isVideoUrl(url: string): boolean {
|
|
69
131
|
return /\.(webm|mp4|ogg|mov)(\?|#|$)/i.test(url);
|
|
70
132
|
}
|
|
71
133
|
|
|
134
|
+
function texImageSourceSize(
|
|
135
|
+
src: TexImageSource,
|
|
136
|
+
): { width: number; height: number } {
|
|
137
|
+
if (
|
|
138
|
+
typeof HTMLVideoElement !== "undefined" &&
|
|
139
|
+
src instanceof HTMLVideoElement
|
|
140
|
+
) {
|
|
141
|
+
return { width: src.videoWidth || 1, height: src.videoHeight || 1 };
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
typeof HTMLImageElement !== "undefined" &&
|
|
145
|
+
src instanceof HTMLImageElement
|
|
146
|
+
) {
|
|
147
|
+
return {
|
|
148
|
+
width: src.naturalWidth || src.width || 1,
|
|
149
|
+
height: src.naturalHeight || src.height || 1,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if ("width" in src && "height" in src) {
|
|
153
|
+
return {
|
|
154
|
+
width: Math.max(1, Number(src.width) || 1),
|
|
155
|
+
height: Math.max(1, Number(src.height) || 1),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { width: 1, height: 1 };
|
|
159
|
+
}
|
|
160
|
+
|
|
72
161
|
function compileShader(
|
|
73
162
|
gl: WebGL2RenderingContext,
|
|
74
163
|
type: number,
|
|
@@ -221,9 +310,12 @@ export function parseChannelSource(
|
|
|
221
310
|
): ChannelSource {
|
|
222
311
|
const v = (value ?? "").trim();
|
|
223
312
|
if (!v) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
313
|
+
const fb = (fallbackUrl ?? "").trim();
|
|
314
|
+
if (fb.startsWith("#")) return { kind: "element", key: fb };
|
|
315
|
+
return fb ? { kind: "url", url: fb } : { kind: "none" };
|
|
316
|
+
}
|
|
317
|
+
if (v.startsWith("#")) {
|
|
318
|
+
return { kind: "element", key: v };
|
|
227
319
|
}
|
|
228
320
|
const lower = v.toLowerCase();
|
|
229
321
|
if (lower === "self") return { kind: "self" };
|
|
@@ -256,6 +348,7 @@ export class ShaderToyRuntime {
|
|
|
256
348
|
private passChannels = new Map<ShaderPassId, PassChannels>();
|
|
257
349
|
private activePasses: ShaderPassId[] = [];
|
|
258
350
|
private blackTexture: WebGLTexture | null = null;
|
|
351
|
+
private elementResolver: ((key: string) => Element | null) | null = null;
|
|
259
352
|
|
|
260
353
|
constructor(canvas: HTMLCanvasElement) {
|
|
261
354
|
const gl = canvas.getContext("webgl2", {
|
|
@@ -383,35 +476,115 @@ export class ShaderToyRuntime {
|
|
|
383
476
|
await Promise.all(unique.map((url) => this.loadTexture(url)));
|
|
384
477
|
}
|
|
385
478
|
|
|
386
|
-
/**
|
|
387
|
-
|
|
479
|
+
/** Alloue un slot texture vide pour un canal `#élément` (upload live ensuite). */
|
|
480
|
+
ensureElementTexture(key: string) {
|
|
481
|
+
if (this.external.has(key)) return;
|
|
388
482
|
const gl = this.gl;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
483
|
+
const tex = gl.createTexture();
|
|
484
|
+
if (!tex) return;
|
|
485
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
486
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
487
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
488
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
489
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
490
|
+
gl.texImage2D(
|
|
491
|
+
gl.TEXTURE_2D,
|
|
492
|
+
0,
|
|
493
|
+
gl.RGBA8,
|
|
494
|
+
1,
|
|
495
|
+
1,
|
|
496
|
+
0,
|
|
497
|
+
gl.RGBA,
|
|
498
|
+
gl.UNSIGNED_BYTE,
|
|
499
|
+
new Uint8Array([0, 0, 0, 255]),
|
|
500
|
+
);
|
|
501
|
+
this.external.set(key, {
|
|
502
|
+
texture: tex,
|
|
503
|
+
width: 1,
|
|
504
|
+
height: 1,
|
|
505
|
+
url: key,
|
|
506
|
+
lastSeq: 0,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Upload live des canaux élément / vidéo.
|
|
512
|
+
* `resolveEl(key)` fournit l’Element (résolution `#id` côté host).
|
|
513
|
+
*/
|
|
514
|
+
updateElementTextures(resolveEl?: (key: string) => Element | null) {
|
|
515
|
+
const gl = this.gl;
|
|
516
|
+
for (const [key, ext] of this.external) {
|
|
517
|
+
let source: unknown = ext.video ?? ext.source ?? null;
|
|
518
|
+
// Canaux #id : re-résoudre chaque frame (élément peut apparaître / partir).
|
|
519
|
+
if (key.startsWith("#") && resolveEl) {
|
|
520
|
+
source = resolveEl(key);
|
|
521
|
+
ext.source = source as TexImageSource | SonicFrameSource | null;
|
|
522
|
+
}
|
|
523
|
+
if (
|
|
524
|
+
typeof HTMLVideoElement !== "undefined" &&
|
|
525
|
+
source instanceof HTMLVideoElement
|
|
526
|
+
) {
|
|
527
|
+
ext.video = source;
|
|
528
|
+
}
|
|
529
|
+
const decision = shouldUploadElementFrame(
|
|
530
|
+
source,
|
|
531
|
+
ext.lastSeq,
|
|
532
|
+
ext.lastSrc,
|
|
533
|
+
);
|
|
534
|
+
if (!decision.upload || !decision.texSource) continue;
|
|
535
|
+
const size = texImageSourceSize(decision.texSource);
|
|
392
536
|
gl.bindTexture(gl.TEXTURE_2D, ext.texture);
|
|
393
537
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
|
|
394
|
-
|
|
395
|
-
gl.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
538
|
+
if (ext.width === size.width && ext.height === size.height && ext.lastSeq > 0) {
|
|
539
|
+
gl.texSubImage2D(
|
|
540
|
+
gl.TEXTURE_2D,
|
|
541
|
+
0,
|
|
542
|
+
0,
|
|
543
|
+
0,
|
|
544
|
+
gl.RGBA,
|
|
545
|
+
gl.UNSIGNED_BYTE,
|
|
546
|
+
decision.texSource,
|
|
547
|
+
);
|
|
548
|
+
} else {
|
|
549
|
+
gl.texImage2D(
|
|
550
|
+
gl.TEXTURE_2D,
|
|
551
|
+
0,
|
|
552
|
+
gl.RGBA8,
|
|
553
|
+
gl.RGBA,
|
|
554
|
+
gl.UNSIGNED_BYTE,
|
|
555
|
+
decision.texSource,
|
|
556
|
+
);
|
|
557
|
+
ext.width = size.width;
|
|
558
|
+
ext.height = size.height;
|
|
559
|
+
}
|
|
402
560
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
|
|
403
|
-
|
|
404
|
-
|
|
561
|
+
if (isFrameSource(source)) {
|
|
562
|
+
ext.lastSeq = source.frameSeq;
|
|
563
|
+
} else {
|
|
564
|
+
ext.lastSeq = (ext.lastSeq || 0) + 1;
|
|
565
|
+
}
|
|
566
|
+
if (decision.nextSrc !== undefined) ext.lastSrc = decision.nextSrc;
|
|
405
567
|
}
|
|
406
568
|
}
|
|
407
569
|
|
|
570
|
+
/** @deprecated alias — préférer `updateElementTextures`. */
|
|
571
|
+
updateVideoTextures() {
|
|
572
|
+
this.updateElementTextures();
|
|
573
|
+
}
|
|
574
|
+
|
|
408
575
|
setVideosPlaying(playing: boolean) {
|
|
409
576
|
for (const ext of this.external.values()) {
|
|
410
|
-
|
|
577
|
+
const video =
|
|
578
|
+
ext.video ||
|
|
579
|
+
(typeof HTMLVideoElement !== "undefined" &&
|
|
580
|
+
ext.source instanceof HTMLVideoElement
|
|
581
|
+
? ext.source
|
|
582
|
+
: null);
|
|
583
|
+
if (!video) continue;
|
|
411
584
|
if (playing) {
|
|
412
|
-
void
|
|
585
|
+
void video.play().catch(() => {});
|
|
413
586
|
} else {
|
|
414
|
-
|
|
587
|
+
video.pause();
|
|
415
588
|
}
|
|
416
589
|
}
|
|
417
590
|
}
|
|
@@ -450,6 +623,7 @@ export class ShaderToyRuntime {
|
|
|
450
623
|
width: img.naturalWidth || img.width,
|
|
451
624
|
height: img.naturalHeight || img.height,
|
|
452
625
|
url,
|
|
626
|
+
lastSeq: 0,
|
|
453
627
|
});
|
|
454
628
|
resolve();
|
|
455
629
|
};
|
|
@@ -503,6 +677,7 @@ export class ShaderToyRuntime {
|
|
|
503
677
|
height: video.videoHeight,
|
|
504
678
|
url,
|
|
505
679
|
video,
|
|
680
|
+
lastSeq: 0,
|
|
506
681
|
});
|
|
507
682
|
void video.play().catch(() => {});
|
|
508
683
|
resolve();
|
|
@@ -524,8 +699,9 @@ export class ShaderToyRuntime {
|
|
|
524
699
|
height: 1,
|
|
525
700
|
};
|
|
526
701
|
if (source.kind === "none") return black;
|
|
527
|
-
if (source.kind === "url") {
|
|
528
|
-
const
|
|
702
|
+
if (source.kind === "url" || source.kind === "element") {
|
|
703
|
+
const key = source.kind === "url" ? source.url : source.key;
|
|
704
|
+
const ext = this.external.get(key);
|
|
529
705
|
return ext
|
|
530
706
|
? { texture: ext.texture, width: ext.width, height: ext.height }
|
|
531
707
|
: black;
|
|
@@ -620,10 +796,15 @@ export class ShaderToyRuntime {
|
|
|
620
796
|
}
|
|
621
797
|
}
|
|
622
798
|
|
|
799
|
+
/** Host fournit la résolution `#id` / live Element. */
|
|
800
|
+
setElementResolver(fn: ((key: string) => Element | null) | null) {
|
|
801
|
+
this.elementResolver = fn;
|
|
802
|
+
}
|
|
803
|
+
|
|
623
804
|
render(frame: FrameUniforms) {
|
|
624
805
|
const gl = this.gl;
|
|
625
806
|
if (!this.vao || this.width < 1 || this.height < 1) return;
|
|
626
|
-
this.
|
|
807
|
+
this.updateElementTextures(this.elementResolver ?? undefined);
|
|
627
808
|
gl.bindVertexArray(this.vao);
|
|
628
809
|
gl.viewport(0, 0, this.width, this.height);
|
|
629
810
|
gl.disable(gl.DEPTH_TEST);
|
|
@@ -24,7 +24,7 @@ CDN (after the core): `concorde-shader.bundle.js` — see [CDN addons](#docs/_ge
|
|
|
24
24
|
|
|
25
25
|
Uniforms supportés : `iResolution`, `iTime`, `iTimeDelta`, `iFrame`, `iFrameRate`, `iMouse`, `iDate`, `iChannel0–3`, `iChannelResolution`, `iChannelTime`, `iSampleRate`, `uParam0–3`.
|
|
26
26
|
|
|
27
|
-
Textures `channel0–3` : images
|
|
27
|
+
Textures `channel0–3` : images, vidéos (`.webm` / `.mp4` / …), **ou** éléments page via `#id` (`<img>`, `<video>`, `<canvas>`, `sonic-3d` / `sonic-webgpu` / `sonic-shader`).
|
|
28
28
|
|
|
29
29
|
## Multipass sur une photo
|
|
30
30
|
|
|
@@ -34,20 +34,22 @@ Buffer A simule une height-map d’ondes (ripple) ; Image réfracte la photo via
|
|
|
34
34
|
|
|
35
35
|
Photo + depth parallax (HF): [Visual stack](#docs/_misc/visual-stack.md/visual-stack).
|
|
36
36
|
|
|
37
|
+
Live 3D → shader (no JPEG): [Visual stack](#docs/_misc/visual-stack.md/visual-stack).
|
|
38
|
+
|
|
37
39
|
## Chaînage d’effets (officiel)
|
|
38
40
|
|
|
39
|
-
**Règle :** brancher des passes dans **un** `sonic-shader` (buffers `A–D` → `Image`), pas deux composants via dataProvider.
|
|
41
|
+
**Règle :** brancher des passes dans **un** `sonic-shader` (buffers `A–D` → `Image`), pas deux composants via dataProvider — **sauf** pont live `#élément` (copie canvas chaque frame).
|
|
40
42
|
|
|
41
43
|
| Besoin | Mécanisme |
|
|
42
44
|
|--------|-----------|
|
|
43
45
|
| Sortie d’un effet → entrée du suivant | `image-ch0="buffer-a"` (ou `buffer-b`… / `self`) — **même contexte GPU**, FBO ping-pong |
|
|
44
|
-
|
|
|
46
|
+
| Scene 3D / autre canvas → post | `channel0="#plateau"` — copie `texImage2D` chaque `frameSeq` |
|
|
47
|
+
| Params / sync UI | `dataProvider` → `uParam0…3` **et** canaux (`channel0`, `bufferACh0`…) — string URL, `#id`, ou `SonicMediaRef` (`url` / `element`) |
|
|
45
48
|
| Hors viewport | `release-offscreen` (défaut) : libère textures/FBO, garde le contexte |
|
|
46
49
|
|
|
47
|
-
Les passes intermédiaires sont **déjà offscreen** (FBO). Pas besoin d’un second `sonic-shader` invisible.
|
|
48
|
-
|
|
49
|
-
Deux instances séparées ne partagent pas leurs textures WebGL (contextes distincts). Un bus texture via DP n’existe pas (et un `toBlob`/`dataURL` ne serait pas temps réel).
|
|
50
|
+
Les passes intermédiaires sont **déjà offscreen** (FBO). Pas besoin d’un second `sonic-shader` invisible pour du multipass interne.
|
|
50
51
|
|
|
52
|
+
Deux instances séparées ne partagent pas leurs textures WebGL (contextes distincts). Le pont live `#id` **copie** le canvas source ; `frame-out` JPEG reste pour export / freeze.
|
|
51
53
|
Démo pédagogique : Buffer A = Sobel → Image = overlay + `param0`.
|
|
52
54
|
|
|
53
55
|
<docs-lit-demo for="docs-shader-chain-demo"></docs-lit-demo>
|
|
@@ -143,8 +145,8 @@ Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/v
|
|
|
143
145
|
| `image` | `string` | GLSL pass Image (**requis**, propriété JS) |
|
|
144
146
|
| `bufferA`…`bufferD` / `buffer-a`… | `string` | Passes multipass |
|
|
145
147
|
| `common` | `string` | GLSL partagé |
|
|
146
|
-
| `channel0`…`channel3` | `string` | URLs
|
|
147
|
-
| `imageCh0`… / `image-ch0`… | `string` | Override canal : URL, `self`, `buffer-a`…, `none` |
|
|
148
|
+
| `channel0`…`channel3` | `string` | URLs, `#id` élément, ou refs buffers (toutes passes) |
|
|
149
|
+
| `imageCh0`… / `image-ch0`… | `string` | Override canal : URL, `#id`, `self`, `buffer-a`…, `none` |
|
|
148
150
|
| `play` | `boolean` | Boucle RAF (défaut `true`) |
|
|
149
151
|
| `playOnHover` / `play-on-hover` | `boolean` | Anime seulement au hover / transition |
|
|
150
152
|
| `mouse` | `boolean` | Pointeur → `iMouse` (défaut `true`) |
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { expect, describe, it, vi, afterEach, beforeAll } from "vitest";
|
|
2
2
|
import TestUtils from "@supersoniks/concorde/test-utils/TestUtils";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseChannelSource,
|
|
5
|
+
shouldUploadElementFrame,
|
|
6
|
+
} from "./gl-runtime";
|
|
4
7
|
import { buildFragmentSource } from "./shadertoy-prelude";
|
|
5
8
|
import "./shader";
|
|
6
|
-
import { SonicShader } from "./shader";
|
|
9
|
+
import { SonicShader, channelFormValue } from "./shader";
|
|
7
10
|
|
|
8
11
|
describe("parseChannelSource", () => {
|
|
9
12
|
it("résout self / buffers / url / fallback", () => {
|
|
@@ -26,6 +29,81 @@ describe("parseChannelSource", () => {
|
|
|
26
29
|
});
|
|
27
30
|
expect(parseChannelSource("none", "")).toEqual({ kind: "none" });
|
|
28
31
|
});
|
|
32
|
+
|
|
33
|
+
it("résout #id comme élément (pas URL)", () => {
|
|
34
|
+
expect(parseChannelSource("#plateau", "")).toEqual({
|
|
35
|
+
kind: "element",
|
|
36
|
+
key: "#plateau",
|
|
37
|
+
});
|
|
38
|
+
expect(parseChannelSource("", "#scene")).toEqual({
|
|
39
|
+
kind: "element",
|
|
40
|
+
key: "#scene",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("channelFormValue", () => {
|
|
46
|
+
it("passe { element } → #id", () => {
|
|
47
|
+
expect(channelFormValue({ element: "#plateau" })).toBe("#plateau");
|
|
48
|
+
expect(channelFormValue({ element: "plateau" })).toBe("#plateau");
|
|
49
|
+
expect(channelFormValue({ url: "https://x/a.png" })).toBe(
|
|
50
|
+
"https://x/a.png",
|
|
51
|
+
);
|
|
52
|
+
expect(channelFormValue({ blob: new Blob() })).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("shouldUploadElementFrame", () => {
|
|
57
|
+
it("canvas : upload chaque frame si taille ok", () => {
|
|
58
|
+
const canvas = document.createElement("canvas");
|
|
59
|
+
canvas.width = 4;
|
|
60
|
+
canvas.height = 4;
|
|
61
|
+
expect(shouldUploadElementFrame(canvas, 0).upload).toBe(true);
|
|
62
|
+
expect(shouldUploadElementFrame(canvas, 3).upload).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("FrameSource : skip si frameSeq inchangé, upload sinon", () => {
|
|
66
|
+
const canvas = document.createElement("canvas");
|
|
67
|
+
canvas.width = 2;
|
|
68
|
+
canvas.height = 2;
|
|
69
|
+
const src = {
|
|
70
|
+
frameSeq: 5,
|
|
71
|
+
getFrameCanvas: () => canvas,
|
|
72
|
+
};
|
|
73
|
+
expect(shouldUploadElementFrame(src, 5).upload).toBe(false);
|
|
74
|
+
const up = shouldUploadElementFrame(src, 4);
|
|
75
|
+
expect(up.upload).toBe(true);
|
|
76
|
+
expect(up.texSource).toBe(canvas);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("source absente → pas d’upload (texture noire côté runtime)", () => {
|
|
80
|
+
expect(shouldUploadElementFrame(null, 0).upload).toBe(false);
|
|
81
|
+
expect(
|
|
82
|
+
shouldUploadElementFrame(
|
|
83
|
+
{ frameSeq: 1, getFrameCanvas: () => null },
|
|
84
|
+
0,
|
|
85
|
+
).upload,
|
|
86
|
+
).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("image : upload au changement de src seulement", () => {
|
|
90
|
+
const img = document.createElement("img");
|
|
91
|
+
Object.defineProperty(img, "complete", { value: true });
|
|
92
|
+
Object.defineProperty(img, "naturalWidth", { value: 8 });
|
|
93
|
+
Object.defineProperty(img, "naturalHeight", { value: 8 });
|
|
94
|
+
Object.defineProperty(img, "currentSrc", {
|
|
95
|
+
value: "https://example.test/a.png",
|
|
96
|
+
});
|
|
97
|
+
const first = shouldUploadElementFrame(img, 0);
|
|
98
|
+
expect(first.upload).toBe(true);
|
|
99
|
+
expect(first.nextSrc).toBe("https://example.test/a.png");
|
|
100
|
+
const again = shouldUploadElementFrame(
|
|
101
|
+
img,
|
|
102
|
+
1,
|
|
103
|
+
"https://example.test/a.png",
|
|
104
|
+
);
|
|
105
|
+
expect(again.upload).toBe(false);
|
|
106
|
+
});
|
|
29
107
|
});
|
|
30
108
|
|
|
31
109
|
describe("buildFragmentSource", () => {
|
|
@@ -207,7 +285,7 @@ describe("SonicShader", () => {
|
|
|
207
285
|
expect(el.param0).toBe(0.99);
|
|
208
286
|
});
|
|
209
287
|
|
|
210
|
-
it("applique les canaux depuis dataProvider (url | MediaRef)", async () => {
|
|
288
|
+
it("applique les canaux depuis dataProvider (url | MediaRef | element)", async () => {
|
|
211
289
|
const { PublisherManager } = await import(
|
|
212
290
|
"@supersoniks/concorde/utils"
|
|
213
291
|
);
|
|
@@ -217,6 +295,7 @@ describe("SonicShader", () => {
|
|
|
217
295
|
bufferACh0: { url: "https://example.test/depth.png" },
|
|
218
296
|
imageCh0: "buffer-a",
|
|
219
297
|
imageCh1: "https://example.test/color.jpg",
|
|
298
|
+
channel0: { element: "#plateau" },
|
|
220
299
|
param0: 0.5,
|
|
221
300
|
});
|
|
222
301
|
const el = TestUtils.bootstrap(
|
|
@@ -228,6 +307,7 @@ describe("SonicShader", () => {
|
|
|
228
307
|
expect(el.bufferACh0).toBe("https://example.test/depth.png");
|
|
229
308
|
expect(el.imageCh0).toBe("buffer-a");
|
|
230
309
|
expect(el.imageCh1).toBe("https://example.test/color.jpg");
|
|
310
|
+
expect(el.channel0).toBe("#plateau");
|
|
231
311
|
expect(el.param0).toBe(0.5);
|
|
232
312
|
});
|
|
233
313
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css, html, LitElement } from "lit";
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
3
|
import HTML from "@supersoniks/concorde/core/utils/HTML";
|
|
4
|
-
import { revokeMediaUrls, toMediaUrl } from "@supersoniks/concorde/core/utils/mediaRef";
|
|
4
|
+
import { revokeMediaUrls, toMediaUrl, mediaElementKey, lookupLiveElement } from "@supersoniks/concorde/core/utils/mediaRef";
|
|
5
5
|
import { PublisherManager } from "@supersoniks/concorde/utils";
|
|
6
6
|
import {
|
|
7
7
|
parseChannelSource,
|
|
@@ -104,16 +104,24 @@ function toNum(v: unknown, fallback: number): number {
|
|
|
104
104
|
return fallback;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** Valeur canal depuis DP : string (url
|
|
107
|
+
/** Valeur canal depuis DP : string (url, buffer-a, #id) | MediaRef → url | { element }. */
|
|
108
108
|
function channelFormValue(value: unknown): string | undefined {
|
|
109
109
|
if (value === null || value === "") return "";
|
|
110
110
|
if (typeof value === "string") return value.trim();
|
|
111
111
|
const url = toMediaUrl(value);
|
|
112
112
|
if (url) return url;
|
|
113
|
-
if (value && typeof value === "object")
|
|
113
|
+
if (value && typeof value === "object") {
|
|
114
|
+
const el = (value as { element?: unknown }).element;
|
|
115
|
+
const key = mediaElementKey(el);
|
|
116
|
+
if (key) return key;
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
114
119
|
return "";
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
/** Exposé pour tests. */
|
|
123
|
+
export { channelFormValue };
|
|
124
|
+
|
|
117
125
|
function parseRois(value: unknown): ShaderRoi[] {
|
|
118
126
|
if (!Array.isArray(value)) return [];
|
|
119
127
|
const out: ShaderRoi[] = [];
|
|
@@ -364,6 +372,8 @@ export class SonicShader extends LitElement {
|
|
|
364
372
|
private startTime = 0;
|
|
365
373
|
private lastTime = 0;
|
|
366
374
|
private frame = 0;
|
|
375
|
+
/** Incrémenté après chaque render (contrat SonicFrameSource). */
|
|
376
|
+
frameSeq = 0;
|
|
367
377
|
private mouseVec: [number, number, number, number] = [0, 0, 0, 0];
|
|
368
378
|
/** Cible pointeur (px drawing buffer, origine bas-gauche). */
|
|
369
379
|
private mouseTarget: [number, number] = [0, 0];
|
|
@@ -702,6 +712,27 @@ export class SonicShader extends LitElement {
|
|
|
702
712
|
);
|
|
703
713
|
}
|
|
704
714
|
|
|
715
|
+
/** Contrat SonicFrameSource — canvas lisible pour un autre sonic-shader. */
|
|
716
|
+
getFrameCanvas(): HTMLCanvasElement | null {
|
|
717
|
+
if (!this.ready || !this.runtime || this.runtime.isLost) return null;
|
|
718
|
+
return this.getCanvas();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Résout `#id` dans getRootNode() puis document ; clés live `__sonic_frame_`. */
|
|
722
|
+
private lookupElement(key: string): Element | null {
|
|
723
|
+
if (!key.startsWith("#")) return null;
|
|
724
|
+
const live = lookupLiveElement(key);
|
|
725
|
+
if (live) return live;
|
|
726
|
+
const id = key.slice(1);
|
|
727
|
+
if (!id || id.startsWith("__sonic_frame_")) return null;
|
|
728
|
+
const root = this.getRootNode();
|
|
729
|
+
if (root instanceof Document || root instanceof ShadowRoot) {
|
|
730
|
+
const el = root.getElementById(id);
|
|
731
|
+
if (el) return el;
|
|
732
|
+
}
|
|
733
|
+
return typeof document !== "undefined" ? document.getElementById(id) : null;
|
|
734
|
+
}
|
|
735
|
+
|
|
705
736
|
private bindContextEvents() {
|
|
706
737
|
const canvas = this.getCanvas();
|
|
707
738
|
canvas?.addEventListener("webglcontextlost", this.onContextLost);
|
|
@@ -743,6 +774,7 @@ export class SonicShader extends LitElement {
|
|
|
743
774
|
try {
|
|
744
775
|
this.errorMessage = "";
|
|
745
776
|
this.runtime = new ShaderToyRuntime(canvas);
|
|
777
|
+
this.runtime.setElementResolver((key) => this.lookupElement(key));
|
|
746
778
|
this.bindContextEvents();
|
|
747
779
|
this.syncSize();
|
|
748
780
|
await this.rebuild();
|
|
@@ -827,6 +859,18 @@ export class SonicShader extends LitElement {
|
|
|
827
859
|
return urls;
|
|
828
860
|
}
|
|
829
861
|
|
|
862
|
+
private collectElementKeys(
|
|
863
|
+
channels: Map<ShaderPassId, PassChannels>,
|
|
864
|
+
): string[] {
|
|
865
|
+
const keys: string[] = [];
|
|
866
|
+
for (const chs of channels.values()) {
|
|
867
|
+
for (const c of chs) {
|
|
868
|
+
if (c.kind === "element") keys.push(c.key);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return [...new Set(keys)];
|
|
872
|
+
}
|
|
873
|
+
|
|
830
874
|
private async rebuild() {
|
|
831
875
|
if (!this.runtime || this.runtime.isLost || !this.image.trim()) return;
|
|
832
876
|
const token = ++this.rebuildToken;
|
|
@@ -853,10 +897,15 @@ export class SonicShader extends LitElement {
|
|
|
853
897
|
this.syncSize();
|
|
854
898
|
const urls = this.collectUrls(channels);
|
|
855
899
|
await this.runtime.ensureTextures(urls);
|
|
900
|
+
for (const key of this.collectElementKeys(channels)) {
|
|
901
|
+
this.runtime.ensureElementTexture(key);
|
|
902
|
+
}
|
|
856
903
|
if (token !== this.rebuildToken || !this.runtime || this.runtime.isLost) {
|
|
857
904
|
return;
|
|
858
905
|
}
|
|
859
|
-
|
|
906
|
+
const aspectHint =
|
|
907
|
+
this.channel0.startsWith("#") ? "" : this.channel0 || urls[0];
|
|
908
|
+
this.applyAspectRatio(aspectHint);
|
|
860
909
|
this.syncSize();
|
|
861
910
|
this.runtime.setSources(sources, channels);
|
|
862
911
|
if (token !== this.rebuildToken || !this.runtime || this.runtime.isLost) {
|
|
@@ -964,6 +1013,7 @@ export class SonicShader extends LitElement {
|
|
|
964
1013
|
mouse: this.mouseVec,
|
|
965
1014
|
params,
|
|
966
1015
|
});
|
|
1016
|
+
this.frameSeq++;
|
|
967
1017
|
this.maybePublishOut(now, time, params);
|
|
968
1018
|
void this.maybeExtract(now, time);
|
|
969
1019
|
}
|
|
@@ -1531,7 +1581,7 @@ export class SonicShader extends LitElement {
|
|
|
1531
1581
|
}
|
|
1532
1582
|
}
|
|
1533
1583
|
|
|
1534
|
-
export { parseChannelSource, ShaderToyRuntime } from "./gl-runtime";
|
|
1584
|
+
export { parseChannelSource, ShaderToyRuntime, shouldUploadElementFrame } from "./gl-runtime";
|
|
1535
1585
|
export { buildFragmentSource } from "./shadertoy-prelude";
|
|
1536
1586
|
export type {
|
|
1537
1587
|
ChannelSource,
|
|
@@ -1548,10 +1598,15 @@ export type {
|
|
|
1548
1598
|
SonicShaderConfig,
|
|
1549
1599
|
SonicShaderOutput,
|
|
1550
1600
|
} from "./types";
|
|
1551
|
-
export type {
|
|
1601
|
+
export type {
|
|
1602
|
+
SonicMediaRef,
|
|
1603
|
+
SonicFrameSource,
|
|
1604
|
+
} from "@supersoniks/concorde/core/utils/mediaRef";
|
|
1552
1605
|
export {
|
|
1553
1606
|
toMediaUrl,
|
|
1554
1607
|
cloneMediaRef,
|
|
1608
|
+
isFrameSource,
|
|
1609
|
+
mediaElementKey,
|
|
1555
1610
|
} from "@supersoniks/concorde/core/utils/mediaRef";
|
|
1556
1611
|
export {
|
|
1557
1612
|
findPeaks,
|
|
@@ -9,7 +9,9 @@ export type ChannelSource =
|
|
|
9
9
|
| { kind: "url"; url: string }
|
|
10
10
|
| { kind: "buffer"; buffer: Exclude<ShaderPassId, "image"> }
|
|
11
11
|
| { kind: "self" }
|
|
12
|
-
| { kind: "none" }
|
|
12
|
+
| { kind: "none" }
|
|
13
|
+
/** Élément page (`#id`) — upload live via `updateElementTextures`. */
|
|
14
|
+
| { kind: "element"; key: string; el?: Element | null };
|
|
13
15
|
|
|
14
16
|
export type PassChannels = [
|
|
15
17
|
ChannelSource,
|
|
@@ -138,6 +138,8 @@ export class SonicWebGpu extends LitElement {
|
|
|
138
138
|
raf = 0;
|
|
139
139
|
startTime = 0;
|
|
140
140
|
frame = 0;
|
|
141
|
+
/** Contrat SonicFrameSource — incrémenté après chaque draw. */
|
|
142
|
+
frameSeq = 0;
|
|
141
143
|
mouseVec: [number, number, number, number] = [0, 0, 0, 0];
|
|
142
144
|
/** Cible normalisée (0–1, origine haut-gauche) depuis dataProvider — tracking / DP. */
|
|
143
145
|
remoteTarget: { x: number; y: number } | null = null;
|
|
@@ -596,6 +598,7 @@ export class SonicWebGpu extends LitElement {
|
|
|
596
598
|
params: [this.param0, this.param1, this.param2, this.param3],
|
|
597
599
|
});
|
|
598
600
|
this.frame++;
|
|
601
|
+
this.frameSeq++;
|
|
599
602
|
this.maybePublishOut(now, time);
|
|
600
603
|
} catch (e) {
|
|
601
604
|
console.warn(`${LOG_PREFIX} frame skip:`, e);
|
|
@@ -620,6 +623,12 @@ export class SonicWebGpu extends LitElement {
|
|
|
620
623
|
outFrame.maybePublishOut(this as never, now, time);
|
|
621
624
|
}
|
|
622
625
|
|
|
626
|
+
/** Contrat SonicFrameSource — canvas WebGPU / fallback. */
|
|
627
|
+
getFrameCanvas(): HTMLCanvasElement | null {
|
|
628
|
+
if (!this.ready || this.errorMessage) return null;
|
|
629
|
+
return this.canvasEl;
|
|
630
|
+
}
|
|
631
|
+
|
|
623
632
|
private revokeFrameUrl() {
|
|
624
633
|
outFrame.revokeFrameUrl(this as never);
|
|
625
634
|
}
|