@stevejtrettel/shader-sandbox 0.1.2 → 0.1.4
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 +259 -235
- package/bin/cli.js +106 -14
- package/dist-lib/app/App.d.ts +143 -15
- package/dist-lib/app/App.d.ts.map +1 -1
- package/dist-lib/app/App.js +1343 -108
- package/dist-lib/app/app.css +349 -24
- package/dist-lib/app/types.d.ts +48 -5
- package/dist-lib/app/types.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.d.ts +2 -2
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.js +1 -1
- package/dist-lib/editor/editor-panel.css +55 -32
- package/dist-lib/editor/prism-editor.css +16 -16
- package/dist-lib/embed.js +1 -1
- package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
- package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShaderEngine.js +1523 -0
- package/dist-lib/engine/glHelpers.d.ts +24 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -1
- package/dist-lib/engine/glHelpers.js +88 -0
- package/dist-lib/engine/std140.d.ts +47 -0
- package/dist-lib/engine/std140.d.ts.map +1 -0
- package/dist-lib/engine/std140.js +119 -0
- package/dist-lib/engine/types.d.ts +55 -5
- package/dist-lib/engine/types.d.ts.map +1 -1
- package/dist-lib/engine/types.js +1 -1
- package/dist-lib/index.d.ts +4 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
- package/dist-lib/layouts/SplitLayout.js +3 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
- package/dist-lib/layouts/UILayout.d.ts +55 -0
- package/dist-lib/layouts/UILayout.d.ts.map +1 -0
- package/dist-lib/layouts/UILayout.js +147 -0
- package/dist-lib/layouts/default.css +2 -2
- package/dist-lib/layouts/index.d.ts +11 -1
- package/dist-lib/layouts/index.d.ts.map +1 -1
- package/dist-lib/layouts/index.js +17 -1
- package/dist-lib/layouts/split.css +33 -31
- package/dist-lib/layouts/tabbed.css +127 -74
- package/dist-lib/layouts/types.d.ts +14 -3
- package/dist-lib/layouts/types.d.ts.map +1 -1
- package/dist-lib/main.js +33 -0
- package/dist-lib/project/configHelpers.d.ts +45 -0
- package/dist-lib/project/configHelpers.d.ts.map +1 -0
- package/dist-lib/project/configHelpers.js +196 -0
- package/dist-lib/project/generatedLoader.d.ts +2 -2
- package/dist-lib/project/generatedLoader.d.ts.map +1 -1
- package/dist-lib/project/generatedLoader.js +23 -5
- package/dist-lib/project/loadProject.d.ts +6 -6
- package/dist-lib/project/loadProject.d.ts.map +1 -1
- package/dist-lib/project/loadProject.js +396 -144
- package/dist-lib/project/loaderHelper.d.ts +4 -4
- package/dist-lib/project/loaderHelper.d.ts.map +1 -1
- package/dist-lib/project/loaderHelper.js +278 -116
- package/dist-lib/project/types.d.ts +292 -13
- package/dist-lib/project/types.d.ts.map +1 -1
- package/dist-lib/project/types.js +13 -1
- package/dist-lib/styles/base.css +5 -1
- package/dist-lib/uniforms/UniformControls.d.ts +60 -0
- package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformControls.js +518 -0
- package/dist-lib/uniforms/UniformStore.d.ts +74 -0
- package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformStore.js +145 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformsPanel.js +124 -0
- package/dist-lib/uniforms/index.d.ts +11 -0
- package/dist-lib/uniforms/index.d.ts.map +1 -0
- package/dist-lib/uniforms/index.js +8 -0
- package/package.json +16 -1
- package/src/app/App.ts +1469 -126
- package/src/app/app.css +349 -24
- package/src/app/types.ts +53 -5
- package/src/editor/EditorPanel.ts +5 -5
- package/src/editor/editor-panel.css +55 -32
- package/src/editor/prism-editor.css +16 -16
- package/src/embed.ts +1 -1
- package/src/engine/ShaderEngine.ts +1934 -0
- package/src/engine/glHelpers.ts +117 -0
- package/src/engine/std140.ts +136 -0
- package/src/engine/types.ts +69 -5
- package/src/index.ts +4 -3
- package/src/layouts/SplitLayout.ts +8 -3
- package/src/layouts/TabbedLayout.ts +3 -3
- package/src/layouts/UILayout.ts +185 -0
- package/src/layouts/default.css +2 -2
- package/src/layouts/index.ts +20 -1
- package/src/layouts/split.css +33 -31
- package/src/layouts/tabbed.css +127 -74
- package/src/layouts/types.ts +19 -3
- package/src/layouts/ui.css +289 -0
- package/src/main.ts +39 -1
- package/src/project/configHelpers.ts +225 -0
- package/src/project/generatedLoader.ts +27 -6
- package/src/project/loadProject.ts +459 -173
- package/src/project/loaderHelper.ts +377 -130
- package/src/project/types.ts +360 -14
- package/src/styles/base.css +5 -1
- package/src/styles/theme.css +292 -0
- package/src/uniforms/UniformControls.ts +660 -0
- package/src/uniforms/UniformStore.ts +166 -0
- package/src/uniforms/UniformsPanel.ts +163 -0
- package/src/uniforms/index.ts +13 -0
- package/src/uniforms/uniform-controls.css +342 -0
- package/src/uniforms/uniforms-panel.css +277 -0
- package/templates/shaders/example-buffer/config.json +1 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
- package/dist-lib/engine/ShadertoyEngine.js +0 -704
- package/src/engine/ShadertoyEngine.ts +0 -929
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Helper functions for loading demo files
|
|
3
|
-
* Called by the generated loader
|
|
2
|
+
* Helper functions for loading demo files in the browser.
|
|
3
|
+
* Called by the generated loader (Vite import.meta.glob).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
ShaderProject,
|
|
8
|
+
ProjectConfig,
|
|
9
9
|
PassName,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
ChannelSource,
|
|
11
|
+
Channels,
|
|
12
|
+
ShaderTexture2D,
|
|
13
|
+
UniformDefinitions,
|
|
14
|
+
DemoScriptHooks,
|
|
15
|
+
StandardBufferConfig,
|
|
12
16
|
} from './types';
|
|
17
|
+
import {
|
|
18
|
+
parseChannelValue,
|
|
19
|
+
defaultSourceForPass,
|
|
20
|
+
validateConfig,
|
|
21
|
+
PASS_ORDER,
|
|
22
|
+
BUFFER_PASS_NAMES,
|
|
23
|
+
CHANNEL_KEYS,
|
|
24
|
+
DEFAULT_LAYOUT,
|
|
25
|
+
DEFAULT_CONTROLS,
|
|
26
|
+
DEFAULT_THEME,
|
|
27
|
+
} from './configHelpers';
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Case-Insensitive File Lookup
|
|
31
|
+
// =============================================================================
|
|
13
32
|
|
|
14
33
|
/**
|
|
15
34
|
* Case-insensitive file lookup helper.
|
|
@@ -19,10 +38,7 @@ function findFileCaseInsensitive<T>(
|
|
|
19
38
|
files: Record<string, T>,
|
|
20
39
|
path: string
|
|
21
40
|
): string | null {
|
|
22
|
-
// First try exact match
|
|
23
41
|
if (path in files) return path;
|
|
24
|
-
|
|
25
|
-
// Try case-insensitive match
|
|
26
42
|
const lowerPath = path.toLowerCase();
|
|
27
43
|
for (const key of Object.keys(files)) {
|
|
28
44
|
if (key.toLowerCase() === lowerPath) {
|
|
@@ -32,61 +48,186 @@ function findFileCaseInsensitive<T>(
|
|
|
32
48
|
return null;
|
|
33
49
|
}
|
|
34
50
|
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Script Loading
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
35
55
|
/**
|
|
36
|
-
*
|
|
56
|
+
* Load script.js from a demo folder if present.
|
|
37
57
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
58
|
+
async function loadScript(
|
|
59
|
+
demoPath: string,
|
|
60
|
+
scriptFiles?: Record<string, () => Promise<any>>
|
|
61
|
+
): Promise<DemoScriptHooks | null> {
|
|
62
|
+
if (!scriptFiles) return null;
|
|
63
|
+
|
|
64
|
+
const scriptPath = `${demoPath}/script.js`;
|
|
65
|
+
const actualPath = findFileCaseInsensitive(scriptFiles, scriptPath);
|
|
66
|
+
if (!actualPath) return null;
|
|
67
|
+
|
|
68
|
+
const mod = await scriptFiles[actualPath]();
|
|
69
|
+
const hooks: DemoScriptHooks = {};
|
|
70
|
+
if (typeof mod.setup === 'function') hooks.setup = mod.setup;
|
|
71
|
+
if (typeof mod.onFrame === 'function') hooks.onFrame = mod.onFrame;
|
|
72
|
+
|
|
73
|
+
return (hooks.setup || hooks.onFrame) ? hooks : null;
|
|
40
74
|
}
|
|
41
75
|
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Common Source Loading
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
42
80
|
/**
|
|
43
|
-
*
|
|
81
|
+
* Load common.glsl source (explicit path or default).
|
|
44
82
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
async function loadCommonSource(
|
|
84
|
+
demoPath: string,
|
|
85
|
+
glslFiles: Record<string, () => Promise<string>>,
|
|
86
|
+
commonPath?: string
|
|
87
|
+
): Promise<string | null> {
|
|
88
|
+
if (commonPath) {
|
|
89
|
+
const fullPath = `${demoPath}/${commonPath}`;
|
|
90
|
+
const actualPath = findFileCaseInsensitive(glslFiles, fullPath);
|
|
91
|
+
return actualPath ? await glslFiles[actualPath]() : null;
|
|
92
|
+
}
|
|
93
|
+
// Check for default common.glsl
|
|
94
|
+
const defaultPath = `${demoPath}/common.glsl`;
|
|
95
|
+
const actualPath = findFileCaseInsensitive(glslFiles, defaultPath);
|
|
96
|
+
return actualPath ? await glslFiles[actualPath]() : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// Channel Normalization
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a channel value into a ChannelSource.
|
|
105
|
+
*/
|
|
106
|
+
function normalizeChannel(
|
|
107
|
+
channelValue: any,
|
|
108
|
+
texturePathToName?: Map<string, string>
|
|
109
|
+
): ChannelSource {
|
|
110
|
+
if (!channelValue) return { kind: 'none' };
|
|
111
|
+
|
|
112
|
+
const parsed = parseChannelValue(channelValue);
|
|
113
|
+
if (!parsed) return { kind: 'none' };
|
|
114
|
+
|
|
115
|
+
if ('buffer' in parsed) {
|
|
116
|
+
return { kind: 'buffer', buffer: parsed.buffer, current: !!parsed.current };
|
|
117
|
+
}
|
|
118
|
+
if ('texture' in parsed) {
|
|
119
|
+
const textureName = texturePathToName?.get(parsed.texture) || parsed.texture;
|
|
120
|
+
return { kind: 'texture', name: textureName, cubemap: parsed.type === 'cubemap' };
|
|
121
|
+
}
|
|
122
|
+
if ('keyboard' in parsed) return { kind: 'keyboard' };
|
|
123
|
+
if ('audio' in parsed) return { kind: 'audio' };
|
|
124
|
+
if ('webcam' in parsed) return { kind: 'webcam' };
|
|
125
|
+
if ('video' in parsed) return { kind: 'video', src: (parsed as any).video };
|
|
126
|
+
if ('script' in parsed) return { kind: 'script', name: (parsed as any).script };
|
|
127
|
+
|
|
128
|
+
return { kind: 'none' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Named Buffers Normalization (Standard Mode)
|
|
133
|
+
// =============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Normalize buffers config: array shorthand to object form.
|
|
137
|
+
*/
|
|
138
|
+
function normalizeBuffersConfig(
|
|
139
|
+
buffers: string[] | Record<string, StandardBufferConfig> | undefined
|
|
140
|
+
): Record<string, StandardBufferConfig> {
|
|
141
|
+
if (!buffers) return {};
|
|
142
|
+
if (Array.isArray(buffers)) {
|
|
143
|
+
const result: Record<string, StandardBufferConfig> = {};
|
|
144
|
+
for (const name of buffers) {
|
|
145
|
+
result[name] = {};
|
|
52
146
|
}
|
|
53
|
-
return
|
|
147
|
+
return result;
|
|
54
148
|
}
|
|
55
|
-
return
|
|
149
|
+
return buffers;
|
|
56
150
|
}
|
|
57
151
|
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// Title Helper
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate a display title from a demo path.
|
|
158
|
+
* e.g. "./demos/my-shader" → "My Shader"
|
|
159
|
+
*/
|
|
160
|
+
function titleFromPath(demoPath: string): string {
|
|
161
|
+
const demoName = demoPath.split('/').pop() || demoPath;
|
|
162
|
+
return demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// Main Entry Point
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
58
169
|
export async function loadDemo(
|
|
59
170
|
demoPath: string,
|
|
60
171
|
glslFiles: Record<string, () => Promise<string>>,
|
|
61
|
-
jsonFiles: Record<string, () => Promise<
|
|
62
|
-
imageFiles: Record<string, () => Promise<string
|
|
63
|
-
)
|
|
64
|
-
|
|
172
|
+
jsonFiles: Record<string, () => Promise<ProjectConfig>>,
|
|
173
|
+
imageFiles: Record<string, () => Promise<string>>,
|
|
174
|
+
scriptFiles?: Record<string, () => Promise<any>>
|
|
175
|
+
): Promise<ShaderProject> {
|
|
176
|
+
// Normalize path
|
|
65
177
|
const normalizedPath = demoPath.startsWith('./') ? demoPath : `./${demoPath}`;
|
|
66
178
|
const configPath = `${normalizedPath}/config.json`;
|
|
67
179
|
const hasConfig = configPath in jsonFiles;
|
|
68
180
|
|
|
69
|
-
if (hasConfig) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
181
|
+
if (!hasConfig) {
|
|
182
|
+
// No config = simple single-pass project
|
|
183
|
+
return loadSinglePass(normalizedPath, glslFiles, 'standard');
|
|
184
|
+
}
|
|
73
185
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
186
|
+
const config = await jsonFiles[configPath]();
|
|
187
|
+
validateConfig(config as Record<string, any>, normalizedPath);
|
|
188
|
+
const mode: 'shadertoy' | 'standard' = config.mode === 'shadertoy' ? 'shadertoy' : 'standard';
|
|
189
|
+
|
|
190
|
+
// Load script hooks (available in both modes)
|
|
191
|
+
const script = await loadScript(normalizedPath, scriptFiles);
|
|
192
|
+
|
|
193
|
+
// Get uniforms (only from standard mode configs)
|
|
194
|
+
const uniforms = mode === 'standard' && 'uniforms' in config ? config.uniforms : undefined;
|
|
195
|
+
|
|
196
|
+
// Check if config uses named buffers or textures (standard mode only)
|
|
197
|
+
const hasNamedBuffers = mode === 'standard' && (('buffers' in config && config.buffers) || ('textures' in config && config.textures));
|
|
198
|
+
|
|
199
|
+
if (hasNamedBuffers) {
|
|
200
|
+
return loadStandardWithNamedBuffers(
|
|
201
|
+
normalizedPath, config as any, glslFiles, imageFiles, uniforms, script
|
|
202
|
+
);
|
|
82
203
|
}
|
|
204
|
+
|
|
205
|
+
// Check for pass-level configs (Image, BufferA, etc.)
|
|
206
|
+
const hasPassConfigs = config.Image || config.BufferA || config.BufferB ||
|
|
207
|
+
config.BufferC || config.BufferD;
|
|
208
|
+
|
|
209
|
+
if (hasPassConfigs) {
|
|
210
|
+
return loadWithPassConfigs(
|
|
211
|
+
normalizedPath, config, glslFiles, imageFiles, mode, uniforms, script
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Config with only settings (layout, controls, etc.) but no passes
|
|
216
|
+
return loadSinglePass(normalizedPath, glslFiles, mode, config, uniforms, script);
|
|
83
217
|
}
|
|
84
218
|
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// Single Pass (no pass configs in JSON)
|
|
221
|
+
// =============================================================================
|
|
222
|
+
|
|
85
223
|
async function loadSinglePass(
|
|
86
224
|
demoPath: string,
|
|
87
225
|
glslFiles: Record<string, () => Promise<string>>,
|
|
88
|
-
|
|
89
|
-
|
|
226
|
+
mode: 'shadertoy' | 'standard',
|
|
227
|
+
configOverrides?: Partial<ProjectConfig>,
|
|
228
|
+
uniforms?: UniformDefinitions,
|
|
229
|
+
script?: DemoScriptHooks | null
|
|
230
|
+
): Promise<ShaderProject> {
|
|
90
231
|
const imagePath = `${demoPath}/image.glsl`;
|
|
91
232
|
const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
|
|
92
233
|
|
|
@@ -95,23 +236,21 @@ async function loadSinglePass(
|
|
|
95
236
|
}
|
|
96
237
|
|
|
97
238
|
const imageSource = await glslFiles[actualImagePath]();
|
|
98
|
-
|
|
99
|
-
const layout = configOverrides?.layout || 'tabbed';
|
|
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;
|
|
103
|
-
const title = configOverrides?.title ||
|
|
104
|
-
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
239
|
+
const title = configOverrides?.title || titleFromPath(demoPath);
|
|
105
240
|
|
|
106
241
|
return {
|
|
242
|
+
mode,
|
|
107
243
|
root: demoPath,
|
|
108
244
|
meta: {
|
|
109
245
|
title,
|
|
110
246
|
author: configOverrides?.author || null,
|
|
111
247
|
description: configOverrides?.description || null,
|
|
112
248
|
},
|
|
113
|
-
layout,
|
|
114
|
-
|
|
249
|
+
layout: configOverrides?.layout ?? DEFAULT_LAYOUT,
|
|
250
|
+
theme: configOverrides?.theme ?? DEFAULT_THEME,
|
|
251
|
+
controls: configOverrides?.controls ?? DEFAULT_CONTROLS,
|
|
252
|
+
startPaused: configOverrides?.startPaused ?? false,
|
|
253
|
+
pixelRatio: configOverrides?.pixelRatio ?? null,
|
|
115
254
|
commonSource: null,
|
|
116
255
|
passes: {
|
|
117
256
|
Image: {
|
|
@@ -126,17 +265,25 @@ async function loadSinglePass(
|
|
|
126
265
|
},
|
|
127
266
|
},
|
|
128
267
|
textures: [],
|
|
268
|
+
uniforms: uniforms ?? {},
|
|
269
|
+
script: script ?? null,
|
|
129
270
|
};
|
|
130
271
|
}
|
|
131
272
|
|
|
132
|
-
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// Pass-Config Mode (both shadertoy and standard with Image/BufferA/etc.)
|
|
275
|
+
// =============================================================================
|
|
276
|
+
|
|
277
|
+
async function loadWithPassConfigs(
|
|
133
278
|
demoPath: string,
|
|
134
|
-
config:
|
|
279
|
+
config: ProjectConfig,
|
|
135
280
|
glslFiles: Record<string, () => Promise<string>>,
|
|
136
|
-
imageFiles: Record<string, () => Promise<string
|
|
137
|
-
|
|
281
|
+
imageFiles: Record<string, () => Promise<string>>,
|
|
282
|
+
mode: 'shadertoy' | 'standard',
|
|
283
|
+
uniforms?: UniformDefinitions,
|
|
284
|
+
script?: DemoScriptHooks | null
|
|
285
|
+
): Promise<ShaderProject> {
|
|
138
286
|
|
|
139
|
-
// Extract pass configs from top level
|
|
140
287
|
const passConfigs = {
|
|
141
288
|
Image: config.Image,
|
|
142
289
|
BufferA: config.BufferA,
|
|
@@ -146,45 +293,42 @@ async function loadWithConfig(
|
|
|
146
293
|
};
|
|
147
294
|
|
|
148
295
|
// Load common source
|
|
149
|
-
|
|
150
|
-
if (config.common) {
|
|
151
|
-
const commonPath = `${demoPath}/${config.common}`;
|
|
152
|
-
const actualCommonPath = findFileCaseInsensitive(glslFiles, commonPath);
|
|
153
|
-
if (actualCommonPath) {
|
|
154
|
-
commonSource = await glslFiles[actualCommonPath]();
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
const defaultCommonPath = `${demoPath}/common.glsl`;
|
|
158
|
-
const actualCommonPath = findFileCaseInsensitive(glslFiles, defaultCommonPath);
|
|
159
|
-
if (actualCommonPath) {
|
|
160
|
-
commonSource = await glslFiles[actualCommonPath]();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
296
|
+
const commonSource = await loadCommonSource(demoPath, glslFiles, config.common);
|
|
163
297
|
|
|
164
|
-
// Collect
|
|
165
|
-
|
|
166
|
-
|
|
298
|
+
// Collect texture references for deduplication
|
|
299
|
+
interface TextureRef {
|
|
300
|
+
path: string;
|
|
301
|
+
filter: 'nearest' | 'linear';
|
|
302
|
+
wrap: 'clamp' | 'repeat';
|
|
303
|
+
}
|
|
304
|
+
const textureRefs = new Map<string, TextureRef>();
|
|
167
305
|
|
|
168
|
-
for (const passName of
|
|
306
|
+
for (const passName of PASS_ORDER) {
|
|
169
307
|
const passConfig = passConfigs[passName];
|
|
170
308
|
if (!passConfig) continue;
|
|
171
309
|
|
|
172
|
-
for (const channelKey of
|
|
310
|
+
for (const channelKey of CHANNEL_KEYS) {
|
|
173
311
|
const channelValue = passConfig[channelKey];
|
|
174
312
|
if (!channelValue) continue;
|
|
175
313
|
|
|
176
314
|
const parsed = parseChannelValue(channelValue);
|
|
177
315
|
if (parsed && 'texture' in parsed) {
|
|
178
|
-
|
|
316
|
+
if (!textureRefs.has(parsed.texture)) {
|
|
317
|
+
textureRefs.set(parsed.texture, {
|
|
318
|
+
path: parsed.texture,
|
|
319
|
+
filter: parsed.filter ?? 'linear',
|
|
320
|
+
wrap: parsed.wrap ?? 'repeat',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
179
323
|
}
|
|
180
324
|
}
|
|
181
325
|
}
|
|
182
326
|
|
|
183
327
|
// Load textures
|
|
184
|
-
const textures:
|
|
328
|
+
const textures: ShaderTexture2D[] = [];
|
|
185
329
|
const texturePathToName = new Map<string, string>();
|
|
186
330
|
|
|
187
|
-
for (const texturePath of
|
|
331
|
+
for (const [texturePath, ref] of textureRefs) {
|
|
188
332
|
const fullPath = `${demoPath}/${texturePath.replace(/^\.\//, '')}`;
|
|
189
333
|
const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
|
|
190
334
|
|
|
@@ -198,31 +342,23 @@ async function loadWithConfig(
|
|
|
198
342
|
|
|
199
343
|
textures.push({
|
|
200
344
|
name: textureName,
|
|
201
|
-
filename: textureFilename,
|
|
345
|
+
filename: textureFilename,
|
|
202
346
|
source: imageUrl,
|
|
203
|
-
filter:
|
|
204
|
-
wrap:
|
|
347
|
+
filter: ref.filter,
|
|
348
|
+
wrap: ref.wrap,
|
|
205
349
|
});
|
|
206
350
|
|
|
207
351
|
texturePathToName.set(texturePath, textureName);
|
|
208
352
|
}
|
|
209
353
|
|
|
210
354
|
// Build passes
|
|
211
|
-
const passes:
|
|
355
|
+
const passes: ShaderProject['passes'] = {} as any;
|
|
212
356
|
|
|
213
|
-
for (const passName of
|
|
357
|
+
for (const passName of PASS_ORDER) {
|
|
214
358
|
const passConfig = passConfigs[passName];
|
|
215
359
|
if (!passConfig) continue;
|
|
216
360
|
|
|
217
|
-
const
|
|
218
|
-
Image: 'image.glsl',
|
|
219
|
-
BufferA: 'bufferA.glsl',
|
|
220
|
-
BufferB: 'bufferB.glsl',
|
|
221
|
-
BufferC: 'bufferC.glsl',
|
|
222
|
-
BufferD: 'bufferD.glsl',
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const sourceFile = passConfig.source || defaultNames[passName];
|
|
361
|
+
const sourceFile = passConfig.source || defaultSourceForPass(passName);
|
|
226
362
|
const sourcePath = `${demoPath}/${sourceFile}`;
|
|
227
363
|
const actualSourcePath = findFileCaseInsensitive(glslFiles, sourcePath);
|
|
228
364
|
|
|
@@ -232,75 +368,186 @@ async function loadWithConfig(
|
|
|
232
368
|
|
|
233
369
|
const glslSource = await glslFiles[actualSourcePath]();
|
|
234
370
|
|
|
235
|
-
const channels = [
|
|
371
|
+
const channels: Channels = [
|
|
236
372
|
normalizeChannel(passConfig.iChannel0, texturePathToName),
|
|
237
373
|
normalizeChannel(passConfig.iChannel1, texturePathToName),
|
|
238
374
|
normalizeChannel(passConfig.iChannel2, texturePathToName),
|
|
239
375
|
normalizeChannel(passConfig.iChannel3, texturePathToName),
|
|
240
376
|
];
|
|
241
377
|
|
|
242
|
-
passes[passName] = {
|
|
243
|
-
name: passName,
|
|
244
|
-
glslSource,
|
|
245
|
-
channels,
|
|
246
|
-
};
|
|
378
|
+
passes[passName] = { name: passName, glslSource, channels };
|
|
247
379
|
}
|
|
248
380
|
|
|
249
381
|
if (!passes.Image) {
|
|
250
382
|
throw new Error(`Demo '${demoPath}' must have an Image pass`);
|
|
251
383
|
}
|
|
252
384
|
|
|
253
|
-
|
|
254
|
-
const demoName = demoPath.split('/').pop() || demoPath;
|
|
255
|
-
const title = config.title ||
|
|
256
|
-
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
257
|
-
const author = config.author || null;
|
|
258
|
-
const description = config.description || null;
|
|
259
|
-
const layout = config.layout || 'tabbed';
|
|
260
|
-
const controls = config.controls ?? true;
|
|
385
|
+
const title = config.title || titleFromPath(demoPath);
|
|
261
386
|
|
|
262
387
|
return {
|
|
388
|
+
mode,
|
|
263
389
|
root: demoPath,
|
|
264
|
-
meta: {
|
|
265
|
-
|
|
266
|
-
|
|
390
|
+
meta: {
|
|
391
|
+
title,
|
|
392
|
+
author: config.author || null,
|
|
393
|
+
description: config.description || null,
|
|
394
|
+
},
|
|
395
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
396
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
397
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
398
|
+
startPaused: config.startPaused ?? false,
|
|
399
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
267
400
|
commonSource,
|
|
268
401
|
passes,
|
|
269
402
|
textures,
|
|
403
|
+
uniforms: uniforms ?? {},
|
|
404
|
+
script: script ?? null,
|
|
270
405
|
};
|
|
271
406
|
}
|
|
272
407
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
408
|
+
// =============================================================================
|
|
409
|
+
// Standard Mode with Named Buffers
|
|
410
|
+
// =============================================================================
|
|
411
|
+
|
|
412
|
+
async function loadStandardWithNamedBuffers(
|
|
413
|
+
demoPath: string,
|
|
414
|
+
config: {
|
|
415
|
+
title?: string;
|
|
416
|
+
author?: string;
|
|
417
|
+
description?: string;
|
|
418
|
+
layout?: 'fullscreen' | 'default' | 'split' | 'tabbed';
|
|
419
|
+
theme?: any;
|
|
420
|
+
controls?: boolean;
|
|
421
|
+
common?: string;
|
|
422
|
+
startPaused?: boolean;
|
|
423
|
+
pixelRatio?: number;
|
|
424
|
+
buffers?: string[] | Record<string, StandardBufferConfig>;
|
|
425
|
+
textures?: Record<string, string>;
|
|
426
|
+
},
|
|
427
|
+
glslFiles: Record<string, () => Promise<string>>,
|
|
428
|
+
imageFiles: Record<string, () => Promise<string>>,
|
|
429
|
+
uniforms?: UniformDefinitions,
|
|
430
|
+
script?: DemoScriptHooks | null
|
|
431
|
+
): Promise<ShaderProject> {
|
|
432
|
+
|
|
433
|
+
const buffersConfig = normalizeBuffersConfig(config.buffers);
|
|
434
|
+
const bufferNames = Object.keys(buffersConfig);
|
|
435
|
+
|
|
436
|
+
if (bufferNames.length > 4) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Standard mode at '${demoPath}' supports max 4 buffers, got ${bufferNames.length}: ${bufferNames.join(', ')}`
|
|
439
|
+
);
|
|
276
440
|
}
|
|
277
441
|
|
|
278
|
-
//
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
442
|
+
// Map buffer names → PassNames
|
|
443
|
+
const bufferNameToPass = new Map<string, PassName>();
|
|
444
|
+
for (let i = 0; i < bufferNames.length; i++) {
|
|
445
|
+
bufferNameToPass.set(bufferNames[i], BUFFER_PASS_NAMES[i]);
|
|
282
446
|
}
|
|
283
447
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
448
|
+
// Texture deduplication
|
|
449
|
+
const textureMap = new Map<string, ShaderTexture2D>();
|
|
450
|
+
|
|
451
|
+
function registerTexture(source: string, filter: 'nearest' | 'linear' = 'linear', wrap: 'clamp' | 'repeat' = 'repeat'): string {
|
|
452
|
+
const key = `${source}|${filter}|${wrap}`;
|
|
453
|
+
const existing = textureMap.get(key);
|
|
454
|
+
if (existing) return existing.name;
|
|
455
|
+
|
|
456
|
+
const name = `tex${textureMap.size}`;
|
|
457
|
+
textureMap.set(key, { name, source, filter, wrap });
|
|
458
|
+
return name;
|
|
290
459
|
}
|
|
291
460
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
};
|
|
461
|
+
// Build namedSamplers map (shared by all passes)
|
|
462
|
+
const namedSamplers = new Map<string, ChannelSource>();
|
|
463
|
+
|
|
464
|
+
// Add buffers
|
|
465
|
+
for (const [bufName, passName] of bufferNameToPass) {
|
|
466
|
+
namedSamplers.set(bufName, { kind: 'buffer', buffer: passName, current: false });
|
|
299
467
|
}
|
|
300
468
|
|
|
301
|
-
|
|
302
|
-
|
|
469
|
+
// Add textures
|
|
470
|
+
for (const [texName, texValue] of Object.entries(config.textures ?? {})) {
|
|
471
|
+
if (texValue === 'keyboard') {
|
|
472
|
+
namedSamplers.set(texName, { kind: 'keyboard' });
|
|
473
|
+
} else if (texValue === 'audio') {
|
|
474
|
+
namedSamplers.set(texName, { kind: 'audio' });
|
|
475
|
+
} else if (texValue === 'webcam') {
|
|
476
|
+
namedSamplers.set(texName, { kind: 'webcam' });
|
|
477
|
+
} else if (/\.\w+$/.test(texValue)) {
|
|
478
|
+
// Image file — resolve via imageFiles
|
|
479
|
+
const fullPath = `${demoPath}/${texValue.replace(/^\.\//, '')}`;
|
|
480
|
+
const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
|
|
481
|
+
if (!actualPath) {
|
|
482
|
+
throw new Error(`Texture not found: ${texValue} (expected at ${fullPath})`);
|
|
483
|
+
}
|
|
484
|
+
const imageUrl = await imageFiles[actualPath]();
|
|
485
|
+
const internalName = registerTexture(imageUrl);
|
|
486
|
+
namedSamplers.set(texName, { kind: 'texture', name: internalName, cubemap: false });
|
|
487
|
+
} else {
|
|
488
|
+
// Script-uploaded texture — name matched by engine.updateTexture() calls
|
|
489
|
+
namedSamplers.set(texName, { kind: 'script', name: texValue });
|
|
490
|
+
}
|
|
303
491
|
}
|
|
304
492
|
|
|
305
|
-
|
|
493
|
+
const noChannels: Channels = [{ kind: 'none' }, { kind: 'none' }, { kind: 'none' }, { kind: 'none' }];
|
|
494
|
+
|
|
495
|
+
// Load common source
|
|
496
|
+
const commonSource = await loadCommonSource(demoPath, glslFiles, config.common);
|
|
497
|
+
|
|
498
|
+
// Load Image pass
|
|
499
|
+
const imagePath = `${demoPath}/image.glsl`;
|
|
500
|
+
const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
|
|
501
|
+
if (!actualImagePath) {
|
|
502
|
+
throw new Error(`Standard mode project at '${demoPath}' requires 'image.glsl'.`);
|
|
503
|
+
}
|
|
504
|
+
const imageSource = await glslFiles[actualImagePath]();
|
|
505
|
+
|
|
506
|
+
const passes: ShaderProject['passes'] = {
|
|
507
|
+
Image: {
|
|
508
|
+
name: 'Image',
|
|
509
|
+
glslSource: imageSource,
|
|
510
|
+
channels: noChannels,
|
|
511
|
+
namedSamplers: new Map(namedSamplers),
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// Load buffer passes
|
|
516
|
+
for (const [bufName, passName] of bufferNameToPass) {
|
|
517
|
+
const sourcePath = `${demoPath}/${bufName}.glsl`;
|
|
518
|
+
const actualPath = findFileCaseInsensitive(glslFiles, sourcePath);
|
|
519
|
+
if (!actualPath) {
|
|
520
|
+
throw new Error(`Buffer '${bufName}' requires '${bufName}.glsl' in '${demoPath}'.`);
|
|
521
|
+
}
|
|
522
|
+
const glslSource = await glslFiles[actualPath]();
|
|
523
|
+
|
|
524
|
+
passes[passName] = {
|
|
525
|
+
name: passName,
|
|
526
|
+
glslSource,
|
|
527
|
+
channels: noChannels,
|
|
528
|
+
namedSamplers: new Map(namedSamplers),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const title = config.title || titleFromPath(demoPath);
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
mode: 'standard',
|
|
536
|
+
root: demoPath,
|
|
537
|
+
meta: {
|
|
538
|
+
title,
|
|
539
|
+
author: config.author ?? null,
|
|
540
|
+
description: config.description ?? null,
|
|
541
|
+
},
|
|
542
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
543
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
544
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
545
|
+
startPaused: config.startPaused ?? false,
|
|
546
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
547
|
+
commonSource,
|
|
548
|
+
passes,
|
|
549
|
+
textures: Array.from(textureMap.values()),
|
|
550
|
+
uniforms: uniforms ?? {},
|
|
551
|
+
script: script ?? null,
|
|
552
|
+
};
|
|
306
553
|
}
|