@stevejtrettel/shader-sandbox 0.1.1 → 0.1.3

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 CHANGED
@@ -1,105 +1,76 @@
1
1
  # Shader Sandbox
2
2
 
3
- A lightweight, Shadertoy-compatible GLSL shader playground built for teaching and learning shader programming.
3
+ A lightweight, Shadertoy-compatible GLSL shader development environment. Copy shaders directly from Shadertoy and run them locally with live editing.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Shadertoy Compatibility** - Copy/paste shaders directly from Shadertoy
8
8
  - **Full Shadertoy Uniforms** - `iTime`, `iResolution`, `iFrame`, `iMouse`, `iTimeDelta`, `iChannel0-3`
9
9
  - **Multi-Buffer Rendering** - BufferA-D passes with correct ping-pong semantics
10
- - **Texture Support** - Load external images with configurable filtering and wrapping
10
+ - **Texture Support** - Load images with configurable filtering and wrapping
11
11
  - **Keyboard Input** - Full keyboard state via Shadertoy-compatible texture
12
+ - **Live Code Editing** - Edit shaders in the browser with instant recompilation
13
+ - **Multiple Layouts** - Fullscreen, split-view, or tabbed code display
12
14
  - **Playback Controls** - Play/pause, reset, and screenshot capture
13
- - **Multiple Layout Modes** - Fullscreen, default, split-view, or tabbed code display
14
- - **Zero Runtime Dependencies** - Pure WebGL2
15
- - **Tiny Builds** - ~26KB JS (gzipped)
16
15
 
17
16
  ## Quick Start
18
17
 
19
18
  ```bash
20
- npm install
21
- npm run new my-shader
22
- npm run dev:demo my-shader
23
- ```
24
-
25
- Open `http://localhost:3000` to see your shader.
26
-
27
- ---
28
-
29
- ## Use as NPM Package
30
-
31
- Create your own shader collection:
32
-
33
- ```bash
34
- # Create a new project (does everything in one step)
35
- npx shader-sandbox create my-shaders
19
+ # Create a new shader project
20
+ npx @stevejtrettel/shader-sandbox create my-shaders
36
21
 
37
- # Run a shader
22
+ # Enter the project
38
23
  cd my-shaders
24
+
25
+ # Run an example shader
39
26
  shader dev example-gradient
40
27
  ```
41
28
 
42
- That's it! The `create` command sets up the directory, installs dependencies, and creates example shaders.
29
+ Open http://localhost:3000 to see your shader running.
43
30
 
44
- ### CLI Commands
31
+ ## CLI Commands
45
32
 
46
33
  ```bash
47
- shader create <name> # Create a new shader project (recommended)
48
- shader init # Initialize shaders in current directory
49
- shader list # List all shaders
50
- shader dev <name> # Run shader in development mode
34
+ shader create <name> # Create a new shader project
35
+ shader dev <name> # Run shader with live reload
51
36
  shader build <name> # Build shader for production
52
37
  shader new <name> # Create a new shader
38
+ shader list # List all shaders
39
+ shader init # Initialize shaders in current directory
53
40
  ```
54
41
 
55
- ### Project Structure
42
+ ## Project Structure
56
43
 
57
- After `shader create`:
44
+ After running `shader create my-shaders`:
58
45
 
59
46
  ```
60
47
  my-shaders/
61
48
  ├── shaders/
62
49
  │ ├── example-gradient/
63
- │ │ ├── image.glsl # Main shader
64
- │ │ └── config.json # Optional config
50
+ │ │ ├── image.glsl # Main shader code
51
+ │ │ └── config.json # Optional configuration
65
52
  │ └── example-buffer/
66
- │ ├── image.glsl
67
- │ ├── bufferA.glsl # Feedback buffer
53
+ │ ├── image.glsl # Final output
54
+ │ ├── bufferA.glsl # Feedback buffer
68
55
  │ └── config.json
69
- ├── main.ts
70
- ├── vite.config.js
56
+ ├── main.ts # Entry point
57
+ ├── vite.config.js # Vite configuration
71
58
  └── package.json
72
59
  ```
