@stevejtrettel/shader-sandbox 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +259 -235
  2. package/bin/cli.js +106 -14
  3. package/dist-lib/app/App.d.ts +143 -15
  4. package/dist-lib/app/App.d.ts.map +1 -1
  5. package/dist-lib/app/App.js +1343 -108
  6. package/dist-lib/app/app.css +349 -24
  7. package/dist-lib/app/types.d.ts +48 -5
  8. package/dist-lib/app/types.d.ts.map +1 -1
  9. package/dist-lib/editor/EditorPanel.d.ts +2 -2
  10. package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
  11. package/dist-lib/editor/EditorPanel.js +1 -1
  12. package/dist-lib/editor/editor-panel.css +55 -32
  13. package/dist-lib/editor/prism-editor.css +16 -16
  14. package/dist-lib/embed.js +1 -1
  15. package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
  16. package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
  17. package/dist-lib/engine/ShaderEngine.js +1523 -0
  18. package/dist-lib/engine/glHelpers.d.ts +24 -0
  19. package/dist-lib/engine/glHelpers.d.ts.map +1 -1
  20. package/dist-lib/engine/glHelpers.js +88 -0
  21. package/dist-lib/engine/std140.d.ts +47 -0
  22. package/dist-lib/engine/std140.d.ts.map +1 -0
  23. package/dist-lib/engine/std140.js +119 -0
  24. package/dist-lib/engine/types.d.ts +55 -5
  25. package/dist-lib/engine/types.d.ts.map +1 -1
  26. package/dist-lib/engine/types.js +1 -1
  27. package/dist-lib/index.d.ts +4 -3
  28. package/dist-lib/index.d.ts.map +1 -1
  29. package/dist-lib/index.js +2 -1
  30. package/dist-lib/layouts/SplitLayout.d.ts +2 -1
  31. package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
  32. package/dist-lib/layouts/SplitLayout.js +3 -0
  33. package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
  34. package/dist-lib/layouts/UILayout.d.ts +55 -0
  35. package/dist-lib/layouts/UILayout.d.ts.map +1 -0
  36. package/dist-lib/layouts/UILayout.js +147 -0
  37. package/dist-lib/layouts/default.css +2 -2
  38. package/dist-lib/layouts/index.d.ts +11 -1
  39. package/dist-lib/layouts/index.d.ts.map +1 -1
  40. package/dist-lib/layouts/index.js +17 -1
  41. package/dist-lib/layouts/split.css +33 -31
  42. package/dist-lib/layouts/tabbed.css +127 -74
  43. package/dist-lib/layouts/types.d.ts +14 -3
  44. package/dist-lib/layouts/types.d.ts.map +1 -1
  45. package/dist-lib/main.js +33 -0
  46. package/dist-lib/project/configHelpers.d.ts +45 -0
  47. package/dist-lib/project/configHelpers.d.ts.map +1 -0
  48. package/dist-lib/project/configHelpers.js +196 -0
  49. package/dist-lib/project/generatedLoader.d.ts +2 -2
  50. package/dist-lib/project/generatedLoader.d.ts.map +1 -1
  51. package/dist-lib/project/generatedLoader.js +23 -5
  52. package/dist-lib/project/loadProject.d.ts +6 -6
  53. package/dist-lib/project/loadProject.d.ts.map +1 -1
  54. package/dist-lib/project/loadProject.js +396 -144
  55. package/dist-lib/project/loaderHelper.d.ts +4 -4
  56. package/dist-lib/project/loaderHelper.d.ts.map +1 -1
  57. package/dist-lib/project/loaderHelper.js +278 -116
  58. package/dist-lib/project/types.d.ts +292 -13
  59. package/dist-lib/project/types.d.ts.map +1 -1
  60. package/dist-lib/project/types.js +13 -1
  61. package/dist-lib/styles/base.css +5 -1
  62. package/dist-lib/uniforms/UniformControls.d.ts +60 -0
  63. package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
  64. package/dist-lib/uniforms/UniformControls.js +518 -0
  65. package/dist-lib/uniforms/UniformStore.d.ts +74 -0
  66. package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
  67. package/dist-lib/uniforms/UniformStore.js +145 -0
  68. package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
  69. package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
  70. package/dist-lib/uniforms/UniformsPanel.js +124 -0
  71. package/dist-lib/uniforms/index.d.ts +11 -0
  72. package/dist-lib/uniforms/index.d.ts.map +1 -0
  73. package/dist-lib/uniforms/index.js +8 -0
  74. package/package.json +16 -1
  75. package/src/app/App.ts +1469 -126
  76. package/src/app/app.css +349 -24
  77. package/src/app/types.ts +53 -5
  78. package/src/editor/EditorPanel.ts +5 -5
  79. package/src/editor/editor-panel.css +55 -32
  80. package/src/editor/prism-editor.css +16 -16
  81. package/src/embed.ts +1 -1
  82. package/src/engine/ShaderEngine.ts +1934 -0
  83. package/src/engine/glHelpers.ts +117 -0
  84. package/src/engine/std140.ts +136 -0
  85. package/src/engine/types.ts +69 -5
  86. package/src/index.ts +4 -3
  87. package/src/layouts/SplitLayout.ts +8 -3
  88. package/src/layouts/TabbedLayout.ts +3 -3
  89. package/src/layouts/UILayout.ts +185 -0
  90. package/src/layouts/default.css +2 -2
  91. package/src/layouts/index.ts +20 -1
  92. package/src/layouts/split.css +33 -31
  93. package/src/layouts/tabbed.css +127 -74
  94. package/src/layouts/types.ts +19 -3
  95. package/src/layouts/ui.css +289 -0
  96. package/src/main.ts +39 -1
  97. package/src/project/configHelpers.ts +225 -0
  98. package/src/project/generatedLoader.ts +27 -6
  99. package/src/project/loadProject.ts +459 -173
  100. package/src/project/loaderHelper.ts +377 -130
  101. package/src/project/types.ts +360 -14
  102. package/src/styles/base.css +5 -1
  103. package/src/styles/theme.css +292 -0
  104. package/src/uniforms/UniformControls.ts +660 -0
  105. package/src/uniforms/UniformStore.ts +166 -0
  106. package/src/uniforms/UniformsPanel.ts +163 -0
  107. package/src/uniforms/index.ts +13 -0
  108. package/src/uniforms/uniform-controls.css +342 -0
  109. package/src/uniforms/uniforms-panel.css +277 -0
  110. package/templates/shaders/example-buffer/config.json +1 -0
  111. package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
  112. package/dist-lib/engine/ShadertoyEngine.js +0 -704
  113. package/src/engine/ShadertoyEngine.ts +0 -929
@@ -0,0 +1,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 { ShadertoyConfig } from './types';
3
+ import { ProjectConfig } from './types';
4
4
 
5
- export const DEMO_NAME = 'course/day5/torus-analytical';
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
- const glslFiles = import.meta.glob<string>('/demos/course/day5/torus-analytical/**/*.glsl', {
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 jsonFiles = import.meta.glob<ShadertoyConfig>('/demos/course/day5/torus-analytical/**/*.json', {
25
+ const jsonFilesRaw = import.meta.glob<ProjectConfig>('/demos/examples/ubo-dynamic/**/*.json', {
14
26
  import: 'default',
15
27
  });
16
28
 
17
- const imageFiles = import.meta.glob<string>('/demos/course/day5/torus-analytical/**/*.{jpg,jpeg,png,gif,webp,bmp}', {
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
- return loadDemo(DEMO_NAME, glslFiles, jsonFiles, imageFiles);
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
  }