@stevejtrettel/shader-sandbox 0.1.3 → 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.
- package/README.md +220 -23
- package/bin/cli.js +106 -14
- package/dist-lib/app/App.d.ts +143 -15
- package/dist-lib/app/App.d.ts.map +1 -1
- package/dist-lib/app/App.js +1343 -108
- package/dist-lib/app/app.css +349 -24
- package/dist-lib/app/types.d.ts +48 -5
- package/dist-lib/app/types.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.d.ts +2 -2
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.js +1 -1
- package/dist-lib/editor/editor-panel.css +55 -32
- package/dist-lib/editor/prism-editor.css +16 -16
- package/dist-lib/embed.js +1 -1
- package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
- package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShaderEngine.js +1523 -0
- package/dist-lib/engine/glHelpers.d.ts +24 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -1
- package/dist-lib/engine/glHelpers.js +88 -0
- package/dist-lib/engine/std140.d.ts +47 -0
- package/dist-lib/engine/std140.d.ts.map +1 -0
- package/dist-lib/engine/std140.js +119 -0
- package/dist-lib/engine/types.d.ts +55 -5
- package/dist-lib/engine/types.d.ts.map +1 -1
- package/dist-lib/engine/types.js +1 -1
- package/dist-lib/index.d.ts +4 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
- package/dist-lib/layouts/SplitLayout.js +3 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
- package/dist-lib/layouts/UILayout.d.ts +55 -0
- package/dist-lib/layouts/UILayout.d.ts.map +1 -0
- package/dist-lib/layouts/UILayout.js +147 -0
- package/dist-lib/layouts/default.css +2 -2
- package/dist-lib/layouts/index.d.ts +11 -1
- package/dist-lib/layouts/index.d.ts.map +1 -1
- package/dist-lib/layouts/index.js +17 -1
- package/dist-lib/layouts/split.css +33 -31
- package/dist-lib/layouts/tabbed.css +127 -74
- package/dist-lib/layouts/types.d.ts +14 -3
- package/dist-lib/layouts/types.d.ts.map +1 -1
- package/dist-lib/main.js +33 -0
- package/dist-lib/project/configHelpers.d.ts +45 -0
- package/dist-lib/project/configHelpers.d.ts.map +1 -0
- package/dist-lib/project/configHelpers.js +196 -0
- package/dist-lib/project/generatedLoader.d.ts +2 -2
- package/dist-lib/project/generatedLoader.d.ts.map +1 -1
- package/dist-lib/project/generatedLoader.js +23 -5
- package/dist-lib/project/loadProject.d.ts +6 -6
- package/dist-lib/project/loadProject.d.ts.map +1 -1
- package/dist-lib/project/loadProject.js +396 -144
- package/dist-lib/project/loaderHelper.d.ts +4 -4
- package/dist-lib/project/loaderHelper.d.ts.map +1 -1
- package/dist-lib/project/loaderHelper.js +278 -116
- package/dist-lib/project/types.d.ts +292 -13
- package/dist-lib/project/types.d.ts.map +1 -1
- package/dist-lib/project/types.js +13 -1
- package/dist-lib/styles/base.css +5 -1
- package/dist-lib/uniforms/UniformControls.d.ts +60 -0
- package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformControls.js +518 -0
- package/dist-lib/uniforms/UniformStore.d.ts +74 -0
- package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformStore.js +145 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformsPanel.js +124 -0
- package/dist-lib/uniforms/index.d.ts +11 -0
- package/dist-lib/uniforms/index.d.ts.map +1 -0
- package/dist-lib/uniforms/index.js +8 -0
- package/package.json +1 -1
- package/src/app/App.ts +1469 -126
- package/src/app/app.css +349 -24
- package/src/app/types.ts +53 -5
- package/src/editor/EditorPanel.ts +5 -5
- package/src/editor/editor-panel.css +55 -32
- package/src/editor/prism-editor.css +16 -16
- package/src/embed.ts +1 -1
- package/src/engine/ShaderEngine.ts +1934 -0
- package/src/engine/glHelpers.ts +117 -0
- package/src/engine/std140.ts +136 -0
- package/src/engine/types.ts +69 -5
- package/src/index.ts +4 -3
- package/src/layouts/SplitLayout.ts +8 -3
- package/src/layouts/TabbedLayout.ts +3 -3
- package/src/layouts/UILayout.ts +185 -0
- package/src/layouts/default.css +2 -2
- package/src/layouts/index.ts +20 -1
- package/src/layouts/split.css +33 -31
- package/src/layouts/tabbed.css +127 -74
- package/src/layouts/types.ts +19 -3
- package/src/layouts/ui.css +289 -0
- package/src/main.ts +39 -1
- package/src/project/configHelpers.ts +225 -0
- package/src/project/generatedLoader.ts +27 -6
- package/src/project/loadProject.ts +459 -173
- package/src/project/loaderHelper.ts +377 -130
- package/src/project/types.ts +360 -14
- package/src/styles/base.css +5 -1
- package/src/styles/theme.css +292 -0
- package/src/uniforms/UniformControls.ts +660 -0
- package/src/uniforms/UniformStore.ts +166 -0
- package/src/uniforms/UniformsPanel.ts +163 -0
- package/src/uniforms/index.ts +13 -0
- package/src/uniforms/uniform-controls.css +342 -0
- package/src/uniforms/uniforms-panel.css +277 -0
- package/templates/shaders/example-buffer/config.json +1 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
- package/dist-lib/engine/ShadertoyEngine.js +0 -704
- 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"}
|