@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
@@ -5,11 +5,64 @@ 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"> }
11
62
  | { kind: "self" }
12
- | { kind: "none" };
63
+ | { kind: "none" }
64
+ /** Élément page (`#id`) — upload live via `updateElementTextures`. */
65
+ | { kind: "element"; key: string; el?: Element | null };
13
66
 
14
67
  export type PassChannels = [
15
68
  ChannelSource,
@@ -159,10 +212,18 @@ export type SonicShaderConfig = {
159
212
  dprMax?: number;
160
213
 
161
214
  /**
162
- * Hors viewport : libère textures/FBO (défaut `true`), sans loseContext.
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`.
163
224
  * Le slot navigateur est libéré au disconnect.
164
225
  */
165
- releaseOffscreen?: boolean;
226
+ releaseOffscreen?: ReleaseOffscreen;
166
227
  };
167
228
 
168
229
  /** Parse `hover-param` → slot ou `null` si désactivé / invalide. */
@@ -272,7 +333,8 @@ export type ReadRectResult = {
272
333
  y: number;
273
334
  width: number;
274
335
  height: number;
275
- data: Uint8Array;
336
+ /** `Uint8Array` (RGBA8) ou `Float32Array` (buffers flottants). */
337
+ data: Uint8Array | Float32Array;
276
338
  };
277
339
 
278
340
  /** Parse `extract-source` / `extract-mask` → pass id ou null. */
@@ -293,9 +355,10 @@ export function parseExtractPass(
293
355
  /**
294
356
  * NMS simple sur canal R (RGBA row-major, origine bas-gauche comme WebGL).
295
357
  * Retourne peaks triés par score décroissant.
358
+ * Accepte Uint8 (0–255) ou Float32 (scores déjà dans ~[0,1]).
296
359
  */
297
360
  export function findPeaks(
298
- data: Uint8Array,
361
+ data: Uint8Array | Float32Array,
299
362
  width: number,
300
363
  height: number,
301
364
  options: FindPeaksOptions = {},
@@ -306,13 +369,15 @@ export function findPeaks(
306
369
  if (maxCount === 0 || width < 1 || height < 1 || data.length < width * height * 4) {
307
370
  return [];
308
371
  }
309
- const thr8 = threshold * 255;
372
+ const isFloat = data instanceof Float32Array;
373
+ const thr = isFloat ? threshold : threshold * 255;
374
+ const toScore = (v: number) => (isFloat ? v : v / 255);
310
375
  const candidates: ShaderPeak[] = [];
311
376
  for (let y = 1; y < height - 1; y++) {
312
377
  for (let x = 1; x < width - 1; x++) {
313
378
  const i = (y * width + x) * 4;
314
379
  const v = data[i];
315
- if (v < thr8) continue;
380
+ if (v < thr) continue;
316
381
  // Local max 3×3
317
382
  let isMax = true;
318
383
  for (let dy = -1; dy <= 1 && isMax; dy++) {
@@ -325,7 +390,7 @@ export function findPeaks(
325
390
  }
326
391
  }
327
392
  }
328
- if (isMax) candidates.push({ x, y, score: v / 255 });
393
+ if (isMax) candidates.push({ x, y, score: toScore(v) });
329
394
  }
330
395
  }
331
396
  candidates.sort((a, b) => b.score - a.score);
@@ -401,7 +466,7 @@ export function roiToPixels(
401
466
  return { x: px, y: py, w: pw, h: ph };
402
467
  }
403
468
 
404
- export function meanRgba(data: Uint8Array): {
469
+ export function meanRgba(data: Uint8Array | Float32Array): {
405
470
  r: number;
406
471
  g: number;
407
472
  b: number;
@@ -410,6 +475,7 @@ export function meanRgba(data: Uint8Array): {
410
475
  } {
411
476
  const n = Math.floor(data.length / 4);
412
477
  if (n < 1) return { r: 0, g: 0, b: 0, a: 0, luma: 0 };
478
+ const isFloat = data instanceof Float32Array;
413
479
  let r = 0;
414
480
  let g = 0;
415
481
  let b = 0;
@@ -421,10 +487,11 @@ export function meanRgba(data: Uint8Array): {
421
487
  b += data[o + 2];
422
488
  a += data[o + 3];
423
489
  }
424
- r /= n * 255;
425
- g /= n * 255;
426
- b /= n * 255;
427
- a /= n * 255;
490
+ const scale = isFloat ? n : n * 255;
491
+ r /= scale;
492
+ g /= scale;
493
+ b /= scale;
494
+ a /= scale;
428
495
  return {
429
496
  r,
430
497
  g,
@@ -434,6 +501,18 @@ export function meanRgba(data: Uint8Array): {
434
501
  };
435
502
  }
436
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
+
437
516
  export class ShaderCompileError extends Error {
438
517
  constructor(
439
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
- const urls = this.channelUrls.filter(Boolean);
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
- const list = this.channelUrls.filter(Boolean);
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
- const texture = this.device.createTexture({
201
- size: [img.width, img.height],
202
- format: "rgba8unorm",
203
- usage:
204
- GPUTextureUsage.TEXTURE_BINDING |
205
- GPUTextureUsage.COPY_DST |
206
- GPUTextureUsage.RENDER_ATTACHMENT,
207
- });
208
- this.device.queue.copyExternalImageToTexture(
209
- { source: img },
210
- { texture },
211
- [img.width, img.height],
212
- );
213
- this.destroyChannel(index);
214
- this.channelTextures[index] = texture;
215
- this.channelViews[index] = texture.createView();
216
- this.channelSizes[index] = { width: img.width, height: img.height };
217
- this.bindGroup = null;
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") return undefined;
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. No live texture bus with `sonic-shader` (separate contexts).
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
  });