73
60
 
74
- ### Adding a New Shader
75
-
76
- ```bash
77
- shader new my-cool-shader
78
- # Creates shaders/my-cool-shader/image.glsl
79
-
80
- shader dev my-cool-shader
81
- # Opens browser with live reload
82
- ```
83
-
84
- ---
61
+ ## Creating Shaders
85
62
 
86
- ## Common Setups
63
+ ### Simple Shader
87
64
 
88
- ### 1. Simple Shader (just image.glsl)
89
-
90
- The simplest setup - no config needed.
65
+ Create a new shader with just an image pass:
91
66
 
92
67
  ```bash
93
- npm run new my-shader
68
+ shader new my-shader
69
+ shader dev my-shader
94
70
  ```
95
71
 
96
- **Files:**
97
- ```
98
- demos/my-shader/
99
- └── image.glsl
100
- ```
72
+ Edit `shaders/my-shader/image.glsl`:
101
73
 
102
- **image.glsl:**
103
74
  ```glsl
104
75
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
105
76
  vec2 uv = fragCoord / iResolution.xy;
@@ -108,45 +79,26 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
108
79
  }
109
80
  ```
110
81
 
111
- ---
82
+ ### Copy from Shadertoy
112
83
 
113
- ### 2. One Buffer (feedback/trails)
84
+ 1. Find a shader on [Shadertoy](https://www.shadertoy.com)
85
+ 2. Copy the code from the "Image" tab
86
+ 3. Paste into `shaders/my-shader/image.glsl`
87
+ 4. Run `shader dev my-shader`
114
88
 
115
- For effects that accumulate over time (trails, paint, fluid).
89
+ Most single-pass shaders work immediately. For multi-buffer shaders, you'll need to create the buffer files and config.
116
90
 
117
- ```bash
118
- npm run new my-shader 1
119
- ```
120
-
121
- **Files:**
122
- ```
123
- demos/my-shader/
124
- ├── bufferA.glsl
125
- ├── image.glsl
126
- └── config.json
127
- ```
91
+ ### Multi-Buffer Shaders
128
92
 
129
- **config.json:**
130
- ```json
131
- {
132
- "BufferA": {
133
- "iChannel0": "BufferA"
134
- },
135
- "Image": {
136
- "iChannel0": "BufferA"
137
- }
138
- }
139
- ```
93
+ For feedback effects (trails, fluid, etc.), create a buffer:
140
94
 
141
- **bufferA.glsl:**
95
+ **shaders/my-effect/bufferA.glsl:**
142
96
  ```glsl
143
97
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
144
98
  vec2 uv = fragCoord / iResolution.xy;
99
+ vec4 prev = texture(iChannel0, uv) * 0.98; // Previous frame with fade
145
100
 
146
- // Read previous frame with fade
147
- vec4 prev = texture(iChannel0, uv) * 0.98;
148
-
149
- // Draw at mouse
101
+ // Draw at mouse position
150
102
  vec2 mouse = iMouse.xy / iResolution.xy;
151
103
  float d = length(uv - mouse);
152
104
  float spot = smoothstep(0.05, 0.0, d);
@@ -155,7 +107,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
155
107
  }
156
108
  ```
157
109
 
158
- **image.glsl:**
110
+ **shaders/my-effect/image.glsl:**
159
111
  ```glsl
160
112
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
161
113
  vec2 uv = fragCoord / iResolution.xy;
@@ -163,60 +115,23 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
163
115
  }
164
116
  ```
165
117
 
166
- ---
167
-
168
- ### 3. Multiple Buffers (interacting simulations)
169
-
170
- For reaction-diffusion, fluid dynamics, etc. All buffers can read all other buffers.
171
-
172
- ```bash
173
- npm run new my-shader 2
174
- ```
175
-
176
- **Files:**
177
- ```
178
- demos/my-shader/
179
- ├── bufferA.glsl
180
- ├── bufferB.glsl
181
- ├── image.glsl
182
- └── config.json
183
- ```
184
-
185
- **config.json:**
118
+ **shaders/my-effect/config.json:**
186
119
  ```json
187
120
  {
188
121
  "BufferA": {
189
- "iChannel0": "BufferA",
190
- "iChannel1": "BufferB"
191
- },
192
- "BufferB": {
193
- "iChannel0": "BufferA",
194
- "iChannel1": "BufferB"
122
+ "iChannel0": "BufferA"
195
123
  },
196
124
  "Image": {
197
- "iChannel0": "BufferA",
198
- "iChannel1": "BufferB"
125
+ "iChannel0": "BufferA"
199
126
  }
200
127
  }
201
128
  ```
202
129
 
203
- **Channel mapping:** `iChannel0` = BufferA, `iChannel1` = BufferB, etc.
204
-
205
- ---
130
+ ### Using Textures
206
131
 
207
- ### 4. Texture + Image (image processing)
208
-
209
- Load an image and process it.
210
-
211
- **Files:**
212
- ```
213
- demos/my-shader/
214
- ├── image.glsl
215
- ├── photo.jpg
216
- └── config.json
217
- ```
132
+ Place an image in your shader folder and reference it in config:
218
133
 
219
- **config.json:**
134
+ **shaders/my-shader/config.json:**
220
135
  ```json
221
136
  {
222
137
  "Image": {
@@ -225,166 +140,78 @@ demos/my-shader/
225
140
  }
226
141
  ```
227
142
 
228
- **image.glsl:**
143
+ **shaders/my-shader/image.glsl:**
229
144
  ```glsl
230
145
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
231
146
  vec2 uv = fragCoord / iResolution.xy;
232
147
  vec4 img = texture(iChannel0, uv);
233
-
234
- // Example: grayscale
235
- float gray = dot(img.rgb, vec3(0.299, 0.587, 0.114));
236
-
237
- fragColor = vec4(vec3(gray), 1.0);
148
+ fragColor = img;
238
149
  }
239
150
  ```
240
151
 
241
- **Texture options (all optional):**
242
- ```json
243
- { "texture": "photo.jpg", "filter": "linear", "wrap": "repeat", "type": "2d" }
244
- ```
245
- - `filter`: `"linear"` (smooth, default) or `"nearest"` (pixelated)
246
- - `wrap`: `"repeat"` (tile, default) or `"clamp"` (stretch edges)
247
- - `type`: `"2d"` (standard, default) or `"cubemap"` (equirectangular environment map)
248
-
249
- **Cubemap textures:** Use `"type": "cubemap"` for equirectangular environment maps (360° panoramas). The engine will automatically convert 3D direction lookups to 2D coordinates:
250
- ```json
251
- { "texture": "environment.jpg", "type": "cubemap" }
252
- ```
253
- ```glsl
254
- // In your shader, sample with a 3D direction:
255
- vec3 dir = normalize(rayDirection);
256
- vec4 sky = texture(iChannel0, dir); // Automatically converted
257
- ```
258
-
259
- ---
260
-
261
- ### 5. Texture + Buffers (image + feedback)
262
-
263
- Combine textures with buffer feedback for effects like painting on an image.
264
-
265
- **Files:**
266
- ```
267
- demos/my-shader/
268
- ├── bufferA.glsl
269
- ├── image.glsl
270
- ├── photo.jpg
271
- └── config.json
272
- ```
273
-
274
- **config.json:**
152
+ Texture options:
275
153
  ```json
276
154
  {
277
- "BufferA": {
278
- "iChannel0": "BufferA",
279
- "iChannel1": "photo.jpg"
280
- },
281
155
  "Image": {
282
- "iChannel0": "BufferA",
283
- "iChannel1": "photo.jpg"
156
+ "iChannel0": {
157
+ "texture": "photo.jpg",
158
+ "filter": "linear",
159
+ "wrap": "repeat"
160
+ }
284
161
  }
285
162
  }
286
163
  ```
287
164
 
288
- **bufferA.glsl:**
289
- ```glsl
290
- void mainImage(out vec4 fragColor, in vec2 fragCoord) {
291
- vec2 uv = fragCoord / iResolution.xy;
292
-
293
- vec4 prev = texture(iChannel0, uv); // Previous frame
294
- vec4 img = texture(iChannel1, uv); // Original image
295
-
296
- // Paint with mouse
297
- vec2 mouse = iMouse.xy / iResolution.xy;
298
- float d = length(uv - mouse);
299
- float brush = smoothstep(0.05, 0.0, d);
300
-
301
- // Blend: painted areas persist, unpainted fade to original
302
- fragColor = mix(mix(prev, img, 0.01), prev + brush, brush);
303
- }
304
- ```
305
-
306
- ---
307
-
308
- ## Buffer Execution & Frame Timing
309
-
310
- **Execution order:** BufferA → BufferB → BufferC → BufferD → Image
311
-
312
- All buffer reads default to the **previous frame**. This is safe for all cases:
313
- - Self-reference (feedback effects)
314
- - Reading buffers that haven't run yet this frame
315
- - Reading buffers that have already run (you get their latest output)
316
-
317
- Use `{ "buffer": "BufferA", "current": true }` only if you specifically need the in-progress current frame (rare).
318
-
319
- ---
165
+ - `filter`: `"linear"` (smooth) or `"nearest"` (pixelated)
166
+ - `wrap`: `"repeat"` (tile) or `"clamp"` (stretch edges)
320
167
 
321
168
  ## Layouts
322
169
 
323
- Control how the shader is displayed with the `layout` option in `config.json`:
170
+ Control how the shader is displayed in `config.json`:
324
171
 
325
172
  ```json
326
173
  {
327
- "layout": "split",
328
- "BufferA": { ... },
329
- "Image": { ... }
174
+ "layout": "split"
330
175
  }
331
176
  ```
332
177
 
333
- | Layout | Description | Best for |
334
- |--------|-------------|----------|
335
- | `fullscreen` | Canvas fills entire viewport | Immersive art, games, installations |
336
- | `default` | Canvas centered with max-width | General viewing (default without config) |
337
- | `tabbed` | Tabs to switch between shader and code | Exploring/debugging |
338
- | `split` | Side-by-side: shader left, code right | Teaching, presentations, tutorials |
339
-
340
- **`fullscreen`** - No chrome, canvas fills the screen:
341
- ```json
342
- { "layout": "fullscreen" }
343
- ```
344
-
345
- **`default`** - Clean centered view with rounded corners:
346
- ```json
347
- { "layout": "default" }
348
- ```
349
-
350
- **`tabbed`** - Click tabs to switch between live shader and source code:
351
- ```json
352
- { "layout": "tabbed" }
353
- ```
354
-
355
- **`split`** - See shader and code simultaneously (code panel has tabs for multi-file projects):
356
- ```json
357
- { "layout": "split" }
358
- ```
359
-
360
- ---
178
+ | Layout | Description |
179
+ |--------|-------------|
180
+ | `fullscreen` | Canvas fills the viewport |
181
+ | `default` | Centered canvas with controls |
182
+ | `tabbed` | Tabs to switch between shader and code |
183
+ | `split` | Side-by-side shader and code editor |
361
184
 
362
185
  ## Keyboard Shortcuts
363
186
 
364
187
  | Key | Action |
365
188
  |-----|--------|
366
189
  | **S** | Save screenshot (PNG) |
367
- | **Space** | Play/Pause (when controls enabled) |
368
- | **R** | Reset to frame 0 (when controls enabled) |
190
+ | **Space** | Play/Pause |
191
+ | **R** | Reset to frame 0 |
369
192
 
370
- ---
193
+ ## Shadertoy Uniforms
371
194
 
372
- ## NPM Scripts
195
+ All standard Shadertoy uniforms are supported:
373
196
 
374
- ```bash
375
- npm run new <name> [buffers] # Create new shader project
376
- npm run dev:demo <name> # Development server with hot reload
377
- npm run build:demo <name> # Production build to dist/
378
- ```
197
+ | Uniform | Type | Description |
198
+ |---------|------|-------------|
199
+ | `iResolution` | `vec3` | Viewport resolution (width, height, 1) |
200
+ | `iTime` | `float` | Elapsed time in seconds |
201
+ | `iTimeDelta` | `float` | Time since last frame |
202
+ | `iFrame` | `int` | Frame counter |
203
+ | `iMouse` | `vec4` | Mouse position and click state |
204
+ | `iChannel0-3` | `sampler2D` | Input textures/buffers |
205
+ | `iChannelResolution[4]` | `vec3[]` | Resolution of each channel |
206
+ | `iDate` | `vec4` | Year, month, day, time in seconds |
379
207
 
380
- ---
208
+ ## Building for Production
381
209
 
382
- ## Documentation
210
+ ```bash
211
+ shader build my-shader
212
+ ```
383
213
 
384
- - [Getting Started](docs/learn/getting-started.md) - Your first shader
385
- - [Buffers and Channels](docs/learn/buffers-and-channels.md) - Multi-pass rendering
386
- - [Configuration](docs/learn/configuration.md) - Full config reference
387
- - [Architecture](docs/dev/architecture.md) - How the engine works
214
+ Output is in `dist/` - a single HTML file with embedded JavaScript that can be hosted anywhere.
388
215
 
389
216
  ## License
390
217
 
@@ -3,5 +3,5 @@
3
3
  * Called by the generated loader
4
4
  */
5
5
  import { ShadertoyProject, ShadertoyConfig } from './types';
6
- export declare function loadDemo(demoName: string, glslFiles: Record<string, () => Promise<string>>, jsonFiles: Record<string, () => Promise<ShadertoyConfig>>, imageFiles: Record<string, () => Promise<string>>): Promise<ShadertoyProject>;
6
+ export declare function loadDemo(demoPath: string, glslFiles: Record<string, () => Promise<string>>, jsonFiles: Record<string, () => Promise<ShadertoyConfig>>, imageFiles: Record<string, () => Promise<string>>): Promise<ShadertoyProject>;
7
7
  //# sourceMappingURL=loaderHelper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loaderHelper.d.ts","sourceRoot":"","sources":["../../src/project/loaderHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,gBAAgB,EAChB,eAAe,EAIhB,MAAM,SAAS,CAAC;AA8CjB,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,EAChD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC,EACzD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAChD,OAAO,CAAC,gBAAgB,CAAC,CAkB3B"}
1
+ {"version":3,"file":"loaderHelper.d.ts","sourceRoot":"","sources":["../../src/project/loaderHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,gBAAgB,EAChB,eAAe,EAIhB,MAAM,SAAS,CAAC;AA8CjB,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,EAChD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC,EACzD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAChD,OAAO,CAAC,gBAAgB,CAAC,CAoB3B"}
@@ -40,38 +40,42 @@ function parseChannelValue(value) {
40
40
  }
41
41
  return value;
42
42
  }
43
- export async function loadDemo(demoName, glslFiles, jsonFiles, imageFiles) {
44
- const configPath = `/demos/${demoName}/config.json`;
43
+ export async function loadDemo(demoPath, glslFiles, jsonFiles, imageFiles) {
44
+ // Normalize path - handle both "./shaders/name" and "shaders/name" formats
45
+ const normalizedPath = demoPath.startsWith('./') ? demoPath : `./${demoPath}`;
46
+ const configPath = `${normalizedPath}/config.json`;
45
47
  const hasConfig = configPath in jsonFiles;
46
48
  if (hasConfig) {
47
49
  const config = await jsonFiles[configPath]();
48
50
  const hasPassConfigs = config.Image || config.BufferA || config.BufferB ||
49
51
  config.BufferC || config.BufferD;
50
52
  if (hasPassConfigs) {
51
- return loadWithConfig(demoName, config, glslFiles, imageFiles);
53
+ return loadWithConfig(normalizedPath, config, glslFiles, imageFiles);
52
54
  }
53
55
  else {
54
56
  // Config with only settings (layout, controls, etc.) but no passes
55
- return loadSinglePass(demoName, glslFiles, config);
57
+ return loadSinglePass(normalizedPath, glslFiles, config);
56
58
  }
57
59
  }
58
60
  else {
59
- return loadSinglePass(demoName, glslFiles);
61
+ return loadSinglePass(normalizedPath, glslFiles);
60
62
  }
61
63
  }
62
- async function loadSinglePass(demoName, glslFiles, configOverrides) {
63
- const imagePath = `/demos/${demoName}/image.glsl`;
64
+ async function loadSinglePass(demoPath, glslFiles, configOverrides) {
65
+ const imagePath = `${demoPath}/image.glsl`;
64
66
  const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
65
67
  if (!actualImagePath) {
66
- throw new Error(`Demo '${demoName}' not found. Expected ${imagePath}`);
68
+ throw new Error(`Demo '${demoPath}' not found. Expected ${imagePath}`);
67
69
  }
68
70
  const imageSource = await glslFiles[actualImagePath]();
69
71
  const layout = configOverrides?.layout || 'tabbed';
70
72
  const controls = configOverrides?.controls ?? true;
73
+ // Extract name from path for title (e.g., "./shaders/example-gradient" -> "example-gradient")
74
+ const demoName = demoPath.split('/').pop() || demoPath;
71
75
  const title = configOverrides?.title ||
72
76
  demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
73
77
  return {
74
- root: `/demos/${demoName}`,
78
+ root: demoPath,
75
79
  meta: {
76
80
  title,
77
81
  author: configOverrides?.author || null,
@@ -95,7 +99,7 @@ async function loadSinglePass(demoName, glslFiles, configOverrides) {
95
99
  textures: [],
96
100
  };
97
101
  }
98
- async function loadWithConfig(demoName, config, glslFiles, imageFiles) {
102
+ async function loadWithConfig(demoPath, config, glslFiles, imageFiles) {
99
103
  // Extract pass configs from top level
100
104
  const passConfigs = {
101
105
  Image: config.Image,
@@ -107,14 +111,14 @@ async function loadWithConfig(demoName, config, glslFiles, imageFiles) {
107
111
  // Load common source
108
112
  let commonSource = null;
109
113
  if (config.common) {
110
- const commonPath = `/demos/${demoName}/${config.common}`;
114
+ const commonPath = `${demoPath}/${config.common}`;
111
115
  const actualCommonPath = findFileCaseInsensitive(glslFiles, commonPath);
112
116
  if (actualCommonPath) {
113
117
  commonSource = await glslFiles[actualCommonPath]();
114
118
  }
115
119
  }
116
120
  else {
117
- const defaultCommonPath = `/demos/${demoName}/common.glsl`;
121
+ const defaultCommonPath = `${demoPath}/common.glsl`;
118
122
  const actualCommonPath = findFileCaseInsensitive(glslFiles, defaultCommonPath);
119
123
  if (actualCommonPath) {
120
124
  commonSource = await glslFiles[actualCommonPath]();
@@ -141,7 +145,7 @@ async function loadWithConfig(demoName, config, glslFiles, imageFiles) {
141
145
  const textures = [];
142
146
  const texturePathToName = new Map();
143
147
  for (const texturePath of texturePathsSet) {
144
- const fullPath = `/demos/${demoName}/${texturePath.replace(/^\.\//, '')}`;
148
+ const fullPath = `${demoPath}/${texturePath.replace(/^\.\//, '')}`;
145
149
  const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
146
150
  if (!actualPath) {
147
151
  throw new Error(`Texture not found: ${texturePath} (expected at ${fullPath})`);
@@ -172,7 +176,7 @@ async function loadWithConfig(demoName, config, glslFiles, imageFiles) {
172
176
  BufferD: 'bufferD.glsl',
173
177
  };
174
178
  const sourceFile = passConfig.source || defaultNames[passName];
175
- const sourcePath = `/demos/${demoName}/${sourceFile}`;
179
+ const sourcePath = `${demoPath}/${sourceFile}`;
176
180
  const actualSourcePath = findFileCaseInsensitive(glslFiles, sourcePath);
177
181
  if (!actualSourcePath) {
178
182
  throw new Error(`Missing shader file: ${sourcePath}`);
@@ -191,8 +195,10 @@ async function loadWithConfig(demoName, config, glslFiles, imageFiles) {
191
195
  };
192
196
  }
193
197
  if (!passes.Image) {
194
- throw new Error(`Demo '${demoName}' must have an Image pass`);
198
+ throw new Error(`Demo '${demoPath}' must have an Image pass`);
195
199
  }
200
+ // Extract name from path for title (e.g., "./shaders/example-gradient" -> "example-gradient")
201
+ const demoName = demoPath.split('/').pop() || demoPath;
196
202
  const title = config.title ||
197
203
  demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
198
204
  const author = config.author || null;
@@ -200,7 +206,7 @@ async function loadWithConfig(demoName, config, glslFiles, imageFiles) {
200
206
  const layout = config.layout || 'tabbed';
201
207
  const controls = config.controls ?? true;
202
208
  return {
203
- root: `/demos/${demoName}`,
209
+ root: demoPath,
204
210
  meta: { title, author, description },
205
211
  layout,
206
212
  controls,
package/package.json CHANGED
@@ -1,7 +1,22 @@
1
1
  {
2
2
  "name": "@stevejtrettel/shader-sandbox",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local Shadertoy-compatible GLSL shader development environment with live editing",
5
+ "author": "Steve Trettel",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/stevejtrettel/shader-sandbox",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/stevejtrettel/shader-sandbox.git"
11
+ },
12
+ "keywords": [
13
+ "shader",
14
+ "glsl",
15
+ "shadertoy",
16
+ "webgl",
17
+ "graphics",
18
+ "creative-coding"
19
+ ],
5
20
  "type": "module",
6
21
  "main": "./dist-lib/index.js",
7
22
  "types": "./dist-lib/index.d.ts",
@@ -56,12 +56,14 @@ function parseChannelValue(value: ChannelValue): ChannelJSONObject | null {
56
56
  }
57
57
 
58
58
  export async function loadDemo(
59
- demoName: string,
59
+ demoPath: string,
60
60
  glslFiles: Record<string, () => Promise<string>>,
61
61
  jsonFiles: Record<string, () => Promise<ShadertoyConfig>>,
62
62
  imageFiles: Record<string, () => Promise<string>>
63
63
  ): Promise<ShadertoyProject> {
64
- const configPath = `/demos/${demoName}/config.json`;
64
+ // Normalize path - handle both "./shaders/name" and "shaders/name" formats
65
+ const normalizedPath = demoPath.startsWith('./') ? demoPath : `./${demoPath}`;
66
+ const configPath = `${normalizedPath}/config.json`;
65
67
  const hasConfig = configPath in jsonFiles;
66
68
 
67
69
  if (hasConfig) {
@@ -70,37 +72,39 @@ export async function loadDemo(
70
72
  config.BufferC || config.BufferD;
71
73
 
72
74
  if (hasPassConfigs) {
73
- return loadWithConfig(demoName, config, glslFiles, imageFiles);
75
+ return loadWithConfig(normalizedPath, config, glslFiles, imageFiles);
74
76
  } else {
75
77
  // Config with only settings (layout, controls, etc.) but no passes
76
- return loadSinglePass(demoName, glslFiles, config);
78
+ return loadSinglePass(normalizedPath, glslFiles, config);
77
79
  }
78
80
  } else {
79
- return loadSinglePass(demoName, glslFiles);
81
+ return loadSinglePass(normalizedPath, glslFiles);
80
82
  }
81
83
  }
82
84
 
83
85
  async function loadSinglePass(
84
- demoName: string,
86
+ demoPath: string,
85
87
  glslFiles: Record<string, () => Promise<string>>,
86
88
  configOverrides?: Partial<ShadertoyConfig>
87
89
  ): Promise<ShadertoyProject> {
88
- const imagePath = `/demos/${demoName}/image.glsl`;
90
+ const imagePath = `${demoPath}/image.glsl`;
89
91
  const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
90
92
 
91
93
  if (!actualImagePath) {
92
- throw new Error(`Demo '${demoName}' not found. Expected ${imagePath}`);
94
+ throw new Error(`Demo '${demoPath}' not found. Expected ${imagePath}`);
93
95
  }
94
96
 
95
97
  const imageSource = await glslFiles[actualImagePath]();
96
98
 
97
99
  const layout = configOverrides?.layout || 'tabbed';
98
100
  const controls = configOverrides?.controls ?? true;
101
+ // Extract name from path for title (e.g., "./shaders/example-gradient" -> "example-gradient")
102
+ const demoName = demoPath.split('/').pop() || demoPath;
99
103
  const title = configOverrides?.title ||
100
104
  demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
101
105
 
102
106
  return {
103
- root: `/demos/${demoName}`,
107
+ root: demoPath,
104
108
  meta: {
105
109
  title,
106
110
  author: configOverrides?.author || null,
@@ -126,7 +130,7 @@ async function loadSinglePass(
126
130
  }
127
131
 
128
132
  async function loadWithConfig(
129
- demoName: string,
133
+ demoPath: string,
130
134
  config: ShadertoyConfig,
131
135
  glslFiles: Record<string, () => Promise<string>>,
132
136
  imageFiles: Record<string, () => Promise<string>>
@@ -144,13 +148,13 @@ async function loadWithConfig(
144
148
  // Load common source
145
149
  let commonSource: string | null = null;
146
150
  if (config.common) {
147
- const commonPath = `/demos/${demoName}/${config.common}`;
151
+ const commonPath = `${demoPath}/${config.common}`;
148
152
  const actualCommonPath = findFileCaseInsensitive(glslFiles, commonPath);
149
153
  if (actualCommonPath) {
150
154
  commonSource = await glslFiles[actualCommonPath]();
151
155
  }
152
156
  } else {
153
- const defaultCommonPath = `/demos/${demoName}/common.glsl`;
157
+ const defaultCommonPath = `${demoPath}/common.glsl`;
154
158
  const actualCommonPath = findFileCaseInsensitive(glslFiles, defaultCommonPath);
155
159
  if (actualCommonPath) {
156
160
  commonSource = await glslFiles[actualCommonPath]();
@@ -181,7 +185,7 @@ async function loadWithConfig(
181
185
  const texturePathToName = new Map<string, string>();
182
186
 
183
187
  for (const texturePath of texturePathsSet) {
184
- const fullPath = `/demos/${demoName}/${texturePath.replace(/^\.\//, '')}`;
188
+ const fullPath = `${demoPath}/${texturePath.replace(/^\.\//, '')}`;
185
189
  const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
186
190
 
187
191
  if (!actualPath) {
@@ -219,7 +223,7 @@ async function loadWithConfig(
219
223
  };
220
224
 
221
225
  const sourceFile = passConfig.source || defaultNames[passName];
222
- const sourcePath = `/demos/${demoName}/${sourceFile}`;
226
+ const sourcePath = `${demoPath}/${sourceFile}`;
223
227
  const actualSourcePath = findFileCaseInsensitive(glslFiles, sourcePath);
224
228
 
225
229
  if (!actualSourcePath) {
@@ -243,9 +247,11 @@ async function loadWithConfig(
243
247
  }
244
248
 
245
249
  if (!passes.Image) {
246
- throw new Error(`Demo '${demoName}' must have an Image pass`);
250
+ throw new Error(`Demo '${demoPath}' must have an Image pass`);
247
251
  }
248
252
 
253
+ // Extract name from path for title (e.g., "./shaders/example-gradient" -> "example-gradient")
254
+ const demoName = demoPath.split('/').pop() || demoPath;
249
255
  const title = config.title ||
250
256
  demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
251
257
  const author = config.author || null;
@@ -254,7 +260,7 @@ async function loadWithConfig(
254
260
  const controls = config.controls ?? true;
255
261
 
256
262
  return {
257
- root: `/demos/${demoName}`,
263
+ root: demoPath,
258
264
  meta: { title, author, description },
259
265
  layout,
260
266
  controls,