@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,81 @@
1
+ /**
2
+ * Split Layout
3
+ *
4
+ * Shader on left, editable code on right with syntax highlighting.
5
+ * Ideal for teaching and presentations where viewers can tweak the code.
6
+ */
7
+
8
+ import './split.css';
9
+
10
+ import { BaseLayout, LayoutOptions, RecompileHandler } from './types';
11
+ import { ShadertoyProject } from '../project/types';
12
+
13
+ export class SplitLayout implements BaseLayout {
14
+ private container: HTMLElement;
15
+ private project: ShadertoyProject;
16
+ private root: HTMLElement;
17
+ private canvasContainer: HTMLElement;
18
+ private codePanel: HTMLElement;
19
+
20
+ private editorPanel: any = null;
21
+ private recompileHandler: RecompileHandler | null = null;
22
+
23
+ constructor(opts: LayoutOptions) {
24
+ this.container = opts.container;
25
+ this.project = opts.project;
26
+
27
+ // Create root layout container
28
+ this.root = document.createElement('div');
29
+ this.root.className = 'layout-split';
30
+
31
+ // Create canvas container (left side)
32
+ this.canvasContainer = document.createElement('div');
33
+ this.canvasContainer.className = 'canvas-container';
34
+
35
+ // Create code panel (right side)
36
+ this.codePanel = document.createElement('div');
37
+ this.codePanel.className = 'code-panel';
38
+
39
+ // Build editor panel
40
+ this.buildEditorPanel();
41
+
42
+ // Assemble and append to DOM
43
+ this.root.appendChild(this.canvasContainer);
44
+ this.root.appendChild(this.codePanel);
45
+ this.container.appendChild(this.root);
46
+ }
47
+
48
+ getCanvasContainer(): HTMLElement {
49
+ return this.canvasContainer;
50
+ }
51
+
52
+ setRecompileHandler(handler: RecompileHandler): void {
53
+ this.recompileHandler = handler;
54
+ if (this.editorPanel) {
55
+ this.editorPanel.setRecompileHandler(handler);
56
+ }
57
+ }
58
+
59
+ dispose(): void {
60
+ if (this.editorPanel) {
61
+ this.editorPanel.dispose();
62
+ this.editorPanel = null;
63
+ }
64
+ this.container.innerHTML = '';
65
+ }
66
+
67
+ /**
68
+ * Build editor panel (dynamically loaded).
69
+ */
70
+ private async buildEditorPanel(): Promise<void> {
71
+ try {
72
+ const { EditorPanel } = await import('../editor/EditorPanel');
73
+ this.editorPanel = new EditorPanel(this.codePanel, this.project);
74
+ if (this.recompileHandler) {
75
+ this.editorPanel.setRecompileHandler(this.recompileHandler);
76
+ }
77
+ } catch (err) {
78
+ console.error('Failed to load editor panel:', err);
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Tabbed Layout
3
+ *
4
+ * Single window with tabs to switch between shader output, editable code, and textures.
5
+ * First tab shows the live shader, remaining tabs show editable GLSL source files and images.
6
+ */
7
+
8
+ import './tabbed.css';
9
+
10
+ import { BaseLayout, LayoutOptions, RecompileHandler } from './types';
11
+ import { ShadertoyProject, PassName } from '../project/types';
12
+
13
+ type ShaderTab = { kind: 'shader'; name: string };
14
+ type CodeTab = { kind: 'code'; name: string; passName: 'common' | PassName; source: string };
15
+ type ImageTab = { kind: 'image'; name: string; url: string };
16
+ type Tab = ShaderTab | CodeTab | ImageTab;
17
+
18
+ export class TabbedLayout implements BaseLayout {
19
+ private container: HTMLElement;
20
+ private project: ShadertoyProject;
21
+ private root: HTMLElement;
22
+ private canvasContainer: HTMLElement;
23
+ private contentArea: HTMLElement;
24
+ private imageViewer: HTMLElement;
25
+
26
+ private editorContainer: HTMLElement;
27
+ private editorInstance: any = null;
28
+ private buttonContainer: HTMLElement;
29
+ private copyButton: HTMLElement;
30
+ private recompileButton: HTMLElement;
31
+ private errorDisplay: HTMLElement;
32
+ private recompileHandler: RecompileHandler | null = null;
33
+ private modifiedSources: Map<string, string> = new Map();
34
+ private tabs: Tab[] = [];
35
+ private activeTabIndex: number = 0;
36
+
37
+ constructor(opts: LayoutOptions) {
38
+ this.container = opts.container;
39
+ this.project = opts.project;
40
+
41
+ // Create root layout container
42
+ this.root = document.createElement('div');
43
+ this.root.className = 'layout-tabbed';
44
+
45
+ // Create wrapper to constrain size (matches default layout)
46
+ const wrapper = document.createElement('div');
47
+ wrapper.className = 'tabbed-wrapper';
48
+
49
+ // Create content area (holds either canvas, code, or image)
50
+ this.contentArea = document.createElement('div');
51
+ this.contentArea.className = 'tabbed-content';
52
+
53
+ // Create canvas container (shown when Shader tab is active)
54
+ this.canvasContainer = document.createElement('div');
55
+ this.canvasContainer.className = 'tabbed-canvas-container';
56
+
57
+ // Create image viewer (shown when image tabs are active)
58
+ this.imageViewer = document.createElement('div');
59
+ this.imageViewer.className = 'tabbed-image-viewer';
60
+ this.imageViewer.style.visibility = 'hidden';
61
+
62
+ this.contentArea.appendChild(this.canvasContainer);
63
+ this.contentArea.appendChild(this.imageViewer);
64
+
65
+ // Create editor container
66
+ this.editorContainer = document.createElement('div');
67
+ this.editorContainer.className = 'tabbed-editor-container';
68
+ this.editorContainer.style.visibility = 'hidden';
69
+ this.contentArea.appendChild(this.editorContainer);
70
+
71
+ // Create button container for copy and recompile (will be added to toolbar)
72
+ this.buttonContainer = document.createElement('div');
73
+ this.buttonContainer.className = 'tabbed-button-container';
74
+ this.buttonContainer.style.display = 'none';
75
+
76
+ // Create copy button (icon only)
77
+ this.copyButton = document.createElement('button');
78
+ this.copyButton.className = 'tabbed-copy-button';
79
+ this.copyButton.innerHTML = `
80
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
81
+ <path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2z" opacity="0.4"/>
82
+ <path d="M2 5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H2zm0 1h7a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/>
83
+ </svg>
84
+ `;
85
+ this.copyButton.title = 'Copy code to clipboard';
86
+ this.copyButton.addEventListener('click', () => this.copyToClipboard());
87
+ this.buttonContainer.appendChild(this.copyButton);
88
+
89
+ // Create recompile button
90
+ this.recompileButton = document.createElement('button');
91
+ this.recompileButton.className = 'tabbed-recompile-button';
92
+ this.recompileButton.innerHTML = `
93
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
94
+ <path d="M4 3v10l8-5-8-5z"/>
95
+ </svg>
96
+ Recompile
97
+ `;
98
+ this.recompileButton.title = 'Recompile shader (Ctrl+Enter)';
99
+ this.recompileButton.addEventListener('click', () => this.recompile());
100
+ this.buttonContainer.appendChild(this.recompileButton);
101
+
102
+ // Create error display
103
+ this.errorDisplay = document.createElement('div');
104
+ this.errorDisplay.className = 'tabbed-error-display';
105
+ this.errorDisplay.style.display = 'none';
106
+ this.contentArea.appendChild(this.errorDisplay);
107
+
108
+ // Set up keyboard shortcut for recompile
109
+ this.setupKeyboardShortcut();
110
+
111
+ // Build tab bar
112
+ const tabBar = this.buildTabBar();
113
+
114
+ // Assemble and append to DOM
115
+ wrapper.appendChild(tabBar);
116
+ wrapper.appendChild(this.contentArea);
117
+ this.root.appendChild(wrapper);
118
+ this.container.appendChild(this.root);
119
+ }
120
+
121
+ getCanvasContainer(): HTMLElement {
122
+ return this.canvasContainer;
123
+ }
124
+
125
+ setRecompileHandler(handler: RecompileHandler): void {
126
+ this.recompileHandler = handler;
127
+ }
128
+
129
+ dispose(): void {
130
+ if (this.editorInstance) {
131
+ this.editorInstance.destroy();
132
+ this.editorInstance = null;
133
+ }
134
+ this.container.innerHTML = '';
135
+ }
136
+
137
+ private setupKeyboardShortcut(): void {
138
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
139
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
140
+ const tab = this.tabs[this.activeTabIndex];
141
+ if (tab.kind === 'code') {
142
+ e.preventDefault();
143
+ this.recompile();
144
+ }
145
+ }
146
+ });
147
+ }
148
+
149
+ private saveCurrentEditorContent(): void {
150
+ if (this.editorInstance) {
151
+ const tab = this.tabs[this.activeTabIndex];
152
+ if (tab.kind === 'code') {
153
+ const source = this.editorInstance.getSource();
154
+ this.modifiedSources.set(tab.passName, source);
155
+ }
156
+ }
157
+ }
158
+
159
+ private recompile(): void {
160
+ if (!this.recompileHandler) {
161
+ console.warn('No recompile handler set');
162
+ return;
163
+ }
164
+
165
+ this.saveCurrentEditorContent();
166
+
167
+ const tab = this.tabs[this.activeTabIndex];
168
+ if (tab.kind !== 'code') return;
169
+
170
+ const source = this.modifiedSources.get(tab.passName) ?? tab.source;
171
+ const result = this.recompileHandler(tab.passName, source);
172
+
173
+ if (result.success) {
174
+ this.hideError();
175
+ tab.source = source;
176
+ } else {
177
+ this.showError(result.error || 'Compilation failed');
178
+ }
179
+ }
180
+
181
+ private showError(message: string): void {
182
+ if (this.errorDisplay) {
183
+ this.errorDisplay.textContent = message;
184
+ this.errorDisplay.style.display = 'block';
185
+ }
186
+ }
187
+
188
+ private hideError(): void {
189
+ if (this.errorDisplay) {
190
+ this.errorDisplay.style.display = 'none';
191
+ }
192
+ }
193
+
194
+ private async copyToClipboard(): Promise<void> {
195
+ const tab = this.tabs[this.activeTabIndex];
196
+ if (tab.kind !== 'code') return;
197
+
198
+ // Get current source (modified or original)
199
+ const source = this.editorInstance
200
+ ? this.editorInstance.getSource()
201
+ : (this.modifiedSources.get(tab.passName) ?? tab.source);
202
+
203
+ try {
204
+ await navigator.clipboard.writeText(source);
205
+ // Show checkmark feedback
206
+ const originalHTML = this.copyButton.innerHTML;
207
+ this.copyButton.innerHTML = `
208
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
209
+ <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
210
+ </svg>
211
+ `;
212
+ this.copyButton.classList.add('copied');
213
+ setTimeout(() => {
214
+ this.copyButton.innerHTML = originalHTML;
215
+ this.copyButton.classList.remove('copied');
216
+ }, 1500);
217
+ } catch (err) {
218
+ console.error('Failed to copy:', err);
219
+ }
220
+ }
221
+
222
+ private buildTabBar(): HTMLElement {
223
+ // Create toolbar container (holds tabs + buttons)
224
+ const toolbar = document.createElement('div');
225
+ toolbar.className = 'tabbed-toolbar';
226
+
227
+ // Create tab bar
228
+ const tabBar = document.createElement('div');
229
+ tabBar.className = 'tabbed-tab-bar';
230
+
231
+ // Build tabs: Shader first, then code files, then textures
232
+ this.tabs = [];
233
+
234
+ // 1. Shader output tab
235
+ this.tabs.push({ kind: 'shader', name: 'Shader' });
236
+
237
+ // 2. Common (if exists)
238
+ if (this.project.commonSource) {
239
+ this.tabs.push({
240
+ kind: 'code',
241
+ name: 'common.glsl',
242
+ passName: 'common',
243
+ source: this.project.commonSource,
244
+ });
245
+ }
246
+
247
+ // 3. Buffers in order
248
+ const bufferOrder: ('BufferA' | 'BufferB' | 'BufferC' | 'BufferD')[] = [
249
+ 'BufferA', 'BufferB', 'BufferC', 'BufferD',
250
+ ];
251
+ for (const bufferName of bufferOrder) {
252
+ const pass = this.project.passes[bufferName];
253
+ if (pass) {
254
+ this.tabs.push({
255
+ kind: 'code',
256
+ name: `${bufferName.toLowerCase()}.glsl`,
257
+ passName: bufferName,
258
+ source: pass.glslSource,
259
+ });
260
+ }
261
+ }
262
+
263
+ // 4. Image pass
264
+ const imagePass = this.project.passes.Image;
265
+ this.tabs.push({
266
+ kind: 'code',
267
+ name: 'image.glsl',
268
+ passName: 'Image',
269
+ source: imagePass.glslSource,
270
+ });
271
+
272
+ // 5. Textures (images)
273
+ for (const texture of this.project.textures) {
274
+ this.tabs.push({
275
+ kind: 'image',
276
+ name: texture.filename || texture.name,
277
+ url: texture.source,
278
+ });
279
+ }
280
+
281
+ // Function to show a specific tab
282
+ const showTab = async (tabIndex: number) => {
283
+ // Save current editor content before switching
284
+ this.saveCurrentEditorContent();
285
+
286
+ const tab = this.tabs[tabIndex];
287
+ this.activeTabIndex = tabIndex;
288
+
289
+ // Update active tab styling
290
+ tabBar.querySelectorAll('.tabbed-tab-button').forEach((b, i) => {
291
+ b.classList.toggle('active', i === tabIndex);
292
+ });
293
+
294
+ // Hide all content first
295
+ this.canvasContainer.style.visibility = 'hidden';
296
+ this.imageViewer.style.visibility = 'hidden';
297
+ this.editorContainer.style.visibility = 'hidden';
298
+ this.buttonContainer.style.display = 'none';
299
+
300
+ // Destroy previous editor instance
301
+ if (this.editorInstance) {
302
+ this.editorInstance.destroy();
303
+ this.editorInstance = null;
304
+ }
305
+
306
+ if (tab.kind === 'shader') {
307
+ // Show shader canvas
308
+ this.canvasContainer.style.visibility = 'visible';
309
+ } else if (tab.kind === 'code') {
310
+ // Show editor and buttons
311
+ this.editorContainer.style.visibility = 'visible';
312
+ this.buttonContainer.style.display = 'flex';
313
+
314
+ // Get source (use modified if available)
315
+ const source = this.modifiedSources.get(tab.passName) ?? tab.source;
316
+
317
+ // Clear and load editor
318
+ this.editorContainer.innerHTML = '';
319
+ try {
320
+ const { createEditor } = await import('../editor/prism-editor');
321
+ this.editorInstance = createEditor(this.editorContainer, source, (newSource) => {
322
+ this.modifiedSources.set(tab.passName, newSource);
323
+ });
324
+ } catch (err) {
325
+ console.error('Failed to load editor:', err);
326
+ // Fallback to textarea
327
+ const textarea = document.createElement('textarea');
328
+ textarea.className = 'tabbed-fallback-textarea';
329
+ textarea.value = source;
330
+ textarea.addEventListener('input', () => {
331
+ this.modifiedSources.set(tab.passName, textarea.value);
332
+ });
333
+ this.editorContainer.appendChild(textarea);
334
+ }
335
+ } else {
336
+ // Show image
337
+ this.imageViewer.style.visibility = 'visible';
338
+
339
+ const img = document.createElement('img');
340
+ img.src = tab.url;
341
+ img.alt = tab.name;
342
+
343
+ this.imageViewer.innerHTML = '';
344
+ this.imageViewer.appendChild(img);
345
+ }
346
+ };
347
+
348
+ // Create tab buttons
349
+ this.tabs.forEach((tab, index) => {
350
+ const tabButton = document.createElement('button');
351
+ tabButton.className = 'tabbed-tab-button';
352
+ if (tab.kind === 'shader') {
353
+ tabButton.classList.add('shader-tab');
354
+ } else if (tab.kind === 'image') {
355
+ tabButton.classList.add('image-tab');
356
+ }
357
+ tabButton.textContent = tab.name;
358
+ if (index === 0) tabButton.classList.add('active');
359
+
360
+ tabButton.addEventListener('click', () => showTab(index));
361
+
362
+ tabBar.appendChild(tabButton);
363
+ });
364
+
365
+ // Assemble toolbar: tabs + buttons
366
+ toolbar.appendChild(tabBar);
367
+ toolbar.appendChild(this.buttonContainer);
368
+
369
+ return toolbar;
370
+ }
371
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Default Layout Styles
3
+ */
4
+
5
+ .layout-default {
6
+ width: 100%;
7
+ height: 100%;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ padding: 60px;
12
+ }
13
+
14
+ .layout-default .canvas-container {
15
+ position: relative;
16
+ width: 800px;
17
+ height: 600px;
18
+ background: #000;
19
+ border-radius: 8px;
20
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25), 0 5px 15px rgba(0, 0, 0, 0.15);
21
+ overflow: hidden;
22
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Fullscreen Layout Styles
3
+ */
4
+
5
+ .layout-fullscreen {
6
+ width: 100%;
7
+ height: 100%;
8
+ }
9
+
10
+ .layout-fullscreen .canvas-container {
11
+ position: relative;
12
+ width: 100%;
13
+ height: 100%;
14
+ background: #000;
15
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Layouts - Modular layout system for Shadertoy viewer
3
+ *
4
+ * Provides four layout modes:
5
+ * - Default: Canvas centered with styling
6
+ * - Fullscreen: Canvas fills entire viewport
7
+ * - Split: Canvas on left, code viewer on right
8
+ * - Tabbed: Single window with tabs for shader and code
9
+ */
10
+
11
+ export { FullscreenLayout } from './FullscreenLayout';
12
+ export { DefaultLayout } from './DefaultLayout';
13
+ export { SplitLayout } from './SplitLayout';
14
+ export { TabbedLayout } from './TabbedLayout';
15
+ export type { BaseLayout, LayoutOptions, LayoutMode } from './types';
16
+
17
+ import { FullscreenLayout } from './FullscreenLayout';
18
+ import { DefaultLayout } from './DefaultLayout';
19
+ import { SplitLayout } from './SplitLayout';
20
+ import { TabbedLayout } from './TabbedLayout';
21
+ import { BaseLayout, LayoutOptions, LayoutMode } from './types';
22
+
23
+ /**
24
+ * Factory function to create the appropriate layout based on mode.
25
+ *
26
+ * @param mode - Layout mode to create
27
+ * @param options - Layout options
28
+ * @returns Layout instance implementing BaseLayout interface
29
+ */
30
+ export function createLayout(
31
+ mode: LayoutMode,
32
+ options: LayoutOptions
33
+ ): BaseLayout {
34
+ switch (mode) {
35
+ case 'fullscreen':
36
+ return new FullscreenLayout(options);
37
+ case 'default':
38
+ return new DefaultLayout(options);
39
+ case 'split':
40
+ return new SplitLayout(options);
41
+ case 'tabbed':
42
+ return new TabbedLayout(options);
43
+ }
44
+ }