@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.
Files changed (113) hide show
  1. package/README.md +220 -23
  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 +1 -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
package/README.md CHANGED
@@ -5,13 +5,20 @@ A lightweight, Shadertoy-compatible GLSL shader development environment. Copy sh
5
5
  ## Features
6
6
 
7
7
  - **Shadertoy Compatibility** - Copy/paste shaders directly from Shadertoy
8
- - **Full Shadertoy Uniforms** - `iTime`, `iResolution`, `iFrame`, `iMouse`, `iTimeDelta`, `iChannel0-3`
8
+ - **Full Shadertoy Uniforms** - `iTime`, `iResolution`, `iFrame`, `iMouse`, `iTimeDelta`, `iDate`, `iFrameRate`, `iChannel0-3`
9
9
  - **Multi-Buffer Rendering** - BufferA-D passes with correct ping-pong semantics
10
- - **Texture Support** - Load images with configurable filtering and wrapping
11
- - **Keyboard Input** - Full keyboard state via Shadertoy-compatible texture
10
+ - **Texture Support** - Load images (including cubemaps) with configurable filtering and wrapping
11
+ - **Keyboard Input** - Full keyboard state via texture. In standard mode, key constants (`KEY_A`–`KEY_Z`, `KEY_SPACE`, etc.) and helpers (`isKeyDown`, `keyToggle`) are auto-injected
12
+ - **Audio Input** - Microphone FFT spectrum and waveform as a texture, as in shadertoy
13
+ - **Webcam & Video** - Live webcam or video files as channel inputs, as in shadertoy
14
+ - **Custom Uniforms** - Float, int, bool, vec2, vec3, vec4 sliders and color pickers via config. *An extension beyond shadertoy*
15
+ - **UBO Array Uniforms** - Large data arrays (positions, colors, matrices) via Uniform Buffer Objects. *An extension beyond shadertoy*
16
+ - **Scripting API** - JavaScript hooks for per-frame computation, texture upload, and GPU readback. *An extension beyond shadertoy*
17
+ - **Touch Support** - Multi-touch, pinch, and gesture uniforms for mobile
12
18
  - **Live Code Editing** - Edit shaders in the browser with instant recompilation
13
19
  - **Multiple Layouts** - Fullscreen, split-view, or tabbed code display
14
20
  - **Playback Controls** - Play/pause, reset, and screenshot capture
21
+ - **Themes** - Light, dark, or system-following theme
15
22
 
16
23
  ## Quick Start
17
24
 
@@ -131,7 +138,6 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
131
138
 
132
139
  Place an image in your shader folder and reference it in config:
133
140
 
134
- **shaders/my-shader/config.json:**
135
141
  ```json
136
142
  {
137
143
  "Image": {
@@ -140,30 +146,135 @@ Place an image in your shader folder and reference it in config:
140
146
  }
141
147
  ```
142
148
 
143
- **shaders/my-shader/image.glsl:**
149
+ With options:
150
+ ```json
151
+ {
152
+ "Image": {
153
+ "iChannel0": {
154
+ "texture": "photo.jpg",
155
+ "filter": "nearest",
156
+ "wrap": "clamp"
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### Custom Uniforms (Slider Controls)
163
+
164
+ Add interactive controls to your shader by defining uniforms in config:
165
+
166
+ ```json
167
+ {
168
+ "controls": true,
169
+ "uniforms": {
170
+ "uSpeed": { "type": "float", "value": 1.0, "min": 0.0, "max": 5.0, "label": "Speed" },
171
+ "uColor": { "type": "vec3", "value": [1.0, 0.5, 0.2], "color": true, "label": "Color" },
172
+ "uAnimate": { "type": "bool", "value": true, "label": "Animate" }
173
+ },
174
+ "Image": {}
175
+ }
176
+ ```
177
+
178
+ Uniforms declared in config are **auto-injected** into your shader code — you don't need to write `uniform` declarations. Just use them directly:
179
+
144
180
  ```glsl
145
181
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
146
182
  vec2 uv = fragCoord / iResolution.xy;
