@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,23 @@
|
|
|
1
|
+
// Auto-generated - DO NOT EDIT
|
|
2
|
+
import { loadDemo } from './loaderHelper';
|
|
3
|
+
import { ShadertoyConfig } from './types';
|
|
4
|
+
|
|
5
|
+
export const DEMO_NAME = 'course/day5/torus-analytical';
|
|
6
|
+
|
|
7
|
+
export async function loadDemoProject() {
|
|
8
|
+
const glslFiles = import.meta.glob<string>('/demos/course/day5/torus-analytical/**/*.glsl', {
|
|
9
|
+
query: '?raw',
|
|
10
|
+
import: 'default',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const jsonFiles = import.meta.glob<ShadertoyConfig>('/demos/course/day5/torus-analytical/**/*.json', {
|
|
14
|
+
import: 'default',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const imageFiles = import.meta.glob<string>('/demos/course/day5/torus-analytical/**/*.{jpg,jpeg,png,gif,webp,bmp}', {
|
|
18
|
+
query: '?url',
|
|
19
|
+
import: 'default',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return loadDemo(DEMO_NAME, glslFiles, jsonFiles, imageFiles);
|
|
23
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
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
|
+
|
|
10
|
+
import { promises as fs } from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import {
|
|
13
|
+
PassName,
|
|
14
|
+
ChannelSource,
|
|
15
|
+
Channels,
|
|
16
|
+
ChannelValue,
|
|
17
|
+
ChannelJSONObject,
|
|
18
|
+
ShadertoyConfig,
|
|
19
|
+
ShadertoyPass,
|
|
20
|
+
ShadertoyProject,
|
|
21
|
+
ShadertoyTexture2D,
|
|
22
|
+
PassConfigSimplified,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Helper Functions
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a file exists.
|
|
31
|
+
*/
|
|
32
|
+
async function fileExists(p: string): Promise<boolean> {
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(p);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Type guard for PassName.
|
|
43
|
+
*/
|
|
44
|
+
function isPassName(s: string): s is PassName {
|
|
45
|
+
return s === 'Image' || s === 'BufferA' || s === 'BufferB' || s === 'BufferC' || s === 'BufferD';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List all .glsl files in a directory.
|
|
50
|
+
*/
|
|
51
|
+
async function listGlslFiles(root: string): Promise<string[]> {
|
|
52
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
53
|
+
return entries
|
|
54
|
+
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.glsl'))
|
|
55
|
+
.map((e) => e.name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if project has a textures/ directory with files.
|
|
60
|
+
*/
|
|
61
|
+
async function hasTexturesDirWithFiles(root: string): Promise<boolean> {
|
|
62
|
+
const dir = path.join(root, 'textures');
|
|
63
|
+
if (!(await fileExists(dir))) return false;
|
|
64
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
65
|
+
return entries.some((e) => e.isFile());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get default source file name for a pass.
|
|
70
|
+
*/
|
|
71
|
+
function defaultSourceForPass(name: PassName): string {
|
|
72
|
+
switch (name) {
|
|
73
|
+
case 'Image':
|
|
74
|
+
return 'image.glsl';
|
|
75
|
+
case 'BufferA':
|
|
76
|
+
return 'bufferA.glsl';
|
|
77
|
+
case 'BufferB':
|
|
78
|
+
return 'bufferB.glsl';
|
|
79
|
+
case 'BufferC':
|
|
80
|
+
return 'bufferC.glsl';
|
|
81
|
+
case 'BufferD':
|
|
82
|
+
return 'bufferD.glsl';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Main Entry Point
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load a Shadertoy project from disk.
|
|
92
|
+
*
|
|
93
|
+
* Automatically detects:
|
|
94
|
+
* - Single-pass mode (no config, just image.glsl)
|
|
95
|
+
* - Multi-pass mode (config.json present)
|
|
96
|
+
*
|
|
97
|
+
* @param root - Absolute path to project directory
|
|
98
|
+
* @returns Fully normalized ShadertoyProject
|
|
99
|
+
* @throws Error with descriptive message if project is invalid
|
|
100
|
+
*/
|
|
101
|
+
export async function loadProject(root: string): Promise<ShadertoyProject> {
|
|
102
|
+
const configPath = path.join(root, 'config.json');
|
|
103
|
+
const hasConfig = await fileExists(configPath);
|
|
104
|
+
|
|
105
|
+
if (hasConfig) {
|
|
106
|
+
// Multi-pass mode: parse config
|
|
107
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
108
|
+
let config: ShadertoyConfig;
|
|
109
|
+
try {
|
|
110
|
+
config = JSON.parse(raw);
|
|
111
|
+
} catch (err: any) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Invalid JSON in config.json at '${root}': ${err?.message ?? String(err)}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return await loadProjectWithConfig(root, config);
|
|
117
|
+
} else {
|
|
118
|
+
// Single-pass mode: just image.glsl
|
|
119
|
+
return await loadSinglePassProject(root);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// Single-Pass Mode (No Config)
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load a simple single-pass project.
|
|
129
|
+
*
|
|
130
|
+
* Requirements:
|
|
131
|
+
* - Must have image.glsl
|
|
132
|
+
* - Cannot have other .glsl files
|
|
133
|
+
* - Cannot have textures/ directory
|
|
134
|
+
* - No common.glsl allowed
|
|
135
|
+
*
|
|
136
|
+
* @param root - Project directory
|
|
137
|
+
* @returns ShadertoyProject with only Image pass
|
|
138
|
+
*/
|
|
139
|
+
async function loadSinglePassProject(root: string): Promise<ShadertoyProject> {
|
|
140
|
+
const imagePath = path.join(root, 'image.glsl');
|
|
141
|
+
if (!(await fileExists(imagePath))) {
|
|
142
|
+
throw new Error(`Single-pass project at '${root}' requires 'image.glsl'.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check for extra GLSL files
|
|
146
|
+
const glslFiles = await listGlslFiles(root);
|
|
147
|
+
const extraGlsl = glslFiles.filter((name) => name !== 'image.glsl');
|
|
148
|
+
if (extraGlsl.length > 0) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Project at '${root}' contains multiple GLSL files (${glslFiles.join(
|
|
151
|
+
', '
|
|
152
|
+
)}) but no 'config.json'. Add a config file to use multiple passes.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check for textures
|
|
157
|
+
if (await hasTexturesDirWithFiles(root)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Project at '${root}' uses textures (in 'textures/' folder) but has no 'config.json'. Add a config file to define texture bindings.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Load shader source
|
|
164
|
+
const imageSource = await fs.readFile(imagePath, 'utf8');
|
|
165
|
+
const title = path.basename(root);
|
|
166
|
+
|
|
167
|
+
const project: ShadertoyProject = {
|
|
168
|
+
root,
|
|
169
|
+
meta: {
|
|
170
|
+
title,
|
|
171
|
+
author: null,
|
|
172
|
+
description: null,
|
|
173
|
+
},
|
|
174
|
+
layout: 'default',
|
|
175
|
+
controls: false,
|
|
176
|
+
commonSource: null,
|
|
177
|
+
passes: {
|
|
178
|
+
Image: {
|
|
179
|
+
name: 'Image',
|
|
180
|
+
glslSource: imageSource,
|
|
181
|
+
channels: [
|
|
182
|
+
{ kind: 'none' },
|
|
183
|
+
{ kind: 'none' },
|
|
184
|
+
{ kind: 'none' },
|
|
185
|
+
{ kind: 'none' },
|
|
186
|
+
] as Channels,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
textures: [],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return project;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Multi-Pass Mode (With Config)
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse a channel value (string shorthand or object) into normalized ChannelJSONObject.
|
|
201
|
+
*
|
|
202
|
+
* String shortcuts:
|
|
203
|
+
* - "BufferA", "BufferB", etc. → buffer reference
|
|
204
|
+
* - "keyboard" → keyboard input
|
|
205
|
+
* - "photo.jpg" (with extension) → texture file
|
|
206
|
+
*/
|
|
207
|
+
function parseChannelValue(value: ChannelValue): ChannelJSONObject | null {
|
|
208
|
+
if (typeof value === 'string') {
|
|
209
|
+
// Check for buffer names
|
|
210
|
+
if (isPassName(value)) {
|
|
211
|
+
return { buffer: value };
|
|
212
|
+
}
|
|
213
|
+
// Check for keyboard
|
|
214
|
+
if (value === 'keyboard') {
|
|
215
|
+
return { keyboard: true };
|
|
216
|
+
}
|
|
217
|
+
// Assume texture (file path)
|
|
218
|
+
return { texture: value };
|
|
219
|
+
}
|
|
220
|
+
// Already an object
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Load a project with config.json.
|
|
226
|
+
*
|
|
227
|
+
* @param root - Project directory
|
|
228
|
+
* @param config - Parsed JSON config
|
|
229
|
+
* @returns Normalized ShadertoyProject
|
|
230
|
+
*/
|
|
231
|
+
async function loadProjectWithConfig(root: string, config: ShadertoyConfig): Promise<ShadertoyProject> {
|
|
232
|
+
// Extract pass configs from top level
|
|
233
|
+
const passConfigs = {
|
|
234
|
+
Image: config.Image,
|
|
235
|
+
BufferA: config.BufferA,
|
|
236
|
+
BufferB: config.BufferB,
|
|
237
|
+
BufferC: config.BufferC,
|
|
238
|
+
BufferD: config.BufferD,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Validate: must have Image pass (or be empty config for simple shader)
|
|
242
|
+
const hasAnyPass = passConfigs.Image || passConfigs.BufferA || passConfigs.BufferB ||
|
|
243
|
+
passConfigs.BufferC || passConfigs.BufferD;
|
|
244
|
+
|
|
245
|
+
if (!hasAnyPass) {
|
|
246
|
+
// Empty config = simple Image pass with no channels
|
|
247
|
+
passConfigs.Image = {};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Resolve commonSource
|
|
251
|
+
let commonSource: string | null = null;
|
|
252
|
+
if (config.common) {
|
|
253
|
+
const commonPath = path.join(root, config.common);
|
|
254
|
+
if (!(await fileExists(commonPath))) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Common GLSL file '${config.common}' not found in '${root}'.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
commonSource = await fs.readFile(commonPath, 'utf8');
|
|
260
|
+
} else {
|
|
261
|
+
// Check for default common.glsl
|
|
262
|
+
const defaultCommonPath = path.join(root, 'common.glsl');
|
|
263
|
+
if (await fileExists(defaultCommonPath)) {
|
|
264
|
+
commonSource = await fs.readFile(defaultCommonPath, 'utf8');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Texture deduplication map
|
|
269
|
+
const textureMap = new Map<string, ShadertoyTexture2D>();
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Register a texture and return its internal name.
|
|
273
|
+
*/
|
|
274
|
+
function registerTexture(j: { texture: string; filter?: 'nearest' | 'linear'; wrap?: 'clamp' | 'repeat' }): string {
|
|
275
|
+
const filter = j.filter ?? 'linear';
|
|
276
|
+
const wrap = j.wrap ?? 'repeat';
|
|
277
|
+
const key = `${j.texture}|${filter}|${wrap}`;
|
|
278
|
+
|
|
279
|
+
let existing = textureMap.get(key);
|
|
280
|
+
if (existing) {
|
|
281
|
+
return existing.name;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const name = `tex${textureMap.size}`;
|
|
285
|
+
const tex: ShadertoyTexture2D = {
|
|
286
|
+
name,
|
|
287
|
+
source: j.texture,
|
|
288
|
+
filter,
|
|
289
|
+
wrap,
|
|
290
|
+
};
|
|
291
|
+
textureMap.set(key, tex);
|
|
292
|
+
return name;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse a channel object into ChannelSource.
|
|
297
|
+
*/
|
|
298
|
+
function parseChannelObject(value: ChannelJSONObject, passName: PassName, channelKey: string): ChannelSource {
|
|
299
|
+
// Buffer channel
|
|
300
|
+
if ('buffer' in value) {
|
|
301
|
+
const buf = value.buffer;
|
|
302
|
+
if (!isPassName(buf)) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Invalid buffer name '${buf}' for ${channelKey} in pass '${passName}' at '${root}'.`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
kind: 'buffer',
|
|
309
|
+
buffer: buf,
|
|
310
|
+
current: !!value.current,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Texture channel
|
|
315
|
+
if ('texture' in value) {
|
|
316
|
+
const internalName = registerTexture(value);
|
|
317
|
+
return {
|
|
318
|
+
kind: 'texture',
|
|
319
|
+
name: internalName,
|
|
320
|
+
cubemap: value.type === 'cubemap',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Keyboard channel
|
|
325
|
+
if ('keyboard' in value) {
|
|
326
|
+
return { kind: 'keyboard' };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Invalid channel object for ${channelKey} in pass '${passName}' at '${root}'.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Load a single pass from simplified config.
|
|
336
|
+
*/
|
|
337
|
+
async function loadPass(
|
|
338
|
+
name: PassName,
|
|
339
|
+
passConfig: PassConfigSimplified | undefined
|
|
340
|
+
): Promise<ShadertoyPass | undefined> {
|
|
341
|
+
if (!passConfig) return undefined;
|
|
342
|
+
|
|
343
|
+
const sourceRel = passConfig.source ?? defaultSourceForPass(name);
|
|
344
|
+
const sourcePath = path.join(root, sourceRel);
|
|
345
|
+
|
|
346
|
+
if (!(await fileExists(sourcePath))) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Source GLSL file for pass '${name}' not found at '${sourceRel}' in '${root}'.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const glslSource = await fs.readFile(sourcePath, 'utf8');
|
|
353
|
+
|
|
354
|
+
// Normalize channels (always 4 channels)
|
|
355
|
+
const channelSources: ChannelSource[] = [];
|
|
356
|
+
const channelKeys = ['iChannel0', 'iChannel1', 'iChannel2', 'iChannel3'] as const;
|
|
357
|
+
|
|
358
|
+
for (const key of channelKeys) {
|
|
359
|
+
const rawValue = passConfig[key];
|
|
360
|
+
if (!rawValue) {
|
|
361
|
+
channelSources.push({ kind: 'none' });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Parse string shorthand or use object directly
|
|
366
|
+
const parsed = parseChannelValue(rawValue);
|
|
367
|
+
if (!parsed) {
|
|
368
|
+
channelSources.push({ kind: 'none' });
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
channelSources.push(parseChannelObject(parsed, name, key));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
name,
|
|
377
|
+
glslSource,
|
|
378
|
+
channels: channelSources as Channels,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Load all passes
|
|
383
|
+
const imagePass = await loadPass('Image', passConfigs.Image);
|
|
384
|
+
const bufferAPass = await loadPass('BufferA', passConfigs.BufferA);
|
|
385
|
+
const bufferBPass = await loadPass('BufferB', passConfigs.BufferB);
|
|
386
|
+
const bufferCPass = await loadPass('BufferC', passConfigs.BufferC);
|
|
387
|
+
const bufferDPass = await loadPass('BufferD', passConfigs.BufferD);
|
|
388
|
+
|
|
389
|
+
// If no Image pass was loaded but we have buffers, that's an error
|
|
390
|
+
if (!imagePass && (bufferAPass || bufferBPass || bufferCPass || bufferDPass)) {
|
|
391
|
+
throw new Error(`config.json at '${root}' has buffers but no Image pass.`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// If still no Image pass, create empty one
|
|
395
|
+
if (!imagePass) {
|
|
396
|
+
throw new Error(`config.json at '${root}' must define an Image pass.`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Build metadata
|
|
400
|
+
const title = config.title ?? path.basename(root);
|
|
401
|
+
const author = config.author ?? null;
|
|
402
|
+
const description = config.description ?? null;
|
|
403
|
+
|
|
404
|
+
const project: ShadertoyProject = {
|
|
405
|
+
root,
|
|
406
|
+
meta: { title, author, description },
|
|
407
|
+
layout: config.layout ?? 'default',
|
|
408
|
+
controls: config.controls ?? false,
|
|
409
|
+
commonSource,
|
|
410
|
+
passes: {
|
|
411
|
+
Image: imagePass,
|
|
412
|
+
BufferA: bufferAPass,
|
|
413
|
+
BufferB: bufferBPass,
|
|
414
|
+
BufferC: bufferCPass,
|
|
415
|
+
BufferD: bufferDPass,
|
|
416
|
+
},
|
|
417
|
+
textures: Array.from(textureMap.values()),
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
return project;
|
|
421
|
+
}
|