@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
@@ -8,10 +8,21 @@ export type PlaybackHost = {
8
8
  playOnHover: boolean;
9
9
  hovering: boolean;
10
10
  autoRotate: boolean;
11
+ playOffscreen: boolean;
12
+ /** Nombre de consommateurs live (`registerFrameConsumer`). */
13
+ frameConsumerCount: number;
11
14
  raf: number;
12
15
  drawFrame: (now: number) => void;
13
16
  };
14
17
 
18
+ /** Hors écran : jouer si `play-offscreen` ou au moins un consommateur live. */
19
+ export function wantsOffscreenPlay(host: {
20
+ playOffscreen: boolean;
21
+ frameConsumerCount: number;
22
+ }): boolean {
23
+ return !!host.playOffscreen || host.frameConsumerCount > 0;
24
+ }
25
+
15
26
  function prefersReducedMotion(): boolean {
16
27
  return (
17
28
  typeof matchMedia === "function" &&
@@ -22,7 +33,7 @@ function prefersReducedMotion(): boolean {
22
33
  function shouldAnimate(host: PlaybackHost): boolean {
23
34
  if (!host.play) return false;
24
35
  if (!host.ready) return false;
25
- if (!host.inView) return false;
36
+ if (!host.inView && !wantsOffscreenPlay(host)) return false;
26
37
  if (!host.runtime) return false;
27
38
  if (prefersReducedMotion()) return false;
28
39
  if (host.playOnHover) return host.hovering;
@@ -64,6 +75,7 @@ function syncPlayback(host: PlaybackHost, loop: () => void) {
64
75
  export const playback = {
65
76
  prefersReducedMotion,
66
77
  shouldAnimate,
78
+ wantsOffscreenPlay,
67
79
  syncAutoRotate,
68
80
  syncPlayback,
69
81
  stopLoop,
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Construit la config PostFx depuis les props Lit / form DP.
3
+ */
4
+ import { lookupLiveElement } from "@supersoniks/concorde/core/utils/mediaRef";
5
+ import {
6
+ clampPostScale,
7
+ parsePostChannelSource,
8
+ type PostChannels,
9
+ type PostPassId,
10
+ type PostSources,
11
+ } from "./post-channels";
12
+ import type { PostFxConfig } from "./post-fx";
13
+ import type { SceneRuntime } from "./runtime";
14
+
15
+ export type PostBindHost = {
16
+ postImage: string;
17
+ postBufferA: string;
18
+ postBufferB: string;
19
+ postBufferC: string;
20
+ postBufferD: string;
21
+ postCommon: string;
22
+ postImageCh0: string;
23
+ postImageCh1: string;
24
+ postImageCh2: string;
25
+ postImageCh3: string;
26
+ postBufferACh0: string;
27
+ postBufferACh1: string;
28
+ postBufferACh2: string;
29
+ postBufferACh3: string;
30
+ postBufferBCh0: string;
31
+ postBufferBCh1: string;
32
+ postBufferBCh2: string;
33
+ postBufferBCh3: string;
34
+ postBufferCCh0: string;
35
+ postBufferCCh1: string;
36
+ postBufferCCh2: string;
37
+ postBufferCCh3: string;
38
+ postBufferDCh0: string;
39
+ postBufferDCh1: string;
40
+ postBufferDCh2: string;
41
+ postBufferDCh3: string;
42
+ postScale: number;
43
+ param0: number;
44
+ param1: number;
45
+ param2: number;
46
+ param3: number;
47
+ runtime: SceneRuntime | null;
48
+ errorMessage: string;
49
+ getRootNode: () => Node;
50
+ dispatchEvent: (event: Event) => boolean;
51
+ };
52
+
53
+ const POST_PROP_KEYS = [
54
+ "postImage",
55
+ "postBufferA",
56
+ "postBufferB",
57
+ "postBufferC",
58
+ "postBufferD",
59
+ "postCommon",
60
+ "postImageCh0",
61
+ "postImageCh1",
62
+ "postImageCh2",
63
+ "postImageCh3",
64
+ "postBufferACh0",
65
+ "postBufferACh1",
66
+ "postBufferACh2",
67
+ "postBufferACh3",
68
+ "postBufferBCh0",
69
+ "postBufferBCh1",
70
+ "postBufferBCh2",
71
+ "postBufferBCh3",
72
+ "postBufferCCh0",
73
+ "postBufferCCh1",
74
+ "postBufferCCh2",
75
+ "postBufferCCh3",
76
+ "postBufferDCh0",
77
+ "postBufferDCh1",
78
+ "postBufferDCh2",
79
+ "postBufferDCh3",
80
+ "postScale",
81
+ ] as const;
82
+
83
+ function buildChannels(host: PostBindHost, pass: PostPassId): PostChannels {
84
+ const fallbacks = ["", "", "", ""];
85
+ // image defaults ch0=scene via parse empty+slot0
86
+ let overrides: string[] = ["", "", "", ""];
87
+ if (pass === "image") {
88
+ overrides = [
89
+ host.postImageCh0,
90
+ host.postImageCh1,
91
+ host.postImageCh2,
92
+ host.postImageCh3,
93
+ ];
94
+ } else if (pass === "bufferA") {
95
+ overrides = [
96
+ host.postBufferACh0,
97
+ host.postBufferACh1,
98
+ host.postBufferACh2,
99
+ host.postBufferACh3,
100
+ ];
101
+ } else if (pass === "bufferB") {
102
+ overrides = [
103
+ host.postBufferBCh0,
104
+ host.postBufferBCh1,
105
+ host.postBufferBCh2,
106
+ host.postBufferBCh3,
107
+ ];
108
+ } else if (pass === "bufferC") {
109
+ overrides = [
110
+ host.postBufferCCh0,
111
+ host.postBufferCCh1,
112
+ host.postBufferCCh2,
113
+ host.postBufferCCh3,
114
+ ];
115
+ } else {
116
+ overrides = [
117
+ host.postBufferDCh0,
118
+ host.postBufferDCh1,
119
+ host.postBufferDCh2,
120
+ host.postBufferDCh3,
121
+ ];
122
+ }
123
+ return [0, 1, 2, 3].map((i) =>
124
+ parsePostChannelSource(
125
+ overrides[i],
126
+ fallbacks[i],
127
+ i as 0 | 1 | 2 | 3,
128
+ ),
129
+ ) as PostChannels;
130
+ }
131
+
132
+ function buildConfig(host: PostBindHost): PostFxConfig | null {
133
+ if (!host.postImage.trim()) return null;
134
+ const sources: PostSources = {
135
+ common: host.postCommon,
136
+ image: host.postImage,
137
+ bufferA: host.postBufferA,
138
+ bufferB: host.postBufferB,
139
+ bufferC: host.postBufferC,
140
+ bufferD: host.postBufferD,
141
+ };
142
+ const channels = new Map<PostPassId, PostChannels>();
143
+ for (const pass of [
144
+ "bufferA",
145
+ "bufferB",
146
+ "bufferC",
147
+ "bufferD",
148
+ "image",
149
+ ] as PostPassId[]) {
150
+ channels.set(pass, buildChannels(host, pass));
151
+ }
152
+ return {
153
+ sources,
154
+ channels,
155
+ scale: clampPostScale(host.postScale),
156
+ };
157
+ }
158
+
159
+ function lookupElement(host: PostBindHost, key: string): Element | null {
160
+ if (!key.startsWith("#")) return null;
161
+ const live = lookupLiveElement(key);
162
+ if (live) return live;
163
+ const id = key.slice(1);
164
+ if (!id || id.startsWith("__sonic_frame_")) return null;
165
+ const root = host.getRootNode();
166
+ if (root instanceof Document || root instanceof ShadowRoot) {
167
+ const el = root.getElementById(id);
168
+ if (el) return el;
169
+ }
170
+ return typeof document !== "undefined" ? document.getElementById(id) : null;
171
+ }
172
+
173
+ function applyConfig(host: PostBindHost) {
174
+ if (!host.runtime) return;
175
+ host.runtime.setPostElementResolver((key) => lookupElement(host, key));
176
+ try {
177
+ host.runtime.setPostConfig(buildConfig(host));
178
+ if (host.errorMessage.startsWith("Post FX")) host.errorMessage = "";
179
+ } catch (err) {
180
+ const message = err instanceof Error ? err.message : String(err);
181
+ host.errorMessage = `Post FX: ${message}`;
182
+ const pass =
183
+ err && typeof err === "object" && "pass" in err
184
+ ? String((err as { pass: string }).pass)
185
+ : "image";
186
+ const infoLog =
187
+ err && typeof err === "object" && "infoLog" in err
188
+ ? String((err as { infoLog: string }).infoLog)
189
+ : "";
190
+ host.dispatchEvent(
191
+ new CustomEvent("error", {
192
+ detail: { message, pass, infoLog },
193
+ bubbles: true,
194
+ composed: true,
195
+ }),
196
+ );
197
+ try {
198
+ host.runtime.setPostConfig(null);
199
+ } catch {
200
+ /* ignore */
201
+ }
202
+ }
203
+ }
204
+
205
+ function syncFrame(
206
+ host: PostBindHost,
207
+ time: number,
208
+ timeDelta: number,
209
+ frame: number,
210
+ mouse: [number, number, number, number] = [0, 0, 0, 0],
211
+ ) {
212
+ host.runtime?.setPostFrame({
213
+ time,
214
+ timeDelta,
215
+ frame,
216
+ mouse,
217
+ params: [host.param0, host.param1, host.param2, host.param3],
218
+ });
219
+ }
220
+
221
+ function propsChanged(
222
+ changed: Map<string | number | symbol, unknown>,
223
+ ): boolean {
224
+ return POST_PROP_KEYS.some((k) => changed.has(k));
225
+ }
226
+
227
+ /** Applique param0…3 + post* depuis un DP form. */
228
+ function applyFormPost(host: PostBindHost, data: Record<string, unknown>) {
229
+ let touched = false;
230
+ for (const k of ["param0", "param1", "param2", "param3"] as const) {
231
+ if (k in data && typeof data[k] === "number") {
232
+ host[k] = data[k] as number;
233
+ touched = true;
234
+ } else if (k in data && typeof data[k] === "string") {
235
+ const n = Number(data[k]);
236
+ if (Number.isFinite(n)) {
237
+ host[k] = n;
238
+ touched = true;
239
+ }
240
+ }
241
+ }
242
+ for (const k of POST_PROP_KEYS) {
243
+ if (!(k in data)) continue;
244
+ const v = data[k];
245
+ if (k === "postScale" && (typeof v === "number" || typeof v === "string")) {
246
+ host.postScale = clampPostScale(Number(v));
247
+ touched = true;
248
+ } else if (typeof v === "string") {
249
+ (host as unknown as Record<string, string>)[k] = v;
250
+ touched = true;
251
+ }
252
+ }
253
+ if (touched) applyConfig(host);
254
+ }
255
+
256
+ export const postBind = {
257
+ applyConfig,
258
+ syncFrame,
259
+ propsChanged,
260
+ applyFormPost,
261
+ POST_PROP_KEYS,
262
+ } as const;
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ clampPostScale,
4
+ parsePostChannelSource,
5
+ } from "./post-channels";
6
+
7
+ describe("parsePostChannelSource", () => {
8
+ it("défaut ch0 = scene", () => {
9
+ expect(parsePostChannelSource("", "", 0)).toEqual({ kind: "scene" });
10
+ expect(parsePostChannelSource("", "", 1)).toEqual({ kind: "none" });
11
+ });
12
+
13
+ it("depth / buffers / url / #id", () => {
14
+ expect(parsePostChannelSource("depth", "", 0)).toEqual({ kind: "depth" });
15
+ expect(parsePostChannelSource("buffer-a", "", 0)).toEqual({
16
+ kind: "buffer",
17
+ buffer: "bufferA",
18
+ });
19
+ expect(parsePostChannelSource("/tex.png", "", 0)).toEqual({
20
+ kind: "url",
21
+ url: "/tex.png",
22
+ });
23
+ expect(parsePostChannelSource("#ref", "", 0)).toEqual({
24
+ kind: "element",
25
+ key: "#ref",
26
+ });
27
+ });
28
+ });
29
+
30
+ describe("clampPostScale", () => {
31
+ it("borne 0.25–1", () => {
32
+ expect(clampPostScale(2)).toBe(1);
33
+ expect(clampPostScale(0.1)).toBe(0.25);
34
+ expect(clampPostScale(0.5)).toBe(0.5);
35
+ });
36
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Routage des canaux post-* (sonic-3d) : scene | depth | buffers | url | #id.
3
+ */
4
+
5
+ export type PostPassId =
6
+ | "image"
7
+ | "bufferA"
8
+ | "bufferB"
9
+ | "bufferC"
10
+ | "bufferD";
11
+
12
+ export type PostChannelSource =
13
+ | { kind: "scene" }
14
+ | { kind: "depth" }
15
+ | { kind: "buffer"; buffer: Exclude<PostPassId, "image"> }
16
+ | { kind: "self" }
17
+ | { kind: "none" }
18
+ | { kind: "url"; url: string }
19
+ | { kind: "element"; key: string };
20
+
21
+ export type PostChannels = [
22
+ PostChannelSource,
23
+ PostChannelSource,
24
+ PostChannelSource,
25
+ PostChannelSource,
26
+ ];
27
+
28
+ export type PostSources = {
29
+ common: string;
30
+ image: string;
31
+ bufferA: string;
32
+ bufferB: string;
33
+ bufferC: string;
34
+ bufferD: string;
35
+ };
36
+
37
+ /** Parse un slot canal post (défaut `scene` pour ch0 si vide). */
38
+ export function parsePostChannelSource(
39
+ value: string | undefined | null,
40
+ fallback: string,
41
+ slot: 0 | 1 | 2 | 3,
42
+ ): PostChannelSource {
43
+ const v = (value ?? "").trim() || (fallback ?? "").trim();
44
+ if (!v) return slot === 0 ? { kind: "scene" } : { kind: "none" };
45
+ if (v.startsWith("#")) return { kind: "element", key: v };
46
+ const lower = v.toLowerCase();
47
+ if (lower === "scene") return { kind: "scene" };
48
+ if (lower === "depth") return { kind: "depth" };
49
+ if (lower === "self") return { kind: "self" };
50
+ if (lower === "none" || lower === "null") return { kind: "none" };
51
+ if (lower === "buffer-a" || lower === "buffera" || lower === "a") {
52
+ return { kind: "buffer", buffer: "bufferA" };
53
+ }
54
+ if (lower === "buffer-b" || lower === "bufferb" || lower === "b") {
55
+ return { kind: "buffer", buffer: "bufferB" };
56
+ }
57
+ if (lower === "buffer-c" || lower === "bufferc" || lower === "c") {
58
+ return { kind: "buffer", buffer: "bufferC" };
59
+ }
60
+ if (lower === "buffer-d" || lower === "bufferd" || lower === "d") {
61
+ return { kind: "buffer", buffer: "bufferD" };
62
+ }
63
+ return { kind: "url", url: v };
64
+ }
65
+
66
+ export function clampPostScale(v: number): number {
67
+ if (!Number.isFinite(v)) return 1;
68
+ return Math.min(1, Math.max(0.25, v));
69
+ }