147
- vec4 img = texture(iChannel0, uv);
148
- fragColor = img;
183
+ float t = uAnimate ? iTime * uSpeed : 0.0;
184
+ vec3 col = uColor * (0.5 + 0.5 * sin(uv.x * 10.0 - t));
185
+ fragColor = vec4(col, 1.0);
149
186
  }
150
187
  ```
151
188
 
152
- Texture options:
189
+ #### Supported Uniform Types
190
+
191
+ | Type | UI Control | Config Fields |
192
+ |------|-----------|---------------|
193
+ | `float` | Slider | `value`, `min` (0), `max` (1), `step` (0.01) |
194
+ | `int` | Discrete slider | `value`, `min` (0), `max` (10), `step` (1) |
195
+ | `bool` | Toggle | `value` |
196
+ | `vec2` | XY pad | `value`, `min` ([0,0]), `max` ([1,1]) |
197
+ | `vec3` | 3 sliders or color picker | `value`, `color` (false), `min`, `max`, `step` |
198
+ | `vec4` | 4 sliders or color+alpha picker | `value`, `color` (false), `min`, `max`, `step` |
199
+
200
+ Set `"hidden": true` on any scalar uniform to exclude it from the UI panel (useful for script-controlled values).
201
+
202
+ #### Array Uniforms (UBOs)
203
+
204
+ For large data arrays (positions, matrices, etc.), use array uniforms backed by Uniform Buffer Objects:
205
+
153
206
  ```json
