@stevejtrettel/shader-sandbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +391 -0
- package/bin/cli.js +389 -0
- package/dist-lib/app/App.d.ts +134 -0
- package/dist-lib/app/App.d.ts.map +1 -0
- package/dist-lib/app/App.js +570 -0
- package/dist-lib/app/types.d.ts +32 -0
- package/dist-lib/app/types.d.ts.map +1 -0
- package/dist-lib/app/types.js +6 -0
- package/dist-lib/editor/EditorPanel.d.ts +39 -0
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -0
- package/dist-lib/editor/EditorPanel.js +274 -0
- package/dist-lib/editor/prism-editor.css +99 -0
- package/dist-lib/editor/prism-editor.d.ts +19 -0
- package/dist-lib/editor/prism-editor.d.ts.map +1 -0
- package/dist-lib/editor/prism-editor.js +96 -0
- package/dist-lib/embed.d.ts +17 -0
- package/dist-lib/embed.d.ts.map +1 -0
- package/dist-lib/embed.js +35 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts +160 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShadertoyEngine.js +704 -0
- package/dist-lib/engine/glHelpers.d.ts +79 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -0
- package/dist-lib/engine/glHelpers.js +298 -0
- package/dist-lib/engine/types.d.ts +77 -0
- package/dist-lib/engine/types.d.ts.map +1 -0
- package/dist-lib/engine/types.js +7 -0
- package/dist-lib/index.d.ts +12 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +9 -0
- package/dist-lib/layouts/DefaultLayout.d.ts +17 -0
- package/dist-lib/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist-lib/layouts/DefaultLayout.js +27 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts +17 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist-lib/layouts/FullscreenLayout.js +27 -0
- package/dist-lib/layouts/SplitLayout.d.ts +26 -0
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -0
- package/dist-lib/layouts/SplitLayout.js +61 -0
- package/dist-lib/layouts/TabbedLayout.d.ts +38 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -0
- package/dist-lib/layouts/TabbedLayout.js +305 -0
- package/dist-lib/layouts/index.d.ts +24 -0
- package/dist-lib/layouts/index.d.ts.map +1 -0
- package/dist-lib/layouts/index.js +36 -0
- package/dist-lib/layouts/split.css +196 -0
- package/dist-lib/layouts/tabbed.css +345 -0
- package/dist-lib/layouts/types.d.ts +48 -0
- package/dist-lib/layouts/types.d.ts.map +1 -0
- package/dist-lib/layouts/types.js +4 -0
- package/dist-lib/main.d.ts +15 -0
- package/dist-lib/main.d.ts.map +1 -0
- package/dist-lib/main.js +102 -0
- package/dist-lib/project/generatedLoader.d.ts +3 -0
- package/dist-lib/project/generatedLoader.d.ts.map +1 -0
- package/dist-lib/project/generatedLoader.js +17 -0
- package/dist-lib/project/loadProject.d.ts +22 -0
- package/dist-lib/project/loadProject.d.ts.map +1 -0
- package/dist-lib/project/loadProject.js +350 -0
- package/dist-lib/project/loaderHelper.d.ts +7 -0
- package/dist-lib/project/loaderHelper.d.ts.map +1 -0
- package/dist-lib/project/loaderHelper.js +240 -0
- package/dist-lib/project/types.d.ts +192 -0
- package/dist-lib/project/types.d.ts.map +1 -0
- package/dist-lib/project/types.js +7 -0
- package/dist-lib/styles/base.css +29 -0
- package/package.json +48 -0
- package/src/app/App.ts +699 -0
- package/src/app/app.css +208 -0
- package/src/app/types.ts +36 -0
- package/src/editor/EditorPanel.ts +340 -0
- package/src/editor/editor-panel.css +175 -0
- package/src/editor/prism-editor.css +99 -0
- package/src/editor/prism-editor.ts +124 -0
- package/src/embed.ts +55 -0
- package/src/engine/ShadertoyEngine.ts +929 -0
- package/src/engine/glHelpers.ts +432 -0
- package/src/engine/types.ts +118 -0
- package/src/index.ts +13 -0
- package/src/layouts/DefaultLayout.ts +40 -0
- package/src/layouts/FullscreenLayout.ts +40 -0
- package/src/layouts/SplitLayout.ts +81 -0
- package/src/layouts/TabbedLayout.ts +371 -0
- package/src/layouts/default.css +22 -0
- package/src/layouts/fullscreen.css +15 -0
- package/src/layouts/index.ts +44 -0
- package/src/layouts/split.css +196 -0
- package/src/layouts/tabbed.css +345 -0
- package/src/layouts/types.ts +58 -0
- package/src/main.ts +114 -0
- package/src/project/generatedLoader.ts +23 -0
- package/src/project/loadProject.ts +421 -0
- package/src/project/loaderHelper.ts +300 -0
- package/src/project/types.ts +243 -0
- package/src/styles/base.css +29 -0
- package/src/styles/embed.css +14 -0
- package/src/vite-env.d.ts +1 -0
- package/templates/index.html +28 -0
- package/templates/main.ts +126 -0
- package/templates/package.json +12 -0
- package/templates/shaders/example-buffer/bufferA.glsl +14 -0
- package/templates/shaders/example-buffer/config.json +10 -0
- package/templates/shaders/example-buffer/image.glsl +5 -0
- package/templates/shaders/example-gradient/config.json +4 -0
- package/templates/shaders/example-gradient/image.glsl +7 -0
- package/templates/vite.config.js +35 -0
|
@@ -0,0 +1,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,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
|
+
}
|