@stevejtrettel/shader-sandbox 0.1.0
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 +391 -0
- package/bin/cli.js +389 -0
- package/dist-lib/app/App.d.ts +134 -0
- package/dist-lib/app/App.d.ts.map +1 -0
- package/dist-lib/app/App.js +570 -0
- package/dist-lib/app/types.d.ts +32 -0
- package/dist-lib/app/types.d.ts.map +1 -0
- package/dist-lib/app/types.js +6 -0
- package/dist-lib/editor/EditorPanel.d.ts +39 -0
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -0
- package/dist-lib/editor/EditorPanel.js +274 -0
- package/dist-lib/editor/prism-editor.css +99 -0
- package/dist-lib/editor/prism-editor.d.ts +19 -0
- package/dist-lib/editor/prism-editor.d.ts.map +1 -0
- package/dist-lib/editor/prism-editor.js +96 -0
- package/dist-lib/embed.d.ts +17 -0
- package/dist-lib/embed.d.ts.map +1 -0
- package/dist-lib/embed.js +35 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts +160 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShadertoyEngine.js +704 -0
- package/dist-lib/engine/glHelpers.d.ts +79 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -0
- package/dist-lib/engine/glHelpers.js +298 -0
- package/dist-lib/engine/types.d.ts +77 -0
- package/dist-lib/engine/types.d.ts.map +1 -0
- package/dist-lib/engine/types.js +7 -0
- package/dist-lib/index.d.ts +12 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +9 -0
- package/dist-lib/layouts/DefaultLayout.d.ts +17 -0
- package/dist-lib/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist-lib/layouts/DefaultLayout.js +27 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts +17 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist-lib/layouts/FullscreenLayout.js +27 -0
- package/dist-lib/layouts/SplitLayout.d.ts +26 -0
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -0
- package/dist-lib/layouts/SplitLayout.js +61 -0
- package/dist-lib/layouts/TabbedLayout.d.ts +38 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -0
- package/dist-lib/layouts/TabbedLayout.js +305 -0
- package/dist-lib/layouts/index.d.ts +24 -0
- package/dist-lib/layouts/index.d.ts.map +1 -0
- package/dist-lib/layouts/index.js +36 -0
- package/dist-lib/layouts/split.css +196 -0
- package/dist-lib/layouts/tabbed.css +345 -0
- package/dist-lib/layouts/types.d.ts +48 -0
- package/dist-lib/layouts/types.d.ts.map +1 -0
- package/dist-lib/layouts/types.js +4 -0
- package/dist-lib/main.d.ts +15 -0
- package/dist-lib/main.d.ts.map +1 -0
- package/dist-lib/main.js +102 -0
- package/dist-lib/project/generatedLoader.d.ts +3 -0
- package/dist-lib/project/generatedLoader.d.ts.map +1 -0
- package/dist-lib/project/generatedLoader.js +17 -0
- package/dist-lib/project/loadProject.d.ts +22 -0
- package/dist-lib/project/loadProject.d.ts.map +1 -0
- package/dist-lib/project/loadProject.js +350 -0
- package/dist-lib/project/loaderHelper.d.ts +7 -0
- package/dist-lib/project/loaderHelper.d.ts.map +1 -0
- package/dist-lib/project/loaderHelper.js +240 -0
- package/dist-lib/project/types.d.ts +192 -0
- package/dist-lib/project/types.d.ts.map +1 -0
- package/dist-lib/project/types.js +7 -0
- package/dist-lib/styles/base.css +29 -0
- package/package.json +48 -0
- package/src/app/App.ts +699 -0
- package/src/app/app.css +208 -0
- package/src/app/types.ts +36 -0
- package/src/editor/EditorPanel.ts +340 -0
- package/src/editor/editor-panel.css +175 -0
- package/src/editor/prism-editor.css +99 -0
- package/src/editor/prism-editor.ts +124 -0
- package/src/embed.ts +55 -0
- package/src/engine/ShadertoyEngine.ts +929 -0
- package/src/engine/glHelpers.ts +432 -0
- package/src/engine/types.ts +118 -0
- package/src/index.ts +13 -0
- package/src/layouts/DefaultLayout.ts +40 -0
- package/src/layouts/FullscreenLayout.ts +40 -0
- package/src/layouts/SplitLayout.ts +81 -0
- package/src/layouts/TabbedLayout.ts +371 -0
- package/src/layouts/default.css +22 -0
- package/src/layouts/fullscreen.css +15 -0
- package/src/layouts/index.ts +44 -0
- package/src/layouts/split.css +196 -0
- package/src/layouts/tabbed.css +345 -0
- package/src/layouts/types.ts +58 -0
- package/src/main.ts +114 -0
- package/src/project/generatedLoader.ts +23 -0
- package/src/project/loadProject.ts +421 -0
- package/src/project/loaderHelper.ts +300 -0
- package/src/project/types.ts +243 -0
- package/src/styles/base.css +29 -0
- package/src/styles/embed.css +14 -0
- package/src/vite-env.d.ts +1 -0
- package/templates/index.html +28 -0
- package/templates/main.ts +126 -0
- package/templates/package.json +12 -0
- package/templates/shaders/example-buffer/bufferA.glsl +14 -0
- package/templates/shaders/example-buffer/config.json +10 -0
- package/templates/shaders/example-buffer/image.glsl +5 -0
- package/templates/shaders/example-gradient/config.json +4 -0
- package/templates/shaders/example-gradient/image.glsl +7 -0
- package/templates/vite.config.js +35 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for loading demo files
|
|
3
|
+
* Called by the generated loader
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ShadertoyProject,
|
|
8
|
+
ShadertoyConfig,
|
|
9
|
+
PassName,
|
|
10
|
+
ChannelValue,
|
|
11
|
+
ChannelJSONObject,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Case-insensitive file lookup helper.
|
|
16
|
+
* Returns the actual key from the record that matches the path (case-insensitive).
|
|
17
|
+
*/
|
|
18
|
+
function findFileCaseInsensitive<T>(
|
|
19
|
+
files: Record<string, T>,
|
|
20
|
+
path: string
|
|
21
|
+
): string | null {
|
|
22
|
+
// First try exact match
|
|
23
|
+
if (path in files) return path;
|
|
24
|
+
|
|
25
|
+
// Try case-insensitive match
|
|
26
|
+
const lowerPath = path.toLowerCase();
|
|
27
|
+
for (const key of Object.keys(files)) {
|
|
28
|
+
if (key.toLowerCase() === lowerPath) {
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Type guard for PassName.
|
|
37
|
+
*/
|
|
38
|
+
function isPassName(s: string): s is PassName {
|
|
39
|
+
return s === 'Image' || s === 'BufferA' || s === 'BufferB' || s === 'BufferC' || s === 'BufferD';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse a channel value (string shorthand or object) into normalized ChannelJSONObject.
|
|
44
|
+
*/
|
|
45
|
+
function parseChannelValue(value: ChannelValue): ChannelJSONObject | null {
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
if (isPassName(value)) {
|
|
48
|
+
return { buffer: value };
|
|
49
|
+
}
|
|
50
|
+
if (value === 'keyboard') {
|
|
51
|
+
return { keyboard: true };
|
|
52
|
+
}
|
|
53
|
+
return { texture: value };
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function loadDemo(
|
|
59
|
+
demoName: string,
|
|
60
|
+
glslFiles: Record<string, () => Promise<string>>,
|
|
61
|
+
jsonFiles: Record<string, () => Promise<ShadertoyConfig>>,
|
|
62
|
+
imageFiles: Record<string, () => Promise<string>>
|
|
63
|
+
): Promise<ShadertoyProject> {
|
|
64
|
+
const configPath = `/demos/${demoName}/config.json`;
|
|
65
|
+
const hasConfig = configPath in jsonFiles;
|
|
66
|
+
|
|
67
|
+
if (hasConfig) {
|
|
68
|
+
const config = await jsonFiles[configPath]();
|
|
69
|
+
const hasPassConfigs = config.Image || config.BufferA || config.BufferB ||
|
|
70
|
+
config.BufferC || config.BufferD;
|
|
71
|
+
|
|
72
|
+
if (hasPassConfigs) {
|
|
73
|
+
return loadWithConfig(demoName, config, glslFiles, imageFiles);
|
|
74
|
+
} else {
|
|
75
|
+
// Config with only settings (layout, controls, etc.) but no passes
|
|
76
|
+
return loadSinglePass(demoName, glslFiles, config);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
return loadSinglePass(demoName, glslFiles);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function loadSinglePass(
|
|
84
|
+
demoName: string,
|
|
85
|
+
glslFiles: Record<string, () => Promise<string>>,
|
|
86
|
+
configOverrides?: Partial<ShadertoyConfig>
|
|
87
|
+
): Promise<ShadertoyProject> {
|
|
88
|
+
const imagePath = `/demos/${demoName}/image.glsl`;
|
|
89
|
+
const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
|
|
90
|
+
|
|
91
|
+
if (!actualImagePath) {
|
|
92
|
+
throw new Error(`Demo '${demoName}' not found. Expected ${imagePath}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const imageSource = await glslFiles[actualImagePath]();
|
|
96
|
+
|
|
97
|
+
const layout = configOverrides?.layout || 'tabbed';
|
|
98
|
+
const controls = configOverrides?.controls ?? true;
|
|
99
|
+
const title = configOverrides?.title ||
|
|
100
|
+
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
root: `/demos/${demoName}`,
|
|
104
|
+
meta: {
|
|
105
|
+
title,
|
|
106
|
+
author: configOverrides?.author || null,
|
|
107
|
+
description: configOverrides?.description || null,
|
|
108
|
+
},
|
|
109
|
+
layout,
|
|
110
|
+
controls,
|
|
111
|
+
commonSource: null,
|
|
112
|
+
passes: {
|
|
113
|
+
Image: {
|
|
114
|
+
name: 'Image',
|
|
115
|
+
glslSource: imageSource,
|
|
116
|
+
channels: [
|
|
117
|
+
{ kind: 'none' },
|
|
118
|
+
{ kind: 'none' },
|
|
119
|
+
{ kind: 'none' },
|
|
120
|
+
{ kind: 'none' },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
textures: [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function loadWithConfig(
|
|
129
|
+
demoName: string,
|
|
130
|
+
config: ShadertoyConfig,
|
|
131
|
+
glslFiles: Record<string, () => Promise<string>>,
|
|
132
|
+
imageFiles: Record<string, () => Promise<string>>
|
|
133
|
+
): Promise<ShadertoyProject> {
|
|
134
|
+
|
|
135
|
+
// Extract pass configs from top level
|
|
136
|
+
const passConfigs = {
|
|
137
|
+
Image: config.Image,
|
|
138
|
+
BufferA: config.BufferA,
|
|
139
|
+
BufferB: config.BufferB,
|
|
140
|
+
BufferC: config.BufferC,
|
|
141
|
+
BufferD: config.BufferD,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Load common source
|
|
145
|
+
let commonSource: string | null = null;
|
|
146
|
+
if (config.common) {
|
|
147
|
+
const commonPath = `/demos/${demoName}/${config.common}`;
|
|
148
|
+
const actualCommonPath = findFileCaseInsensitive(glslFiles, commonPath);
|
|
149
|
+
if (actualCommonPath) {
|
|
150
|
+
commonSource = await glslFiles[actualCommonPath]();
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const defaultCommonPath = `/demos/${demoName}/common.glsl`;
|
|
154
|
+
const actualCommonPath = findFileCaseInsensitive(glslFiles, defaultCommonPath);
|
|
155
|
+
if (actualCommonPath) {
|
|
156
|
+
commonSource = await glslFiles[actualCommonPath]();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Collect all texture paths
|
|
161
|
+
const texturePathsSet = new Set<string>();
|
|
162
|
+
const passOrder = ['Image', 'BufferA', 'BufferB', 'BufferC', 'BufferD'] as const;
|
|
163
|
+
|
|
164
|
+
for (const passName of passOrder) {
|
|
165
|
+
const passConfig = passConfigs[passName];
|
|
166
|
+
if (!passConfig) continue;
|
|
167
|
+
|
|
168
|
+
for (const channelKey of ['iChannel0', 'iChannel1', 'iChannel2', 'iChannel3'] as const) {
|
|
169
|
+
const channelValue = passConfig[channelKey];
|
|
170
|
+
if (!channelValue) continue;
|
|
171
|
+
|
|
172
|
+
const parsed = parseChannelValue(channelValue);
|
|
173
|
+
if (parsed && 'texture' in parsed) {
|
|
174
|
+
texturePathsSet.add(parsed.texture);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Load textures
|
|
180
|
+
const textures: any[] = [];
|
|
181
|
+
const texturePathToName = new Map<string, string>();
|
|
182
|
+
|
|
183
|
+
for (const texturePath of texturePathsSet) {
|
|
184
|
+
const fullPath = `/demos/${demoName}/${texturePath.replace(/^\.\//, '')}`;
|
|
185
|
+
const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
|
|
186
|
+
|
|
187
|
+
if (!actualPath) {
|
|
188
|
+
throw new Error(`Texture not found: ${texturePath} (expected at ${fullPath})`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const imageUrl = await imageFiles[actualPath]();
|
|
192
|
+
const textureFilename = texturePath.split('/').pop()!;
|
|
193
|
+
const textureName = textureFilename.replace(/\.[^.]+$/, '');
|
|
194
|
+
|
|
195
|
+
textures.push({
|
|
196
|
+
name: textureName,
|
|
197
|
+
filename: textureFilename, // Preserve original filename for display
|
|
198
|
+
source: imageUrl,
|
|
199
|
+
filter: 'linear' as const,
|
|
200
|
+
wrap: 'repeat' as const,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
texturePathToName.set(texturePath, textureName);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build passes
|
|
207
|
+
const passes: any = {};
|
|
208
|
+
|
|
209
|
+
for (const passName of passOrder) {
|
|
210
|
+
const passConfig = passConfigs[passName];
|
|
211
|
+
if (!passConfig) continue;
|
|
212
|
+
|
|
213
|
+
const defaultNames: Record<string, string> = {
|
|
214
|
+
Image: 'image.glsl',
|
|
215
|
+
BufferA: 'bufferA.glsl',
|
|
216
|
+
BufferB: 'bufferB.glsl',
|
|
217
|
+
BufferC: 'bufferC.glsl',
|
|
218
|
+
BufferD: 'bufferD.glsl',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const sourceFile = passConfig.source || defaultNames[passName];
|
|
222
|
+
const sourcePath = `/demos/${demoName}/${sourceFile}`;
|
|
223
|
+
const actualSourcePath = findFileCaseInsensitive(glslFiles, sourcePath);
|
|
224
|
+
|
|
225
|
+
if (!actualSourcePath) {
|
|
226
|
+
throw new Error(`Missing shader file: ${sourcePath}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const glslSource = await glslFiles[actualSourcePath]();
|
|
230
|
+
|
|
231
|
+
const channels = [
|
|
232
|
+
normalizeChannel(passConfig.iChannel0, texturePathToName),
|
|
233
|
+
normalizeChannel(passConfig.iChannel1, texturePathToName),
|
|
234
|
+
normalizeChannel(passConfig.iChannel2, texturePathToName),
|
|
235
|
+
normalizeChannel(passConfig.iChannel3, texturePathToName),
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
passes[passName] = {
|
|
239
|
+
name: passName,
|
|
240
|
+
glslSource,
|
|
241
|
+
channels,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!passes.Image) {
|
|
246
|
+
throw new Error(`Demo '${demoName}' must have an Image pass`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const title = config.title ||
|
|
250
|
+
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
251
|
+
const author = config.author || null;
|
|
252
|
+
const description = config.description || null;
|
|
253
|
+
const layout = config.layout || 'tabbed';
|
|
254
|
+
const controls = config.controls ?? true;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
root: `/demos/${demoName}`,
|
|
258
|
+
meta: { title, author, description },
|
|
259
|
+
layout,
|
|
260
|
+
controls,
|
|
261
|
+
commonSource,
|
|
262
|
+
passes,
|
|
263
|
+
textures,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeChannel(channelValue: ChannelValue | undefined, texturePathToName?: Map<string, string>): any {
|
|
268
|
+
if (!channelValue) {
|
|
269
|
+
return { kind: 'none' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Parse string shorthand
|
|
273
|
+
const parsed = parseChannelValue(channelValue);
|
|
274
|
+
if (!parsed) {
|
|
275
|
+
return { kind: 'none' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if ('buffer' in parsed) {
|
|
279
|
+
return {
|
|
280
|
+
kind: 'buffer',
|
|
281
|
+
buffer: parsed.buffer,
|
|
282
|
+
current: !!parsed.current,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if ('texture' in parsed) {
|
|
287
|
+
const textureName = texturePathToName?.get(parsed.texture) || parsed.texture;
|
|
288
|
+
return {
|
|
289
|
+
kind: 'texture',
|
|
290
|
+
name: textureName,
|
|
291
|
+
cubemap: parsed.type === 'cubemap',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if ('keyboard' in parsed) {
|
|
296
|
+
return { kind: 'keyboard' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { kind: 'none' };
|
|
300
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Layer - Type Definitions for Shadertoy Projects
|
|
3
|
+
*
|
|
4
|
+
* Pure TypeScript interfaces matching Shadertoy's mental model.
|
|
5
|
+
* Based on docs/project-spec.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Pass Names (Fixed set matching Shadertoy)
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
export type PassName = 'Image' | 'BufferA' | 'BufferB' | 'BufferC' | 'BufferD';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Channel Definitions (JSON Config Format)
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reference to another buffer pass.
|
|
20
|
+
* By default, reads the previous frame (safe for all cases).
|
|
21
|
+
* Use current: true to read from a buffer that has already run this frame.
|
|
22
|
+
*/
|
|
23
|
+
export interface ChannelJSONBuffer {
|
|
24
|
+
buffer: PassName;
|
|
25
|
+
current?: boolean; // Default: false (read previous frame)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reference to external texture (image file).
|
|
30
|
+
*/
|
|
31
|
+
export interface ChannelJSONTexture {
|
|
32
|
+
texture: string; // Path to image file
|
|
33
|
+
filter?: 'nearest' | 'linear'; // Default: 'linear'
|
|
34
|
+
wrap?: 'clamp' | 'repeat'; // Default: 'repeat'
|
|
35
|
+
type?: '2d' | 'cubemap'; // Default: '2d'. Cubemap uses equirectangular projection.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reference to keyboard texture (runtime-provided).
|
|
40
|
+
*/
|
|
41
|
+
export interface ChannelJSONKeyboard {
|
|
42
|
+
keyboard: true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Union type for channel sources in JSON config (object form).
|
|
47
|
+
*/
|
|
48
|
+
export type ChannelJSONObject =
|
|
49
|
+
| ChannelJSONBuffer
|
|
50
|
+
| ChannelJSONTexture
|
|
51
|
+
| ChannelJSONKeyboard;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Channel value in simplified config format.
|
|
55
|
+
* Can be a string shorthand or full object:
|
|
56
|
+
* - "BufferA", "BufferB", etc. → buffer reference
|
|
57
|
+
* - "keyboard" → keyboard input
|
|
58
|
+
* - "photo.jpg" (with extension) → texture file
|
|
59
|
+
* - { buffer: "BufferA" } → explicit buffer with options
|
|
60
|
+
* - { texture: "photo.jpg", filter: "nearest" } → texture with options
|
|
61
|
+
*/
|
|
62
|
+
export type ChannelValue = string | ChannelJSONObject;
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Config Format (config.json) - Simplified flat format
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pass configuration in simplified format.
|
|
70
|
+
* Channel bindings are directly on the pass object.
|
|
71
|
+
*
|
|
72
|
+
* Example:
|
|
73
|
+
* {
|
|
74
|
+
* "iChannel0": "BufferA",
|
|
75
|
+
* "iChannel1": "photo.jpg",
|
|
76
|
+
* "source": "custom.glsl" // optional
|
|
77
|
+
* }
|
|
78
|
+
*/
|
|
79
|
+
export interface PassConfigSimplified {
|
|
80
|
+
/** Optional custom source file path */
|
|
81
|
+
source?: string;
|
|
82
|
+
/** Channel bindings - string shorthand or full object */
|
|
83
|
+
iChannel0?: ChannelValue;
|
|
84
|
+
iChannel1?: ChannelValue;
|
|
85
|
+
iChannel2?: ChannelValue;
|
|
86
|
+
iChannel3?: ChannelValue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Top-level config.json structure (simplified flat format).
|
|
91
|
+
*
|
|
92
|
+
* Example:
|
|
93
|
+
* {
|
|
94
|
+
* "title": "My Shader",
|
|
95
|
+
* "layout": "split",
|
|
96
|
+
* "controls": true,
|
|
97
|
+
*
|
|
98
|
+
* "BufferA": {
|
|
99
|
+
* "iChannel0": "BufferA"
|
|
100
|
+
* },
|
|
101
|
+
* "Image": {
|
|
102
|
+
* "iChannel0": "BufferA"
|
|
103
|
+
* }
|
|
104
|
+
* }
|
|
105
|
+
*/
|
|
106
|
+
export interface ShadertoyConfig {
|
|
107
|
+
// Metadata (flat, not nested)
|
|
108
|
+
title?: string;
|
|
109
|
+
author?: string;
|
|
110
|
+
description?: string;
|
|
111
|
+
|
|
112
|
+
// Settings
|
|
113
|
+
layout?: 'fullscreen' | 'default' | 'split' | 'tabbed';
|
|
114
|
+
controls?: boolean;
|
|
115
|
+
common?: string;
|
|
116
|
+
|
|
117
|
+
// Passes (at top level)
|
|
118
|
+
Image?: PassConfigSimplified;
|
|
119
|
+
BufferA?: PassConfigSimplified;
|
|
120
|
+
BufferB?: PassConfigSimplified;
|
|
121
|
+
BufferC?: PassConfigSimplified;
|
|
122
|
+
BufferD?: PassConfigSimplified;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// Internal Channel Representation (Normalized)
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalized channel source for engine consumption.
|
|
131
|
+
* All channels are represented as one of these discriminated union variants.
|
|
132
|
+
*/
|
|
133
|
+
export type ChannelSource =
|
|
134
|
+
| { kind: 'none' }
|
|
135
|
+
| { kind: 'buffer'; buffer: PassName; current: boolean }
|
|
136
|
+
| { kind: 'texture'; name: string; cubemap: boolean } // Internal texture ID (e.g., "tex0")
|
|
137
|
+
| { kind: 'keyboard' };
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Exactly 4 channels (iChannel0-3), matching Shadertoy's fixed channel count.
|
|
141
|
+
*/
|
|
142
|
+
export type Channels = [ChannelSource, ChannelSource, ChannelSource, ChannelSource];
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Texture Definitions
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* External 2D texture loaded from image file.
|
|
150
|
+
* Textures are deduplicated by (source, filter, wrap) tuple.
|
|
151
|
+
*/
|
|
152
|
+
export interface ShadertoyTexture2D {
|
|
153
|
+
name: string; // Internal ID (e.g., "tex0", "tex1")
|
|
154
|
+
filename?: string; // Original filename for display (e.g., "texture.png")
|
|
155
|
+
source: string; // Path/URL to image file
|
|
156
|
+
filter: 'nearest' | 'linear';
|
|
157
|
+
wrap: 'clamp' | 'repeat';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// =============================================================================
|
|
161
|
+
// Pass Definition (In-Memory)
|
|
162
|
+
// =============================================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A single shader pass in the rendering pipeline.
|
|
166
|
+
*/
|
|
167
|
+
export interface ShadertoyPass {
|
|
168
|
+
name: PassName;
|
|
169
|
+
glslSource: string; // Full GLSL source code
|
|
170
|
+
channels: Channels; // iChannel0..3
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// Project Metadata
|
|
175
|
+
// =============================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Project metadata (title, author, description).
|
|
179
|
+
*/
|
|
180
|
+
export interface ShadertoyMeta {
|
|
181
|
+
title: string;
|
|
182
|
+
author: string | null;
|
|
183
|
+
description: string | null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// Main Project Definition (Normalized, Engine-Ready)
|
|
188
|
+
// =============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Complete in-memory representation of a Shadertoy project.
|
|
192
|
+
* Produced by loadProject() and consumed by ShadertoyEngine.
|
|
193
|
+
*
|
|
194
|
+
* Guarantees:
|
|
195
|
+
* - passes.Image always exists
|
|
196
|
+
* - All passes have exactly 4 channels (missing → kind: 'none')
|
|
197
|
+
* - Textures are deduplicated
|
|
198
|
+
* - All paths resolved and GLSL loaded
|
|
199
|
+
*/
|
|
200
|
+
export interface ShadertoyProject {
|
|
201
|
+
/**
|
|
202
|
+
* Project root directory path.
|
|
203
|
+
*/
|
|
204
|
+
root: string;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Project metadata.
|
|
208
|
+
*/
|
|
209
|
+
meta: ShadertoyMeta;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Layout mode for the shader viewer.
|
|
213
|
+
*/
|
|
214
|
+
layout: 'fullscreen' | 'default' | 'split' | 'tabbed';
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Whether to show playback controls (play/pause, reset).
|
|
218
|
+
*/
|
|
219
|
+
controls: boolean;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Common GLSL code (prepended to all shaders), or null if none.
|
|
223
|
+
*/
|
|
224
|
+
commonSource: string | null;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Pass definitions.
|
|
228
|
+
* Image is always present, BufferA-D are optional.
|
|
229
|
+
*/
|
|
230
|
+
passes: {
|
|
231
|
+
Image: ShadertoyPass;
|
|
232
|
+
BufferA?: ShadertoyPass;
|
|
233
|
+
BufferB?: ShadertoyPass;
|
|
234
|
+
BufferC?: ShadertoyPass;
|
|
235
|
+
BufferD?: ShadertoyPass;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Deduplicated list of external textures.
|
|
240
|
+
* All ChannelSource with kind: 'texture2D' refer to names in this list.
|
|
241
|
+
*/
|
|
242
|
+
textures: ShadertoyTexture2D[];
|
|
243
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Styles - Global resets and root element styling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: 0;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html, body {
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
background: #f5f5f5;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#app {
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
canvas {
|
|
28
|
+
display: block;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Shader Collection</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
html, body {
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 100%;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
#app {
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div id="app"></div>
|
|
26
|
+
<script type="module" src="./main.ts"></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|