@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,660 @@
|
|
|
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
|
+
|
|
8
|
+
import './uniform-controls.css';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
UniformDefinitions,
|
|
12
|
+
UniformDefinition,
|
|
13
|
+
UniformValue,
|
|
14
|
+
UniformValues,
|
|
15
|
+
FloatUniformDefinition,
|
|
16
|
+
IntUniformDefinition,
|
|
17
|
+
BoolUniformDefinition,
|
|
18
|
+
Vec2UniformDefinition,
|
|
19
|
+
Vec3UniformDefinition,
|
|
20
|
+
Vec4UniformDefinition,
|
|
21
|
+
isArrayUniform,
|
|
22
|
+
} from '../project/types';
|
|
23
|
+
|
|
24
|
+
export interface UniformControlsOptions {
|
|
25
|
+
/** Container element to mount controls into */
|
|
26
|
+
container: HTMLElement;
|
|
27
|
+
/** Uniform definitions from project config */
|
|
28
|
+
uniforms: UniformDefinitions;
|
|
29
|
+
/** Callback when a uniform value changes */
|
|
30
|
+
onChange: (name: string, value: UniformValue) => void;
|
|
31
|
+
/** Initial values (optional, defaults to definition values) */
|
|
32
|
+
initialValues?: UniformValues;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SliderRowResult {
|
|
36
|
+
element: HTMLElement;
|
|
37
|
+
update: (value: number) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class UniformControls {
|
|
41
|
+
private container: HTMLElement;
|
|
42
|
+
private uniforms: UniformDefinitions;
|
|
43
|
+
private onChange: (name: string, value: UniformValue) => void;
|
|
44
|
+
private values: UniformValues = {};
|
|
45
|
+
private updaters: Map<string, (value: UniformValue) => void> = new Map();
|
|
46
|
+
|
|
47
|
+
// Track document-level event listeners for cleanup
|
|
48
|
+
private documentListeners: Array<{ type: string; handler: EventListener }> = [];
|
|
49
|
+
|
|
50
|
+
constructor(opts: UniformControlsOptions) {
|
|
51
|
+
this.container = opts.container;
|
|
52
|
+
this.uniforms = opts.uniforms;
|
|
53
|
+
this.onChange = opts.onChange;
|
|
54
|
+
|
|
55
|
+
// Initialize values
|
|
56
|
+
for (const [name, def] of Object.entries(this.uniforms)) {
|
|
57
|
+
if (isArrayUniform(def) || def.hidden) continue;
|
|
58
|
+
this.values[name] = opts.initialValues?.[name] ?? def.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.render();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Render all uniform controls.
|
|
66
|
+
*/
|
|
67
|
+
private render(): void {
|
|
68
|
+
this.container.innerHTML = '';
|
|
69
|
+
this.container.className = 'uniform-controls';
|
|
70
|
+
|
|
71
|
+
const uniformEntries = Object.entries(this.uniforms);
|
|
72
|
+
|
|
73
|
+
if (uniformEntries.length === 0) {
|
|
74
|
+
const emptyMsg = document.createElement('div');
|
|
75
|
+
emptyMsg.className = 'uniform-controls-empty';
|
|
76
|
+
emptyMsg.textContent = 'No uniforms defined';
|
|
77
|
+
this.container.appendChild(emptyMsg);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Header with reset button
|
|
82
|
+
const header = document.createElement('div');
|
|
83
|
+
header.className = 'uniform-controls-header';
|
|
84
|
+
|
|
85
|
+
const resetButton = document.createElement('button');
|
|
86
|
+
resetButton.className = 'uniform-controls-reset';
|
|
87
|
+
resetButton.textContent = 'Reset';
|
|
88
|
+
resetButton.title = 'Reset all uniforms to defaults';
|
|
89
|
+
resetButton.addEventListener('click', () => this.resetToDefaults());
|
|
90
|
+
|
|
91
|
+
header.appendChild(resetButton);
|
|
92
|
+
this.container.appendChild(header);
|
|
93
|
+
|
|
94
|
+
// Control list
|
|
95
|
+
const controlList = document.createElement('div');
|
|
96
|
+
controlList.className = 'uniform-controls-list';
|
|
97
|
+
|
|
98
|
+
for (const [name, def] of uniformEntries) {
|
|
99
|
+
if (isArrayUniform(def) || def.hidden) continue;
|
|
100
|
+
const result = this.createControl(name, def);
|
|
101
|
+
if (result) {
|
|
102
|
+
this.updaters.set(name, result.update);
|
|
103
|
+
controlList.appendChild(result.element);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.container.appendChild(controlList);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a control element for a uniform.
|
|
112
|
+
*/
|
|
113
|
+
private createControl(name: string, def: UniformDefinition): { element: HTMLElement; update: (v: UniformValue) => void } | null {
|
|
114
|
+
if (isArrayUniform(def) || def.hidden) return null;
|
|
115
|
+
switch (def.type) {
|
|
116
|
+
case 'float':
|
|
117
|
+
return this.createFloatSlider(name, def);
|
|
118
|
+
case 'int':
|
|
119
|
+
return this.createIntSlider(name, def);
|
|
120
|
+
case 'bool':
|
|
121
|
+
return this.createBoolToggle(name, def);
|
|
122
|
+
case 'vec2':
|
|
123
|
+
return this.createVec2Pad(name, def);
|
|
124
|
+
case 'vec3':
|
|
125
|
+
return def.color ? this.createColorPicker(name, def) : this.createVecSliders(name, def, 3);
|
|
126
|
+
case 'vec4':
|
|
127
|
+
return def.color ? this.createColorPicker4(name, def) : this.createVecSliders(name, def, 4);
|
|
128
|
+
default:
|
|
129
|
+
console.warn(`Uniform '${name}': unknown type '${(def as any).type}'`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ===========================================================================
|
|
135
|
+
// Shared Slider Row Helper
|
|
136
|
+
// ===========================================================================
|
|
137
|
+
|
|
138
|
+
private createSliderRow(opts: {
|
|
139
|
+
label: string;
|
|
140
|
+
min: number;
|
|
141
|
+
max: number;
|
|
142
|
+
step: number;
|
|
143
|
+
value: number;
|
|
144
|
+
format: (v: number) => string;
|
|
145
|
+
onInput: (v: number) => void;
|
|
146
|
+
}): SliderRowResult {
|
|
147
|
+
const wrapper = document.createElement('div');
|
|
148
|
+
wrapper.className = 'uniform-control-label-row';
|
|
149
|
+
|
|
150
|
+
const labelEl = document.createElement('label');
|
|
151
|
+
labelEl.className = 'uniform-control-label';
|
|
152
|
+
labelEl.textContent = opts.label;
|
|
153
|
+
|
|
154
|
+
const valueDisplay = document.createElement('span');
|
|
155
|
+
valueDisplay.className = 'uniform-control-value';
|
|
156
|
+
valueDisplay.textContent = opts.format(opts.value);
|
|
157
|
+
|
|
158
|
+
wrapper.appendChild(labelEl);
|
|
159
|
+
wrapper.appendChild(valueDisplay);
|
|
160
|
+
|
|
161
|
+
const slider = document.createElement('input');
|
|
162
|
+
slider.type = 'range';
|
|
163
|
+
slider.className = 'uniform-control-slider';
|
|
164
|
+
slider.min = String(opts.min);
|
|
165
|
+
slider.max = String(opts.max);
|
|
166
|
+
slider.step = String(opts.step);
|
|
167
|
+
slider.value = String(opts.value);
|
|
168
|
+
|
|
169
|
+
slider.addEventListener('input', () => {
|
|
170
|
+
const v = parseFloat(slider.value);
|
|
171
|
+
opts.onInput(v);
|
|
172
|
+
valueDisplay.textContent = opts.format(v);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const container = document.createElement('div');
|
|
176
|
+
container.appendChild(wrapper);
|
|
177
|
+
container.appendChild(slider);
|
|
178
|
+
|
|
179
|
+
const update = (v: number) => {
|
|
180
|
+
slider.value = String(v);
|
|
181
|
+
valueDisplay.textContent = opts.format(v);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return { element: container, update };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ===========================================================================
|
|
188
|
+
// Float Slider
|
|
189
|
+
// ===========================================================================
|
|
190
|
+
|
|
191
|
+
private createFloatSlider(name: string, def: FloatUniformDefinition): { element: HTMLElement; update: (v: UniformValue) => void } {
|
|
192
|
+
const step = def.step ?? 0.01;
|
|
193
|
+
const { element, update: sliderUpdate } = this.createSliderRow({
|
|
194
|
+
label: def.label ?? name,
|
|
195
|
+
min: def.min ?? 0,
|
|
196
|
+
max: def.max ?? 1,
|
|
197
|
+
step,
|
|
198
|
+
value: this.values[name] as number,
|
|
199
|
+
format: (v) => this.formatNumber(v, step),
|
|
200
|
+
onInput: (v) => {
|
|
201
|
+
this.values[name] = v;
|
|
202
|
+
this.onChange(name, v);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const wrapper = document.createElement('div');
|
|
207
|
+
wrapper.className = 'uniform-control uniform-control-float';
|
|
208
|
+
wrapper.appendChild(element);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
element: wrapper,
|
|
212
|
+
update: (v) => sliderUpdate(v as number),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ===========================================================================
|
|
217
|
+
// Int Slider
|
|
218
|
+
// ===========================================================================
|
|
219
|
+
|
|
220
|
+
private createIntSlider(name: string, def: IntUniformDefinition): { element: HTMLElement; update: (v: UniformValue) => void } {
|
|
221
|
+
const { element, update: sliderUpdate } = this.createSliderRow({
|
|
222
|
+
label: def.label ?? name,
|
|
223
|
+
min: def.min ?? 0,
|
|
224
|
+
max: def.max ?? 10,
|
|
225
|
+
step: def.step ?? 1,
|
|
226
|
+
value: this.values[name] as number,
|
|
227
|
+
format: (v) => String(Math.round(v)),
|
|
228
|
+
onInput: (v) => {
|
|
229
|
+
const intVal = Math.round(v);
|
|
230
|
+
this.values[name] = intVal;
|
|
231
|
+
this.onChange(name, intVal);
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const wrapper = document.createElement('div');
|
|
236
|
+
wrapper.className = 'uniform-control uniform-control-int';
|
|
237
|
+
wrapper.appendChild(element);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
element: wrapper,
|
|
241
|
+
update: (v) => sliderUpdate(v as number),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ===========================================================================
|
|
246
|
+
// Bool Toggle
|
|
247
|
+
// ===========================================================================
|
|
248
|
+
|
|
249
|
+
private createBoolToggle(name: string, def: BoolUniformDefinition): { element: HTMLElement; update: (v: UniformValue) => void } {
|
|
250
|
+
const value = this.values[name] as boolean;
|
|
251
|
+
const label = def.label ?? name;
|
|
252
|
+
|
|
253
|
+
const wrapper = document.createElement('div');
|
|
254
|
+
wrapper.className = 'uniform-control uniform-control-bool';
|
|
255
|
+
|
|
256
|
+
const labelRow = document.createElement('div');
|
|
257
|
+
labelRow.className = 'uniform-control-label-row';
|
|
258
|
+
|
|
259
|
+
const labelEl = document.createElement('label');
|
|
260
|
+
labelEl.className = 'uniform-control-label';
|
|
261
|
+
labelEl.textContent = label;
|
|
262
|
+
|
|
263
|
+
const toggleWrapper = document.createElement('label');
|
|
264
|
+
toggleWrapper.className = 'uniform-control-toggle';
|
|
265
|
+
|
|
266
|
+
const checkbox = document.createElement('input');
|
|
267
|
+
checkbox.type = 'checkbox';
|
|
268
|
+
checkbox.checked = value;
|
|
269
|
+
|
|
270
|
+
const slider = document.createElement('span');
|
|
271
|
+
slider.className = 'uniform-control-toggle-slider';
|
|
272
|
+
|
|
273
|
+
checkbox.addEventListener('change', () => {
|
|
274
|
+
const newValue = checkbox.checked;
|
|
275
|
+
this.values[name] = newValue;
|
|
276
|
+
this.onChange(name, newValue);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
toggleWrapper.appendChild(checkbox);
|
|
280
|
+
toggleWrapper.appendChild(slider);
|
|
281
|
+
|
|
282
|
+
labelRow.appendChild(labelEl);
|
|
283
|
+
labelRow.appendChild(toggleWrapper);
|
|
284
|
+
|
|
285
|
+
wrapper.appendChild(labelRow);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
element: wrapper,
|
|
289
|
+
update: (v) => { checkbox.checked = v as boolean; },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ===========================================================================
|
|
294
|
+
// Vec2 XY Pad
|
|
295
|
+
// ===========================================================================
|
|
296
|
+
|
|
297
|
+
private createVec2Pad(name: string, def: Vec2UniformDefinition): { element: HTMLElement; update: (v: UniformValue) => void } {
|
|
298
|
+
const value = this.values[name] as number[];
|
|
299
|
+
const min = def.min ?? [0, 0];
|
|
300
|
+
const max = def.max ?? [1, 1];
|
|
301
|
+
const label = def.label ?? name;
|
|
302
|
+
|
|
303
|
+
const wrapper = document.createElement('div');
|
|
304
|
+
wrapper.className = 'uniform-control uniform-control-vec2';
|
|
305
|
+
|
|
306
|
+
const labelRow = document.createElement('div');
|
|
307
|
+
labelRow.className = 'uniform-control-label-row';
|
|
308
|
+
|
|
309
|
+
const labelEl = document.createElement('label');
|
|
310
|
+
labelEl.className = 'uniform-control-label';
|
|
311
|
+
labelEl.textContent = label;
|
|
312
|
+
|
|
313
|
+
const valueDisplay = document.createElement('span');
|
|
314
|
+
valueDisplay.className = 'uniform-control-value';
|
|
315
|
+
valueDisplay.textContent = this.formatVec2(value);
|
|
316
|
+
|
|
317
|
+
labelRow.appendChild(labelEl);
|
|
318
|
+
labelRow.appendChild(valueDisplay);
|
|
319
|
+
|
|
320
|
+
const padContainer = document.createElement('div');
|
|
321
|
+
padContainer.className = 'uniform-control-xy-pad';
|
|
322
|
+
|
|
323
|
+
const handle = document.createElement('div');
|
|
324
|
+
handle.className = 'uniform-control-xy-handle';
|
|
325
|
+
|
|
326
|
+
padContainer.appendChild(handle);
|
|
327
|
+
|
|
328
|
+
const positionHandle = (v: number[]) => {
|
|
329
|
+
const xPercent = ((v[0] - min[0]) / (max[0] - min[0])) * 100;
|
|
330
|
+
const yPercent = (1 - (v[1] - min[1]) / (max[1] - min[1])) * 100;
|
|
331
|
+
handle.style.left = `${xPercent}%`;
|
|
332
|
+
handle.style.top = `${yPercent}%`;
|
|
333
|
+
};
|
|
334
|
+
positionHandle(value);
|
|
335
|
+
|
|
336
|
+
let isDragging = false;
|
|
337
|
+
|
|
338
|
+
const updateFromEvent = (e: MouseEvent | TouchEvent) => {
|
|
339
|
+
const rect = padContainer.getBoundingClientRect();
|
|
340
|
+
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
341
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
342
|
+
|
|
343
|
+
let xPercent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
344
|
+
let yPercent = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
|
|
345
|
+
|
|
346
|
+
const newX = min[0] + xPercent * (max[0] - min[0]);
|
|
347
|
+
const newY = min[1] + (1 - yPercent) * (max[1] - min[1]);
|
|
348
|
+
|
|
349
|
+
const newValue = [newX, newY];
|
|
350
|
+
this.values[name] = newValue;
|
|
351
|
+
|
|
352
|
+
handle.style.left = `${xPercent * 100}%`;
|
|
353
|
+
handle.style.top = `${yPercent * 100}%`;
|
|
354
|
+
valueDisplay.textContent = this.formatVec2(newValue);
|
|
355
|
+
|
|
356
|
+
this.onChange(name, newValue);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const onMouseDown = (e: MouseEvent) => { isDragging = true; updateFromEvent(e); e.preventDefault(); };
|
|
360
|
+
const onMouseMove = (e: Event) => { if (isDragging) updateFromEvent(e as MouseEvent); };
|
|
361
|
+
const onMouseUp = () => { isDragging = false; };
|
|
362
|
+
|
|
363
|
+
padContainer.addEventListener('mousedown', onMouseDown);
|
|
364
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
365
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
366
|
+
this.documentListeners.push({ type: 'mousemove', handler: onMouseMove });
|
|
367
|
+
this.documentListeners.push({ type: 'mouseup', handler: onMouseUp });
|
|
368
|
+
|
|
369
|
+
const onTouchStart = (e: TouchEvent) => { isDragging = true; updateFromEvent(e); e.preventDefault(); };
|
|
370
|
+
const onTouchMove = (e: TouchEvent) => { if (isDragging) updateFromEvent(e); };
|
|
371
|
+
|
|
372
|
+
padContainer.addEventListener('touchstart', onTouchStart);
|
|
373
|
+
document.addEventListener('touchmove', onTouchMove as EventListener);
|
|
374
|
+
document.addEventListener('touchend', onMouseUp);
|
|
375
|
+
this.documentListeners.push({ type: 'touchmove', handler: onTouchMove as EventListener });
|
|
376
|
+
this.documentListeners.push({ type: 'touchend', handler: onMouseUp });
|
|
377
|
+
|
|
378
|
+
wrapper.appendChild(labelRow);
|
|
379
|
+
wrapper.appendChild(padContainer);
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
element: wrapper,
|
|
383
|
+
update: (v) => {
|
|
384
|
+
const vec = v as number[];
|
|
385
|
+
positionHandle(vec);
|
|
386
|
+
valueDisplay.textContent = this.formatVec2(vec);
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ===========================================================================
|
|
392
|
+
// Vec3 Color Picker
|
|
393
|
+
// ===========================================================================
|
|
394
|
+
|
|
395
|
+
private createColorPicker(name: string, def: Vec3UniformDefinition): { element: HTMLElement; update: (v: UniformValue) => void } {
|
|
396
|
+
const value = this.values[name] as number[];
|
|
397
|
+
const label = def.label ?? name;
|
|
398
|
+
|
|
399
|
+
const wrapper = document.createElement('div');
|
|
400
|
+
wrapper.className = 'uniform-control uniform-control-color';
|
|
401
|
+
|
|
402
|
+
const labelRow = document.createElement('div');
|
|
403
|
+
labelRow.className = 'uniform-control-label-row';
|
|
404
|
+
|
|
405
|
+
const labelEl = document.createElement('label');
|
|
406
|
+
labelEl.className = 'uniform-control-label';
|
|
407
|
+
labelEl.textContent = label;
|
|
408
|
+
|
|
409
|
+
const valueDisplay = document.createElement('span');
|
|
410
|
+
valueDisplay.className = 'uniform-control-value';
|
|
411
|
+
valueDisplay.textContent = this.rgbToHex(value);
|
|
412
|
+
|
|
413
|
+
labelRow.appendChild(labelEl);
|
|
414
|
+
labelRow.appendChild(valueDisplay);
|
|
415
|
+
|
|
416
|
+
const colorWrapper = document.createElement('div');
|
|
417
|
+
colorWrapper.className = 'uniform-control-color-wrapper';
|
|
418
|
+
|
|
419
|
+
const colorInput = document.createElement('input');
|
|
420
|
+
colorInput.type = 'color';
|
|
421
|
+
colorInput.className = 'uniform-control-color-input';
|
|
422
|
+
colorInput.value = this.rgbToHex(value);
|
|
423
|
+
|
|
424
|
+
const swatch = document.createElement('div');
|
|
425
|
+
swatch.className = 'uniform-control-color-swatch';
|
|
426
|
+
swatch.style.backgroundColor = this.rgbToHex(value);
|
|
427
|
+
|
|
428
|
+
colorInput.addEventListener('input', () => {
|
|
429
|
+
const newValue = this.hexToRgb(colorInput.value);
|
|
430
|
+
this.values[name] = newValue;
|
|
431
|
+
valueDisplay.textContent = colorInput.value;
|
|
432
|
+
swatch.style.backgroundColor = colorInput.value;
|
|
433
|
+
this.onChange(name, newValue);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
swatch.addEventListener('click', () => colorInput.click());
|
|
437
|
+
|
|
438
|
+
colorWrapper.appendChild(swatch);
|
|
439
|
+
colorWrapper.appendChild(colorInput);
|
|
440
|
+
|
|
441
|
+
wrapper.appendChild(labelRow);
|
|
442
|
+
wrapper.appendChild(colorWrapper);
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
element: wrapper,
|
|
446
|
+
update: (v) => {
|
|
447
|
+
const hex = this.rgbToHex(v as number[]);
|
|
448
|
+
colorInput.value = hex;
|
|
449
|
+
swatch.style.backgroundColor = hex;
|
|
450
|
+
valueDisplay.textContent = hex;
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ===========================================================================
|
|
456
|
+
// Vec4 Color Picker (with alpha)
|
|
457
|
+
// ===========================================================================
|
|
458
|
+
|
|
459
|
+
private createColorPicker4(name: string, def: Vec4UniformDefinition): { element: HTMLElement; update: (v: UniformValue) => void } {
|
|
460
|
+
// For vec4 color, use color picker for RGB + a slider for alpha
|
|
461
|
+
const value = this.values[name] as number[];
|
|
462
|
+
const label = def.label ?? name;
|
|
463
|
+
|
|
464
|
+
const wrapper = document.createElement('div');
|
|
465
|
+
wrapper.className = 'uniform-control uniform-control-color';
|
|
466
|
+
|
|
467
|
+
// Color picker for RGB
|
|
468
|
+
const colorResult = this.createColorPicker(name, {
|
|
469
|
+
type: 'vec3',
|
|
470
|
+
value: [value[0], value[1], value[2]],
|
|
471
|
+
color: true,
|
|
472
|
+
label,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Alpha slider
|
|
476
|
+
const alphaStep = def.step?.[3] ?? 0.01;
|
|
477
|
+
const { element: alphaEl, update: alphaUpdate } = this.createSliderRow({
|
|
478
|
+
label: 'Alpha',
|
|
479
|
+
min: def.min?.[3] ?? 0,
|
|
480
|
+
max: def.max?.[3] ?? 1,
|
|
481
|
+
step: alphaStep,
|
|
482
|
+
value: value[3],
|
|
483
|
+
format: (v) => this.formatNumber(v, alphaStep),
|
|
484
|
+
onInput: (v) => {
|
|
485
|
+
const current = this.values[name] as number[];
|
|
486
|
+
current[3] = v;
|
|
487
|
+
this.onChange(name, [...current]);
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Override the color picker's onChange to include alpha
|
|
492
|
+
const origColorInput = colorResult.element.querySelector('.uniform-control-color-input') as HTMLInputElement;
|
|
493
|
+
if (origColorInput) {
|
|
494
|
+
// Remove old listener by replacing element
|
|
495
|
+
const newInput = origColorInput.cloneNode(true) as HTMLInputElement;
|
|
496
|
+
origColorInput.parentNode!.replaceChild(newInput, origColorInput);
|
|
497
|
+
const swatch = colorResult.element.querySelector('.uniform-control-color-swatch') as HTMLElement;
|
|
498
|
+
const valueDisplay = colorResult.element.querySelector('.uniform-control-value') as HTMLElement;
|
|
499
|
+
|
|
500
|
+
newInput.addEventListener('input', () => {
|
|
501
|
+
const rgb = this.hexToRgb(newInput.value);
|
|
502
|
+
const current = this.values[name] as number[];
|
|
503
|
+
current[0] = rgb[0]; current[1] = rgb[1]; current[2] = rgb[2];
|
|
504
|
+
if (valueDisplay) valueDisplay.textContent = newInput.value;
|
|
505
|
+
if (swatch) swatch.style.backgroundColor = newInput.value;
|
|
506
|
+
this.onChange(name, [...current]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (swatch) swatch.addEventListener('click', () => newInput.click());
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
wrapper.appendChild(colorResult.element.querySelector('.uniform-control-label-row')!);
|
|
513
|
+
wrapper.appendChild(colorResult.element.querySelector('.uniform-control-color-wrapper')!);
|
|
514
|
+
wrapper.appendChild(alphaEl);
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
element: wrapper,
|
|
518
|
+
update: (v) => {
|
|
519
|
+
const vec = v as number[];
|
|
520
|
+
colorResult.update([vec[0], vec[1], vec[2]]);
|
|
521
|
+
alphaUpdate(vec[3]);
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ===========================================================================
|
|
527
|
+
// Vec3/Vec4 Component Sliders
|
|
528
|
+
// ===========================================================================
|
|
529
|
+
|
|
530
|
+
private createVecSliders(name: string, def: Vec3UniformDefinition | Vec4UniformDefinition, count: 3 | 4): { element: HTMLElement; update: (v: UniformValue) => void } {
|
|
531
|
+
const value = this.values[name] as number[];
|
|
532
|
+
const label = def.label ?? name;
|
|
533
|
+
const components = count === 3 ? ['X', 'Y', 'Z'] : ['X', 'Y', 'Z', 'W'];
|
|
534
|
+
|
|
535
|
+
const wrapper = document.createElement('div');
|
|
536
|
+
wrapper.className = `uniform-control uniform-control-vec${count}`;
|
|
537
|
+
|
|
538
|
+
const labelEl = document.createElement('div');
|
|
539
|
+
labelEl.className = 'uniform-control-label';
|
|
540
|
+
labelEl.textContent = label;
|
|
541
|
+
wrapper.appendChild(labelEl);
|
|
542
|
+
|
|
543
|
+
const sliderUpdaters: Array<(v: number) => void> = [];
|
|
544
|
+
|
|
545
|
+
components.forEach((comp, i) => {
|
|
546
|
+
const step = def.step?.[i] ?? 0.01;
|
|
547
|
+
const { element: row, update: rowUpdate } = this.createSliderRow({
|
|
548
|
+
label: comp,
|
|
549
|
+
min: def.min?.[i] ?? 0,
|
|
550
|
+
max: def.max?.[i] ?? 1,
|
|
551
|
+
step,
|
|
552
|
+
value: value[i],
|
|
553
|
+
format: (v) => this.formatNumber(v, step),
|
|
554
|
+
onInput: (v) => {
|
|
555
|
+
const currentValue = this.values[name] as number[];
|
|
556
|
+
currentValue[i] = v;
|
|
557
|
+
this.onChange(name, [...currentValue]);
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Style the row for vec component layout
|
|
562
|
+
const labelRow = row.querySelector('.uniform-control-label-row');
|
|
563
|
+
if (labelRow) {
|
|
564
|
+
labelRow.classList.add('uniform-control-vec-slider-row');
|
|
565
|
+
const lbl = labelRow.querySelector('.uniform-control-label');
|
|
566
|
+
if (lbl) {
|
|
567
|
+
lbl.classList.add('uniform-control-vec-component');
|
|
568
|
+
}
|
|
569
|
+
const val = labelRow.querySelector('.uniform-control-value');
|
|
570
|
+
if (val) {
|
|
571
|
+
val.classList.add('uniform-control-vec-value');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const slider = row.querySelector('.uniform-control-slider');
|
|
575
|
+
if (slider) {
|
|
576
|
+
slider.classList.add('uniform-control-vec-slider');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
sliderUpdaters.push(rowUpdate);
|
|
580
|
+
wrapper.appendChild(row);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
element: wrapper,
|
|
585
|
+
update: (v) => {
|
|
586
|
+
const vec = v as number[];
|
|
587
|
+
sliderUpdaters.forEach((upd, i) => upd(vec[i]));
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ===========================================================================
|
|
593
|
+
// Utility Methods
|
|
594
|
+
// ===========================================================================
|
|
595
|
+
|
|
596
|
+
private formatNumber(value: number, step: number): string {
|
|
597
|
+
const stepStr = String(step);
|
|
598
|
+
const decimalIndex = stepStr.indexOf('.');
|
|
599
|
+
const decimals = decimalIndex === -1 ? 0 : stepStr.length - decimalIndex - 1;
|
|
600
|
+
return value.toFixed(decimals);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private formatVec2(value: number[]): string {
|
|
604
|
+
return `(${value[0].toFixed(2)}, ${value[1].toFixed(2)})`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private rgbToHex(rgb: number[]): string {
|
|
608
|
+
const r = Math.round(rgb[0] * 255);
|
|
609
|
+
const g = Math.round(rgb[1] * 255);
|
|
610
|
+
const b = Math.round(rgb[2] * 255);
|
|
611
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private hexToRgb(hex: string): number[] {
|
|
615
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
616
|
+
if (!result) return [0, 0, 0];
|
|
617
|
+
return [
|
|
618
|
+
parseInt(result[1], 16) / 255,
|
|
619
|
+
parseInt(result[2], 16) / 255,
|
|
620
|
+
parseInt(result[3], 16) / 255,
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ===========================================================================
|
|
625
|
+
// Public Methods
|
|
626
|
+
// ===========================================================================
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Update a uniform value externally (e.g., from reset).
|
|
630
|
+
*/
|
|
631
|
+
setValue(name: string, value: UniformValue): void {
|
|
632
|
+
if (!(name in this.uniforms)) return;
|
|
633
|
+
this.values[name] = value;
|
|
634
|
+
this.updaters.get(name)?.(value);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Reset all uniforms to their default values.
|
|
639
|
+
*/
|
|
640
|
+
resetToDefaults(): void {
|
|
641
|
+
for (const [name, def] of Object.entries(this.uniforms)) {
|
|
642
|
+
if (isArrayUniform(def) || def.hidden) continue;
|
|
643
|
+
this.setValue(name, def.value);
|
|
644
|
+
this.onChange(name, def.value);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Destroy the controls and clean up.
|
|
650
|
+
*/
|
|
651
|
+
destroy(): void {
|
|
652
|
+
for (const { type, handler } of this.documentListeners) {
|
|
653
|
+
document.removeEventListener(type, handler);
|
|
654
|
+
}
|
|
655
|
+
this.documentListeners = [];
|
|
656
|
+
|
|
657
|
+
this.container.innerHTML = '';
|
|
658
|
+
this.updaters.clear();
|
|
659
|
+
}
|
|
660
|
+
}
|