@stevejtrettel/shader-sandbox 0.1.2 → 0.1.4

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 (113) hide show
  1. package/README.md +259 -235
  2. package/bin/cli.js +106 -14
  3. package/dist-lib/app/App.d.ts +143 -15
  4. package/dist-lib/app/App.d.ts.map +1 -1
  5. package/dist-lib/app/App.js +1343 -108
  6. package/dist-lib/app/app.css +349 -24
  7. package/dist-lib/app/types.d.ts +48 -5
  8. package/dist-lib/app/types.d.ts.map +1 -1
  9. package/dist-lib/editor/EditorPanel.d.ts +2 -2
  10. package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
  11. package/dist-lib/editor/EditorPanel.js +1 -1
  12. package/dist-lib/editor/editor-panel.css +55 -32
  13. package/dist-lib/editor/prism-editor.css +16 -16
  14. package/dist-lib/embed.js +1 -1
  15. package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
  16. package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
  17. package/dist-lib/engine/ShaderEngine.js +1523 -0
  18. package/dist-lib/engine/glHelpers.d.ts +24 -0
  19. package/dist-lib/engine/glHelpers.d.ts.map +1 -1
  20. package/dist-lib/engine/glHelpers.js +88 -0
  21. package/dist-lib/engine/std140.d.ts +47 -0
  22. package/dist-lib/engine/std140.d.ts.map +1 -0
  23. package/dist-lib/engine/std140.js +119 -0
  24. package/dist-lib/engine/types.d.ts +55 -5
  25. package/dist-lib/engine/types.d.ts.map +1 -1
  26. package/dist-lib/engine/types.js +1 -1
  27. package/dist-lib/index.d.ts +4 -3
  28. package/dist-lib/index.d.ts.map +1 -1
  29. package/dist-lib/index.js +2 -1
  30. package/dist-lib/layouts/SplitLayout.d.ts +2 -1
  31. package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
  32. package/dist-lib/layouts/SplitLayout.js +3 -0
  33. package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
  34. package/dist-lib/layouts/UILayout.d.ts +55 -0
  35. package/dist-lib/layouts/UILayout.d.ts.map +1 -0
  36. package/dist-lib/layouts/UILayout.js +147 -0
  37. package/dist-lib/layouts/default.css +2 -2
  38. package/dist-lib/layouts/index.d.ts +11 -1
  39. package/dist-lib/layouts/index.d.ts.map +1 -1
  40. package/dist-lib/layouts/index.js +17 -1
  41. package/dist-lib/layouts/split.css +33 -31
  42. package/dist-lib/layouts/tabbed.css +127 -74
  43. package/dist-lib/layouts/types.d.ts +14 -3
  44. package/dist-lib/layouts/types.d.ts.map +1 -1
  45. package/dist-lib/main.js +33 -0
  46. package/dist-lib/project/configHelpers.d.ts +45 -0
  47. package/dist-lib/project/configHelpers.d.ts.map +1 -0
  48. package/dist-lib/project/configHelpers.js +196 -0
  49. package/dist-lib/project/generatedLoader.d.ts +2 -2
  50. package/dist-lib/project/generatedLoader.d.ts.map +1 -1
  51. package/dist-lib/project/generatedLoader.js +23 -5
  52. package/dist-lib/project/loadProject.d.ts +6 -6
  53. package/dist-lib/project/loadProject.d.ts.map +1 -1
  54. package/dist-lib/project/loadProject.js +396 -144
  55. package/dist-lib/project/loaderHelper.d.ts +4 -4
  56. package/dist-lib/project/loaderHelper.d.ts.map +1 -1
  57. package/dist-lib/project/loaderHelper.js +278 -116
  58. package/dist-lib/project/types.d.ts +292 -13
  59. package/dist-lib/project/types.d.ts.map +1 -1
  60. package/dist-lib/project/types.js +13 -1
  61. package/dist-lib/styles/base.css +5 -1
  62. package/dist-lib/uniforms/UniformControls.d.ts +60 -0
  63. package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
  64. package/dist-lib/uniforms/UniformControls.js +518 -0
  65. package/dist-lib/uniforms/UniformStore.d.ts +74 -0
  66. package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
  67. package/dist-lib/uniforms/UniformStore.js +145 -0
  68. package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
  69. package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
  70. package/dist-lib/uniforms/UniformsPanel.js +124 -0
  71. package/dist-lib/uniforms/index.d.ts +11 -0
  72. package/dist-lib/uniforms/index.d.ts.map +1 -0
  73. package/dist-lib/uniforms/index.js +8 -0
  74. package/package.json +16 -1
  75. package/src/app/App.ts +1469 -126
  76. package/src/app/app.css +349 -24
  77. package/src/app/types.ts +53 -5
  78. package/src/editor/EditorPanel.ts +5 -5
  79. package/src/editor/editor-panel.css +55 -32
  80. package/src/editor/prism-editor.css +16 -16
  81. package/src/embed.ts +1 -1
  82. package/src/engine/ShaderEngine.ts +1934 -0
  83. package/src/engine/glHelpers.ts +117 -0
  84. package/src/engine/std140.ts +136 -0
  85. package/src/engine/types.ts +69 -5
  86. package/src/index.ts +4 -3
  87. package/src/layouts/SplitLayout.ts +8 -3
  88. package/src/layouts/TabbedLayout.ts +3 -3
  89. package/src/layouts/UILayout.ts +185 -0
  90. package/src/layouts/default.css +2 -2
  91. package/src/layouts/index.ts +20 -1
  92. package/src/layouts/split.css +33 -31
  93. package/src/layouts/tabbed.css +127 -74
  94. package/src/layouts/types.ts +19 -3
  95. package/src/layouts/ui.css +289 -0
  96. package/src/main.ts +39 -1
  97. package/src/project/configHelpers.ts +225 -0
  98. package/src/project/generatedLoader.ts +27 -6
  99. package/src/project/loadProject.ts +459 -173
  100. package/src/project/loaderHelper.ts +377 -130
  101. package/src/project/types.ts +360 -14
  102. package/src/styles/base.css +5 -1
  103. package/src/styles/theme.css +292 -0
  104. package/src/uniforms/UniformControls.ts +660 -0
  105. package/src/uniforms/UniformStore.ts +166 -0
  106. package/src/uniforms/UniformsPanel.ts +163 -0
  107. package/src/uniforms/index.ts +13 -0
  108. package/src/uniforms/uniform-controls.css +342 -0
  109. package/src/uniforms/uniforms-panel.css +277 -0
  110. package/templates/shaders/example-buffer/config.json +1 -0
  111. package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
  112. package/dist-lib/engine/ShadertoyEngine.js +0 -704
  113. package/src/engine/ShadertoyEngine.ts +0 -929
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Uniform Controls Component
3
+ *
4
+ * Renders UI controls for custom uniforms defined in config.json.
5
+ * Supports: float, int, bool, vec2 (XY pad), vec3 (color picker or sliders), vec4 (sliders)
6
+ */
7
+ import './uniform-controls.css';
8
+ import { isArrayUniform, } from '../project/types';
9
+ export class UniformControls {
10
+ constructor(opts) {
11
+ this.values = {};
12
+ this.updaters = new Map();
13
+ // Track document-level event listeners for cleanup
14
+ this.documentListeners = [];
15
+ this.container = opts.container;
16
+ this.uniforms = opts.uniforms;
17
+ this.onChange = opts.onChange;
18
+ // Initialize values
19
+ for (const [name, def] of Object.entries(this.uniforms)) {
20
+ if (isArrayUniform(def) || def.hidden)
21
+ continue;
22
+ this.values[name] = opts.initialValues?.[name] ?? def.value;
23
+ }
24
+ this.render();
25
+ }
26
+ /**
27
+ * Render all uniform controls.
28
+ */
29
+ render() {
30
+ this.container.innerHTML = '';
31
+ this.container.className = 'uniform-controls';
32
+ const uniformEntries = Object.entries(this.uniforms);
33
+ if (uniformEntries.length === 0) {
34
+ const emptyMsg = document.createElement('div');
35
+ emptyMsg.className = 'uniform-controls-empty';
36
+ emptyMsg.textContent = 'No uniforms defined';
37
+ this.container.appendChild(emptyMsg);
38
+ return;
39
+ }
40
+ // Header with reset button
41
+ const header = document.createElement('div');
42
+ header.className = 'uniform-controls-header';
43
+ const resetButton = document.createElement('button');
44
+ resetButton.className = 'uniform-controls-reset';
45
+ resetButton.textContent = 'Reset';
46
+ resetButton.title = 'Reset all uniforms to defaults';
47
+ resetButton.addEventListener('click', () => this.resetToDefaults());
48
+ header.appendChild(resetButton);
49
+ this.container.appendChild(header);
50
+ // Control list
51
+ const controlList = document.createElement('div');
52
+ controlList.className = 'uniform-controls-list';
53
+ for (const [name, def] of uniformEntries) {
54
+ if (isArrayUniform(def) || def.hidden)
55
+ continue;
56
+ const result = this.createControl(name, def);
57
+ if (result) {
58
+ this.updaters.set(name, result.update);
59
+ controlList.appendChild(result.element);
60
+ }
61
+ }
62
+ this.container.appendChild(controlList);
63
+ }
64
+ /**
65
+ * Create a control element for a uniform.
66
+ */
67
+ createControl(name, def) {
68
+ if (isArrayUniform(def) || def.hidden)
69
+ return null;
70
+ switch (def.type) {
71
+ case 'float':
72
+ return this.createFloatSlider(name, def);
73
+ case 'int':
74
+ return this.createIntSlider(name, def);
75
+ case 'bool':
76
+ return this.createBoolToggle(name, def);
77
+ case 'vec2':
78
+ return this.createVec2Pad(name, def);
79
+ case 'vec3':
80
+ return def.color ? this.createColorPicker(name, def) : this.createVecSliders(name, def, 3);
81
+ case 'vec4':
82
+ return def.color ? this.createColorPicker4(name, def) : this.createVecSliders(name, def, 4);
83
+ default:
84
+ console.warn(`Uniform '${name}': unknown type '${def.type}'`);
85
+ return null;
86
+ }
87
+ }
88
+ // ===========================================================================
89
+ // Shared Slider Row Helper
90
+ // ===========================================================================
91
+ createSliderRow(opts) {
92
+ const wrapper = document.createElement('div');
93
+ wrapper.className = 'uniform-control-label-row';
94
+ const labelEl = document.createElement('label');
95
+ labelEl.className = 'uniform-control-label';
96
+ labelEl.textContent = opts.label;
97
+ const valueDisplay = document.createElement('span');
98
+ valueDisplay.className = 'uniform-control-value';
99
+ valueDisplay.textContent = opts.format(opts.value);
100
+ wrapper.appendChild(labelEl);
101
+ wrapper.appendChild(valueDisplay);
102
+ const slider = document.createElement('input');
103
+ slider.type = 'range';
104
+ slider.className = 'uniform-control-slider';
105
+ slider.min = String(opts.min);
106
+ slider.max = String(opts.max);
107
+ slider.step = String(opts.step);
108
+ slider.value = String(opts.value);
109
+ slider.addEventListener('input', () => {
110
+ const v = parseFloat(slider.value);
111
+ opts.onInput(v);
112
+ valueDisplay.textContent = opts.format(v);
113
+ });
114
+ const container = document.createElement('div');
115
+ container.appendChild(wrapper);
116
+ container.appendChild(slider);
117
+ const update = (v) => {
118
+ slider.value = String(v);
119
+ valueDisplay.textContent = opts.format(v);
120
+ };
121
+ return { element: container, update };
122
+ }
123
+ // ===========================================================================
124
+ // Float Slider
125
+ // ===========================================================================
126
+ createFloatSlider(name, def) {
127
+ const step = def.step ?? 0.01;
128
+ const { element, update: sliderUpdate } = this.createSliderRow({
129
+ label: def.label ?? name,
130
+ min: def.min ?? 0,
131
+ max: def.max ?? 1,
132
+ step,
133
+ value: this.values[name],
134
+ format: (v) => this.formatNumber(v, step),
135
+ onInput: (v) => {
136
+ this.values[name] = v;
137
+ this.onChange(name, v);
138
+ },
139
+ });
140
+ const wrapper = document.createElement('div');
141
+ wrapper.className = 'uniform-control uniform-control-float';
142
+ wrapper.appendChild(element);
143
+ return {
144
+ element: wrapper,
145
+ update: (v) => sliderUpdate(v),
146
+ };
147
+ }
148
+ // ===========================================================================
149
+ // Int Slider
150
+ // ===========================================================================
151
+ createIntSlider(name, def) {
152
+ const { element, update: sliderUpdate } = this.createSliderRow({
153
+ label: def.label ?? name,
154
+ min: def.min ?? 0,
155
+ max: def.max ?? 10,
156
+ step: def.step ?? 1,
157
+ value: this.values[name],
158
+ format: (v) => String(Math.round(v)),
159
+ onInput: (v) => {
160
+ const intVal = Math.round(v);
161
+ this.values[name] = intVal;
162
+ this.onChange(name, intVal);
163
+ },
164
+ });
165
+ const wrapper = document.createElement('div');
166
+ wrapper.className = 'uniform-control uniform-control-int';
167
+ wrapper.appendChild(element);
168
+ return {
169
+ element: wrapper,
170
+ update: (v) => sliderUpdate(v),
171
+ };
172
+ }
173
+ // ===========================================================================
174
+ // Bool Toggle
175
+ // ===========================================================================
176
+ createBoolToggle(name, def) {
177
+ const value = this.values[name];
178
+ const label = def.label ?? name;
179
+ const wrapper = document.createElement('div');
180
+ wrapper.className = 'uniform-control uniform-control-bool';
181
+ const labelRow = document.createElement('div');
182
+ labelRow.className = 'uniform-control-label-row';
183
+ const labelEl = document.createElement('label');
184
+ labelEl.className = 'uniform-control-label';
185
+ labelEl.textContent = label;
186
+ const toggleWrapper = document.createElement('label');
187
+ toggleWrapper.className = 'uniform-control-toggle';
188
+ const checkbox = document.createElement('input');
189
+ checkbox.type = 'checkbox';
190
+ checkbox.checked = value;
191
+ const slider = document.createElement('span');
192
+ slider.className = 'uniform-control-toggle-slider';
193
+ checkbox.addEventListener('change', () => {
194
+ const newValue = checkbox.checked;
195
+ this.values[name] = newValue;
196
+ this.onChange(name, newValue);
197
+ });
198
+ toggleWrapper.appendChild(checkbox);
199
+ toggleWrapper.appendChild(slider);
200
+ labelRow.appendChild(labelEl);
201
+ labelRow.appendChild(toggleWrapper);
202
+ wrapper.appendChild(labelRow);
203
+ return {
204
+ element: wrapper,
205
+ update: (v) => { checkbox.checked = v; },
206
+ };
207
+ }
208
+ // ===========================================================================
209
+ // Vec2 XY Pad
210
+ // ===========================================================================
211
+ createVec2Pad(name, def) {
212
+ const value = this.values[name];
213
+ const min = def.min ?? [0, 0];
214
+ const max = def.max ?? [1, 1];
215
+ const label = def.label ?? name;
216
+ const wrapper = document.createElement('div');
217
+ wrapper.className = 'uniform-control uniform-control-vec2';
218
+ const labelRow = document.createElement('div');
219
+ labelRow.className = 'uniform-control-label-row';
220
+ const labelEl = document.createElement('label');
221
+ labelEl.className = 'uniform-control-label';
222
+ labelEl.textContent = label;
223
+ const valueDisplay = document.createElement('span');
224
+ valueDisplay.className = 'uniform-control-value';
225
+ valueDisplay.textContent = this.formatVec2(value);
226
+ labelRow.appendChild(labelEl);
227
+ labelRow.appendChild(valueDisplay);
228
+ const padContainer = document.createElement('div');
229
+ padContainer.className = 'uniform-control-xy-pad';
230
+ const handle = document.createElement('div');
231
+ handle.className = 'uniform-control-xy-handle';
232
+ padContainer.appendChild(handle);
233
+ const positionHandle = (v) => {
234
+ const xPercent = ((v[0] - min[0]) / (max[0] - min[0])) * 100;
235
+ const yPercent = (1 - (v[1] - min[1]) / (max[1] - min[1])) * 100;
236
+ handle.style.left = `${xPercent}%`;
237
+ handle.style.top = `${yPercent}%`;
238
+ };
239
+ positionHandle(value);
240
+ let isDragging = false;
241
+ const updateFromEvent = (e) => {
242
+ const rect = padContainer.getBoundingClientRect();
243
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
244
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
245
+ let xPercent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
246
+ let yPercent = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
247
+ const newX = min[0] + xPercent * (max[0] - min[0]);
248
+ const newY = min[1] + (1 - yPercent) * (max[1] - min[1]);
249
+ const newValue = [newX, newY];
250
+ this.values[name] = newValue;
251
+ handle.style.left = `${xPercent * 100}%`;
252
+ handle.style.top = `${yPercent * 100}%`;
253
+ valueDisplay.textContent = this.formatVec2(newValue);
254
+ this.onChange(name, newValue);
255
+ };
256
+ const onMouseDown = (e) => { isDragging = true; updateFromEvent(e); e.preventDefault(); };
257
+ const onMouseMove = (e) => { if (isDragging)
258
+ updateFromEvent(e); };
259
+ const onMouseUp = () => { isDragging = false; };
260
+ padContainer.addEventListener('mousedown', onMouseDown);
261
+ document.addEventListener('mousemove', onMouseMove);
262
+ document.addEventListener('mouseup', onMouseUp);
263
+ this.documentListeners.push({ type: 'mousemove', handler: onMouseMove });
264
+ this.documentListeners.push({ type: 'mouseup', handler: onMouseUp });
265
+ const onTouchStart = (e) => { isDragging = true; updateFromEvent(e); e.preventDefault(); };
266
+ const onTouchMove = (e) => { if (isDragging)
267
+ updateFromEvent(e); };
268
+ padContainer.addEventListener('touchstart', onTouchStart);
269
+ document.addEventListener('touchmove', onTouchMove);
270
+ document.addEventListener('touchend', onMouseUp);
271
+ this.documentListeners.push({ type: 'touchmove', handler: onTouchMove });
272
+ this.documentListeners.push({ type: 'touchend', handler: onMouseUp });
273
+ wrapper.appendChild(labelRow);
274
+ wrapper.appendChild(padContainer);
275
+ return {
276
+ element: wrapper,
277
+ update: (v) => {
278
+ const vec = v;
279
+ positionHandle(vec);
280
+ valueDisplay.textContent = this.formatVec2(vec);
281
+ },
282
+ };
283
+ }
284
+ // ===========================================================================
285
+ // Vec3 Color Picker
286
+ // ===========================================================================
287
+ createColorPicker(name, def) {
288
+ const value = this.values[name];
289
+ const label = def.label ?? name;
290
+ const wrapper = document.createElement('div');
291
+ wrapper.className = 'uniform-control uniform-control-color';
292
+ const labelRow = document.createElement('div');
293
+ labelRow.className = 'uniform-control-label-row';
294
+ const labelEl = document.createElement('label');
295
+ labelEl.className = 'uniform-control-label';
296
+ labelEl.textContent = label;
297
+ const valueDisplay = document.createElement('span');
298
+ valueDisplay.className = 'uniform-control-value';
299
+ valueDisplay.textContent = this.rgbToHex(value);
300
+ labelRow.appendChild(labelEl);
301
+ labelRow.appendChild(valueDisplay);
302
+ const colorWrapper = document.createElement('div');
303
+ colorWrapper.className = 'uniform-control-color-wrapper';
304
+ const colorInput = document.createElement('input');
305
+ colorInput.type = 'color';
306
+ colorInput.className = 'uniform-control-color-input';
307
+ colorInput.value = this.rgbToHex(value);
308
+ const swatch = document.createElement('div');
309
+ swatch.className = 'uniform-control-color-swatch';
310
+ swatch.style.backgroundColor = this.rgbToHex(value);
311
+ colorInput.addEventListener('input', () => {
312
+ const newValue = this.hexToRgb(colorInput.value);
313
+ this.values[name] = newValue;
314
+ valueDisplay.textContent = colorInput.value;
315
+ swatch.style.backgroundColor = colorInput.value;
316
+ this.onChange(name, newValue);
317
+ });
318
+ swatch.addEventListener('click', () => colorInput.click());
319
+ colorWrapper.appendChild(swatch);
320
+ colorWrapper.appendChild(colorInput);
321
+ wrapper.appendChild(labelRow);
322
+ wrapper.appendChild(colorWrapper);
323
+ return {
324
+ element: wrapper,
325
+ update: (v) => {
326
+ const hex = this.rgbToHex(v);
327
+ colorInput.value = hex;
328
+ swatch.style.backgroundColor = hex;
329
+ valueDisplay.textContent = hex;
330
+ },
331
+ };
332
+ }
333
+ // ===========================================================================
334
+ // Vec4 Color Picker (with alpha)
335
+ // ===========================================================================
336
+ createColorPicker4(name, def) {
337
+ // For vec4 color, use color picker for RGB + a slider for alpha
338
+ const value = this.values[name];
339
+ const label = def.label ?? name;
340
+ const wrapper = document.createElement('div');
341
+ wrapper.className = 'uniform-control uniform-control-color';
342
+ // Color picker for RGB
343
+ const colorResult = this.createColorPicker(name, {
344
+ type: 'vec3',
345
+ value: [value[0], value[1], value[2]],
346
+ color: true,
347
+ label,
348
+ });
349
+ // Alpha slider
350
+ const alphaStep = def.step?.[3] ?? 0.01;
351
+ const { element: alphaEl, update: alphaUpdate } = this.createSliderRow({
352
+ label: 'Alpha',
353
+ min: def.min?.[3] ?? 0,
354
+ max: def.max?.[3] ?? 1,
355
+ step: alphaStep,
356
+ value: value[3],
357
+ format: (v) => this.formatNumber(v, alphaStep),
358
+ onInput: (v) => {
359
+ const current = this.values[name];
360
+ current[3] = v;
361
+ this.onChange(name, [...current]);
362
+ },
363
+ });
364
+ // Override the color picker's onChange to include alpha
365
+ const origColorInput = colorResult.element.querySelector('.uniform-control-color-input');
366
+ if (origColorInput) {
367
+ // Remove old listener by replacing element
368
+ const newInput = origColorInput.cloneNode(true);
369
+ origColorInput.parentNode.replaceChild(newInput, origColorInput);
370
+ const swatch = colorResult.element.querySelector('.uniform-control-color-swatch');
371
+ const valueDisplay = colorResult.element.querySelector('.uniform-control-value');
372
+ newInput.addEventListener('input', () => {
373
+ const rgb = this.hexToRgb(newInput.value);
374
+ const current = this.values[name];
375
+ current[0] = rgb[0];
376
+ current[1] = rgb[1];
377
+ current[2] = rgb[2];
378
+ if (valueDisplay)
379
+ valueDisplay.textContent = newInput.value;
380
+ if (swatch)
381
+ swatch.style.backgroundColor = newInput.value;
382
+ this.onChange(name, [...current]);
383
+ });
384
+ if (swatch)
385
+ swatch.addEventListener('click', () => newInput.click());
386
+ }
387
+ wrapper.appendChild(colorResult.element.querySelector('.uniform-control-label-row'));
388
+ wrapper.appendChild(colorResult.element.querySelector('.uniform-control-color-wrapper'));
389
+ wrapper.appendChild(alphaEl);
390
+ return {
391
+ element: wrapper,
392
+ update: (v) => {
393
+ const vec = v;
394
+ colorResult.update([vec[0], vec[1], vec[2]]);
395
+ alphaUpdate(vec[3]);
396
+ },
397
+ };
398
+ }
399
+ // ===========================================================================
400
+ // Vec3/Vec4 Component Sliders
401
+ // ===========================================================================
402
+ createVecSliders(name, def, count) {
403
+ const value = this.values[name];
404
+ const label = def.label ?? name;
405
+ const components = count === 3 ? ['X', 'Y', 'Z'] : ['X', 'Y', 'Z', 'W'];
406
+ const wrapper = document.createElement('div');
407
+ wrapper.className = `uniform-control uniform-control-vec${count}`;
408
+ const labelEl = document.createElement('div');
409
+ labelEl.className = 'uniform-control-label';
410
+ labelEl.textContent = label;
411
+ wrapper.appendChild(labelEl);
412
+ const sliderUpdaters = [];
413
+ components.forEach((comp, i) => {
414
+ const step = def.step?.[i] ?? 0.01;
415
+ const { element: row, update: rowUpdate } = this.createSliderRow({
416
+ label: comp,
417
+ min: def.min?.[i] ?? 0,
418
+ max: def.max?.[i] ?? 1,
419
+ step,
420
+ value: value[i],
421
+ format: (v) => this.formatNumber(v, step),
422
+ onInput: (v) => {
423
+ const currentValue = this.values[name];
424
+ currentValue[i] = v;
425
+ this.onChange(name, [...currentValue]);
426
+ },
427
+ });
428
+ // Style the row for vec component layout
429
+ const labelRow = row.querySelector('.uniform-control-label-row');
430
+ if (labelRow) {
431
+ labelRow.classList.add('uniform-control-vec-slider-row');
432
+ const lbl = labelRow.querySelector('.uniform-control-label');
433
+ if (lbl) {
434
+ lbl.classList.add('uniform-control-vec-component');
435
+ }
436
+ const val = labelRow.querySelector('.uniform-control-value');
437
+ if (val) {
438
+ val.classList.add('uniform-control-vec-value');
439
+ }
440
+ }
441
+ const slider = row.querySelector('.uniform-control-slider');
442
+ if (slider) {
443
+ slider.classList.add('uniform-control-vec-slider');
444
+ }
445
+ sliderUpdaters.push(rowUpdate);
446
+ wrapper.appendChild(row);
447
+ });
448
+ return {
449
+ element: wrapper,
450
+ update: (v) => {
451
+ const vec = v;
452
+ sliderUpdaters.forEach((upd, i) => upd(vec[i]));
453
+ },
454
+ };
455
+ }
456
+ // ===========================================================================
457
+ // Utility Methods
458
+ // ===========================================================================
459
+ formatNumber(value, step) {
460
+ const stepStr = String(step);
461
+ const decimalIndex = stepStr.indexOf('.');
462
+ const decimals = decimalIndex === -1 ? 0 : stepStr.length - decimalIndex - 1;
463
+ return value.toFixed(decimals);
464
+ }
465
+ formatVec2(value) {
466
+ return `(${value[0].toFixed(2)}, ${value[1].toFixed(2)})`;
467
+ }
468
+ rgbToHex(rgb) {
469
+ const r = Math.round(rgb[0] * 255);
470
+ const g = Math.round(rgb[1] * 255);
471
+ const b = Math.round(rgb[2] * 255);
472
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
473
+ }
474
+ hexToRgb(hex) {
475
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
476
+ if (!result)
477
+ return [0, 0, 0];
478
+ return [
479
+ parseInt(result[1], 16) / 255,
480
+ parseInt(result[2], 16) / 255,
481
+ parseInt(result[3], 16) / 255,
482
+ ];
483
+ }
484
+ // ===========================================================================
485
+ // Public Methods
486
+ // ===========================================================================
487
+ /**
488
+ * Update a uniform value externally (e.g., from reset).
489
+ */
490
+ setValue(name, value) {
491
+ if (!(name in this.uniforms))
492
+ return;
493
+ this.values[name] = value;
494
+ this.updaters.get(name)?.(value);
495
+ }
496
+ /**
497
+ * Reset all uniforms to their default values.
498
+ */
499
+ resetToDefaults() {
500
+ for (const [name, def] of Object.entries(this.uniforms)) {
501
+ if (isArrayUniform(def) || def.hidden)
502
+ continue;
503
+ this.setValue(name, def.value);
504
+ this.onChange(name, def.value);
505
+ }
506
+ }
507
+ /**
508
+ * Destroy the controls and clean up.
509
+ */
510
+ destroy() {
511
+ for (const { type, handler } of this.documentListeners) {
512
+ document.removeEventListener(type, handler);
513
+ }
514
+ this.documentListeners = [];
515
+ this.container.innerHTML = '';
516
+ this.updaters.clear();
517
+ }
518
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * UniformStore - Simple state manager for custom uniforms
3
+ *
4
+ * Encapsulates uniform values and provides clean get/set interface.
5
+ * Used by the engine to manage uniform state.
6
+ */
7
+ import { UniformDefinitions, UniformDefinition, UniformValue, UniformValues } from '../project/types';
8
+ export declare class UniformStore {
9
+ private definitions;
10
+ private values;
11
+ constructor(definitions: UniformDefinitions);
12
+ /**
13
+ * Initialize all values to their definition defaults.
14
+ */
15
+ private initializeDefaults;
16
+ /**
17
+ * Clone a value to avoid mutation of arrays.
18
+ */
19
+ private cloneValue;
20
+ /**
21
+ * Get the definition for a uniform.
22
+ */
23
+ getDefinition(name: string): UniformDefinition | undefined;
24
+ /**
25
+ * Get all definitions.
26
+ */
27
+ getDefinitions(): UniformDefinitions;
28
+ /**
29
+ * Check if a uniform exists.
30
+ */
31
+ has(name: string): boolean;
32
+ /**
33
+ * Get the current value of a uniform.
34
+ */
35
+ get(name: string): UniformValue | undefined;
36
+ /**
37
+ * Get all current values (returns a shallow copy).
38
+ */
39
+ getAll(): UniformValues;
40
+ /**
41
+ * Set the value of a uniform.
42
+ * Returns true if the value was set, false if the uniform doesn't exist.
43
+ */
44
+ set(name: string, value: UniformValue): boolean;
45
+ /**
46
+ * Set multiple values at once.
47
+ */
48
+ setAll(values: Partial<UniformValues>): void;
49
+ /**
50
+ * Reset a single uniform to its default value.
51
+ */
52
+ reset(name: string): boolean;
53
+ /**
54
+ * Reset all uniforms to their default values.
55
+ */
56
+ resetAll(): void;
57
+ /**
58
+ * Get the default value for a uniform.
59
+ */
60
+ getDefault(name: string): UniformValue | undefined;
61
+ /**
62
+ * Iterate over all uniforms (name, definition, current value).
63
+ */
64
+ entries(): IterableIterator<[string, UniformDefinition, UniformValue]>;
65
+ /**
66
+ * Get the number of uniforms.
67
+ */
68
+ get size(): number;
69
+ /**
70
+ * Check if there are any uniforms.
71
+ */
72
+ get isEmpty(): boolean;
73
+ }
74
+ //# sourceMappingURL=UniformStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UniformStore.d.ts","sourceRoot":"","sources":["../../src/uniforms/UniformStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EAEd,MAAM,kBAAkB,CAAC;AAI1B,qBAAa,YAAY;IACvB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,MAAM,CAAqB;gBAEvB,WAAW,EAAE,kBAAkB;IAK3C;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACH,OAAO,CAAC,UAAU;IAKlB;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAI1D;;OAEG;IACH,cAAc,IAAI,kBAAkB;IAIpC;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI3C;;OAEG;IACH,MAAM,IAAI,aAAa;IAQvB;;;OAGG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO;IAQ/C;;OAEG;IACH,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAQ5C;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAa5B;;OAEG;IACH,QAAQ,IAAI,IAAI;IAIhB;;OAEG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAOlD;;OAEG;IACF,OAAO,IAAI,gBAAgB,CAAC,CAAC,MAAM,EAAE,iBAAiB,EAAE,YAAY,CAAC,CAAC;IAMvE;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;CACF"}