@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
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
29
|
+
Open http://localhost:3000 to see your shader running.
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
## CLI Commands
|
|
45
32
|
|
|
46
33
|
```bash
|
|
47
|
-
shader create <name> # Create a new shader project
|
|
48
|
-
shader
|
|
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
|
-
|
|
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
|
|
64
|
-
│ │ └── config.json
|
|
50
|
+
│ │ ├── image.glsl # Main shader code
|
|
51
|
+
│ │ └── config.json # Optional configuration
|
|
65
52
|
│ └── example-buffer/
|
|
66
|
-
│ ├── image.glsl
|
|
67
|
-
│ ├── bufferA.glsl
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
### Simple Shader
|
|
87
64
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
The simplest setup - no config needed.
|
|
65
|
+
Create a new shader with just an image pass:
|
|
91
66
|
|
|
92
67
|
```bash
|
|
93
|
-
|
|
68
|
+
shader new my-shader
|
|
69
|
+
shader dev my-shader
|
|
94
70
|
```
|
|
95
71
|
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
+
Most single-pass shaders work immediately. For multi-buffer shaders, you'll need to create the buffer files and config.
|
|
116
90
|
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
---
|
|
130
|
+
### Using Textures
|
|
206
131
|
|
|
207
|
-
|
|
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
|
-
|
|
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":
|
|
283
|
-
|
|
156
|
+
"iChannel0": {
|
|
157
|
+
"texture": "photo.jpg",
|
|
158
|
+
"filter": "linear",
|
|
159
|
+
"wrap": "repeat"
|
|
160
|
+
}
|
|
284
161
|
}
|
|
285
162
|
}
|
|
286
163
|
```
|
|
287
164
|
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
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 |
|
|
334
|
-
|
|
335
|
-
| `fullscreen` | Canvas fills
|
|
336
|
-
| `default` |
|
|
337
|
-
| `tabbed` | Tabs to switch between shader and code |
|
|
338
|
-
| `split` | Side-by-side
|
|
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
|
|
368
|
-
| **R** | Reset to frame 0
|
|
190
|
+
| **Space** | Play/Pause |
|
|
191
|
+
| **R** | Reset to frame 0 |
|
|
369
192
|
|
|
370
|
-
|
|
193
|
+
## Shadertoy Uniforms
|
|
371
194
|
|
|
372
|
-
|
|
195
|
+
All standard Shadertoy uniforms are supported:
|
|
373
196
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
210
|
+
```bash
|
|
211
|
+
shader build my-shader
|
|
212
|
+
```
|
|
383
213
|
|
|
384
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
44
|
-
|
|
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(
|
|
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(
|
|
57
|
+
return loadSinglePass(normalizedPath, glslFiles, config);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
else {
|
|
59
|
-
return loadSinglePass(
|
|
61
|
+
return loadSinglePass(normalizedPath, glslFiles);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
|
-
async function loadSinglePass(
|
|
63
|
-
const imagePath =
|
|
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 '${
|
|
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:
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 '${
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
78
|
+
return loadSinglePass(normalizedPath, glslFiles, config);
|
|
77
79
|
}
|
|
78
80
|
} else {
|
|
79
|
-
return loadSinglePass(
|
|
81
|
+
return loadSinglePass(normalizedPath, glslFiles);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
async function loadSinglePass(
|
|
84
|
-
|
|
86
|
+
demoPath: string,
|
|
85
87
|
glslFiles: Record<string, () => Promise<string>>,
|
|
86
88
|
configOverrides?: Partial<ShadertoyConfig>
|
|
87
89
|
): Promise<ShadertoyProject> {
|
|
88
|
-
const imagePath =
|
|
90
|
+
const imagePath = `${demoPath}/image.glsl`;
|
|
89
91
|
const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
|
|
90
92
|
|
|
91
93
|
if (!actualImagePath) {
|
|
92
|
-
throw new Error(`Demo '${
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 '${
|
|
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:
|
|
263
|
+
root: demoPath,
|
|
258
264
|
meta: { title, author, description },
|
|
259
265
|
layout,
|
|
260
266
|
controls,
|