@supersoniks/concorde 4.9.98-visual-stack.0 → 4.9.98-visual-stack.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-3d.bundle.js +201 -152
  3. package/concorde-3d.es.js +14687 -13905
  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 +2773 -2547
  10. package/concorde-webgpu.bundle.js +43 -32
  11. package/concorde-webgpu.es.js +1819 -1553
  12. package/dist/concorde-3d.bundle.js +201 -152
  13. package/dist/concorde-3d.es.js +14687 -13905
  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 +2773 -2547
  20. package/dist/concorde-webgpu.bundle.js +43 -32
  21. package/dist/concorde-webgpu.es.js +1819 -1553
  22. package/dist/js/core/components/functional/3d/3d.js +90 -2
  23. package/dist/js/core/components/functional/3d/3d.js.map +1 -1
  24. package/dist/js/core/components/functional/3d/constants.js +1 -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/life.js +2 -1
  31. package/dist/js/core/components/functional/3d/life.js.map +1 -1
  32. package/dist/js/core/components/functional/3d/out-publish.js +3 -2
  33. package/dist/js/core/components/functional/3d/out-publish.js.map +1 -1
  34. package/dist/js/core/components/functional/3d/playback.js +6 -1
  35. package/dist/js/core/components/functional/3d/playback.js.map +1 -1
  36. package/dist/js/core/components/functional/3d/post-bind.js +207 -0
  37. package/dist/js/core/components/functional/3d/post-bind.js.map +1 -0
  38. package/dist/js/core/components/functional/3d/post-channels.js +39 -0
  39. package/dist/js/core/components/functional/3d/post-channels.js.map +1 -0
  40. package/dist/js/core/components/functional/3d/post-fx.js +495 -0
  41. package/dist/js/core/components/functional/3d/post-fx.js.map +1 -0
  42. package/dist/js/core/components/functional/3d/props.js +142 -0
  43. package/dist/js/core/components/functional/3d/props.js.map +1 -1
  44. package/dist/js/core/components/functional/3d/reload.js +2 -0
  45. package/dist/js/core/components/functional/3d/reload.js.map +1 -1
  46. package/dist/js/core/components/functional/3d/runtime.js +80 -3
  47. package/dist/js/core/components/functional/3d/runtime.js.map +1 -1
  48. package/dist/js/core/components/functional/3d/scene-runtime.js +1 -1
  49. package/dist/js/core/components/functional/3d/scene-runtime.js.map +1 -1
  50. package/dist/js/core/components/functional/3d/tune.js +21 -0
  51. package/dist/js/core/components/functional/3d/tune.js.map +1 -1
  52. package/dist/js/core/components/functional/3d/updated.js +13 -1
  53. package/dist/js/core/components/functional/3d/updated.js.map +1 -1
  54. package/dist/js/core/components/functional/shader/gl-runtime.js +128 -14
  55. package/dist/js/core/components/functional/shader/gl-runtime.js.map +1 -1
  56. package/dist/js/core/components/functional/shader/shader.js +154 -57
  57. package/dist/js/core/components/functional/shader/shader.js.map +1 -1
  58. package/dist/js/core/components/functional/shader/shadertoy-prelude.js +39 -5
  59. package/dist/js/core/components/functional/shader/shadertoy-prelude.js.map +1 -1
  60. package/dist/js/core/components/functional/shader/types.js +58 -7
  61. package/dist/js/core/components/functional/shader/types.js.map +1 -1
  62. package/dist/js/core/components/functional/webgpu/gl-fallback.js +27 -6
  63. package/dist/js/core/components/functional/webgpu/gl-fallback.js.map +1 -1
  64. package/dist/js/core/components/functional/webgpu/gpu-runtime.js +115 -13
  65. package/dist/js/core/components/functional/webgpu/gpu-runtime.js.map +1 -1
  66. package/dist/js/core/components/functional/webgpu/helpers.js +8 -2
  67. package/dist/js/core/components/functional/webgpu/helpers.js.map +1 -1
  68. package/dist/js/core/components/functional/webgpu/webgpu.js +34 -1
  69. package/dist/js/core/components/functional/webgpu/webgpu.js.map +1 -1
  70. package/dist/js/core/utils/mediaRef.js +37 -0
  71. package/dist/js/core/utils/mediaRef.js.map +1 -1
  72. package/dist/types/core/components/functional/3d/3d.d.ts +26 -3
  73. package/dist/types/core/components/functional/3d/3d.d.ts.map +1 -1
  74. package/dist/types/core/components/functional/3d/constants.d.ts +1 -1
  75. package/dist/types/core/components/functional/3d/constants.d.ts.map +1 -1
  76. package/dist/types/core/components/functional/3d/ensure.d.ts +3 -0
  77. package/dist/types/core/components/functional/3d/ensure.d.ts.map +1 -1
  78. package/dist/types/core/components/functional/3d/form-params.d.ts.map +1 -1
  79. package/dist/types/core/components/functional/3d/life.d.ts +2 -0
  80. package/dist/types/core/components/functional/3d/life.d.ts.map +1 -1
  81. package/dist/types/core/components/functional/3d/out-publish.d.ts +1 -0
  82. package/dist/types/core/components/functional/3d/out-publish.d.ts.map +1 -1
  83. package/dist/types/core/components/functional/3d/playback.d.ts +9 -0
  84. package/dist/types/core/components/functional/3d/playback.d.ts.map +1 -1
  85. package/dist/types/core/components/functional/3d/post-bind.d.ts +52 -0
  86. package/dist/types/core/components/functional/3d/post-bind.d.ts.map +1 -0
  87. package/dist/types/core/components/functional/3d/post-channels.d.ts +40 -0
  88. package/dist/types/core/components/functional/3d/post-channels.d.ts.map +1 -0
  89. package/dist/types/core/components/functional/3d/post-fx.d.ts +73 -0
  90. package/dist/types/core/components/functional/3d/post-fx.d.ts.map +1 -0
  91. package/dist/types/core/components/functional/3d/props.d.ts +42 -0
  92. package/dist/types/core/components/functional/3d/props.d.ts.map +1 -1
  93. package/dist/types/core/components/functional/3d/reload.d.ts.map +1 -1
  94. package/dist/types/core/components/functional/3d/runtime.d.ts +16 -1
  95. package/dist/types/core/components/functional/3d/runtime.d.ts.map +1 -1
  96. package/dist/types/core/components/functional/3d/scene-runtime.d.ts +1 -1
  97. package/dist/types/core/components/functional/3d/scene-runtime.d.ts.map +1 -1
  98. package/dist/types/core/components/functional/3d/tune.d.ts +5 -0
  99. package/dist/types/core/components/functional/3d/tune.d.ts.map +1 -1
  100. package/dist/types/core/components/functional/3d/types.d.ts +9 -0
  101. package/dist/types/core/components/functional/3d/types.d.ts.map +1 -1
  102. package/dist/types/core/components/functional/3d/updated.d.ts.map +1 -1
  103. package/dist/types/core/components/functional/shader/gl-runtime.d.ts +33 -1
  104. package/dist/types/core/components/functional/shader/gl-runtime.d.ts.map +1 -1
  105. package/dist/types/core/components/functional/shader/shader.d.ts +28 -7
  106. package/dist/types/core/components/functional/shader/shader.d.ts.map +1 -1
  107. package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts +16 -2
  108. package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts.map +1 -1
  109. package/dist/types/core/components/functional/shader/types.d.ts +29 -5
  110. package/dist/types/core/components/functional/shader/types.d.ts.map +1 -1
  111. package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts +4 -0
  112. package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts.map +1 -1
  113. package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts +14 -0
  114. package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts.map +1 -1
  115. package/dist/types/core/components/functional/webgpu/helpers.d.ts +1 -0
  116. package/dist/types/core/components/functional/webgpu/helpers.d.ts.map +1 -1
  117. package/dist/types/core/components/functional/webgpu/webgpu.d.ts +7 -1
  118. package/dist/types/core/components/functional/webgpu/webgpu.d.ts.map +1 -1
  119. package/dist/types/core/utils/mediaRef.d.ts +15 -0
  120. package/dist/types/core/utils/mediaRef.d.ts.map +1 -1
  121. package/package.json +37 -1
  122. package/src/core/components/functional/3d/3d.md +34 -1
  123. package/src/core/components/functional/3d/3d.spec.ts +42 -0
  124. package/src/core/components/functional/3d/3d.ts +101 -3
  125. package/src/core/components/functional/3d/constants.ts +1 -0
  126. package/src/core/components/functional/3d/ensure.ts +11 -3
  127. package/src/core/components/functional/3d/form-params.ts +2 -0
  128. package/src/core/components/functional/3d/life.ts +4 -1
  129. package/src/core/components/functional/3d/out-publish.ts +4 -3
  130. package/src/core/components/functional/3d/playback.ts +13 -1
  131. package/src/core/components/functional/3d/post-bind.ts +262 -0
  132. package/src/core/components/functional/3d/post-channels.spec.ts +36 -0
  133. package/src/core/components/functional/3d/post-channels.ts +69 -0
  134. package/src/core/components/functional/3d/post-fx.ts +594 -0
  135. package/src/core/components/functional/3d/props.ts +45 -0
  136. package/src/core/components/functional/3d/reload.ts +2 -0
  137. package/src/core/components/functional/3d/runtime.ts +93 -4
  138. package/src/core/components/functional/3d/scene-runtime.ts +3 -0
  139. package/src/core/components/functional/3d/tune.ts +21 -0
  140. package/src/core/components/functional/3d/types.ts +9 -0
  141. package/src/core/components/functional/3d/updated.ts +17 -1
  142. package/src/core/components/functional/shader/gl-runtime.ts +169 -12
  143. package/src/core/components/functional/shader/shader.md +27 -4
  144. package/src/core/components/functional/shader/shader.spec.ts +267 -0
  145. package/src/core/components/functional/shader/shader.ts +177 -55
  146. package/src/core/components/functional/shader/shadertoy-prelude.ts +49 -5
  147. package/src/core/components/functional/shader/types.ts +89 -12
  148. package/src/core/components/functional/webgpu/gl-fallback.ts +28 -4
  149. package/src/core/components/functional/webgpu/gpu-runtime.ts +154 -18
  150. package/src/core/components/functional/webgpu/helpers.ts +8 -2
  151. package/src/core/components/functional/webgpu/webgpu.md +13 -2
  152. package/src/core/components/functional/webgpu/webgpu.spec.ts +37 -0
  153. package/src/core/components/functional/webgpu/webgpu.ts +39 -1
  154. package/src/core/utils/mediaRef.spec.ts +25 -0
  155. package/src/core/utils/mediaRef.ts +45 -0
  156. package/src/docs/_misc/visual-stack.md +19 -1
  157. package/src/docs/components/docs-demo-sources.ts +28 -0
  158. package/src/docs/docs.ts +2 -0
  159. package/src/docs/example/docs-3d-post-depth-demo.ts +68 -0
  160. package/src/docs/example/docs-3d-webgpu-live-demo.ts +83 -0
  161. package/src/docs/example/docs-harris-webgpu-demo.ts +53 -55
  162. package/src/docs/search/docs-search.json +252 -2
  163. package/src/tsconfig.json +27 -0
  164. package/src/tsconfig.tsbuildinfo +1 -1
