@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,7 +1,7 @@
|
|
|
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
|
-
import {
|
|
6
|
-
export declare function loadDemo(demoPath: string, glslFiles: Record<string, () => Promise<string>>, jsonFiles: Record<string, () => Promise<
|
|
5
|
+
import { ShaderProject, ProjectConfig } from './types';
|
|
6
|
+
export declare function loadDemo(demoPath: string, glslFiles: Record<string, () => Promise<string>>, jsonFiles: Record<string, () => Promise<ProjectConfig>>, imageFiles: Record<string, () => Promise<string>>, scriptFiles?: Record<string, () => Promise<any>>): Promise<ShaderProject>;
|
|
7
7
|
//# sourceMappingURL=loaderHelper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loaderHelper.d.ts","sourceRoot":"","sources":["../../src/project/loaderHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"loaderHelper.d.ts","sourceRoot":"","sources":["../../src/project/loaderHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,aAAa,EACb,aAAa,EAQd,MAAM,SAAS,CAAC;AAyJjB,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,aAAa,CAAC,CAAC,EACvD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,EACjD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,GAC/C,OAAO,CAAC,aAAa,CAAC,CA0CxB"}
|
|
@@ -1,16 +1,18 @@
|
|
|
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
|
+
import { parseChannelValue, defaultSourceForPass, validateConfig, PASS_ORDER, BUFFER_PASS_NAMES, CHANNEL_KEYS, DEFAULT_LAYOUT, DEFAULT_CONTROLS, DEFAULT_THEME, } from './configHelpers';
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Case-Insensitive File Lookup
|
|
8
|
+
// =============================================================================
|
|
5
9
|
/**
|
|
6
10
|
* Case-insensitive file lookup helper.
|
|
7
11
|
* Returns the actual key from the record that matches the path (case-insensitive).
|
|
8
12
|
*/
|
|
9
13
|
function findFileCaseInsensitive(files, path) {
|
|
10
|
-
// First try exact match
|
|
11
14
|
if (path in files)
|
|
12
15
|
return path;
|
|
13
|
-
// Try case-insensitive match
|
|
14
16
|
const lowerPath = path.toLowerCase();
|
|
15
17
|
for (const key of Object.keys(files)) {
|
|
16
18
|
if (key.toLowerCase() === lowerPath) {
|
|
@@ -19,70 +21,161 @@ function findFileCaseInsensitive(files, path) {
|
|
|
19
21
|
}
|
|
20
22
|
return null;
|
|
21
23
|
}
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Script Loading
|
|
26
|
+
// =============================================================================
|
|
22
27
|
/**
|
|
23
|
-
*
|
|
28
|
+
* Load script.js from a demo folder if present.
|
|
24
29
|
*/
|
|
25
|
-
function
|
|
26
|
-
|
|
30
|
+
async function loadScript(demoPath, scriptFiles) {
|
|
31
|
+
if (!scriptFiles)
|
|
32
|
+
return null;
|
|
33
|
+
const scriptPath = `${demoPath}/script.js`;
|
|
34
|
+
const actualPath = findFileCaseInsensitive(scriptFiles, scriptPath);
|
|
35
|
+
if (!actualPath)
|
|
36
|
+
return null;
|
|
37
|
+
const mod = await scriptFiles[actualPath]();
|
|
38
|
+
const hooks = {};
|
|
39
|
+
if (typeof mod.setup === 'function')
|
|
40
|
+
hooks.setup = mod.setup;
|
|
41
|
+
if (typeof mod.onFrame === 'function')
|
|
42
|
+
hooks.onFrame = mod.onFrame;
|
|
43
|
+
return (hooks.setup || hooks.onFrame) ? hooks : null;
|
|
27
44
|
}
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Common Source Loading
|
|
47
|
+
// =============================================================================
|
|
28
48
|
/**
|
|
29
|
-
*
|
|
49
|
+
* Load common.glsl source (explicit path or default).
|
|
30
50
|
*/
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
async function loadCommonSource(demoPath, glslFiles, commonPath) {
|
|
52
|
+
if (commonPath) {
|
|
53
|
+
const fullPath = `${demoPath}/${commonPath}`;
|
|
54
|
+
const actualPath = findFileCaseInsensitive(glslFiles, fullPath);
|
|
55
|
+
return actualPath ? await glslFiles[actualPath]() : null;
|
|
56
|
+
}
|
|
57
|
+
// Check for default common.glsl
|
|
58
|
+
const defaultPath = `${demoPath}/common.glsl`;
|
|
59
|
+
const actualPath = findFileCaseInsensitive(glslFiles, defaultPath);
|
|
60
|
+
return actualPath ? await glslFiles[actualPath]() : null;
|
|
61
|
+
}
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Channel Normalization
|
|
64
|
+
// =============================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Normalize a channel value into a ChannelSource.
|
|
67
|
+
*/
|
|
68
|
+
function normalizeChannel(channelValue, texturePathToName) {
|
|
69
|
+
if (!channelValue)
|
|
70
|
+
return { kind: 'none' };
|
|
71
|
+
const parsed = parseChannelValue(channelValue);
|
|
72
|
+
if (!parsed)
|
|
73
|
+
return { kind: 'none' };
|
|
74
|
+
if ('buffer' in parsed) {
|
|
75
|
+
return { kind: 'buffer', buffer: parsed.buffer, current: !!parsed.current };
|
|
76
|
+
}
|
|
77
|
+
if ('texture' in parsed) {
|
|
78
|
+
const textureName = texturePathToName?.get(parsed.texture) || parsed.texture;
|
|
79
|
+
return { kind: 'texture', name: textureName, cubemap: parsed.type === 'cubemap' };
|
|
80
|
+
}
|
|
81
|
+
if ('keyboard' in parsed)
|
|
82
|
+
return { kind: 'keyboard' };
|
|
83
|
+
if ('audio' in parsed)
|
|
84
|
+
return { kind: 'audio' };
|
|
85
|
+
if ('webcam' in parsed)
|
|
86
|
+
return { kind: 'webcam' };
|
|
87
|
+
if ('video' in parsed)
|
|
88
|
+
return { kind: 'video', src: parsed.video };
|
|
89
|
+
if ('script' in parsed)
|
|
90
|
+
return { kind: 'script', name: parsed.script };
|
|
91
|
+
return { kind: 'none' };
|
|
92
|
+
}
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Named Buffers Normalization (Standard Mode)
|
|
95
|
+
// =============================================================================
|
|
96
|
+
/**
|
|
97
|
+
* Normalize buffers config: array shorthand to object form.
|
|
98
|
+
*/
|
|
99
|
+
function normalizeBuffersConfig(buffers) {
|
|
100
|
+
if (!buffers)
|
|
101
|
+
return {};
|
|
102
|
+
if (Array.isArray(buffers)) {
|
|
103
|
+
const result = {};
|
|
104
|
+
for (const name of buffers) {
|
|
105
|
+
result[name] = {};
|
|
38
106
|
}
|
|
39
|
-
return
|
|
107
|
+
return result;
|
|
40
108
|
}
|
|
41
|
-
return
|
|
109
|
+
return buffers;
|
|
110
|
+
}
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Title Helper
|
|
113
|
+
// =============================================================================
|
|
114
|
+
/**
|
|
115
|
+
* Generate a display title from a demo path.
|
|
116
|
+
* e.g. "./demos/my-shader" → "My Shader"
|
|
117
|
+
*/
|
|
118
|
+
function titleFromPath(demoPath) {
|
|
119
|
+
const demoName = demoPath.split('/').pop() || demoPath;
|
|
120
|
+
return demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
42
121
|
}
|
|
43
|
-
|
|
44
|
-
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// Main Entry Point
|
|
124
|
+
// =============================================================================
|
|
125
|
+
export async function loadDemo(demoPath, glslFiles, jsonFiles, imageFiles, scriptFiles) {
|
|
126
|
+
// Normalize path
|
|
45
127
|
const normalizedPath = demoPath.startsWith('./') ? demoPath : `./${demoPath}`;
|
|
46
128
|
const configPath = `${normalizedPath}/config.json`;
|
|
47
129
|
const hasConfig = configPath in jsonFiles;
|
|
48
|
-
if (hasConfig) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
130
|
+
if (!hasConfig) {
|
|
131
|
+
// No config = simple single-pass project
|
|
132
|
+
return loadSinglePass(normalizedPath, glslFiles, 'standard');
|
|
133
|
+
}
|
|
134
|
+
const config = await jsonFiles[configPath]();
|
|
135
|
+
validateConfig(config, normalizedPath);
|
|
136
|
+
const mode = config.mode === 'shadertoy' ? 'shadertoy' : 'standard';
|
|
137
|
+
// Load script hooks (available in both modes)
|
|
138
|
+
const script = await loadScript(normalizedPath, scriptFiles);
|
|
139
|
+
// Get uniforms (only from standard mode configs)
|
|
140
|
+
const uniforms = mode === 'standard' && 'uniforms' in config ? config.uniforms : undefined;
|
|
141
|
+
// Check if config uses named buffers or textures (standard mode only)
|
|
142
|
+
const hasNamedBuffers = mode === 'standard' && (('buffers' in config && config.buffers) || ('textures' in config && config.textures));
|
|
143
|
+
if (hasNamedBuffers) {
|
|
144
|
+
return loadStandardWithNamedBuffers(normalizedPath, config, glslFiles, imageFiles, uniforms, script);
|
|
59
145
|
}
|
|
60
|
-
|
|
61
|
-
|
|
146
|
+
// Check for pass-level configs (Image, BufferA, etc.)
|
|
147
|
+
const hasPassConfigs = config.Image || config.BufferA || config.BufferB ||
|
|
148
|
+
config.BufferC || config.BufferD;
|
|
149
|
+
if (hasPassConfigs) {
|
|
150
|
+
return loadWithPassConfigs(normalizedPath, config, glslFiles, imageFiles, mode, uniforms, script);
|
|
62
151
|
}
|
|
152
|
+
// Config with only settings (layout, controls, etc.) but no passes
|
|
153
|
+
return loadSinglePass(normalizedPath, glslFiles, mode, config, uniforms, script);
|
|
63
154
|
}
|
|
64
|
-
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// Single Pass (no pass configs in JSON)
|
|
157
|
+
// =============================================================================
|
|
158
|
+
async function loadSinglePass(demoPath, glslFiles, mode, configOverrides, uniforms, script) {
|
|
65
159
|
const imagePath = `${demoPath}/image.glsl`;
|
|
66
160
|
const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
|
|
67
161
|
if (!actualImagePath) {
|
|
68
162
|
throw new Error(`Demo '${demoPath}' not found. Expected ${imagePath}`);
|
|
69
163
|
}
|
|
70
164
|
const imageSource = await glslFiles[actualImagePath]();
|
|
71
|
-
const
|
|
72
|
-
const controls = configOverrides?.controls ?? true;
|
|
73
|
-
// Extract name from path for title (e.g., "./shaders/example-gradient" -> "example-gradient")
|
|
74
|
-
const demoName = demoPath.split('/').pop() || demoPath;
|
|
75
|
-
const title = configOverrides?.title ||
|
|
76
|
-
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
165
|
+
const title = configOverrides?.title || titleFromPath(demoPath);
|
|
77
166
|
return {
|
|
167
|
+
mode,
|
|
78
168
|
root: demoPath,
|
|
79
169
|
meta: {
|
|
80
170
|
title,
|
|
81
171
|
author: configOverrides?.author || null,
|
|
82
172
|
description: configOverrides?.description || null,
|
|
83
173
|
},
|
|
84
|
-
layout,
|
|
85
|
-
|
|
174
|
+
layout: configOverrides?.layout ?? DEFAULT_LAYOUT,
|
|
175
|
+
theme: configOverrides?.theme ?? DEFAULT_THEME,
|
|
176
|
+
controls: configOverrides?.controls ?? DEFAULT_CONTROLS,
|
|
177
|
+
startPaused: configOverrides?.startPaused ?? false,
|
|
178
|
+
pixelRatio: configOverrides?.pixelRatio ?? null,
|
|
86
179
|
commonSource: null,
|
|
87
180
|
passes: {
|
|
88
181
|
Image: {
|
|
@@ -97,10 +190,14 @@ async function loadSinglePass(demoPath, glslFiles, configOverrides) {
|
|
|
97
190
|
},
|
|
98
191
|
},
|
|
99
192
|
textures: [],
|
|
193
|
+
uniforms: uniforms ?? {},
|
|
194
|
+
script: script ?? null,
|
|
100
195
|
};
|
|
101
196
|
}
|
|
102
|
-
|
|
103
|
-
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// Pass-Config Mode (both shadertoy and standard with Image/BufferA/etc.)
|
|
199
|
+
// =============================================================================
|
|
200
|
+
async function loadWithPassConfigs(demoPath, config, glslFiles, imageFiles, mode, uniforms, script) {
|
|
104
201
|
const passConfigs = {
|
|
105
202
|
Image: config.Image,
|
|
106
203
|
BufferA: config.BufferA,
|
|
@@ -109,42 +206,32 @@ async function loadWithConfig(demoPath, config, glslFiles, imageFiles) {
|
|
|
109
206
|
BufferD: config.BufferD,
|
|
110
207
|
};
|
|
111
208
|
// Load common source
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const actualCommonPath = findFileCaseInsensitive(glslFiles, commonPath);
|
|
116
|
-
if (actualCommonPath) {
|
|
117
|
-
commonSource = await glslFiles[actualCommonPath]();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
const defaultCommonPath = `${demoPath}/common.glsl`;
|
|
122
|
-
const actualCommonPath = findFileCaseInsensitive(glslFiles, defaultCommonPath);
|
|
123
|
-
if (actualCommonPath) {
|
|
124
|
-
commonSource = await glslFiles[actualCommonPath]();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// Collect all texture paths
|
|
128
|
-
const texturePathsSet = new Set();
|
|
129
|
-
const passOrder = ['Image', 'BufferA', 'BufferB', 'BufferC', 'BufferD'];
|
|
130
|
-
for (const passName of passOrder) {
|
|
209
|
+
const commonSource = await loadCommonSource(demoPath, glslFiles, config.common);
|
|
210
|
+
const textureRefs = new Map();
|
|
211
|
+
for (const passName of PASS_ORDER) {
|
|
131
212
|
const passConfig = passConfigs[passName];
|
|
132
213
|
if (!passConfig)
|
|
133
214
|
continue;
|
|
134
|
-
for (const channelKey of
|
|
215
|
+
for (const channelKey of CHANNEL_KEYS) {
|
|
135
216
|
const channelValue = passConfig[channelKey];
|
|
136
217
|
if (!channelValue)
|
|
137
218
|
continue;
|
|
138
219
|
const parsed = parseChannelValue(channelValue);
|
|
139
220
|
if (parsed && 'texture' in parsed) {
|
|
140
|
-
|
|
221
|
+
if (!textureRefs.has(parsed.texture)) {
|
|
222
|
+
textureRefs.set(parsed.texture, {
|
|
223
|
+
path: parsed.texture,
|
|
224
|
+
filter: parsed.filter ?? 'linear',
|
|
225
|
+
wrap: parsed.wrap ?? 'repeat',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
141
228
|
}
|
|
142
229
|
}
|
|
143
230
|
}
|
|
144
231
|
// Load textures
|
|
145
232
|
const textures = [];
|
|
146
233
|
const texturePathToName = new Map();
|
|
147
|
-
for (const texturePath of
|
|
234
|
+
for (const [texturePath, ref] of textureRefs) {
|
|
148
235
|
const fullPath = `${demoPath}/${texturePath.replace(/^\.\//, '')}`;
|
|
149
236
|
const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
|
|
150
237
|
if (!actualPath) {
|
|
@@ -155,27 +242,20 @@ async function loadWithConfig(demoPath, config, glslFiles, imageFiles) {
|
|
|
155
242
|
const textureName = textureFilename.replace(/\.[^.]+$/, '');
|
|
156
243
|
textures.push({
|
|
157
244
|
name: textureName,
|
|
158
|
-
filename: textureFilename,
|
|
245
|
+
filename: textureFilename,
|
|
159
246
|
source: imageUrl,
|
|
160
|
-
filter:
|
|
161
|
-
wrap:
|
|
247
|
+
filter: ref.filter,
|
|
248
|
+
wrap: ref.wrap,
|
|
162
249
|
});
|
|
163
250
|
texturePathToName.set(texturePath, textureName);
|
|
164
251
|
}
|
|
165
252
|
// Build passes
|
|
166
253
|
const passes = {};
|
|
167
|
-
for (const passName of
|
|
254
|
+
for (const passName of PASS_ORDER) {
|
|
168
255
|
const passConfig = passConfigs[passName];
|
|
169
256
|
if (!passConfig)
|
|
170
257
|
continue;
|
|
171
|
-
const
|
|
172
|
-
Image: 'image.glsl',
|
|
173
|
-
BufferA: 'bufferA.glsl',
|
|
174
|
-
BufferB: 'bufferB.glsl',
|
|
175
|
-
BufferC: 'bufferC.glsl',
|
|
176
|
-
BufferD: 'bufferD.glsl',
|
|
177
|
-
};
|
|
178
|
-
const sourceFile = passConfig.source || defaultNames[passName];
|
|
258
|
+
const sourceFile = passConfig.source || defaultSourceForPass(passName);
|
|
179
259
|
const sourcePath = `${demoPath}/${sourceFile}`;
|
|
180
260
|
const actualSourcePath = findFileCaseInsensitive(glslFiles, sourcePath);
|
|
181
261
|
if (!actualSourcePath) {
|
|
@@ -188,59 +268,141 @@ async function loadWithConfig(demoPath, config, glslFiles, imageFiles) {
|
|
|
188
268
|
normalizeChannel(passConfig.iChannel2, texturePathToName),
|
|
189
269
|
normalizeChannel(passConfig.iChannel3, texturePathToName),
|
|
190
270
|
];
|
|
191
|
-
passes[passName] = {
|
|
192
|
-
name: passName,
|
|
193
|
-
glslSource,
|
|
194
|
-
channels,
|
|
195
|
-
};
|
|
271
|
+
passes[passName] = { name: passName, glslSource, channels };
|
|
196
272
|
}
|
|
197
273
|
if (!passes.Image) {
|
|
198
274
|
throw new Error(`Demo '${demoPath}' must have an Image pass`);
|
|
199
275
|
}
|
|
200
|
-
|
|
201
|
-
const demoName = demoPath.split('/').pop() || demoPath;
|
|
202
|
-
const title = config.title ||
|
|
203
|
-
demoName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
204
|
-
const author = config.author || null;
|
|
205
|
-
const description = config.description || null;
|
|
206
|
-
const layout = config.layout || 'tabbed';
|
|
207
|
-
const controls = config.controls ?? true;
|
|
276
|
+
const title = config.title || titleFromPath(demoPath);
|
|
208
277
|
return {
|
|
278
|
+
mode,
|
|
209
279
|
root: demoPath,
|
|
210
|
-
meta: {
|
|
211
|
-
|
|
212
|
-
|
|
280
|
+
meta: {
|
|
281
|
+
title,
|
|
282
|
+
author: config.author || null,
|
|
283
|
+
description: config.description || null,
|
|
284
|
+
},
|
|
285
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
286
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
287
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
288
|
+
startPaused: config.startPaused ?? false,
|
|
289
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
213
290
|
commonSource,
|
|
214
291
|
passes,
|
|
215
292
|
textures,
|
|
293
|
+
uniforms: uniforms ?? {},
|
|
294
|
+
script: script ?? null,
|
|
216
295
|
};
|
|
217
296
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// Standard Mode with Named Buffers
|
|
299
|
+
// =============================================================================
|
|
300
|
+
async function loadStandardWithNamedBuffers(demoPath, config, glslFiles, imageFiles, uniforms, script) {
|
|
301
|
+
const buffersConfig = normalizeBuffersConfig(config.buffers);
|
|
302
|
+
const bufferNames = Object.keys(buffersConfig);
|
|
303
|
+
if (bufferNames.length > 4) {
|
|
304
|
+
throw new Error(`Standard mode at '${demoPath}' supports max 4 buffers, got ${bufferNames.length}: ${bufferNames.join(', ')}`);
|
|
221
305
|
}
|
|
222
|
-
//
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
306
|
+
// Map buffer names → PassNames
|
|
307
|
+
const bufferNameToPass = new Map();
|
|
308
|
+
for (let i = 0; i < bufferNames.length; i++) {
|
|
309
|
+
bufferNameToPass.set(bufferNames[i], BUFFER_PASS_NAMES[i]);
|
|
226
310
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
311
|
+
// Texture deduplication
|
|
312
|
+
const textureMap = new Map();
|
|
313
|
+
function registerTexture(source, filter = 'linear', wrap = 'repeat') {
|
|
314
|
+
const key = `${source}|${filter}|${wrap}`;
|
|
315
|
+
const existing = textureMap.get(key);
|
|
316
|
+
if (existing)
|
|
317
|
+
return existing.name;
|
|
318
|
+
const name = `tex${textureMap.size}`;
|
|
319
|
+
textureMap.set(key, { name, source, filter, wrap });
|
|
320
|
+
return name;
|
|
233
321
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
cubemap: parsed.type === 'cubemap',
|
|
240
|
-
};
|
|
322
|
+
// Build namedSamplers map (shared by all passes)
|
|
323
|
+
const namedSamplers = new Map();
|
|
324
|
+
// Add buffers
|
|
325
|
+
for (const [bufName, passName] of bufferNameToPass) {
|
|
326
|
+
namedSamplers.set(bufName, { kind: 'buffer', buffer: passName, current: false });
|
|
241
327
|
}
|
|
242
|
-
|
|
243
|
-
|
|
328
|
+
// Add textures
|
|
329
|
+
for (const [texName, texValue] of Object.entries(config.textures ?? {})) {
|
|
330
|
+
if (texValue === 'keyboard') {
|
|
331
|
+
namedSamplers.set(texName, { kind: 'keyboard' });
|
|
332
|
+
}
|
|
333
|
+
else if (texValue === 'audio') {
|
|
334
|
+
namedSamplers.set(texName, { kind: 'audio' });
|
|
335
|
+
}
|
|
336
|
+
else if (texValue === 'webcam') {
|
|
337
|
+
namedSamplers.set(texName, { kind: 'webcam' });
|
|
338
|
+
}
|
|
339
|
+
else if (/\.\w+$/.test(texValue)) {
|
|
340
|
+
// Image file — resolve via imageFiles
|
|
341
|
+
const fullPath = `${demoPath}/${texValue.replace(/^\.\//, '')}`;
|
|
342
|
+
const actualPath = findFileCaseInsensitive(imageFiles, fullPath);
|
|
343
|
+
if (!actualPath) {
|
|
344
|
+
throw new Error(`Texture not found: ${texValue} (expected at ${fullPath})`);
|
|
345
|
+
}
|
|
346
|
+
const imageUrl = await imageFiles[actualPath]();
|
|
347
|
+
const internalName = registerTexture(imageUrl);
|
|
348
|
+
namedSamplers.set(texName, { kind: 'texture', name: internalName, cubemap: false });
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Script-uploaded texture — name matched by engine.updateTexture() calls
|
|
352
|
+
namedSamplers.set(texName, { kind: 'script', name: texValue });
|
|
353
|
+
}
|
|
244
354
|
}
|
|
245
|
-
|
|
355
|
+
const noChannels = [{ kind: 'none' }, { kind: 'none' }, { kind: 'none' }, { kind: 'none' }];
|
|
356
|
+
// Load common source
|
|
357
|
+
const commonSource = await loadCommonSource(demoPath, glslFiles, config.common);
|
|
358
|
+
// Load Image pass
|
|
359
|
+
const imagePath = `${demoPath}/image.glsl`;
|
|
360
|
+
const actualImagePath = findFileCaseInsensitive(glslFiles, imagePath);
|
|
361
|
+
if (!actualImagePath) {
|
|
362
|
+
throw new Error(`Standard mode project at '${demoPath}' requires 'image.glsl'.`);
|
|
363
|
+
}
|
|
364
|
+
const imageSource = await glslFiles[actualImagePath]();
|
|
365
|
+
const passes = {
|
|
366
|
+
Image: {
|
|
367
|
+
name: 'Image',
|
|
368
|
+
glslSource: imageSource,
|
|
369
|
+
channels: noChannels,
|
|
370
|
+
namedSamplers: new Map(namedSamplers),
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
// Load buffer passes
|
|
374
|
+
for (const [bufName, passName] of bufferNameToPass) {
|
|
375
|
+
const sourcePath = `${demoPath}/${bufName}.glsl`;
|
|
376
|
+
const actualPath = findFileCaseInsensitive(glslFiles, sourcePath);
|
|
377
|
+
if (!actualPath) {
|
|
378
|
+
throw new Error(`Buffer '${bufName}' requires '${bufName}.glsl' in '${demoPath}'.`);
|
|
379
|
+
}
|
|
380
|
+
const glslSource = await glslFiles[actualPath]();
|
|
381
|
+
passes[passName] = {
|
|
382
|
+
name: passName,
|
|
383
|
+
glslSource,
|
|
384
|
+
channels: noChannels,
|
|
385
|
+
namedSamplers: new Map(namedSamplers),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const title = config.title || titleFromPath(demoPath);
|
|
389
|
+
return {
|
|
390
|
+
mode: 'standard',
|
|
391
|
+
root: demoPath,
|
|
392
|
+
meta: {
|
|
393
|
+
title,
|
|
394
|
+
author: config.author ?? null,
|
|
395
|
+
description: config.description ?? null,
|
|
396
|
+
},
|
|
397
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
398
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
399
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
400
|
+
startPaused: config.startPaused ?? false,
|
|
401
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
402
|
+
commonSource,
|
|
403
|
+
passes,
|
|
404
|
+
textures: Array.from(textureMap.values()),
|
|
405
|
+
uniforms: uniforms ?? {},
|
|
406
|
+
script: script ?? null,
|
|
407
|
+
};
|
|
246
408
|
}
|