@supersoniks/concorde 4.9.97-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.
Files changed (171) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-3d.bundle.js +201 -152
  3. package/concorde-3d.es.js +14621 -13823
  4. package/concorde-core.bundle.js +1 -1
  5. package/concorde-core.es.js +1 -1
  6. package/concorde-hugging-face-infer.bundle.js +1 -1
  7. package/concorde-hugging-face-infer.es.js +1 -1
  8. package/concorde-shader.bundle.js +31 -20
  9. package/concorde-shader.es.js +3236 -2865
  10. package/concorde-webgpu.bundle.js +43 -32
  11. package/concorde-webgpu.es.js +2977 -2618
  12. package/dist/concorde-3d.bundle.js +201 -152
  13. package/dist/concorde-3d.es.js +14621 -13823
  14. package/dist/concorde-core.bundle.js +1 -1
  15. package/dist/concorde-core.es.js +1 -1
  16. package/dist/concorde-hugging-face-infer.bundle.js +1 -1
  17. package/dist/concorde-hugging-face-infer.es.js +1 -1
  18. package/dist/concorde-shader.bundle.js +31 -20
  19. package/dist/concorde-shader.es.js +3236 -2865
  20. package/dist/concorde-webgpu.bundle.js +43 -32
  21. package/dist/concorde-webgpu.es.js +2977 -2618
  22. package/dist/js/core/components/functional/3d/3d.js +100 -2
  23. package/dist/js/core/components/functional/3d/3d.js.map +1 -1
  24. package/dist/js/core/components/functional/3d/constants.js +3 -0
  25. package/dist/js/core/components/functional/3d/constants.js.map +1 -1
  26. package/dist/js/core/components/functional/3d/ensure.js +6 -3
  27. package/dist/js/core/components/functional/3d/ensure.js.map +1 -1
  28. package/dist/js/core/components/functional/3d/form-params.js +2 -0
  29. package/dist/js/core/components/functional/3d/form-params.js.map +1 -1
  30. package/dist/js/core/components/functional/3d/frame-source.js +22 -0
  31. package/dist/js/core/components/functional/3d/frame-source.js.map +1 -0
  32. package/dist/js/core/components/functional/3d/life.js +2 -1
  33. package/dist/js/core/components/functional/3d/life.js.map +1 -1
  34. package/dist/js/core/components/functional/3d/out-publish.js +3 -2
  35. package/dist/js/core/components/functional/3d/out-publish.js.map +1 -1
  36. package/dist/js/core/components/functional/3d/playback.js +6 -1
  37. package/dist/js/core/components/functional/3d/playback.js.map +1 -1
  38. package/dist/js/core/components/functional/3d/post-bind.js +207 -0
  39. package/dist/js/core/components/functional/3d/post-bind.js.map +1 -0
  40. package/dist/js/core/components/functional/3d/post-channels.js +39 -0
  41. package/dist/js/core/components/functional/3d/post-channels.js.map +1 -0
  42. package/dist/js/core/components/functional/3d/post-fx.js +495 -0
  43. package/dist/js/core/components/functional/3d/post-fx.js.map +1 -0
  44. package/dist/js/core/components/functional/3d/props.js +142 -0
  45. package/dist/js/core/components/functional/3d/props.js.map +1 -1
  46. package/dist/js/core/components/functional/3d/reload.js +5 -0
  47. package/dist/js/core/components/functional/3d/reload.js.map +1 -1
  48. package/dist/js/core/components/functional/3d/runtime.js +80 -3
  49. package/dist/js/core/components/functional/3d/runtime.js.map +1 -1
  50. package/dist/js/core/components/functional/3d/scene-runtime.js +1 -1
  51. package/dist/js/core/components/functional/3d/scene-runtime.js.map +1 -1
  52. package/dist/js/core/components/functional/3d/tune.js +21 -0
  53. package/dist/js/core/components/functional/3d/tune.js.map +1 -1
  54. package/dist/js/core/components/functional/3d/updated.js +13 -1
  55. package/dist/js/core/components/functional/3d/updated.js.map +1 -1
  56. package/dist/js/core/components/functional/shader/gl-runtime.js +280 -31
  57. package/dist/js/core/components/functional/shader/gl-runtime.js.map +1 -1
  58. package/dist/js/core/components/functional/shader/shader.js +207 -61
  59. package/dist/js/core/components/functional/shader/shader.js.map +1 -1
  60. package/dist/js/core/components/functional/shader/shadertoy-prelude.js +39 -5
  61. package/dist/js/core/components/functional/shader/shadertoy-prelude.js.map +1 -1
  62. package/dist/js/core/components/functional/shader/types.js +58 -7
  63. package/dist/js/core/components/functional/shader/types.js.map +1 -1
  64. package/dist/js/core/components/functional/webgpu/gl-fallback.js +27 -6
  65. package/dist/js/core/components/functional/webgpu/gl-fallback.js.map +1 -1
  66. package/dist/js/core/components/functional/webgpu/gpu-runtime.js +115 -13
  67. package/dist/js/core/components/functional/webgpu/gpu-runtime.js.map +1 -1
  68. package/dist/js/core/components/functional/webgpu/helpers.js +8 -2
  69. package/dist/js/core/components/functional/webgpu/helpers.js.map +1 -1
  70. package/dist/js/core/components/functional/webgpu/webgpu.js +43 -1
  71. package/dist/js/core/components/functional/webgpu/webgpu.js.map +1 -1
  72. package/dist/js/core/utils/mediaRef.js +86 -0
  73. package/dist/js/core/utils/mediaRef.js.map +1 -1
  74. package/dist/types/core/components/functional/3d/3d.d.ts +31 -3
  75. package/dist/types/core/components/functional/3d/3d.d.ts.map +1 -1
  76. package/dist/types/core/components/functional/3d/constants.d.ts +1 -1
  77. package/dist/types/core/components/functional/3d/constants.d.ts.map +1 -1
  78. package/dist/types/core/components/functional/3d/ensure.d.ts +3 -0
  79. package/dist/types/core/components/functional/3d/ensure.d.ts.map +1 -1
  80. package/dist/types/core/components/functional/3d/form-params.d.ts.map +1 -1
  81. package/dist/types/core/components/functional/3d/frame-source.d.ts +23 -0
  82. package/dist/types/core/components/functional/3d/frame-source.d.ts.map +1 -0
  83. package/dist/types/core/components/functional/3d/life.d.ts +2 -0
  84. package/dist/types/core/components/functional/3d/life.d.ts.map +1 -1
  85. package/dist/types/core/components/functional/3d/out-publish.d.ts +1 -0
  86. package/dist/types/core/components/functional/3d/out-publish.d.ts.map +1 -1
  87. package/dist/types/core/components/functional/3d/playback.d.ts +9 -0
  88. package/dist/types/core/components/functional/3d/playback.d.ts.map +1 -1
  89. package/dist/types/core/components/functional/3d/post-bind.d.ts +52 -0
  90. package/dist/types/core/components/functional/3d/post-bind.d.ts.map +1 -0
  91. package/dist/types/core/components/functional/3d/post-channels.d.ts +40 -0
  92. package/dist/types/core/components/functional/3d/post-channels.d.ts.map +1 -0
  93. package/dist/types/core/components/functional/3d/post-fx.d.ts +73 -0
  94. package/dist/types/core/components/functional/3d/post-fx.d.ts.map +1 -0
  95. package/dist/types/core/components/functional/3d/props.d.ts +42 -0
  96. package/dist/types/core/components/functional/3d/props.d.ts.map +1 -1
  97. package/dist/types/core/components/functional/3d/reload.d.ts +3 -0
  98. package/dist/types/core/components/functional/3d/reload.d.ts.map +1 -1
  99. package/dist/types/core/components/functional/3d/runtime.d.ts +16 -1
  100. package/dist/types/core/components/functional/3d/runtime.d.ts.map +1 -1
  101. package/dist/types/core/components/functional/3d/scene-runtime.d.ts +1 -1
  102. package/dist/types/core/components/functional/3d/scene-runtime.d.ts.map +1 -1
  103. package/dist/types/core/components/functional/3d/tune.d.ts +5 -0
  104. package/dist/types/core/components/functional/3d/tune.d.ts.map +1 -1
  105. package/dist/types/core/components/functional/3d/types.d.ts +16 -0
  106. package/dist/types/core/components/functional/3d/types.d.ts.map +1 -1
  107. package/dist/types/core/components/functional/3d/updated.d.ts.map +1 -1
  108. package/dist/types/core/components/functional/shader/gl-runtime.d.ts +53 -2
  109. package/dist/types/core/components/functional/shader/gl-runtime.d.ts.map +1 -1
  110. package/dist/types/core/components/functional/shader/shader.d.ts +41 -9
  111. package/dist/types/core/components/functional/shader/shader.d.ts.map +1 -1
  112. package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts +16 -2
  113. package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts.map +1 -1
  114. package/dist/types/core/components/functional/shader/types.d.ts +35 -5
  115. package/dist/types/core/components/functional/shader/types.d.ts.map +1 -1
  116. package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts +4 -0
  117. package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts.map +1 -1
  118. package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts +14 -0
  119. package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts.map +1 -1
  120. package/dist/types/core/components/functional/webgpu/helpers.d.ts +1 -0
  121. package/dist/types/core/components/functional/webgpu/helpers.d.ts.map +1 -1
  122. package/dist/types/core/components/functional/webgpu/webgpu.d.ts +11 -1
  123. package/dist/types/core/components/functional/webgpu/webgpu.d.ts.map +1 -1
  124. package/dist/types/core/utils/mediaRef.d.ts +34 -0
  125. package/dist/types/core/utils/mediaRef.d.ts.map +1 -1
  126. package/package.json +49 -1
  127. package/src/core/components/functional/3d/3d.md +46 -3
  128. package/src/core/components/functional/3d/3d.spec.ts +42 -0
  129. package/src/core/components/functional/3d/3d.ts +112 -3
  130. package/src/core/components/functional/3d/constants.ts +3 -0
  131. package/src/core/components/functional/3d/ensure.ts +11 -3
  132. package/src/core/components/functional/3d/form-params.ts +2 -0
  133. package/src/core/components/functional/3d/frame-source.ts +30 -0
  134. package/src/core/components/functional/3d/life.ts +4 -1
  135. package/src/core/components/functional/3d/out-publish.ts +4 -3
  136. package/src/core/components/functional/3d/playback.ts +13 -1
  137. package/src/core/components/functional/3d/post-bind.ts +262 -0
  138. package/src/core/components/functional/3d/post-channels.spec.ts +36 -0
  139. package/src/core/components/functional/3d/post-channels.ts +69 -0
  140. package/src/core/components/functional/3d/post-fx.ts +594 -0
  141. package/src/core/components/functional/3d/props.ts +45 -0
  142. package/src/core/components/functional/3d/reload.ts +8 -0
  143. package/src/core/components/functional/3d/runtime.ts +93 -4
  144. package/src/core/components/functional/3d/scene-runtime.ts +3 -0
  145. package/src/core/components/functional/3d/tune.ts +21 -0
  146. package/src/core/components/functional/3d/types.ts +14 -0
  147. package/src/core/components/functional/3d/updated.ts +17 -1
  148. package/src/core/components/functional/shader/gl-runtime.ts +374 -36
  149. package/src/core/components/functional/shader/shader.md +36 -11
  150. package/src/core/components/functional/shader/shader.spec.ts +350 -3
  151. package/src/core/components/functional/shader/shader.ts +236 -59
  152. package/src/core/components/functional/shader/shadertoy-prelude.ts +49 -5
  153. package/src/core/components/functional/shader/types.ts +92 -13
  154. package/src/core/components/functional/webgpu/gl-fallback.ts +28 -4
  155. package/src/core/components/functional/webgpu/gpu-runtime.ts +154 -18
  156. package/src/core/components/functional/webgpu/helpers.ts +8 -2
  157. package/src/core/components/functional/webgpu/webgpu.md +13 -2
  158. package/src/core/components/functional/webgpu/webgpu.spec.ts +37 -0
  159. package/src/core/components/functional/webgpu/webgpu.ts +48 -1
  160. package/src/core/utils/mediaRef.spec.ts +61 -0
  161. package/src/core/utils/mediaRef.ts +108 -0
  162. package/src/docs/_misc/visual-stack.md +37 -3
  163. package/src/docs/components/docs-demo-sources.ts +42 -0
  164. package/src/docs/docs.ts +3 -0
  165. package/src/docs/example/docs-3d-post-depth-demo.ts +68 -0
  166. package/src/docs/example/docs-3d-shader-live-demo.ts +98 -0
  167. package/src/docs/example/docs-3d-webgpu-live-demo.ts +83 -0
  168. package/src/docs/example/docs-harris-webgpu-demo.ts +53 -55
  169. package/src/docs/search/docs-search.json +2583 -863
  170. package/src/tsconfig.json +36 -0
  171. package/src/tsconfig.tsbuildinfo +1 -1
