@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.
- package/README.md +259 -235
- 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 +16 -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,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Layout Styles
|
|
3
|
+
* Shader canvas with adjacent uniforms panel
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* ===== UI Layout ===== */
|
|
7
|
+
.layout-ui {
|
|
8
|
+
width: 100%;
|
|
9
|
+
height: 100%;
|
|
10
|
+
display: flex;
|
|
11
|
+
gap: 24px;
|
|
12
|
+
padding: 60px 80px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Canvas container - takes remaining space */
|
|
16
|
+
.layout-ui .ui-canvas-container {
|
|
17
|
+
position: relative;
|
|
18
|
+
flex: 1;
|
|
19
|
+
max-width: 1200px;
|
|
20
|
+
background: var(--bg-canvas);
|
|
21
|
+
border-radius: 8px;
|
|
22
|
+
box-shadow: var(--shadow-md), var(--shadow-sm);
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* UI Panel - fixed width on right */
|
|
27
|
+
.layout-ui .ui-panel {
|
|
28
|
+
width: 200px;
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
background: var(--bg-secondary);
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
box-shadow: var(--shadow-md), var(--shadow-sm);
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Uniforms container - scrollable, vertically centered content */
|
|
39
|
+
.layout-ui .ui-uniforms-container {
|
|
40
|
+
flex: 1;
|
|
41
|
+
min-height: 0;
|
|
42
|
+
overflow-y: auto;
|
|
43
|
+
overflow-x: hidden;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Empty state for no uniforms */
|
|
50
|
+
.layout-ui .ui-empty-state {
|
|
51
|
+
text-align: center;
|
|
52
|
+
color: var(--text-muted);
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
55
|
+
padding: 20px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Override uniform controls for UI panel */
|
|
59
|
+
.layout-ui .uniform-controls {
|
|
60
|
+
padding: 20px;
|
|
61
|
+
gap: 16px;
|
|
62
|
+
background: transparent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.layout-ui .uniform-controls-header {
|
|
66
|
+
display: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.layout-ui .uniform-controls-list {
|
|
70
|
+
gap: 20px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.layout-ui .uniform-control {
|
|
74
|
+
gap: 8px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.layout-ui .uniform-control-label {
|
|
78
|
+
font-size: 12px;
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
color: var(--text-primary);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.layout-ui .uniform-control-value {
|
|
84
|
+
font-size: 11px;
|
|
85
|
+
padding: 3px 8px;
|
|
86
|
+
min-width: 44px;
|
|
87
|
+
color: var(--text-muted);
|
|
88
|
+
background: var(--bg-tertiary);
|
|
89
|
+
border-radius: 4px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Slider styling */
|
|
93
|
+
.layout-ui .uniform-control-slider {
|
|
94
|
+
height: 4px;
|
|
95
|
+
background: var(--border-primary);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.layout-ui .uniform-control-slider::-webkit-slider-runnable-track {
|
|
99
|
+
height: 4px;
|
|
100
|
+
background: var(--border-primary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.layout-ui .uniform-control-slider::-webkit-slider-thumb {
|
|
104
|
+
width: 14px;
|
|
105
|
+
height: 14px;
|
|
106
|
+
margin-top: -5px;
|
|
107
|
+
background: var(--accent-primary);
|
|
108
|
+
border: 2px solid var(--bg-secondary);
|
|
109
|
+
box-shadow: var(--shadow-sm);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.layout-ui .uniform-control-slider::-moz-range-track {
|
|
113
|
+
height: 4px;
|
|
114
|
+
background: var(--border-primary);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.layout-ui .uniform-control-slider::-moz-range-thumb {
|
|
118
|
+
width: 14px;
|
|
119
|
+
height: 14px;
|
|
120
|
+
background: var(--accent-primary);
|
|
121
|
+
border: 2px solid var(--bg-secondary);
|
|
122
|
+
box-shadow: var(--shadow-sm);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* XY pad styling */
|
|
126
|
+
.layout-ui .uniform-control-xy-pad {
|
|
127
|
+
height: 100px;
|
|
128
|
+
background: var(--bg-tertiary);
|
|
129
|
+
border: 1px solid var(--border-primary);
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.layout-ui .uniform-control-xy-handle {
|
|
134
|
+
width: 12px;
|
|
135
|
+
height: 12px;
|
|
136
|
+
background: var(--accent-primary);
|
|
137
|
+
border: 2px solid var(--bg-secondary);
|
|
138
|
+
box-shadow: var(--shadow-sm);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Color swatch */
|
|
142
|
+
.layout-ui .uniform-control-color-swatch {
|
|
143
|
+
height: 28px;
|
|
144
|
+
border-radius: 4px;
|
|
145
|
+
border: 1px solid var(--border-primary);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Toggle switch */
|
|
149
|
+
.layout-ui .uniform-control-toggle {
|
|
150
|
+
width: 36px;
|
|
151
|
+
height: 20px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.layout-ui .uniform-control-toggle-slider {
|
|
155
|
+
background: var(--border-primary);
|
|
156
|
+
border-radius: 20px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.layout-ui .uniform-control-toggle-slider::before {
|
|
160
|
+
width: 14px;
|
|
161
|
+
height: 14px;
|
|
162
|
+
left: 3px;
|
|
163
|
+
bottom: 3px;
|
|
164
|
+
background: var(--bg-primary);
|
|
165
|
+
box-shadow: var(--shadow-sm);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.layout-ui .uniform-control-toggle input:checked + .uniform-control-toggle-slider {
|
|
169
|
+
background: var(--accent-primary);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.layout-ui .uniform-control-toggle input:checked + .uniform-control-toggle-slider::before {
|
|
173
|
+
transform: translateX(16px);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Vec component labels */
|
|
177
|
+
.layout-ui .uniform-control-vec-component {
|
|
178
|
+
font-size: 10px;
|
|
179
|
+
width: 14px;
|
|
180
|
+
color: var(--text-muted);
|
|
181
|
+
font-weight: 500;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.layout-ui .uniform-control-vec-value {
|
|
185
|
+
font-size: 10px;
|
|
186
|
+
min-width: 36px;
|
|
187
|
+
padding: 2px 6px;
|
|
188
|
+
color: var(--text-muted);
|
|
189
|
+
background: var(--bg-tertiary);
|
|
190
|
+
border-radius: 4px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Scrollbar styling */
|
|
194
|
+
.layout-ui .ui-uniforms-container::-webkit-scrollbar {
|
|
195
|
+
width: 6px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.layout-ui .ui-uniforms-container::-webkit-scrollbar-track {
|
|
199
|
+
background: transparent;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.layout-ui .ui-uniforms-container::-webkit-scrollbar-thumb {
|
|
203
|
+
background: var(--border-primary);
|
|
204
|
+
border-radius: 3px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.layout-ui .ui-uniforms-container::-webkit-scrollbar-thumb:hover {
|
|
208
|
+
background: var(--text-muted);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ===== Playback Controls ===== */
|
|
212
|
+
.layout-ui .ui-playback-container {
|
|
213
|
+
display: flex;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
gap: 12px;
|
|
216
|
+
padding: 16px 20px;
|
|
217
|
+
border-top: 1px solid var(--border-primary);
|
|
218
|
+
background: var(--bg-tertiary);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.layout-ui .ui-control-button {
|
|
222
|
+
width: 40px;
|
|
223
|
+
height: 40px;
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
background: var(--button-bg);
|
|
228
|
+
border: 1px solid var(--border-primary);
|
|
229
|
+
border-radius: 8px;
|
|
230
|
+
color: var(--text-primary);
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
transition: all 0.15s ease;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.layout-ui .ui-control-button:hover {
|
|
236
|
+
background: var(--button-bg-hover);
|
|
237
|
+
color: var(--text-primary);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.layout-ui .ui-control-button:active {
|
|
241
|
+
transform: scale(0.95);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.layout-ui .ui-control-button svg {
|
|
245
|
+
width: 18px;
|
|
246
|
+
height: 18px;
|
|
247
|
+
fill: currentColor;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ===== Responsive adjustments ===== */
|
|
251
|
+
|
|
252
|
+
@media (max-width: 1200px) {
|
|
253
|
+
.layout-ui {
|
|
254
|
+
padding: 40px 60px;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@media (max-width: 1000px) {
|
|
259
|
+
.layout-ui {
|
|
260
|
+
padding: 30px 40px;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@media (max-width: 800px) {
|
|
265
|
+
.layout-ui {
|
|
266
|
+
padding: 24px 30px;
|
|
267
|
+
gap: 16px;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Stack vertically on small screens */
|
|
272
|
+
@media (max-width: 600px) {
|
|
273
|
+
.layout-ui {
|
|
274
|
+
flex-direction: column;
|
|
275
|
+
padding: 16px;
|
|
276
|
+
gap: 16px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.layout-ui .ui-canvas-container {
|
|
280
|
+
flex: none;
|
|
281
|
+
/* Maintain aspect ratio when stacked */
|
|
282
|
+
aspect-ratio: 16 / 9;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.layout-ui .ui-panel {
|
|
286
|
+
width: 100%;
|
|
287
|
+
max-height: 300px;
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -16,8 +16,9 @@ import './styles/base.css';
|
|
|
16
16
|
|
|
17
17
|
import { App } from './app/App';
|
|
18
18
|
import { createLayout } from './layouts';
|
|
19
|
+
import { UILayout } from './layouts/UILayout';
|
|
19
20
|
import { loadDemoProject, DEMO_NAME } from './project/generatedLoader';
|
|
20
|
-
import { PassName } from './project/types';
|
|
21
|
+
import { PassName, UniformValue } from './project/types';
|
|
21
22
|
import { RecompileResult } from './layouts/types';
|
|
22
23
|
|
|
23
24
|
async function main() {
|
|
@@ -46,11 +47,16 @@ async function main() {
|
|
|
46
47
|
// Get canvas container from layout
|
|
47
48
|
const canvasContainer = layout.getCanvasContainer();
|
|
48
49
|
|
|
50
|
+
// Check if this is a UILayout (which has its own uniforms panel and playback controls)
|
|
51
|
+
const isUILayout = layout instanceof UILayout;
|
|
52
|
+
|
|
49
53
|
// Create app
|
|
50
54
|
const app = new App({
|
|
51
55
|
container: canvasContainer,
|
|
52
56
|
project,
|
|
53
57
|
pixelRatio: window.devicePixelRatio,
|
|
58
|
+
skipUniformsPanel: isUILayout,
|
|
59
|
+
skipPlaybackControls: isUILayout,
|
|
54
60
|
});
|
|
55
61
|
|
|
56
62
|
// Wire up recompile handler for layouts that support it (split, tabbed)
|
|
@@ -79,6 +85,38 @@ async function main() {
|
|
|
79
85
|
});
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
// Wire up uniform change handler for layouts that support it (split, tabbed)
|
|
89
|
+
if (layout.setUniformHandler) {
|
|
90
|
+
layout.setUniformHandler((name: string, value: UniformValue) => {
|
|
91
|
+
const engine = app.getEngine();
|
|
92
|
+
if (engine) {
|
|
93
|
+
engine.setUniformValue(name, value);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Wire up UILayout callbacks (playback controls and uniforms)
|
|
99
|
+
if (layout instanceof UILayout) {
|
|
100
|
+
layout.setPlaybackCallbacks({
|
|
101
|
+
onPlayPause: () => {
|
|
102
|
+
app.togglePlayPause();
|
|
103
|
+
layout.setPaused(app.getPaused());
|
|
104
|
+
},
|
|
105
|
+
onReset: () => app.reset(),
|
|
106
|
+
onScreenshot: () => app.screenshot(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
layout.setUniformCallback((name: string, value: UniformValue) => {
|
|
110
|
+
const engine = app.getEngine();
|
|
111
|
+
if (engine) {
|
|
112
|
+
engine.setUniformValue(name, value);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Sync initial paused state (from project.startPaused)
|
|
117
|
+
layout.setPaused(app.getPaused());
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
// Only start animation loop if there are no compilation errors
|
|
83
121
|
// If there are errors, the error overlay is already shown by App constructor
|
|
84
122
|
if (!app.hasErrors()) {
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for config loading.
|
|
3
|
+
* Used by both the Node/CLI loader (loadProject.ts) and
|
|
4
|
+
* the browser/Vite loader (loaderHelper.ts).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PassName, ChannelValue, ChannelJSONObject } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type guard for PassName.
|
|
11
|
+
*/
|
|
12
|
+
export function isPassName(s: string): s is PassName {
|
|
13
|
+
return s === 'Image' || s === 'BufferA' || s === 'BufferB' || s === 'BufferC' || s === 'BufferD';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get default source file name for a pass.
|
|
18
|
+
*/
|
|
19
|
+
export function defaultSourceForPass(name: PassName): string {
|
|
20
|
+
switch (name) {
|
|
21
|
+
case 'Image':
|
|
22
|
+
return 'image.glsl';
|
|
23
|
+
case 'BufferA':
|
|
24
|
+
return 'bufferA.glsl';
|
|
25
|
+
case 'BufferB':
|
|
26
|
+
return 'bufferB.glsl';
|
|
27
|
+
case 'BufferC':
|
|
28
|
+
return 'bufferC.glsl';
|
|
29
|
+
case 'BufferD':
|
|
30
|
+
return 'bufferD.glsl';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a channel value (string shorthand or object) into normalized ChannelJSONObject.
|
|
36
|
+
*
|
|
37
|
+
* String shortcuts:
|
|
38
|
+
* - "BufferA", "BufferB", etc. → buffer reference
|
|
39
|
+
* - "keyboard" → keyboard input
|
|
40
|
+
* - "audio" → microphone audio input
|
|
41
|
+
* - "webcam" → webcam video input
|
|
42
|
+
* - "photo.jpg" (with extension) → texture file
|
|
43
|
+
*/
|
|
44
|
+
export function parseChannelValue(value: ChannelValue): ChannelJSONObject | null {
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
if (isPassName(value)) {
|
|
47
|
+
return { buffer: value };
|
|
48
|
+
}
|
|
49
|
+
if (value === 'keyboard') {
|
|
50
|
+
return { keyboard: true };
|
|
51
|
+
}
|
|
52
|
+
if (value === 'audio') {
|
|
53
|
+
return { audio: true };
|
|
54
|
+
}
|
|
55
|
+
if (value === 'webcam') {
|
|
56
|
+
return { webcam: true };
|
|
57
|
+
}
|
|
58
|
+
// Assume texture (file path)
|
|
59
|
+
return { texture: value };
|
|
60
|
+
}
|
|
61
|
+
// Already an object
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The ordered list of pass names for iteration. */
|
|
66
|
+
export const PASS_ORDER = ['Image', 'BufferA', 'BufferB', 'BufferC', 'BufferD'] as const;
|
|
67
|
+
|
|
68
|
+
/** The four buffer pass names (excludes Image). */
|
|
69
|
+
export const BUFFER_PASS_NAMES: PassName[] = ['BufferA', 'BufferB', 'BufferC', 'BufferD'];
|
|
70
|
+
|
|
71
|
+
/** The four channel keys. */
|
|
72
|
+
export const CHANNEL_KEYS = ['iChannel0', 'iChannel1', 'iChannel2', 'iChannel3'] as const;
|
|
73
|
+
|
|
74
|
+
/** Default layout for projects. */
|
|
75
|
+
export const DEFAULT_LAYOUT = 'default' as const;
|
|
76
|
+
|
|
77
|
+
/** Default controls setting. */
|
|
78
|
+
export const DEFAULT_CONTROLS = false;
|
|
79
|
+
|
|
80
|
+
/** Default theme. */
|
|
81
|
+
export const DEFAULT_THEME = 'light' as const;
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Config Validation
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
/** Built-in uniform names that cannot be used as custom uniform names. */
|
|
88
|
+
const RESERVED_UNIFORM_NAMES = new Set([
|
|
89
|
+
'iResolution', 'iTime', 'iTimeDelta', 'iFrame', 'iMouse',
|
|
90
|
+
'iDate', 'iFrameRate', 'iChannelResolution',
|
|
91
|
+
'iChannel0', 'iChannel1', 'iChannel2', 'iChannel3',
|
|
92
|
+
'iTouchCount', 'iTouch0', 'iTouch1', 'iTouch2',
|
|
93
|
+
'iPinch', 'iPinchDelta', 'iPinchCenter',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const GLSL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
97
|
+
|
|
98
|
+
const GLSL_RESERVED_WORDS = new Set([
|
|
99
|
+
'attribute', 'const', 'uniform', 'varying', 'break', 'continue',
|
|
100
|
+
'do', 'for', 'while', 'if', 'else', 'in', 'out', 'inout',
|
|
101
|
+
'float', 'int', 'void', 'bool', 'true', 'false',
|
|
102
|
+
'discard', 'return', 'mat2', 'mat3', 'mat4',
|
|
103
|
+
'vec2', 'vec3', 'vec4', 'ivec2', 'ivec3', 'ivec4',
|
|
104
|
+
'bvec2', 'bvec3', 'bvec4', 'sampler2D', 'samplerCube',
|
|
105
|
+
'struct', 'precision', 'highp', 'mediump', 'lowp',
|
|
106
|
+
'layout', 'centroid', 'flat', 'smooth', 'noperspective',
|
|
107
|
+
'switch', 'case', 'default',
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
/** Check if a string is a valid GLSL identifier (not a reserved word). */
|
|
111
|
+
export function isValidGLSLIdentifier(name: string): boolean {
|
|
112
|
+
return GLSL_IDENTIFIER_RE.test(name) && !GLSL_RESERVED_WORDS.has(name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const VALID_LAYOUTS = new Set(['fullscreen', 'default', 'split', 'tabbed']);
|
|
116
|
+
const VALID_THEMES = new Set(['light', 'dark', 'system']);
|
|
117
|
+
|
|
118
|
+
const VALID_TOP_LEVEL_KEYS = new Set([
|
|
119
|
+
'mode', 'title', 'author', 'description', 'layout', 'theme', 'controls',
|
|
120
|
+
'common', 'startPaused', 'pixelRatio', 'uniforms', 'buffers', 'textures',
|
|
121
|
+
'Image', 'BufferA', 'BufferB', 'BufferC', 'BufferD',
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const VALID_PASS_KEYS = new Set(['source', 'iChannel0', 'iChannel1', 'iChannel2', 'iChannel3']);
|
|
125
|
+
|
|
126
|
+
const SPECIAL_TEXTURE_SOURCES = new Set(['keyboard', 'audio', 'webcam']);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Validate a project config and throw on errors.
|
|
130
|
+
* Logs warnings for non-fatal issues.
|
|
131
|
+
*/
|
|
132
|
+
export function validateConfig(config: Record<string, any>, root: string): void {
|
|
133
|
+
const warnings: string[] = [];
|
|
134
|
+
const errors: string[] = [];
|
|
135
|
+
|
|
136
|
+
// Warn on unknown top-level keys
|
|
137
|
+
for (const key of Object.keys(config)) {
|
|
138
|
+
if (!VALID_TOP_LEVEL_KEYS.has(key)) {
|
|
139
|
+
warnings.push(`Unknown config key '${key}'`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Validate layout
|
|
144
|
+
if (config.layout !== undefined && !VALID_LAYOUTS.has(config.layout)) {
|
|
145
|
+
errors.push(`Invalid layout '${config.layout}'. Expected one of: ${[...VALID_LAYOUTS].join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate theme
|
|
149
|
+
if (config.theme !== undefined && !VALID_THEMES.has(config.theme)) {
|
|
150
|
+
errors.push(`Invalid theme '${config.theme}'. Expected one of: ${[...VALID_THEMES].join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate uniform names
|
|
154
|
+
if (config.uniforms && typeof config.uniforms === 'object') {
|
|
155
|
+
for (const name of Object.keys(config.uniforms)) {
|
|
156
|
+
if (RESERVED_UNIFORM_NAMES.has(name)) {
|
|
157
|
+
errors.push(`Uniform name '${name}' is reserved (built-in uniform)`);
|
|
158
|
+
}
|
|
159
|
+
if (!isValidGLSLIdentifier(name)) {
|
|
160
|
+
errors.push(`Uniform name '${name}' is not a valid GLSL identifier`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Validate buffer names
|
|
166
|
+
const bufferNames = new Set<string>();
|
|
167
|
+
if (config.buffers) {
|
|
168
|
+
const names = Array.isArray(config.buffers) ? config.buffers : Object.keys(config.buffers);
|
|
169
|
+
for (const name of names) {
|
|
170
|
+
if (typeof name !== 'string') {
|
|
171
|
+
errors.push(`Buffer name must be a string, got ${typeof name}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!isValidGLSLIdentifier(name)) {
|
|
175
|
+
errors.push(`Buffer name '${name}' is not a valid GLSL identifier`);
|
|
176
|
+
}
|
|
177
|
+
bufferNames.add(name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Validate texture names and sources
|
|
182
|
+
if (config.textures && typeof config.textures === 'object') {
|
|
183
|
+
for (const [name, value] of Object.entries(config.textures)) {
|
|
184
|
+
if (!isValidGLSLIdentifier(name)) {
|
|
185
|
+
errors.push(`Texture name '${name}' is not a valid GLSL identifier`);
|
|
186
|
+
}
|
|
187
|
+
if (bufferNames.has(name)) {
|
|
188
|
+
errors.push(`Texture name '${name}' collides with a buffer name`);
|
|
189
|
+
}
|
|
190
|
+
if (typeof value !== 'string') {
|
|
191
|
+
errors.push(`Texture source for '${name}' must be a string`);
|
|
192
|
+
} else if (!SPECIAL_TEXTURE_SOURCES.has(value) && !/\.\w+$/.test(value) && !isValidGLSLIdentifier(value)) {
|
|
193
|
+
errors.push(`Invalid texture source '${value}' for '${name}'. Expected a file path with extension, a script texture name, or one of: ${[...SPECIAL_TEXTURE_SOURCES].join(', ')}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate pass configs
|
|
199
|
+
for (const passName of PASS_ORDER) {
|
|
200
|
+
const passConfig = config[passName];
|
|
201
|
+
if (!passConfig || typeof passConfig !== 'object') continue;
|
|
202
|
+
|
|
203
|
+
for (const key of Object.keys(passConfig)) {
|
|
204
|
+
if (!VALID_PASS_KEYS.has(key)) {
|
|
205
|
+
warnings.push(`Unknown key '${key}' in pass '${passName}'`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check channel buffer references
|
|
210
|
+
for (const chKey of CHANNEL_KEYS) {
|
|
211
|
+
const val = passConfig[chKey];
|
|
212
|
+
if (!val) continue;
|
|
213
|
+
if (typeof val === 'string' && isPassName(val) && val !== 'Image' && !config[val]) {
|
|
214
|
+
warnings.push(`${passName}.${chKey} references '${val}' but no ${val} pass is configured`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const w of warnings) console.warn(`[config] ${root}: ${w}`);
|
|
220
|
+
if (errors.length > 0) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Config validation failed for '${root}':\n${errors.map(e => ` - ${e}`).join('\n')}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
// Auto-generated - DO NOT EDIT
|
|
2
2
|
import { loadDemo } from './loaderHelper';
|
|
3
|
-
import {
|
|
3
|
+
import { ProjectConfig } from './types';
|
|
4
4
|
|
|
5
|
-
export const DEMO_NAME = '
|
|
5
|
+
export const DEMO_NAME = 'demos/examples/ubo-dynamic';
|
|
6
|
+
|
|
7
|
+
// Transform glob keys from "/path" to "./path" format
|
|
8
|
+
function transformKeys<T>(files: Record<string, T>): Record<string, T> {
|
|
9
|
+
const result: Record<string, T> = {};
|
|
10
|
+
for (const [key, value] of Object.entries(files)) {
|
|
11
|
+
// Convert /demos/... to ./demos/...
|
|
12
|
+
const newKey = key.startsWith('/') ? '.' + key : key;
|
|
13
|
+
result[newKey] = value;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
export async function loadDemoProject() {
|
|
8
|
-
|
|
19
|
+
// Use root-relative paths for globs (works from any file location)
|
|
20
|
+
const glslFilesRaw = import.meta.glob<string>('/demos/examples/ubo-dynamic/**/*.glsl', {
|
|
9
21
|
query: '?raw',
|
|
10
22
|
import: 'default',
|
|
11
23
|
});
|
|
12
24
|
|
|
13
|
-
const
|
|
25
|
+
const jsonFilesRaw = import.meta.glob<ProjectConfig>('/demos/examples/ubo-dynamic/**/*.json', {
|
|
14
26
|
import: 'default',
|
|
15
27
|
});
|
|
16
28
|
|
|
17
|
-
const
|
|
29
|
+
const imageFilesRaw = import.meta.glob<string>('/demos/examples/ubo-dynamic/**/*.{jpg,jpeg,png,gif,webp,bmp}', {
|
|
18
30
|
query: '?url',
|
|
19
31
|
import: 'default',
|
|
20
32
|
});
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
// Script files (setup.js / script.js hooks for JS-driven computation)
|
|
35
|
+
const scriptFilesRaw = import.meta.glob<any>('/demos/examples/ubo-dynamic/**/script.js');
|
|
36
|
+
|
|
37
|
+
// Transform keys to ./ format that loadDemo expects
|
|
38
|
+
const glslFiles = transformKeys(glslFilesRaw);
|
|
39
|
+
const jsonFiles = transformKeys(jsonFilesRaw);
|
|
40
|
+
const imageFiles = transformKeys(imageFilesRaw);
|
|
41
|
+
const scriptFiles = transformKeys(scriptFilesRaw);
|
|
42
|
+
|
|
43
|
+
return loadDemo(DEMO_NAME, glslFiles, jsonFiles, imageFiles, scriptFiles);
|
|
23
44
|
}
|