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