@@ -1,11 +1,17 @@
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,
4
8
  PassChannels,
5
9
  ReadRectResult,
10
+ ShaderBufferFormat,
6
11
  ShaderCompileError,
7
12
  ShaderPassId,
8
13
  ShaderSources,
14
+ parseBufferFormat,
9
15
  } from "./types";
10
16
 
11
17
  const PASS_ORDER: ShaderPassId[] = [
@@ -55,6 +61,10 @@ type ExternalTexture = {
55
61
  height: number;
56
62
  url: string;
57
63
  video?: HTMLVideoElement;
64
+ /** Source live (`#élément`) — image / vidéo / canvas / SonicFrameSource. */
65
+ source?: TexImageSource | SonicFrameSource | null;
66
+ lastSeq: number;
67
+ lastSrc?: string;
58
68
  };
59
69
 
60
70
  export type FrameUniforms = {
@@ -65,10 +75,91 @@ export type FrameUniforms = {
65
75
  params: [number, number, number, number];
66
76
  };
67
77
 
78
+ /**
79
+ * Décide si un slot élément doit être re-uploadé cette frame.
80
+ * Exporté pour les tests unitaires (stratégie par type).
81
+ */
82
+ export function shouldUploadElementFrame(
83
+ source: unknown,
84
+ lastSeq: number,
85
+ lastSrc?: string,
86
+ ): { upload: boolean; texSource: TexImageSource | null; nextSrc?: string } {
87
+ if (!source) return { upload: false, texSource: null };
88
+ if (
89
+ typeof HTMLImageElement !== "undefined" &&
90
+ source instanceof HTMLImageElement
91
+ ) {
92
+ const src = source.currentSrc || source.src || "";
93
+ if (!src || !source.complete || source.naturalWidth < 1) {
94
+ return { upload: false, texSource: null };
95
+ }
96
+ if (lastSrc === src && lastSeq > 0) {
97
+ return { upload: false, texSource: null, nextSrc: src };
98
+ }
99
+ return { upload: true, texSource: source, nextSrc: src };
100
+ }
101
+ if (
102
+ typeof HTMLVideoElement !== "undefined" &&
103
+ source instanceof HTMLVideoElement
104
+ ) {
105
+ if (source.readyState < 2 || source.videoWidth < 1) {
106
+ return { upload: false, texSource: null };
107
+ }
108
+ return { upload: true, texSource: source };
109
+ }
110
+ if (
111
+ typeof HTMLCanvasElement !== "undefined" &&
112
+ source instanceof HTMLCanvasElement
113
+ ) {
114
+ if (source.width < 1 || source.height < 1) {
115
+ return { upload: false, texSource: null };
116
+ }
117
+ return { upload: true, texSource: source };
118
+ }
119
+ if (isFrameSource(source)) {
120
+ if (source.frameSeq === lastSeq) {
121
+ return { upload: false, texSource: null };
122
+ }
123
+ const canvas = source.getFrameCanvas();
124
+ if (!canvas || canvas.width < 1 || canvas.height < 1) {
125
+ return { upload: false, texSource: null };
126
+ }
127
+ return { upload: true, texSource: canvas };
128
+ }
129
+ return { upload: false, texSource: null };
130
+ }
131
+
68
132
  function isVideoUrl(url: string): boolean {
69
133
  return /\.(webm|mp4|ogg|mov)(\?|#|$)/i.test(url);
70
134
  }
71
135
 
136
+ function texImageSourceSize(
137
+ src: TexImageSource,
138
+ ): { width: number; height: number } {
139
+ if (
140
+ typeof HTMLVideoElement !== "undefined" &&
141
+ src instanceof HTMLVideoElement
142
+ ) {
143
+ return { width: src.videoWidth || 1, height: src.videoHeight || 1 };
144
+ }
145
+ if (
146
+ typeof HTMLImageElement !== "undefined" &&
147
+ src instanceof HTMLImageElement
148
+ ) {
149
+ return {
150
+ width: src.naturalWidth || src.width || 1,
151
+ height: src.naturalHeight || src.height || 1,
152
+ };
153
+ }
154
+ if ("width" in src && "height" in src) {
155
+ return {
156
+ width: Math.max(1, Number(src.width) || 1),
157
+ height: Math.max(1, Number(src.height) || 1),
158
+ };
159
+ }
160
+ return { width: 1, height: 1 };
161
+ }
162
+
72
163
  function compileShader(
73
164
  gl: WebGL2RenderingContext,
74
165
  type: number,
@@ -146,27 +237,110 @@ function getUniforms(
146
237
  };
147
238
  }
148
239
 
240
+ type BufferGlFormat = {
241
+ requested: ShaderBufferFormat;
242
+ active: ShaderBufferFormat;
243
+ internal: number;
244
+ format: number;
245
+ type: number;
246
+ filter: number;
247
+ float: boolean;
248
+ };
249
+
250
+ let warnedFloatFallback = false;
251
+
252
+ /**
253
+ * Résout le format GPU des buffers A–D.
254
+ * Exporté pour tests (mock `getExtension`).
255
+ */
256
+ export function resolveBufferGlFormat(
257
+ gl: WebGL2RenderingContext,
258
+ requested: ShaderBufferFormat,
259
+ ): BufferGlFormat {
260
+ const want = parseBufferFormat(requested);
261
+ if (want === "rgba8") {
262
+ return {
263
+ requested: want,
264
+ active: "rgba8",
265
+ internal: gl.RGBA8,
266
+ format: gl.RGBA,
267
+ type: gl.UNSIGNED_BYTE,
268
+ filter: gl.LINEAR,
269
+ float: false,
270
+ };
271
+ }
272
+ const ext = gl.getExtension("EXT_color_buffer_float");
273
+ if (!ext) {
274
+ if (!warnedFloatFallback) {
275
+ warnedFloatFallback = true;
276
+ console.warn(
277
+ `[sonic-shader] buffer-format="${want}" indisponible (pas d'EXT_color_buffer_float) → rgba8`,
278
+ );
279
+ }
280
+ return resolveBufferGlFormat(gl, "rgba8");
281
+ }
282
+ if (want === "rgba16f") {
283
+ return {
284
+ requested: want,
285
+ active: "rgba16f",
286
+ internal: gl.RGBA16F,
287
+ format: gl.RGBA,
288
+ type: gl.HALF_FLOAT,
289
+ filter: gl.LINEAR,
290
+ float: true,
291
+ };
292
+ }
293
+ // rgba32f
294
+ const linearOk = !!gl.getExtension("OES_texture_float_linear");
295
+ return {
296
+ requested: want,
297
+ active: "rgba32f",
298
+ internal: gl.RGBA32F,
299
+ format: gl.RGBA,
300
+ type: gl.FLOAT,
301
+ filter: linearOk ? gl.LINEAR : gl.NEAREST,
302
+ float: true,
303
+ };
304
+ }
305
+
306
+ /** Reset du warn unique (tests). */
307
+ export function resetBufferFormatWarnFlag() {
308
+ warnedFloatFallback = false;
309
+ }
310
+
149
311
  function createEmptyTexture(
150
312
  gl: WebGL2RenderingContext,
151
313
  width: number,
152
314
  height: number,
315
+ fmt?: BufferGlFormat,
153
316
  ): WebGLTexture {
317
+ const f =
318
+ fmt ??
319
+ ({
320
+ requested: "rgba8",
321
+ active: "rgba8",
322
+ internal: gl.RGBA8,
323
+ format: gl.RGBA,
324
+ type: gl.UNSIGNED_BYTE,
325
+ filter: gl.LINEAR,
326
+ float: false,
327
+ } satisfies BufferGlFormat);
154
328
  const tex = gl.createTexture();
155
329
  if (!tex) throw new Error("Unable to create texture");
156
330
  gl.bindTexture(gl.TEXTURE_2D, tex);
157
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
158
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
331
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, f.filter);
332
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, f.filter);
159
333
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
160
334
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
161
335
  gl.texImage2D(
162
336
  gl.TEXTURE_2D,
163
337
  0,
164
- gl.RGBA8,
338
+ f.internal,
165
339
  width,
166
340
  height,
167
341
  0,
168
- gl.RGBA,
169
- gl.UNSIGNED_BYTE,
342
+ f.format,
343
+ f.type,
170
344
  null,
171
345
  );
172
346
  return tex;
@@ -198,10 +372,11 @@ function createPingPong(
198
372
  gl: WebGL2RenderingContext,
199
373
  width: number,
200
374
  height: number,
375
+ fmt: BufferGlFormat,
201
376
  ): PingPong {
202
377
  const textures: [WebGLTexture, WebGLTexture] = [
203
- createEmptyTexture(gl, width, height),
204
- createEmptyTexture(gl, width, height),
378
+ createEmptyTexture(gl, width, height, fmt),
379
+ createEmptyTexture(gl, width, height, fmt),
205
380
  ];
206
381
  const framebuffers: [WebGLFramebuffer, WebGLFramebuffer] = [
207
382
  createFramebuffer(gl, textures[0]),
@@ -221,9 +396,12 @@ export function parseChannelSource(
221
396
  ): ChannelSource {
222
397
  const v = (value ?? "").trim();
223
398
  if (!v) {
224
- return fallbackUrl
225
- ? { kind: "url", url: fallbackUrl }
226
- : { kind: "none" };
399
+ const fb = (fallbackUrl ?? "").trim();
400
+ if (fb.startsWith("#")) return { kind: "element", key: fb };
401
+ return fb ? { kind: "url", url: fb } : { kind: "none" };
402
+ }
403
+ if (v.startsWith("#")) {
404
+ return { kind: "element", key: v };
227
405
  }
228
406
  const lower = v.toLowerCase();
229
407
  if (lower === "self") return { kind: "self" };
@@ -256,6 +434,8 @@ export class ShaderToyRuntime {
256
434
  private passChannels = new Map<ShaderPassId, PassChannels>();
257
435
  private activePasses: ShaderPassId[] = [];
258
436
  private blackTexture: WebGLTexture | null = null;
437
+ private elementResolver: ((key: string) => Element | null) | null = null;
438
+ private bufferFmt: BufferGlFormat;
259
439
 
260
440
  constructor(canvas: HTMLCanvasElement) {
261
441
  const gl = canvas.getContext("webgl2", {
@@ -271,6 +451,7 @@ export class ShaderToyRuntime {
271
451
  }
272
452
  this.canvas = canvas;
273
453
  this.gl = gl;
454
+ this.bufferFmt = resolveBufferGlFormat(gl, "rgba8");
274
455
  this.initGeometry();
275
456
  this.blackTexture = createEmptyTexture(gl, 1, 1);
276
457
  gl.bindTexture(gl.TEXTURE_2D, this.blackTexture);
@@ -287,6 +468,42 @@ export class ShaderToyRuntime {
287
468
  );
288
469
  }
289
470
 
471
+ /** Format réellement utilisé pour les buffers A–D (après repli éventuel). */
472
+ get activeBufferFormat(): ShaderBufferFormat {
473
+ return this.bufferFmt.active;
474
+ }
475
+
476
+ /**
477
+ * Change le format des buffers A–D. Recrée les FBO si la taille est déjà connue.
478
+ * Retourne le format actif (peut différer si extension manquante).
479
+ */
480
+ setBufferFormat(requested: ShaderBufferFormat): ShaderBufferFormat {
481
+ const next = resolveBufferGlFormat(this.gl, requested);
482
+ if (
483
+ next.active === this.bufferFmt.active &&
484
+ next.filter === this.bufferFmt.filter
485
+ ) {
486
+ this.bufferFmt = next;
487
+ return next.active;
488
+ }
489
+ this.bufferFmt = next;
490
+ if (this.width >= 1 && this.height >= 1) {
491
+ for (const id of BUFFER_PASSES) {
492
+ const existing = this.buffers.get(id);
493
+ if (existing) destroyPingPong(this.gl, existing);
494
+ if (this.programs.has(id)) {
495
+ this.buffers.set(
496
+ id,
497
+ createPingPong(this.gl, this.width, this.height, this.bufferFmt),
498
+ );
499
+ } else {
500
+ this.buffers.delete(id);
501
+ }
502
+ }
503
+ }
504
+ return next.active;
505
+ }
506
+
290
507
  /** True si le contexte a été perdu / déjà disposé. */
291
508
  get isLost(): boolean {
292
509
  return this.gl.isContextLost();
@@ -323,7 +540,10 @@ export class ShaderToyRuntime {
323
540
  const existing = this.buffers.get(id);
324
541
  if (existing) destroyPingPong(this.gl, existing);
325
542
  if (this.programs.has(id)) {
326
- this.buffers.set(id, createPingPong(this.gl, w, h));
543
+ this.buffers.set(
544
+ id,
545
+ createPingPong(this.gl, w, h, this.bufferFmt),
546
+ );
327
547
  }
328
548
  }
329
549
  }
@@ -365,7 +585,10 @@ export class ShaderToyRuntime {
365
585
  if (pass !== "image" && this.width > 0) {
366
586
  const prev = this.buffers.get(pass);
367
587
  if (prev) destroyPingPong(this.gl, prev);
368
- this.buffers.set(pass, createPingPong(this.gl, this.width, this.height));
588
+ this.buffers.set(
589
+ pass,
590
+ createPingPong(this.gl, this.width, this.height, this.bufferFmt),
591
+ );
369
592
  }
370
593
  }
371
594
 
@@ -378,40 +601,132 @@ export class ShaderToyRuntime {
378
601
  }
379
602
  }
380
603
 
604
+ /**
605
+ * Met à jour les bindings de canaux sans recompiler les programmes.
606
+ * Les URLs / clés `#élément` absentes des textures sont allouées à la demande
607
+ * (via `ensureTextures` / `ensureElementTexture` côté composant).
608
+ */
609
+ setPassChannels(channels: Map<ShaderPassId, PassChannels>) {
610
+ if (this.gl.isContextLost()) {
611
+ throw new Error("WebGL context lost");
612
+ }
613
+ this.passChannels = channels;
614
+ }
615
+
381
616
  async ensureTextures(urls: string[]) {
382
617
  const unique = [...new Set(urls.filter(Boolean))];
383
618
  await Promise.all(unique.map((url) => this.loadTexture(url)));
384
619
  }
385
620
 
386
- /** Upload the current video frame for any channel backed by a video element. */
387
- updateVideoTextures() {
621
+ /** Alloue un slot texture vide pour un canal `#élément` (upload live ensuite). */
622
+ ensureElementTexture(key: string) {
623
+ if (this.external.has(key)) return;
388
624
  const gl = this.gl;
389
- for (const ext of this.external.values()) {
390
- const video = ext.video;
391
- if (!video || video.readyState < 2) continue;
625
+ const tex = gl.createTexture();
626
+ if (!tex) return;
627
+ gl.bindTexture(gl.TEXTURE_2D, tex);
628
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
629
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
630
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
631
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
632
+ gl.texImage2D(
633
+ gl.TEXTURE_2D,
634
+ 0,
635
+ gl.RGBA8,
636
+ 1,
637
+ 1,
638
+ 0,
639
+ gl.RGBA,
640
+ gl.UNSIGNED_BYTE,
641
+ new Uint8Array([0, 0, 0, 255]),
642
+ );
643
+ this.external.set(key, {
644
+ texture: tex,
645
+ width: 1,
646
+ height: 1,
647
+ url: key,
648
+ lastSeq: 0,
649
+ });
650
+ }
651
+
652
+ /**
653
+ * Upload live des canaux élément / vidéo.
654
+ * `resolveEl(key)` fournit l’Element (résolution `#id` côté host).
655
+ */
656
+ updateElementTextures(resolveEl?: (key: string) => Element | null) {
657
+ const gl = this.gl;
658
+ for (const [key, ext] of this.external) {
659
+ let source: unknown = ext.video ?? ext.source ?? null;
660
+ // Canaux #id : re-résoudre chaque frame (élément peut apparaître / partir).
661
+ if (key.startsWith("#") && resolveEl) {
662
+ source = resolveEl(key);
663
+ ext.source = source as TexImageSource | SonicFrameSource | null;
664
+ }
665
+ if (
666
+ typeof HTMLVideoElement !== "undefined" &&
667
+ source instanceof HTMLVideoElement
668
+ ) {
669
+ ext.video = source;
670
+ }
671
+ const decision = shouldUploadElementFrame(
672
+ source,
673
+ ext.lastSeq,
674
+ ext.lastSrc,
675
+ );
676
+ if (!decision.upload || !decision.texSource) continue;
677
+ const size = texImageSourceSize(decision.texSource);
392
678
  gl.bindTexture(gl.TEXTURE_2D, ext.texture);
393
679
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
394
- gl.texImage2D(
395
- gl.TEXTURE_2D,
396
- 0,
397
- gl.RGBA8,
398
- gl.RGBA,
399
- gl.UNSIGNED_BYTE,
400
- video,
401
- );
680
+ if (ext.width === size.width && ext.height === size.height && ext.lastSeq > 0) {
681
+ gl.texSubImage2D(
682
+ gl.TEXTURE_2D,
683
+ 0,
684
+ 0,
685
+ 0,
686
+ gl.RGBA,
687
+ gl.UNSIGNED_BYTE,
688
+ decision.texSource,
689
+ );
690
+ } else {
691
+ gl.texImage2D(
692
+ gl.TEXTURE_2D,
693
+ 0,
694
+ gl.RGBA8,
695
+ gl.RGBA,
696
+ gl.UNSIGNED_BYTE,
697
+ decision.texSource,
698
+ );
699
+ ext.width = size.width;
700
+ ext.height = size.height;
701
+ }
402
702
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
403
- ext.width = video.videoWidth || ext.width;
404
- ext.height = video.videoHeight || ext.height;
703
+ if (isFrameSource(source)) {
704
+ ext.lastSeq = source.frameSeq;
705
+ } else {
706
+ ext.lastSeq = (ext.lastSeq || 0) + 1;
707
+ }
708
+ if (decision.nextSrc !== undefined) ext.lastSrc = decision.nextSrc;
405
709
  }
406
710
  }
407
711
 
712
+ /** @deprecated alias — préférer `updateElementTextures`. */
713
+ updateVideoTextures() {
714
+ this.updateElementTextures();
715
+ }
716
+
408
717
  setVideosPlaying(playing: boolean) {
409
718
  for (const ext of this.external.values()) {
410
- if (!ext.video) continue;
719
+ const video =
720
+ ext.video ||
721
+ (typeof HTMLVideoElement !== "undefined" &&
722
+ ext.source instanceof HTMLVideoElement
723
+ ? ext.source
724
+ : null);
725
+ if (!video) continue;
411
726
  if (playing) {
412
- void ext.video.play().catch(() => {});
727
+ void video.play().catch(() => {});
413
728
  } else {
414
- ext.video.pause();
729
+ video.pause();
415
730
  }
416
731
  }
417
732
  }
@@ -450,6 +765,7 @@ export class ShaderToyRuntime {
450
765
  width: img.naturalWidth || img.width,
451
766
  height: img.naturalHeight || img.height,
452
767
  url,
768
+ lastSeq: 0,
453
769
  });
454
770
  resolve();
455
771
  };
@@ -503,6 +819,7 @@ export class ShaderToyRuntime {
503
819
  height: video.videoHeight,
504
820
  url,
505
821
  video,
822
+ lastSeq: 0,
506
823
  });
507
824
  void video.play().catch(() => {});
508
825
  resolve();
@@ -524,8 +841,9 @@ export class ShaderToyRuntime {
524
841
  height: 1,
525
842
  };
526
843
  if (source.kind === "none") return black;
527
- if (source.kind === "url") {
528
- const ext = this.external.get(source.url);
844
+ if (source.kind === "url" || source.kind === "element") {
845
+ const key = source.kind === "url" ? source.url : source.key;
846
+ const ext = this.external.get(key);
529
847
  return ext
530
848
  ? { texture: ext.texture, width: ext.width, height: ext.height }
531
849
  : black;
@@ -620,10 +938,15 @@ export class ShaderToyRuntime {
620
938
  }
621
939
  }
622
940
 
941
+ /** Host fournit la résolution `#id` / live Element. */
942
+ setElementResolver(fn: ((key: string) => Element | null) | null) {
943
+ this.elementResolver = fn;
944
+ }
945
+
623
946
  render(frame: FrameUniforms) {
624
947
  const gl = this.gl;
625
948
  if (!this.vao || this.width < 1 || this.height < 1) return;
626
- this.updateVideoTextures();
949
+ this.updateElementTextures(this.elementResolver ?? undefined);
627
950
  gl.bindVertexArray(this.vao);
628
951
  gl.viewport(0, 0, this.width, this.height);
629
952
  gl.disable(gl.DEPTH_TEST);
@@ -665,6 +988,7 @@ export class ShaderToyRuntime {
665
988
 
666
989
  /**
667
990
  * Lit un rectangle d’un buffer ping-pong (dernier write) ou de l’Image (`pass === "image"`).
991
+ * Les buffers flottants renvoient un `Float32Array` ; Image / rgba8 → `Uint8Array`.
668
992
  */
669
993
  readBufferRect(
670
994
  pass: ShaderPassId,
@@ -676,7 +1000,14 @@ export class ShaderToyRuntime {
676
1000
  if (pass === "image") return this.readRect(x, y, w, h);
677
1001
  const pp = this.buffers.get(pass);
678
1002
  if (!pp) return null;
679
- return this.readFramebufferRect(pp.framebuffers[pp.index], x, y, w, h);
1003
+ return this.readFramebufferRect(
1004
+ pp.framebuffers[pp.index],
1005
+ x,
1006
+ y,
1007
+ w,
1008
+ h,
1009
+ this.bufferFmt.float,
1010
+ );
680
1011
  }
681
1012
 
682
1013
  /**
@@ -695,6 +1026,7 @@ export class ShaderToyRuntime {
695
1026
  y: number,
696
1027
  w: number,
697
1028
  h: number,
1029
+ asFloat = false,
698
1030
  ): ReadRectResult | null {
699
1031
  const gl = this.gl;
700
1032
  if (gl.isContextLost() || this.width < 1 || this.height < 1) return null;
@@ -714,9 +1046,15 @@ export class ShaderToyRuntime {
714
1046
  if (px + pw > this.width) pw = this.width - px;
715
1047
  if (py + ph > this.height) ph = this.height - py;
716
1048
  if (pw < 1 || ph < 1) return null;
717
- const data = new Uint8Array(pw * ph * 4);
718
1049
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
719
- gl.readPixels(px, py, pw, ph, gl.RGBA, gl.UNSIGNED_BYTE, data);
1050
+ let data: Uint8Array | Float32Array;
1051
+ if (asFloat) {
1052
+ data = new Float32Array(pw * ph * 4);
1053
+ gl.readPixels(px, py, pw, ph, gl.RGBA, gl.FLOAT, data);
1054
+ } else {
1055
+ data = new Uint8Array(pw * ph * 4);
1056
+ gl.readPixels(px, py, pw, ph, gl.RGBA, gl.UNSIGNED_BYTE, data);
1057
+ }
720
1058
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
721
1059
  return { x: px, y: py, width: pw, height: ph, data };
722
1060
  }
@@ -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 **ou** vidéos (`.webm` / `.mp4` / …).
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
- | Params / sync UI | `dataProvider` → `uParam0…3` **et** canaux (`channel0`, `bufferACh0`…) — string URL ou `SonicMediaRef` |
45
- | Hors viewport | `release-offscreen` (défaut) : libère textures/FBO, garde le contexte |
46
-
47
- Les passes intermédiaires sont **déjà offscreen** (FBO). Pas besoin d’un second `sonic-shader` invisible.
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`). Changer un canal ne recompile pas (rebind). |
48
+ | Hors viewport | `release-offscreen` : `auto` (défaut) libère sauf si une passe lit `self` ; `true` / `false` forcent |
48
49
 
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&nbsp;A = Sobel → Image = overlay + `param0`.
52
54
 
53
55
  <docs-lit-demo for="docs-shader-chain-demo"></docs-lit-demo>
@@ -136,6 +138,22 @@ onExtract() {
136
138
 
137
139
  Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/visual-stack).
138
140
 
141
+ ## Précision des buffers (`buffer-format`)
142
+
143
+ Par défaut, les buffers A–D sont en **RGBA8** (unsigned). Un tenseur Harris naïf (`Ix*Ix`, `Iy*Iy`, `Ix*Iy`) **perd le signe** de `IxIy` et sature les carrés → bords au lieu de coins.
144
+
145
+ | Valeur | Usage |
146
+ |--------|--------|
147
+ | `rgba8` (défaut) | Couleur / masques 0–1 |
148
+ | `rgba16f` | Calculs signés (Harris, gradients) — recommandé |
149
+ | `rgba32f` | Haute précision ; filtrage `NEAREST` si `OES_texture_float_linear` manque |
150
+
151
+ Repli auto en `rgba8` si `EXT_color_buffer_float` est absent (`console.warn` une fois). L’attribut reflété `active-buffer-format` indique le format réellement utilisé. La passe Image reste le canvas RGBA8.
152
+
153
+ ```html
154
+ <sonic-shader buffer-format="rgba16f" buffer-a="…" …></sonic-shader>
155
+ ```
156
+
139
157
  ## API typée (`SonicShaderConfig`)
140
158
 
141
159
  | Champ / attribut | Type | Rôle |
@@ -143,8 +161,9 @@ Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/v
143
161
  | `image` | `string` | GLSL pass Image (**requis**, propriété JS) |
144
162
  | `bufferA`…`bufferD` / `buffer-a`… | `string` | Passes multipass |
145
163
  | `common` | `string` | GLSL partagé |
146
- | `channel0`…`channel3` | `string` | URLs textures (toutes passes) |
147
- | `imageCh0`… / `image-ch0`… | `string` | Override canal : URL, `self`, `buffer-a`…, `none` |
164
+ | `bufferFormat` / `buffer-format` | `rgba8` \| `rgba16f` \| `rgba32f` | Format buffers A–D (défaut `rgba8`) ; `active-buffer-format` reflète le format réellement utilisé |
165
+ | `channel0`…`channel3` | `string` | URLs, `#id` élément, ou refs buffers (toutes passes) |
166
+ | `imageCh0`… / `image-ch0`… | `string` | Override canal : URL, `#id`, `self`, `buffer-a`…, `none` |
148
167
  | `play` | `boolean` | Boucle RAF (défaut `true`) |
149
168
  | `playOnHover` / `play-on-hover` | `boolean` | Anime seulement au hover / transition |
150
169
  | `mouse` | `boolean` | Pointeur → `iMouse` (défaut `true`) |
@@ -168,7 +187,13 @@ Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/v
168
187
  | `extractDataProvider` / `extract-data-provider` | `string` | Publisher extract (défaut = out) |
169
188
  | `aspectRatio` / `aspect-ratio` | `string` | Ratio CSS forcé |
170
189
  | `dprMax` / `dpr-max` | `number` | Plafond DPR (défaut `2`) |
171
- | `releaseOffscreen` / `release-offscreen` | `boolean` | Hors viewport : libère textures/FBO (garde le contexte). `dispose` + loseContext au disconnect (défaut `true`) |
190
+ | `releaseOffscreen` / `release-offscreen` | `boolean` \| `"auto"` | Hors viewport : libère textures/FBO (garde le contexte). `auto` (défaut) : ne libère pas si une passe lit `self`. `dispose` + loseContext au disconnect |
191
+
192
+ ## État et visibilité
193
+
194
+ Un shader qui garde un état via `self` (feedback frame précédente) n’est plus réinitialisé quand il sort de l’écran : avec `release-offscreen="auto"` (défaut), seuls le RAF et les vidéos sont mis en pause ; les FBO restent. Forcer `release-offscreen` (attribut booléen / `true`) restaure l’ancien comportement (libération hors viewport même avec `self`).
195
+
196
+ Changer `channel0…3` / `*-ch*` re-lie les textures **sans** recompiler ni vider les buffers.
172
197
 
173
198
  ## Markup
174
199
 
@@ -182,7 +207,7 @@ Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/v
182
207
  hover-speed="12"
183
208
  play
184
209
  play-on-hover
185
- release-offscreen
210
+ release-offscreen="auto"
186
211
  channel0="/docs/shader-samples/color.jpg"
187
212
  &gt;&lt;/sonic-shader&gt;
188
213
  </template>