@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,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Layer - Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads Shadertoy projects from disk into normalized ShadertoyProject representation.
|
|
5
|
+
* Handles both single-pass (no config) and multi-pass (with config) projects.
|
|
6
|
+
*
|
|
7
|
+
* Based on docs/project-spec.md
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Helper Functions
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Check if a file exists.
|
|
16
|
+
*/
|
|
17
|
+
async function fileExists(p) {
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(p);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Type guard for PassName.
|
|
28
|
+
*/
|
|
29
|
+
function isPassName(s) {
|
|
30
|
+
return s === 'Image' || s === 'BufferA' || s === 'BufferB' || s === 'BufferC' || s === 'BufferD';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* List all .glsl files in a directory.
|
|
34
|
+
*/
|
|
35
|
+
async function listGlslFiles(root) {
|
|
36
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
37
|
+
return entries
|
|
38
|
+
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.glsl'))
|
|
39
|
+
.map((e) => e.name);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if project has a textures/ directory with files.
|
|
43
|
+
*/
|
|
44
|
+
async function hasTexturesDirWithFiles(root) {
|
|
45
|
+
const dir = path.join(root, 'textures');
|
|
46
|
+
if (!(await fileExists(dir)))
|
|
47
|
+
return false;
|
|
48
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
49
|
+
return entries.some((e) => e.isFile());
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get default source file name for a pass.
|
|
53
|
+
*/
|
|
54
|
+
function defaultSourceForPass(name) {
|
|
55
|
+
switch (name) {
|
|
56
|
+
case 'Image':
|
|
57
|
+
return 'image.glsl';
|
|
58
|
+
case 'BufferA':
|
|
59
|
+
return 'bufferA.glsl';
|
|
60
|
+
case 'BufferB':
|
|
61
|
+
return 'bufferB.glsl';
|
|
62
|
+
case 'BufferC':
|
|
63
|
+
return 'bufferC.glsl';
|
|
64
|
+
case 'BufferD':
|
|
65
|
+
return 'bufferD.glsl';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Main Entry Point
|
|
70
|
+
// =============================================================================
|
|
71
|
+
/**
|
|
72
|
+
* Load a Shadertoy project from disk.
|
|
73
|
+
*
|
|
74
|
+
* Automatically detects:
|
|
75
|
+
* - Single-pass mode (no config, just image.glsl)
|
|
76
|
+
* - Multi-pass mode (config.json present)
|
|
77
|
+
*
|
|
78
|
+
* @param root - Absolute path to project directory
|
|
79
|
+
* @returns Fully normalized ShadertoyProject
|
|
80
|
+
* @throws Error with descriptive message if project is invalid
|
|
81
|
+
*/
|
|
82
|
+
export async function loadProject(root) {
|
|
83
|
+
const configPath = path.join(root, 'config.json');
|
|
84
|
+
const hasConfig = await fileExists(configPath);
|
|
85
|
+
if (hasConfig) {
|
|
86
|
+
// Multi-pass mode: parse config
|
|
87
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
88
|
+
let config;
|
|
89
|
+
try {
|
|
90
|
+
config = JSON.parse(raw);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
throw new Error(`Invalid JSON in config.json at '${root}': ${err?.message ?? String(err)}`);
|
|
94
|
+
}
|
|
95
|
+
return await loadProjectWithConfig(root, config);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Single-pass mode: just image.glsl
|
|
99
|
+
return await loadSinglePassProject(root);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Single-Pass Mode (No Config)
|
|
104
|
+
// =============================================================================
|
|
105
|
+
/**
|
|
106
|
+
* Load a simple single-pass project.
|
|
107
|
+
*
|
|
108
|
+
* Requirements:
|
|
109
|
+
* - Must have image.glsl
|
|
110
|
+
* - Cannot have other .glsl files
|
|
111
|
+
* - Cannot have textures/ directory
|
|
112
|
+
* - No common.glsl allowed
|
|
113
|
+
*
|
|
114
|
+
* @param root - Project directory
|
|
115
|
+
* @returns ShadertoyProject with only Image pass
|
|
116
|
+
*/
|
|
117
|
+
async function loadSinglePassProject(root) {
|
|
118
|
+
const imagePath = path.join(root, 'image.glsl');
|
|
119
|
+
if (!(await fileExists(imagePath))) {
|
|
120
|
+
throw new Error(`Single-pass project at '${root}' requires 'image.glsl'.`);
|
|
121
|
+
}
|
|
122
|
+
// Check for extra GLSL files
|
|
123
|
+
const glslFiles = await listGlslFiles(root);
|
|
124
|
+
const extraGlsl = glslFiles.filter((name) => name !== 'image.glsl');
|
|
125
|
+
if (extraGlsl.length > 0) {
|
|
126
|
+
throw new Error(`Project at '${root}' contains multiple GLSL files (${glslFiles.join(', ')}) but no 'config.json'. Add a config file to use multiple passes.`);
|
|
127
|
+
}
|
|
128
|
+
// Check for textures
|
|
129
|
+
if (await hasTexturesDirWithFiles(root)) {
|
|
130
|
+
throw new Error(`Project at '${root}' uses textures (in 'textures/' folder) but has no 'config.json'. Add a config file to define texture bindings.`);
|
|
131
|
+
}
|
|
132
|
+
// Load shader source
|
|
133
|
+
const imageSource = await fs.readFile(imagePath, 'utf8');
|
|
134
|
+
const title = path.basename(root);
|
|
135
|
+
const project = {
|
|
136
|
+
root,
|
|
137
|
+
meta: {
|
|
138
|
+
title,
|
|
139
|
+
author: null,
|
|
140
|
+
description: null,
|
|
141
|
+
},
|
|
142
|
+
layout: 'default',
|
|
143
|
+
controls: false,
|
|
144
|
+
commonSource: null,
|
|
145
|
+
passes: {
|
|
146
|
+
Image: {
|
|
147
|
+
name: 'Image',
|
|
148
|
+
glslSource: imageSource,
|
|
149
|
+
channels: [
|
|
150
|
+
{ kind: 'none' },
|
|
151
|
+
{ kind: 'none' },
|
|
152
|
+
{ kind: 'none' },
|
|
153
|
+
{ kind: 'none' },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
textures: [],
|
|
158
|
+
};
|
|
159
|
+
return project;
|
|
160
|
+
}
|
|
161
|
+
// =============================================================================
|
|
162
|
+
// Multi-Pass Mode (With Config)
|
|
163
|
+
// =============================================================================
|
|
164
|
+
/**
|
|
165
|
+
* Parse a channel value (string shorthand or object) into normalized ChannelJSONObject.
|
|
166
|
+
*
|
|
167
|
+
* String shortcuts:
|
|
168
|
+
* - "BufferA", "BufferB", etc. → buffer reference
|
|
169
|
+
* - "keyboard" → keyboard input
|
|
170
|
+
* - "photo.jpg" (with extension) → texture file
|
|
171
|
+
*/
|
|
172
|
+
function parseChannelValue(value) {
|
|
173
|
+
if (typeof value === 'string') {
|
|
174
|
+
// Check for buffer names
|
|
175
|
+
if (isPassName(value)) {
|
|
176
|
+
return { buffer: value };
|
|
177
|
+
}
|
|
178
|
+
// Check for keyboard
|
|
179
|
+
if (value === 'keyboard') {
|
|
180
|
+
return { keyboard: true };
|
|
181
|
+
}
|
|
182
|
+
// Assume texture (file path)
|
|
183
|
+
return { texture: value };
|
|
184
|
+
}
|
|
185
|
+
// Already an object
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Load a project with config.json.
|
|
190
|
+
*
|
|
191
|
+
* @param root - Project directory
|
|
192
|
+
* @param config - Parsed JSON config
|
|
193
|
+
* @returns Normalized ShadertoyProject
|
|
194
|
+
*/
|
|
195
|
+
async function loadProjectWithConfig(root, config) {
|
|
196
|
+
// Extract pass configs from top level
|
|
197
|
+
const passConfigs = {
|
|
198
|
+
Image: config.Image,
|
|
199
|
+
BufferA: config.BufferA,
|
|
200
|
+
BufferB: config.BufferB,
|
|
201
|
+
BufferC: config.BufferC,
|
|
202
|
+
BufferD: config.BufferD,
|
|
203
|
+
};
|
|
204
|
+
// Validate: must have Image pass (or be empty config for simple shader)
|
|
205
|
+
const hasAnyPass = passConfigs.Image || passConfigs.BufferA || passConfigs.BufferB ||
|
|
206
|
+
passConfigs.BufferC || passConfigs.BufferD;
|
|
207
|
+
if (!hasAnyPass) {
|
|
208
|
+
// Empty config = simple Image pass with no channels
|
|
209
|
+
passConfigs.Image = {};
|
|
210
|
+
}
|
|
211
|
+
// Resolve commonSource
|
|
212
|
+
let commonSource = null;
|
|
213
|
+
if (config.common) {
|
|
214
|
+
const commonPath = path.join(root, config.common);
|
|
215
|
+
if (!(await fileExists(commonPath))) {
|
|
216
|
+
throw new Error(`Common GLSL file '${config.common}' not found in '${root}'.`);
|
|
217
|
+
}
|
|
218
|
+
commonSource = await fs.readFile(commonPath, 'utf8');
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Check for default common.glsl
|
|
222
|
+
const defaultCommonPath = path.join(root, 'common.glsl');
|
|
223
|
+
if (await fileExists(defaultCommonPath)) {
|
|
224
|
+
commonSource = await fs.readFile(defaultCommonPath, 'utf8');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Texture deduplication map
|
|
228
|
+
const textureMap = new Map();
|
|
229
|
+
/**
|
|
230
|
+
* Register a texture and return its internal name.
|
|
231
|
+
*/
|
|
232
|
+
function registerTexture(j) {
|
|
233
|
+
const filter = j.filter ?? 'linear';
|
|
234
|
+
const wrap = j.wrap ?? 'repeat';
|
|
235
|
+
const key = `${j.texture}|${filter}|${wrap}`;
|
|
236
|
+
let existing = textureMap.get(key);
|
|
237
|
+
if (existing) {
|
|
238
|
+
return existing.name;
|
|
239
|
+
}
|
|
240
|
+
const name = `tex${textureMap.size}`;
|
|
241
|
+
const tex = {
|
|
242
|
+
name,
|
|
243
|
+
source: j.texture,
|
|
244
|
+
filter,
|
|
245
|
+
wrap,
|
|
246
|
+
};
|
|
247
|
+
textureMap.set(key, tex);
|
|
248
|
+
return name;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Parse a channel object into ChannelSource.
|
|
252
|
+
*/
|
|
253
|
+
function parseChannelObject(value, passName, channelKey) {
|
|
254
|
+
// Buffer channel
|
|
255
|
+
if ('buffer' in value) {
|
|
256
|
+
const buf = value.buffer;
|
|
257
|
+
if (!isPassName(buf)) {
|
|
258
|
+
throw new Error(`Invalid buffer name '${buf}' for ${channelKey} in pass '${passName}' at '${root}'.`);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
kind: 'buffer',
|
|
262
|
+
buffer: buf,
|
|
263
|
+
current: !!value.current,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Texture channel
|
|
267
|
+
if ('texture' in value) {
|
|
268
|
+
const internalName = registerTexture(value);
|
|
269
|
+
return {
|
|
270
|
+
kind: 'texture',
|
|
271
|
+
name: internalName,
|
|
272
|
+
cubemap: value.type === 'cubemap',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// Keyboard channel
|
|
276
|
+
if ('keyboard' in value) {
|
|
277
|
+
return { kind: 'keyboard' };
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`Invalid channel object for ${channelKey} in pass '${passName}' at '${root}'.`);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Load a single pass from simplified config.
|
|
283
|
+
*/
|
|
284
|
+
async function loadPass(name, passConfig) {
|
|
285
|
+
if (!passConfig)
|
|
286
|
+
return undefined;
|
|
287
|
+
const sourceRel = passConfig.source ?? defaultSourceForPass(name);
|
|
288
|
+
const sourcePath = path.join(root, sourceRel);
|
|
289
|
+
if (!(await fileExists(sourcePath))) {
|
|
290
|
+
throw new Error(`Source GLSL file for pass '${name}' not found at '${sourceRel}' in '${root}'.`);
|
|
291
|
+
}
|
|
292
|
+
const glslSource = await fs.readFile(sourcePath, 'utf8');
|
|
293
|
+
// Normalize channels (always 4 channels)
|
|
294
|
+
const channelSources = [];
|
|
295
|
+
const channelKeys = ['iChannel0', 'iChannel1', 'iChannel2', 'iChannel3'];
|
|
296
|
+
for (const key of channelKeys) {
|
|
297
|
+
const rawValue = passConfig[key];
|
|
298
|
+
if (!rawValue) {
|
|
299
|
+
channelSources.push({ kind: 'none' });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
// Parse string shorthand or use object directly
|
|
303
|
+
const parsed = parseChannelValue(rawValue);
|
|
304
|
+
if (!parsed) {
|
|
305
|
+
channelSources.push({ kind: 'none' });
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
channelSources.push(parseChannelObject(parsed, name, key));
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
name,
|
|
312
|
+
glslSource,
|
|
313
|
+
channels: channelSources,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// Load all passes
|
|
317
|
+
const imagePass = await loadPass('Image', passConfigs.Image);
|
|
318
|
+
const bufferAPass = await loadPass('BufferA', passConfigs.BufferA);
|
|
319
|
+
const bufferBPass = await loadPass('BufferB', passConfigs.BufferB);
|
|
320
|
+
const bufferCPass = await loadPass('BufferC', passConfigs.BufferC);
|
|
321
|
+
const bufferDPass = await loadPass('BufferD', passConfigs.BufferD);
|
|
322
|
+
// If no Image pass was loaded but we have buffers, that's an error
|
|
323
|
+
if (!imagePass && (bufferAPass || bufferBPass || bufferCPass || bufferDPass)) {
|
|
324
|
+
throw new Error(`config.json at '${root}' has buffers but no Image pass.`);
|
|
325
|
+
}
|
|
326
|
+
// If still no Image pass, create empty one
|
|
327
|
+
if (!imagePass) {
|
|
328
|
+
throw new Error(`config.json at '${root}' must define an Image pass.`);
|
|
329
|
+
}
|
|
330
|
+
// Build metadata
|
|
331
|
+
const title = config.title ?? path.basename(root);
|
|
332
|
+
const author = config.author ?? null;
|
|
333
|
+
const description = config.description ?? null;
|
|
334
|
+
const project = {
|
|
335
|
+
root,
|
|
336
|
+
meta: { title, author, description },
|
|
337
|
+
layout: config.layout ?? 'default',
|
|
338
|
+
controls: config.controls ?? false,
|
|
339
|
+
commonSource,
|
|
340
|
+
passes: {
|
|
341
|
+
Image: imagePass,
|
|
342
|
+
BufferA: bufferAPass,
|
|
343
|
+
BufferB: bufferBPass,
|
|
344
|
+
BufferC: bufferCPass,
|
|
345
|
+
BufferD: bufferDPass,
|
|
346
|
+
},
|
|
347
|
+
textures: Array.from(textureMap.values()),
|
|
348
|
+
};
|
|
349
|
+
return project;
|
|
350
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for loading demo files
|
|
3
|
+
* Called by the generated loader
|
|
4
|
+
*/
|
|
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>;
|
|
7
|
+
//# sourceMappingURL=loaderHelper.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for loading demo files
|
|
3
|
+
* Called by the generated loader
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Case-insensitive file lookup helper.
|
|
7
|
+
* Returns the actual key from the record that matches the path (case-insensitive).
|
|
8
|
+
*/
|
|
9
|
+
function findFileCaseInsensitive(files, path) {
|
|
10
|
+
// First try exact match
|
|
11
|
+
if (path in files)
|
|
12
|
+
return path;
|
|
13
|
+
// Try case-insensitive match
|
|
14
|
+
const lowerPath = path.toLowerCase();
|
|
15
|
+
for (const key of Object.keys(files)) {
|
|
16
|
+
if (key.toLowerCase() === lowerPath) {
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Type guard for PassName.
|
|
24
|
+
*/
|
|
25
|
+
function isPassName(s) {
|
|
26
|
+
return s === 'Image' || s === 'BufferA' || s === 'BufferB' || s === 'BufferC' || s === 'BufferD';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a channel value (string shorthand or object) into normalized ChannelJSONObject.
|
|
30
|
+
*/
|
|
31
|
+
function parseChannelValue(value) {
|
|
32
|
+
if (typeof value === 'string') {
|
|
33
|
+
if (isPassName(value)) {
|
|
34
|
+
return { buffer: value };
|
|
35
|
+
}
|
|
36
|
+
if (value === 'keyboard') {
|
|
37
|
+
return { keyboard: true };
|
|
38
|
+
}
|
|
39
|
+
return { texture: value };
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
export async function loadDemo(demoName, glslFiles, jsonFiles, imageFiles) {
|
|
44
|
+
const configPath = `/demos/${demoName}/config.json`;
|
|
45
|
+
const hasConfig = configPath in jsonFiles;
|
|
46
|
+
if (hasConfig) {
|
|
47
|
+
const config = await jsonFiles[configPath]();
|
|
48
|
+
const hasPassConfigs = config.Image || config.BufferA || config.BufferB ||
|
|
49
|
+
config.BufferC || config.BufferD;
|
|
50
|
+
if (hasPassConfigs) {
|
|
51
|
+
return loadWithConfig(demoName, config, glslFiles, imageFiles);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Config with only settings (layout, controls, etc.) but no passes
|
|
55
|
+
return loadSinglePass(demoName, glslFiles, config);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return loadSinglePass(demoName, glslFiles);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function loadSinglePass(demoName, glslFiles, configOverrides) {
|
|
63
|
+
const imagePath = `/demos/${demoName}/image.glsl`;
|
|
64
|
+
const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
|
|
65
|
+
if (!actualImagePath) {
|
|
66
|
+
throw new Error(`Demo '${demoName}' not found. Expected ${imagePath}`);
|
|
67
|
+
}
|
|
68
|
+
const imageSource = await glslFiles[actualImagePath]();
|
|
69
|
+
const layout = configOverrides?.layout || 'tabbed';
|
|
70
|
+
const controls = configOverrides?.controls ?? true;
|
|
71
|
+
const title = configOverrides?.title ||
|
|
72
|
+
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
73
|
+
return {
|
|
74
|
+
root: `/demos/${demoName}`,
|
|
75
|
+
meta: {
|
|
76
|
+
title,
|
|
77
|
+
author: configOverrides?.author || null,
|
|
78
|
+
description: configOverrides?.description || null,
|
|
79
|
+
},
|
|
80
|
+
layout,
|
|
81
|
+
controls,
|
|
82
|
+
commonSource: null,
|
|
83
|
+
passes: {
|
|
84
|
+
Image: {
|
|
85
|
+
name: 'Image',
|
|
86
|
+
glslSource: imageSource,
|
|
87
|
+
channels: [
|
|
88
|
+
{ kind: 'none' },
|
|
89
|
+
{ kind: 'none' },
|
|
90
|
+
{ kind: 'none' },
|
|
91
|
+
{ kind: 'none' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
textures: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function loadWithConfig(demoName, config, glslFiles, imageFiles) {
|
|
99
|
+
// Extract pass configs from top level
|
|
100
|
+
const passConfigs = {
|
|
101
|
+
Image: config.Image,
|
|
102
|
+
BufferA: config.BufferA,
|
|
103
|
+
BufferB: config.BufferB,
|
|
104
|
+
BufferC: config.BufferC,
|
|
105
|
+
BufferD: config.BufferD,
|
|
106
|
+
};
|
|
107
|
+
// Load common source
|
|
108
|
+
let commonSource = null;
|
|
109
|
+
if (config.common) {
|
|
110
|
+
const commonPath = `/demos/${demoName}/${config.common}`;
|
|
111
|
+
const actualCommonPath = findFileCaseInsensitive(glslFiles, commonPath);
|
|
112
|
+
if (actualCommonPath) {
|
|
113
|
+
commonSource = await glslFiles[actualCommonPath]();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const defaultCommonPath = `/demos/${demoName}/common.glsl`;
|
|
118
|
+
const actualCommonPath = findFileCaseInsensitive(glslFiles, defaultCommonPath);
|
|
119
|
+
if (actualCommonPath) {
|
|
120
|
+
commonSource = await glslFiles[actualCommonPath]();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Collect all texture paths
|
|
124
|
+
const texturePathsSet = new Set();
|
|
125
|
+
const passOrder = ['Image', 'BufferA', 'BufferB', 'BufferC', 'BufferD'];
|
|
126
|
+
for (const passName of passOrder) {
|
|
127
|
+
const passConfig = passConfigs[passName];
|
|
128
|
+
if (!passConfig)
|
|
129
|
+
continue;
|
|
130
|
+
for (const channelKey of ['iChannel0', 'iChannel1', 'iChannel2', 'iChannel3']) {
|
|
131
|
+
const channelValue = passConfig[channelKey];
|
|
132
|
+
if (!channelValue)
|
|
133
|
+
continue;
|
|
134
|
+
const parsed = parseChannelValue(channelValue);
|
|
135
|
+
if (parsed && 'texture' in parsed) {
|
|
136
|
+
texturePathsSet.add(parsed.texture);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Load textures
|
|
141
|
+
const textures = [];
|
|
142
|
+
const texturePathToName = new Map();
|
|
143
|
+
for (const texturePath of texturePathsSet) {
|
|
144
|
+
const fullPath = `/demos/${demoName}/${texturePath.replace(/^\.\//, '')}`;
|
|
145
|
+
const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
|
|
146
|
+
if (!actualPath) {
|
|
147
|
+
throw new Error(`Texture not found: ${texturePath} (expected at ${fullPath})`);
|
|
148
|
+
}
|
|
149
|
+
const imageUrl = await imageFiles[actualPath]();
|
|
150
|
+
const textureFilename = texturePath.split('/').pop();
|
|
151
|
+
const textureName = textureFilename.replace(/\.[^.]+$/, '');
|
|
152
|
+
textures.push({
|
|
153
|
+
name: textureName,
|
|
154
|
+
filename: textureFilename, // Preserve original filename for display
|
|
155
|
+
source: imageUrl,
|
|
156
|
+
filter: 'linear',
|
|
157
|
+
wrap: 'repeat',
|
|
158
|
+
});
|
|
159
|
+
texturePathToName.set(texturePath, textureName);
|
|
160
|
+
}
|
|
161
|
+
// Build passes
|
|
162
|
+
const passes = {};
|
|
163
|
+
for (const passName of passOrder) {
|
|
164
|
+
const passConfig = passConfigs[passName];
|
|
165
|
+
if (!passConfig)
|
|
166
|
+
continue;
|
|
167
|
+
const defaultNames = {
|
|
168
|
+
Image: 'image.glsl',
|
|
169
|
+
BufferA: 'bufferA.glsl',
|
|
170
|
+
BufferB: 'bufferB.glsl',
|
|
171
|
+
BufferC: 'bufferC.glsl',
|
|
172
|
+
BufferD: 'bufferD.glsl',
|
|
173
|
+
};
|
|
174
|
+
const sourceFile = passConfig.source || defaultNames[passName];
|
|
175
|
+
const sourcePath = `/demos/${demoName}/${sourceFile}`;
|
|
176
|
+
const actualSourcePath = findFileCaseInsensitive(glslFiles, sourcePath);
|
|
177
|
+
if (!actualSourcePath) {
|
|
178
|
+
throw new Error(`Missing shader file: ${sourcePath}`);
|
|
179
|
+
}
|
|
180
|
+
const glslSource = await glslFiles[actualSourcePath]();
|
|
181
|
+
const channels = [
|
|
182
|
+
normalizeChannel(passConfig.iChannel0, texturePathToName),
|
|
183
|
+
normalizeChannel(passConfig.iChannel1, texturePathToName),
|
|
184
|
+
normalizeChannel(passConfig.iChannel2, texturePathToName),
|
|
185
|
+
normalizeChannel(passConfig.iChannel3, texturePathToName),
|
|
186
|
+
];
|
|
187
|
+
passes[passName] = {
|
|
188
|
+
name: passName,
|
|
189
|
+
glslSource,
|
|
190
|
+
channels,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (!passes.Image) {
|
|
194
|
+
throw new Error(`Demo '${demoName}' must have an Image pass`);
|
|
195
|
+
}
|
|
196
|
+
const title = config.title ||
|
|
197
|
+
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
198
|
+
const author = config.author || null;
|
|
199
|
+
const description = config.description || null;
|
|
200
|
+
const layout = config.layout || 'tabbed';
|
|
201
|
+
const controls = config.controls ?? true;
|
|
202
|
+
return {
|
|
203
|
+
root: `/demos/${demoName}`,
|
|
204
|
+
meta: { title, author, description },
|
|
205
|
+
layout,
|
|
206
|
+
controls,
|
|
207
|
+
commonSource,
|
|
208
|
+
passes,
|
|
209
|
+
textures,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function normalizeChannel(channelValue, texturePathToName) {
|
|
213
|
+
if (!channelValue) {
|
|
214
|
+
return { kind: 'none' };
|
|
215
|
+
}
|
|
216
|
+
// Parse string shorthand
|
|
217
|
+
const parsed = parseChannelValue(channelValue);
|
|
218
|
+
if (!parsed) {
|
|
219
|
+
return { kind: 'none' };
|
|
220
|
+
}
|
|
221
|
+
if ('buffer' in parsed) {
|
|
222
|
+
return {
|
|
223
|
+
kind: 'buffer',
|
|
224
|
+
buffer: parsed.buffer,
|
|
225
|
+
current: !!parsed.current,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if ('texture' in parsed) {
|
|
229
|
+
const textureName = texturePathToName?.get(parsed.texture) || parsed.texture;
|
|
230
|
+
return {
|
|
231
|
+
kind: 'texture',
|
|
232
|
+
name: textureName,
|
|
233
|
+
cubemap: parsed.type === 'cubemap',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if ('keyboard' in parsed) {
|
|
237
|
+
return { kind: 'keyboard' };
|
|
238
|
+
}
|
|
239
|
+
return { kind: 'none' };
|
|
240
|
+
}
|