154
207
  {
155
- "Image": {
156
- "iChannel0": {
157
- "texture": "photo.jpg",
158
- "filter": "linear",
159
- "wrap": "repeat"
208
+ "uniforms": {
209
+ "matrices": { "type": "mat3", "count": 128 },
210
+ "matrixCount": { "type": "int", "value": 1, "hidden": true }
211
+ }
212
+ }
213
+ ```
214
+
215
+ Array uniforms support types: `float`, `vec2`, `vec3`, `vec4`, `mat3`, `mat4`. They have no UI — data is provided from JavaScript via `engine.setUniformValue()`.
216
+
217
+ In the compiled shader, array uniforms are wrapped in a `layout(std140)` uniform block with a `_ub_` prefix (e.g., `_ub_matrices`). The array variable itself uses the original name, so you reference it directly in GLSL:
218
+
219
+ ```glsl
220
+ // Auto-injected by the engine (you don't write this):
221
+ // layout(std140) uniform _ub_matrices {
222
+ // mat3 matrices[128];
223
+ // };
224
+
225
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
226
+ for (int i = 0; i < matrixCount; i++) {
227
+ vec3 p = matrices[i] * vec3(fragCoord, 1.0);
228
+ // ...
160
229
  }
230
+ }
231
+ ```
232
+
233
+ See the [Configuration Reference](docs/learn/configuration.md) for all uniform types.
234
+
235
+ ### Scripting (JavaScript Hooks)
236
+
237
+ For computed data that changes every frame, add a `script.js` to your shader folder:
238
+
239
+ **shaders/my-shader/script.js:**
240
+ ```js
241
+ const COUNT = 32;
242
+
243
+ export function onFrame(engine, time) {
244
+ const data = new Float32Array(COUNT * 4);
245
+ for (let i = 0; i < COUNT; i++) {
246
+ const phase = (i / COUNT) * Math.PI * 2.0;
247
+ data[i * 4 + 0] = 0.5 + Math.cos(time + phase) * 0.3; // x
248
+ data[i * 4 + 1] = 0.5 + Math.sin(time + phase) * 0.3; // y
249
+ data[i * 4 + 2] = 0.02; // radius
250
+ data[i * 4 + 3] = i / COUNT; // hue
161
251
  }
252
+ engine.setUniformValue('positions', data);
162
253
  }
163
254
  ```
164
255
 
165
- - `filter`: `"linear"` (smooth) or `"nearest"` (pixelated)
166
- - `wrap`: `"repeat"` (tile) or `"clamp"` (stretch edges)
256
+ Scripts can export `setup(engine)` (called once) and/or `onFrame(engine, time, deltaTime, frame)` (called every frame).
257
+
258
+ The script API provides:
259
+ - `engine.setUniformValue(name, value)` — set any uniform
260
+ - `engine.getUniformValue(name)` — read current value
261
+ - `engine.updateTexture(name, width, height, data)` — upload a texture from JS
262
+ - `engine.readPixels(passName, x, y, w, h)` — read pixels from a buffer (GPU readback)
263
+ - `engine.width` / `engine.height` — canvas dimensions
264
+
265
+ ## Channel Types
266
+
267
+ Channels can be bound using string shortcuts or full objects:
268
+
269
+ | Shorthand | Object Form | Description |
270
+ |-----------|-------------|-------------|
271
+ | `"BufferA"` | `{ "buffer": "BufferA" }` | Buffer pass output |
272
+ | `"photo.jpg"` | `{ "texture": "photo.jpg" }` | Image texture |
273
+ | `"keyboard"` | `{ "keyboard": true }` | Keyboard state |
274
+ | `"audio"` | `{ "audio": true }` | Microphone FFT + waveform |
275
+ | `"webcam"` | `{ "webcam": true }` | Live webcam feed |
276
+ | — | `{ "video": "clip.mp4" }` | Video file |
277
+ | — | `{ "script": "myData" }` | Script-uploaded texture |
167
278
 
168
279
  ## Layouts
169
280
 
@@ -182,14 +293,6 @@ Control how the shader is displayed in `config.json`:
182
293
  | `tabbed` | Tabs to switch between shader and code |
183
294
  | `split` | Side-by-side shader and code editor |
184
295
 
185
- ## Keyboard Shortcuts
186
-
187
- | Key | Action |
188
- |-----|--------|
189
- | **S** | Save screenshot (PNG) |
190
- | **Space** | Play/Pause |
191
- | **R** | Reset to frame 0 |
192
-
193
296
  ## Shadertoy Uniforms
194
297
 
195
298
  All standard Shadertoy uniforms are supported:
@@ -200,11 +303,105 @@ All standard Shadertoy uniforms are supported:
200
303
  | `iTime` | `float` | Elapsed time in seconds |
201
304
  | `iTimeDelta` | `float` | Time since last frame |
202
305
  | `iFrame` | `int` | Frame counter |
306
+ | `iFrameRate` | `float` | Frames per second |
203
307
  | `iMouse` | `vec4` | Mouse position and click state |
204
308
  | `iChannel0-3` | `sampler2D` | Input textures/buffers |
205
309
  | `iChannelResolution[4]` | `vec3[]` | Resolution of each channel |
206
310
  | `iDate` | `vec4` | Year, month, day, time in seconds |
207
311
 
312
+ ### Touch Uniforms (Mobile / Multi-touch)
313
+
314
+ | Uniform | Type | Description |
315
+ |---------|------|-------------|
316
+ | `iTouchCount` | `int` | Number of active touches |
317
+ | `iTouch0-2` | `vec4` | Per-touch position and state |
318
+ | `iPinch` | `float` | Current pinch distance |
319
+ | `iPinchDelta` | `float` | Change in pinch distance |
320
+ | `iPinchCenter` | `vec2` | Center point between pinch fingers |
321
+
322
+ ## Keyboard Shortcuts
323
+
324
+ | Key | Action |
325
+ |-----|--------|
326
+ | **S** | Save screenshot (PNG) |
327
+ | **Space** | Play/Pause |
328
+ | **R** | Reset to frame 0 |
329
+
330
+ ## Recording and Export
331
+
332
+ ### Video Recording
333
+
334
+ When `controls: true`, click the record button (or use the controls menu) to capture your shader as a WebM video. Recording uses the browser's `MediaRecorder` API at 60fps with VP9 encoding. Click the stop button to end recording — the video downloads automatically.
335
+
336
+ ### HTML Export
337
+
338
+ Click the export button in the controls menu to generate a standalone HTML file with your shader embedded. The exported file includes:
339
+ - All shader passes and common code
340
+ - Current custom uniform values baked in
341
+ - Full WebGL2 rendering pipeline (no dependencies)
342
+ - Mouse interaction support
343
+ - Resize handling
344
+
345
+ **Limitations:** Array uniforms (UBOs), audio, webcam, video, and script hooks are not included in the export. Textures are replaced with a procedural checkerboard pattern.
346
+
347
+ ## Embedding as a Library
348
+
349
+ The package exports its core classes for use in custom applications:
350
+
351
+ ```typescript
352
+ import { App, createLayout, loadDemo } from '@stevejtrettel/shader-sandbox';
353
+ import type { ShaderProject, LayoutMode } from '@stevejtrettel/shader-sandbox';
354
+ ```
355
+
356
+ ### Exports
357
+
358
+ | Export | Description |
359
+ |--------|-------------|
360
+ | `App` | Main application class — creates canvas, engine, and animation loop |
361
+ | `createLayout(mode, options)` | Factory to create a layout (fullscreen, default, split, tabbed) |
362
+ | `applyTheme(mode)` | Apply a theme (light, dark, system) |
363
+ | `loadDemo(files)` | Load a shader project from bundled file data |
364
+
365
+ ### Example: Embedding a shader
366
+
367
+ ```typescript
368
+ import { App, createLayout } from '@stevejtrettel/shader-sandbox';
369
+
370
+ const project = /* your ShaderProject object */;
371
+
372
+ const layout = createLayout(project.layout, {
373
+ container: document.getElementById('shader-container'),
374
+ project,
375
+ });
376
+
377
+ const app = new App({
378
+ container: layout.getCanvasContainer(),
379
+ project,
380
+ });
381
+
382
+ if (!app.hasErrors()) {
383
+ app.start();
384
+ }
385
+
386
+ // Clean up when done
387
+ app.dispose();
388
+ ```
389
+
390
+ ### Embed entry point
391
+
392
+ For build-time embedding of a specific shader, the package also provides an `embed()` function:
393
+
394
+ ```typescript
395
+ import { embed } from '@stevejtrettel/shader-sandbox/embed';
396
+
397
+ const { app, destroy } = await embed({
398
+ container: '#my-container', // CSS selector or HTMLElement
399
+ pixelRatio: window.devicePixelRatio,
400
+ });
401
+
402
+ // Later: destroy() to clean up
403
+ ```
404
+
208
405
  ## Building for Production
209
406
 
210
407
  ```bash
package/bin/cli.js CHANGED
@@ -63,24 +63,41 @@ function copyDir(src, dest, skipFiles = []) {
63
63
  }
64
64
  }