@@ -6,14 +6,22 @@ import { bootScene } from "./boot";
6
6
  import { ops, type OpsCtx } from "./ops";
7
7
  import type { SceneRuntimeOptions } from "./opts";
8
8
  import type { SceneHit } from "./pick";
9
+ import {
10
+ PostFxCompileError,
11
+ PostFxPipeline,
12
+ type PostFxConfig,
13
+ type PostFxUniforms,
14
+ } from "./post-fx";
9
15
  import { tune, type TuneCtx } from "./tune";
10
16
  import type { Sonic3dAsset, Sonic3dOutput } from "./types";
11
17
  import { view, type ViewCtx } from "./view";
12
18
 
13
19
  export type { SceneHit, SceneRuntimeOptions };
20
+ export { PostFxCompileError };
21
+ export type { PostFxConfig, PostFxUniforms };
14
22
 
15
23
  /**
16
- * Runtime Three.js encapsulé : renderer, scène, caméra, load, fit, pick.
24
+ * Runtime Three.js encapsulé : renderer, scène, caméra, load, fit, pick, post FX.
17
25
  * Le composant Lit gère RAF / viewport / DP.
18
26
  */
19
27
  export class SceneRuntime {
@@ -46,6 +54,19 @@ export class SceneRuntime {
46
54
  private highlighted: THREE.Mesh | null = null;
47
55
  private prevEmissive = new THREE.Color();
48
56
  private prevEmissiveIntensity = 0;
57
+ private postFx: PostFxPipeline;
58
+ private lastCssW = 1;
59
+ private lastCssH = 1;
60
+ private damping = 0.08;
61
+ private postFrame: PostFxUniforms = {
62
+ time: 0,
63
+ timeDelta: 1 / 60,
64
+ frame: 0,
65
+ mouse: [0, 0, 0, 0],
66
+ params: [0, 0, 0, 0],
67
+ near: 0.1,
68
+ far: 100,
69
+ };
49
70
 
50
71
  constructor(opts: SceneRuntimeOptions) {
51
72
  const b = bootScene(opts);
@@ -62,9 +83,32 @@ export class SceneRuntime {
62
83
  this.fov = b.fov;
63
84
  this.fitEnabled = b.fitEnabled;
64
85
  this.limits = b.limits;
86
+ this.postFx = new PostFxPipeline(this.renderer);
65
87
  this.applyCameraMode(this.mode);
66
88
  if (b.autoRotate) this.setAutoRotate(true);
67
- void [this.canvas, this.perspCam, this.orthoCam, this.root, this.content, this.lights, this.loader, this.loadToken, this.blobUrl, this.dprMax, this.fov, this.fitEnabled, this.limits, this.bbox, this.target, this.raycaster, this.pointerNdc, this.highlighted, this.prevEmissive, this.prevEmissiveIntensity, this.unbindWheelRelay];
89
+ void [
90
+ this.canvas,
91
+ this.perspCam,
92
+ this.orthoCam,
93
+ this.root,
94
+ this.content,
95
+ this.lights,
96
+ this.loader,
97
+ this.loadToken,
98
+ this.blobUrl,
99
+ this.dprMax,
100
+ this.fov,
101
+ this.fitEnabled,
102
+ this.limits,
103
+ this.bbox,
104
+ this.target,
105
+ this.raycaster,
106
+ this.pointerNdc,
107
+ this.highlighted,
108
+ this.prevEmissive,
109
+ this.prevEmissiveIntensity,
110
+ this.unbindWheelRelay,
111
+ ];
68
112
  }
69
113
 
70
114
  get ready() {
@@ -79,6 +123,9 @@ export class SceneRuntime {
79
123
  get isLost() {
80
124
  return this.renderer.getContext()?.isContextLost?.() ?? false;
81
125
  }
126
+ get hasPostFx() {
127
+ return this.postFx.isEnabled;
128
+ }
82
129
 
83
130
  setDprMax(v: number) {
84
131
  tune.setDprMax(this.asTuneCtx(), v);
@@ -95,6 +142,15 @@ export class SceneRuntime {
95
142
  setAutoRotate(v: boolean) {
96
143
  tune.setAutoRotate(this.asTuneCtx(), v);
97
144
  }
145
+ setDamping(v: number) {
146
+ this.damping = Number.isFinite(v) ? Math.max(0, v) : 0.08;
147
+ tune.setDamping(this.asTuneCtx(), this.damping);
148
+ }
149
+ /** Coupe autoRotate + inertie OrbitControls. */
150
+ stopMotion() {
151
+ tune.setAutoRotate(this.asTuneCtx(), false);
152
+ tune.stopMotion(this.asTuneCtx());
153
+ }
98
154
  setOrbitLimits(opts: Parameters<typeof tune.setOrbitLimits>[1]) {
99
155
  tune.setOrbitLimits(this.asTuneCtx(), opts);
100
156
  }
@@ -104,9 +160,17 @@ export class SceneRuntime {
104
160
 
105
161
  applyCameraMode(mode: Sonic3dCameraMode) {
106
162
  view.applyCameraMode(this.asViewCtx(), mode);
163
+ tune.setDamping(this.asTuneCtx(), this.damping);
107
164
  }
108
165
  resize(cssW: number, cssH: number) {
166
+ this.lastCssW = cssW;
167
+ this.lastCssH = cssH;
109
168
  view.resize(this.asViewCtx(), cssW, cssH);
169
+ const dpr = Math.min(
170
+ typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
171
+ this.dprMax,
172
+ );
173
+ this.postFx.resize(cssW * dpr, cssH * dpr);
110
174
  }
111
175
  async load(asset: Sonic3dAsset | null) {
112
176
  return ops.runLoad(this.asOpsCtx(), asset);
@@ -114,9 +178,34 @@ export class SceneRuntime {
114
178
  fitToContent() {
115
179
  ops.fitToContent(this.asOpsCtx());
116
180
  }
181
+
182
+ setPostConfig(config: PostFxConfig | null) {
183
+ this.postFx.setConfig(config);
184
+ if (config && this.lastCssW > 0) {
185
+ this.resize(this.lastCssW, this.lastCssH);
186
+ }
187
+ }
188
+
189
+ setPostElementResolver(fn: ((key: string) => Element | null) | null) {
190
+ this.postFx.setElementResolver(fn);
191
+ }
192
+
193
+ setPostFrame(frame: Partial<PostFxUniforms>) {
194
+ Object.assign(this.postFrame, frame);
195
+ const cam = this.activeCam as THREE.PerspectiveCamera;
196
+ if ("near" in cam && "far" in cam) {
197
+ this.postFrame.near = cam.near;
198
+ this.postFrame.far = cam.far;
199
+ }
200
+ }
201
+
117
202
  render() {
118
203
  this.controls?.update();
119
- this.renderer.render(this.scene, this.activeCam);
204
+ if (this.postFx.isEnabled) {
205
+ this.postFx.render(this.scene, this.activeCam, this.postFrame);
206
+ } else {
207
+ this.renderer.render(this.scene, this.activeCam);
208
+ }
120
209
  }
121
210
  pick(normX: number, normY: number): SceneHit | null {
122
211
  return ops.pick(this.asOpsCtx(), normX, normY);
@@ -138,6 +227,7 @@ export class SceneRuntime {
138
227
  return ops.buildSnapshot(this.asOpsCtx(), base);
139
228
  }
140
229
  dispose(loseContext = false) {
230
+ this.postFx.dispose();
141
231
  ops.dispose(this.asOpsCtx(), loseContext);
142
232
  }
143
233
 
@@ -145,4 +235,3 @@ export class SceneRuntime {
145
235
  private asViewCtx = () => this as unknown as ViewCtx;
146
236
  private asOpsCtx = () => this as unknown as OpsCtx;
147
237
  }
148
-
@@ -4,6 +4,9 @@
4
4
  */
5
5
  export {
6
6
  SceneRuntime,
7
+ PostFxCompileError,
7
8
  type SceneHit,
8
9
  type SceneRuntimeOptions,
10
+ type PostFxConfig,
11
+ type PostFxUniforms,
9
12
  } from "./runtime";
@@ -37,6 +37,25 @@ function setAutoRotate(ctx: TuneCtx, v: boolean) {
37
37
  if (ctx.controls) ctx.controls.autoRotate = v;
38
38
  }
39
39
 
40
+ function setDamping(ctx: TuneCtx, v: number) {
41
+ if (!ctx.controls) return;
42
+ const d = Number.isFinite(v) ? Math.max(0, v) : 0.08;
43
+ ctx.controls.enableDamping = d > 0;
44
+ ctx.controls.dampingFactor = d > 0 ? d : 0.08;
45
+ }
46
+
47
+ /** Coupe autoRotate + vide l’inertie OrbitControls. */
48
+ function stopMotion(ctx: TuneCtx) {
49
+ if (!ctx.controls) return;
50
+ ctx.controls.autoRotate = false;
51
+ const wasDamping = ctx.controls.enableDamping;
52
+ const factor = ctx.controls.dampingFactor;
53
+ ctx.controls.enableDamping = false;
54
+ ctx.controls.update();
55
+ ctx.controls.enableDamping = wasDamping;
56
+ ctx.controls.dampingFactor = factor;
57
+ }
58
+
40
59
  function setOrbitLimits(ctx: TuneCtx, opts: Partial<OrbitLimits>) {
41
60
  ctx.limits = orbit.mergeLimits(ctx.limits, opts);
42
61
  orbit.applyLimitsToControls(ctx.controls, ctx.limits);
@@ -67,6 +86,8 @@ export const tune = {
67
86
  setFov,
68
87
  setFit,
69
88
  setAutoRotate,
89
+ setDamping,
90
+ stopMotion,
70
91
  setOrbitLimits,
71
92
  setOrbitSpherical,
72
93
  } as const;
@@ -65,6 +65,13 @@ export type Sonic3dConfig = {
65
65
  playOnHover?: boolean;
66
66
  /** Libère le renderer hors viewport (défaut true). */
67
67
  releaseOffscreen?: boolean;
68
+ /**
69
+ * Garde RAF / runtime hors viewport (pont live).
70
+ * Aussi auto via `registerFrameConsumer`.
71
+ */
72
+ playOffscreen?: boolean;
73
+ /** Inertie OrbitControls (défaut 0.08, 0 = off). */
74
+ damping?: number;
68
75
  dprMax?: number;
69
76
  aspectRatio?: string;
70
77
 
@@ -124,6 +131,8 @@ export type Sonic3dOutput = {
124
131
  distance: number;
125
132
  yaw: number;
126
133
  pitch: number;
134
+ /** 1 si la caméra bouge (orbit / inertie / autoRotate), sinon 0. */
135
+ moving?: number;
127
136
  /** Présents après un pick réussi. */
128
137
  hitX?: number;
129
138
  hitY?: number;
@@ -3,6 +3,7 @@ import type { LifeHost } from "./life";
3
3
  import { life } from "./life";
4
4
  import { parse } from "./parse";
5
5
  import { playback } from "./playback";
6
+ import { postBind } from "./post-bind";
6
7
  import { reload } from "./reload";
7
8
 
8
9
  function hasAny(
@@ -61,10 +62,25 @@ function updated(
61
62
  if (changed.has("autoRotate") || changed.has("play")) {
62
63
  playback.syncAutoRotate(host as never);
63
64
  }
64
- if (changed.has("play") || changed.has("playOnHover")) {
65
+ if (
66
+ changed.has("play") ||
67
+ changed.has("playOnHover") ||
68
+ changed.has("playOffscreen")
69
+ ) {
65
70
  host.syncPlayback();
66
71
  }
72
+ if (changed.has("playOffscreen") || changed.has("releaseOffscreen")) {
73
+ ensure.syncVisibility(host as never);
74
+ }
75
+ if (changed.has("damping") && host.runtime) {
76
+ host.runtime.setDamping(
77
+ (host as unknown as { damping: number }).damping,
78
+ );
79
+ }
67
80
  if (changed.has("src") || changed.has("asset")) void reload.run(host as never);
81
+ if (postBind.propsChanged(changed)) {
82
+ postBind.applyConfig(host as never);
83
+ }
68
84
  }
69
85
 
70
86
  export const propUpdate = { updated } as const;
@@ -7,9 +7,11 @@ import {
7
7
  ChannelSource,
8
8
  PassChannels,
9
9
  ReadRectResult,
10
+ ShaderBufferFormat,
10
11
  ShaderCompileError,
11
12
  ShaderPassId,
12
13
  ShaderSources,
14
+ parseBufferFormat,
13
15
  } from "./types";
14
16
 
15
17
  const PASS_ORDER: ShaderPassId[] = [
@@ -235,27 +237,110 @@ function getUniforms(
235
237
  };
236
238
  }
237
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
+
238
311
  function createEmptyTexture(
239
312
  gl: WebGL2RenderingContext,
240
313
  width: number,
241
314
  height: number,
315
+ fmt?: BufferGlFormat,
242
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);
243
328
  const tex = gl.createTexture();
244
329
  if (!tex) throw new Error("Unable to create texture");
245
330
  gl.bindTexture(gl.TEXTURE_2D, tex);
246
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
247
- 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);
248
333
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
249
334
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
250
335
  gl.texImage2D(
251
336
  gl.TEXTURE_2D,
252
337
  0,
253
- gl.RGBA8,
338
+ f.internal,
254
339
  width,
255
340
  height,
256
341
  0,
257
- gl.RGBA,
258
- gl.UNSIGNED_BYTE,
342
+ f.format,
343
+ f.type,
259
344
  null,
260
345
  );
261
346
  return tex;
@@ -287,10 +372,11 @@ function createPingPong(
287
372
  gl: WebGL2RenderingContext,
288
373
  width: number,
289
374
  height: number,
375
+ fmt: BufferGlFormat,
290
376
  ): PingPong {
291
377
  const textures: [WebGLTexture, WebGLTexture] = [
292
- createEmptyTexture(gl, width, height),
293
- createEmptyTexture(gl, width, height),
378
+ createEmptyTexture(gl, width, height, fmt),
379
+ createEmptyTexture(gl, width, height, fmt),
294
380
  ];
295
381
  const framebuffers: [WebGLFramebuffer, WebGLFramebuffer] = [
296
382
  createFramebuffer(gl, textures[0]),
@@ -349,6 +435,7 @@ export class ShaderToyRuntime {
349
435
  private activePasses: ShaderPassId[] = [];
350
436
  private blackTexture: WebGLTexture | null = null;
351
437
  private elementResolver: ((key: string) => Element | null) | null = null;
438
+ private bufferFmt: BufferGlFormat;
352
439
 
353
440
  constructor(canvas: HTMLCanvasElement) {
354
441
  const gl = canvas.getContext("webgl2", {
@@ -364,6 +451,7 @@ export class ShaderToyRuntime {
364
451
  }
365
452
  this.canvas = canvas;
366
453
  this.gl = gl;
454
+ this.bufferFmt = resolveBufferGlFormat(gl, "rgba8");
367
455
  this.initGeometry();
368
456
  this.blackTexture = createEmptyTexture(gl, 1, 1);
369
457
  gl.bindTexture(gl.TEXTURE_2D, this.blackTexture);
@@ -380,6 +468,42 @@ export class ShaderToyRuntime {
380
468
  );
381
469
  }
382
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
+
383
507
  /** True si le contexte a été perdu / déjà disposé. */
384
508
  get isLost(): boolean {
385
509
  return this.gl.isContextLost();
@@ -416,7 +540,10 @@ export class ShaderToyRuntime {
416
540
  const existing = this.buffers.get(id);
417
541
  if (existing) destroyPingPong(this.gl, existing);
418
542
  if (this.programs.has(id)) {
419
- this.buffers.set(id, createPingPong(this.gl, w, h));
543
+ this.buffers.set(
544
+ id,
545
+ createPingPong(this.gl, w, h, this.bufferFmt),
546
+ );
420
547
  }
421
548
  }
422
549
  }
@@ -458,7 +585,10 @@ export class ShaderToyRuntime {
458
585
  if (pass !== "image" && this.width > 0) {
459
586
  const prev = this.buffers.get(pass);
460
587
  if (prev) destroyPingPong(this.gl, prev);
461
- 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
+ );
462
592
  }
463
593
  }
464
594
 
@@ -471,6 +601,18 @@ export class ShaderToyRuntime {
471
601
  }
472
602
  }
473
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
+
474
616
  async ensureTextures(urls: string[]) {
475
617
  const unique = [...new Set(urls.filter(Boolean))];
476
618
  await Promise.all(unique.map((url) => this.loadTexture(url)));
@@ -846,6 +988,7 @@ export class ShaderToyRuntime {
846
988
 
847
989
  /**
848
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`.
849
992
  */
850
993
  readBufferRect(
851
994
  pass: ShaderPassId,
@@ -857,7 +1000,14 @@ export class ShaderToyRuntime {
857
1000
  if (pass === "image") return this.readRect(x, y, w, h);
858
1001
  const pp = this.buffers.get(pass);
859
1002
  if (!pp) return null;
860
- 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
+ );
861
1011
  }
862
1012
 
863
1013
  /**
@@ -876,6 +1026,7 @@ export class ShaderToyRuntime {
876
1026
  y: number,
877
1027
  w: number,
878
1028
  h: number,
1029
+ asFloat = false,
879
1030
  ): ReadRectResult | null {
880
1031
  const gl = this.gl;
881
1032
  if (gl.isContextLost() || this.width < 1 || this.height < 1) return null;
@@ -895,9 +1046,15 @@ export class ShaderToyRuntime {
895
1046
  if (px + pw > this.width) pw = this.width - px;
896
1047
  if (py + ph > this.height) ph = this.height - py;
897
1048
  if (pw < 1 || ph < 1) return null;
898
- const data = new Uint8Array(pw * ph * 4);
899
1049
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
900
- 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
+ }
901
1058
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
902
1059
  return { x: px, y: py, width: pw, height: ph, data };
903
1060
  }
@@ -44,8 +44,8 @@ Live 3D → shader (no JPEG): [Visual stack](#docs/_misc/visual-stack.md/visual-
44
44
  |--------|-----------|
45
45
  | Sortie d’un effet → entrée du suivant | `image-ch0="buffer-a"` (ou `buffer-b`… / `self`) — **même contexte GPU**, FBO ping-pong |
46
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`) |
48
- | Hors viewport | `release-offscreen` (défaut) : libère textures/FBO, garde le contexte |
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 |
49
49
 
50
50
  Les passes intermédiaires sont **déjà offscreen** (FBO). Pas besoin d’un second `sonic-shader` invisible pour du multipass interne.
51
51
 
@@ -138,6 +138,22 @@ onExtract() {
138
138
 
139
139
  Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/visual-stack).
140
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
+
141
157
  ## API typée (`SonicShaderConfig`)
142
158
 
143
159
  | Champ / attribut | Type | Rôle |
@@ -145,6 +161,7 @@ Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/v
145
161
  | `image` | `string` | GLSL pass Image (**requis**, propriété JS) |
146
162
  | `bufferA`…`bufferD` / `buffer-a`… | `string` | Passes multipass |
147
163
  | `common` | `string` | GLSL partagé |
164
+ | `bufferFormat` / `buffer-format` | `rgba8` \| `rgba16f` \| `rgba32f` | Format buffers A–D (défaut `rgba8`) ; `active-buffer-format` reflète le format réellement utilisé |
148
165
  | `channel0`…`channel3` | `string` | URLs, `#id` élément, ou refs buffers (toutes passes) |
149
166
  | `imageCh0`… / `image-ch0`… | `string` | Override canal : URL, `#id`, `self`, `buffer-a`…, `none` |
150
167
  | `play` | `boolean` | Boucle RAF (défaut `true`) |
@@ -170,7 +187,13 @@ Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/v
170
187
  | `extractDataProvider` / `extract-data-provider` | `string` | Publisher extract (défaut = out) |
171
188
  | `aspectRatio` / `aspect-ratio` | `string` | Ratio CSS forcé |
172
189
  | `dprMax` / `dpr-max` | `number` | Plafond DPR (défaut `2`) |
173
- | `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.
174
197
 
175
198
  ## Markup
176
199
 
@@ -184,7 +207,7 @@ Live Harris → patches → WebGPU: [Visual stack](#docs/_misc/visual-stack.md/v
184
207
  hover-speed="12"
185
208
  play
186
209
  play-on-hover
187
- release-offscreen
210
+ release-offscreen="auto"
188
211
  channel0="/docs/shader-samples/color.jpg"
189
212
  &gt;&lt;/sonic-shader&gt;
190
213
  </template>