65
65
 
66
- function listShaders(cwd) {
66
+ function getShaderList(cwd) {
67
67
  const shadersDir = path.join(cwd, 'shaders');
68
68
  if (!fs.existsSync(shadersDir)) {
69
- console.error('Error: shaders/ directory not found');
70
- console.error('Run "shader init" first');
71
- process.exit(1);
69
+ return null;
72
70
  }
73
71
 
74
72
  const entries = fs.readdirSync(shadersDir, { withFileTypes: true });
75
- const shaders = entries.filter(e => e.isDirectory()).map(e => e.name);
73
+ return entries.filter(e => e.isDirectory()).map(e => e.name);
74
+ }
75
+
76
+ function listShaders(cwd) {
77
+ const shaders = getShaderList(cwd);
78
+
79
+ if (shaders === null) {
80
+ console.error('Error: shaders/ directory not found');
81
+ console.error('');
82
+ console.error('To get started:');
83
+ console.error(' shader init Initialize shaders in current directory');
84
+ console.error(' shader create Create a new shader project');
85
+ process.exit(1);
86
+ }
76
87
 
77
88
  if (shaders.length === 0) {
78
- console.log('No shaders found. Run "shader new <name>" to create one.');
89
+ console.log('No shaders found.');
90
+ console.log('');
91
+ console.log('Create your first shader:');
92
+ console.log(' shader new my-shader');
79
93
  return;
80
94
  }
81
95
 
82
96
  console.log('Available shaders:');
83
97
  shaders.forEach(s => console.log(` ${s}`));
98
+ console.log('');
99
+ console.log('Run a shader:');
100
+ console.log(` shader dev ${shaders[0]}`);
84
101
  }
85
102
 
86
103
  async function create(projectName) {
@@ -328,18 +345,61 @@ switch (command) {
328
345
 
329
346
  case 'dev': {
330
347
  const shaderName = args[1];
348
+ const cwd = process.cwd();
349
+
331
350
  if (!shaderName) {
332
- console.error('Error: Specify a shader name');
333
- console.error(' shader dev <shader-name>');
334
- console.error(' shader list');
351
+ const shaders = getShaderList(cwd);
352
+
353
+ if (shaders === null) {
354
+ console.error('Error: shaders/ directory not found');
355
+ console.error('');
356
+ console.error('To get started:');
357
+ console.error(' shader init Initialize shaders in current directory');
358
+ console.error(' shader create Create a new shader project');
359
+ process.exit(1);
360
+ }
361
+
362
+ if (shaders.length === 0) {
363
+ console.error('Error: No shaders found');
364
+ console.error('');
365
+ console.error('Create your first shader:');
366
+ console.error(' shader new my-shader');
367
+ process.exit(1);
368
+ }
369
+
370
+ console.error('Error: Specify which shader to run');
371
+ console.error('');
372
+ console.error('Available shaders:');
373
+ shaders.forEach(s => console.error(` ${s}`));
374
+ console.error('');
375
+ console.error('Usage:');
376
+ console.error(` shader dev ${shaders[0]}`);
335
377
  process.exit(1);
336
378
  }
337
379
 
338
- const cwd = process.cwd();
339
380
  const shaderPath = path.join(cwd, 'shaders', shaderName);
340
381
  if (!fs.existsSync(shaderPath)) {
382
+ const shaders = getShaderList(cwd);
383
+
341
384
  console.error(`Error: Shader "${shaderName}" not found`);
342
- console.error('Run "shader list" to see available shaders');
385
+
386
+ if (shaders && shaders.length > 0) {
387
+ // Check for similar names (typo detection)
388
+ const similar = shaders.filter(s =>
389
+ s.toLowerCase().includes(shaderName.toLowerCase()) ||
390
+ shaderName.toLowerCase().includes(s.toLowerCase())
391
+ );
392
+
393
+ if (similar.length > 0) {
394
+ console.error('');
395
+ console.error('Did you mean:');
396
+ similar.forEach(s => console.error(` ${s}`));
397
+ } else {
398
+ console.error('');
399
+ console.error('Available shaders:');
400
+ shaders.forEach(s => console.error(` ${s}`));
401
+ }
402
+ }
343
403
  process.exit(1);
344
404
  }
345
405
 
@@ -350,16 +410,48 @@ switch (command) {
350
410
 
351
411
  case 'build': {
352
412
  const shaderName = args[1];
413
+ const cwd = process.cwd();
414
+
353
415
  if (!shaderName) {
354
- console.error('Error: Specify a shader name');
355
- console.error(' shader build <shader-name>');
416
+ const shaders = getShaderList(cwd);
417
+
418
+ if (shaders && shaders.length > 0) {
419
+ console.error('Error: Specify which shader to build');
420
+ console.error('');
421
+ console.error('Available shaders:');
422
+ shaders.forEach(s => console.error(` ${s}`));
423
+ console.error('');
424
+ console.error('Usage:');
425
+ console.error(` shader build ${shaders[0]}`);
426
+ } else {
427
+ console.error('Error: Specify a shader name');
428
+ console.error(' shader build <shader-name>');
429
+ }
356
430
  process.exit(1);
357
431
  }
358
432
 
359
- const cwd = process.cwd();
360
433
  const shaderPath = path.join(cwd, 'shaders', shaderName);
361
434
  if (!fs.existsSync(shaderPath)) {
435
+ const shaders = getShaderList(cwd);
436
+
362
437
  console.error(`Error: Shader "${shaderName}" not found`);
438
+
439
+ if (shaders && shaders.length > 0) {
440
+ const similar = shaders.filter(s =>
441
+ s.toLowerCase().includes(shaderName.toLowerCase()) ||
442
+ shaderName.toLowerCase().includes(s.toLowerCase())
443
+ );
444
+
445
+ if (similar.length > 0) {
446
+ console.error('');
447
+ console.error('Did you mean:');
448
+ similar.forEach(s => console.error(` ${s}`));
449
+ } else {
450
+ console.error('');
451
+ console.error('Available shaders:');
452
+ shaders.forEach(s => console.error(` ${s}`));
453
+ }
454
+ }
363
455
  process.exit(1);
364
456
  }
365
457