@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,61 @@
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
+ import './split.css';
8
+ export class SplitLayout {
9
+ constructor(opts) {
10
+ this.editorPanel = null;
11
+ this.recompileHandler = null;
12
+ this.container = opts.container;
13
+ this.project = opts.project;
14
+ // Create root layout container
15
+ this.root = document.createElement('div');
16
+ this.root.className = 'layout-split';
17
+ // Create canvas container (left side)
18
+ this.canvasContainer = document.createElement('div');
19
+ this.canvasContainer.className = 'canvas-container';
20
+ // Create code panel (right side)
21
+ this.codePanel = document.createElement('div');
22
+ this.codePanel.className = 'code-panel';
23
+ // Build editor panel
24
+ this.buildEditorPanel();
25
+ // Assemble and append to DOM
26
+ this.root.appendChild(this.canvasContainer);
27
+ this.root.appendChild(this.codePanel);
28
+ this.container.appendChild(this.root);
29
+ }
30
+ getCanvasContainer() {
31
+ return this.canvasContainer;
32
+ }
33
+ setRecompileHandler(handler) {
34
+ this.recompileHandler = handler;
35
+ if (this.editorPanel) {
36
+ this.editorPanel.setRecompileHandler(handler);
37
+ }
38
+ }
39
+ dispose() {
40
+ if (this.editorPanel) {
41
+ this.editorPanel.dispose();
42
+ this.editorPanel = null;
43
+ }
44
+ this.container.innerHTML = '';
45
+ }
46
+ /**
47
+ * Build editor panel (dynamically loaded).
48
+ */
49
+ async buildEditorPanel() {
50
+ try {
51
+ const { EditorPanel } = await import('../editor/EditorPanel');
52
+ this.editorPanel = new EditorPanel(this.codePanel, this.project);
53
+ if (this.recompileHandler) {
54
+ this.editorPanel.setRecompileHandler(this.recompileHandler);
55
+ }
56
+ }
57
+ catch (err) {
58
+ console.error('Failed to load editor panel:', err);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,38 @@
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
+ import './tabbed.css';
8
+ import { BaseLayout, LayoutOptions, RecompileHandler } from './types';
9
+ export declare class TabbedLayout implements BaseLayout {
10
+ private container;
11
+ private project;
12
+ private root;
13
+ private canvasContainer;
14
+ private contentArea;
15
+ private imageViewer;
16
+ private editorContainer;
17
+ private editorInstance;
18
+ private buttonContainer;
19
+ private copyButton;
20
+ private recompileButton;
21
+ private errorDisplay;
22
+ private recompileHandler;
23
+ private modifiedSources;
24
+ private tabs;
25
+ private activeTabIndex;
26
+ constructor(opts: LayoutOptions);
27
+ getCanvasContainer(): HTMLElement;
28
+ setRecompileHandler(handler: RecompileHandler): void;
29
+ dispose(): void;
30
+ private setupKeyboardShortcut;
31
+ private saveCurrentEditorContent;
32
+ private recompile;
33
+ private showError;
34
+ private hideError;
35
+ private copyToClipboard;
36
+ private buildTabBar;
37
+ }
38
+ //# sourceMappingURL=TabbedLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabbedLayout.d.ts","sourceRoot":"","sources":["../../src/layouts/TabbedLayout.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,cAAc,CAAC;AAEtB,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAQtE,qBAAa,YAAa,YAAW,UAAU;IAC7C,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,WAAW,CAAc;IAEjC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,cAAc,CAAa;gBAEvB,IAAI,EAAE,aAAa;IAoF/B,kBAAkB,IAAI,WAAW;IAIjC,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAIpD,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,wBAAwB;IAUhC,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,SAAS;YAMH,eAAe;IA4B7B,OAAO,CAAC,WAAW;CAqJpB"}
@@ -0,0 +1,305 @@
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
+ import './tabbed.css';
8
+ export class TabbedLayout {
9
+ constructor(opts) {
10
+ this.editorInstance = null;
11
+ this.recompileHandler = null;
12
+ this.modifiedSources = new Map();
13
+ this.tabs = [];
14
+ this.activeTabIndex = 0;
15
+ this.container = opts.container;
16
+ this.project = opts.project;
17
+ // Create root layout container
18
+ this.root = document.createElement('div');
19
+ this.root.className = 'layout-tabbed';
20
+ // Create wrapper to constrain size (matches default layout)
21
+ const wrapper = document.createElement('div');
22
+ wrapper.className = 'tabbed-wrapper';
23
+ // Create content area (holds either canvas, code, or image)
24
+ this.contentArea = document.createElement('div');
25
+ this.contentArea.className = 'tabbed-content';
26
+ // Create canvas container (shown when Shader tab is active)
27
+ this.canvasContainer = document.createElement('div');
28
+ this.canvasContainer.className = 'tabbed-canvas-container';
29
+ // Create image viewer (shown when image tabs are active)
30
+ this.imageViewer = document.createElement('div');
31
+ this.imageViewer.className = 'tabbed-image-viewer';
32
+ this.imageViewer.style.visibility = 'hidden';
33
+ this.contentArea.appendChild(this.canvasContainer);
34
+ this.contentArea.appendChild(this.imageViewer);
35
+ // Create editor container
36
+ this.editorContainer = document.createElement('div');
37
+ this.editorContainer.className = 'tabbed-editor-container';
38
+ this.editorContainer.style.visibility = 'hidden';
39
+ this.contentArea.appendChild(this.editorContainer);
40
+ // Create button container for copy and recompile (will be added to toolbar)
41
+ this.buttonContainer = document.createElement('div');
42
+ this.buttonContainer.className = 'tabbed-button-container';
43
+ this.buttonContainer.style.display = 'none';
44
+ // Create copy button (icon only)
45
+ this.copyButton = document.createElement('button');
46
+ this.copyButton.className = 'tabbed-copy-button';
47
+ this.copyButton.innerHTML = `
48
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
49
+ <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"/>
50
+ <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"/>
51
+ </svg>
52
+ `;
53
+ this.copyButton.title = 'Copy code to clipboard';
54
+ this.copyButton.addEventListener('click', () => this.copyToClipboard());
55
+ this.buttonContainer.appendChild(this.copyButton);
56
+ // Create recompile button
57
+ this.recompileButton = document.createElement('button');
58
+ this.recompileButton.className = 'tabbed-recompile-button';
59
+ this.recompileButton.innerHTML = `
60
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
61
+ <path d="M4 3v10l8-5-8-5z"/>
62
+ </svg>
63
+ Recompile
64
+ `;
65
+ this.recompileButton.title = 'Recompile shader (Ctrl+Enter)';
66
+ this.recompileButton.addEventListener('click', () => this.recompile());
67
+ this.buttonContainer.appendChild(this.recompileButton);
68
+ // Create error display
69
+ this.errorDisplay = document.createElement('div');
70
+ this.errorDisplay.className = 'tabbed-error-display';
71
+ this.errorDisplay.style.display = 'none';
72
+ this.contentArea.appendChild(this.errorDisplay);
73
+ // Set up keyboard shortcut for recompile
74
+ this.setupKeyboardShortcut();
75
+ // Build tab bar
76
+ const tabBar = this.buildTabBar();
77
+ // Assemble and append to DOM
78
+ wrapper.appendChild(tabBar);
79
+ wrapper.appendChild(this.contentArea);
80
+ this.root.appendChild(wrapper);
81
+ this.container.appendChild(this.root);
82
+ }
83
+ getCanvasContainer() {
84
+ return this.canvasContainer;
85
+ }
86
+ setRecompileHandler(handler) {
87
+ this.recompileHandler = handler;
88
+ }
89
+ dispose() {
90
+ if (this.editorInstance) {
91
+ this.editorInstance.destroy();
92
+ this.editorInstance = null;
93
+ }
94
+ this.container.innerHTML = '';
95
+ }
96
+ setupKeyboardShortcut() {
97
+ document.addEventListener('keydown', (e) => {
98
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
99
+ const tab = this.tabs[this.activeTabIndex];
100
+ if (tab.kind === 'code') {
101
+ e.preventDefault();
102
+ this.recompile();
103
+ }
104
+ }
105
+ });
106
+ }
107
+ saveCurrentEditorContent() {
108
+ if (this.editorInstance) {
109
+ const tab = this.tabs[this.activeTabIndex];
110
+ if (tab.kind === 'code') {
111
+ const source = this.editorInstance.getSource();
112
+ this.modifiedSources.set(tab.passName, source);
113
+ }
114
+ }
115
+ }
116
+ recompile() {
117
+ if (!this.recompileHandler) {
118
+ console.warn('No recompile handler set');
119
+ return;
120
+ }
121
+ this.saveCurrentEditorContent();
122
+ const tab = this.tabs[this.activeTabIndex];
123
+ if (tab.kind !== 'code')
124
+ return;
125
+ const source = this.modifiedSources.get(tab.passName) ?? tab.source;
126
+ const result = this.recompileHandler(tab.passName, source);
127
+ if (result.success) {
128
+ this.hideError();
129
+ tab.source = source;
130
+ }
131
+ else {
132
+ this.showError(result.error || 'Compilation failed');
133
+ }
134
+ }
135
+ showError(message) {
136
+ if (this.errorDisplay) {
137
+ this.errorDisplay.textContent = message;
138
+ this.errorDisplay.style.display = 'block';
139
+ }
140
+ }
141
+ hideError() {
142
+ if (this.errorDisplay) {
143
+ this.errorDisplay.style.display = 'none';
144
+ }
145
+ }
146
+ async copyToClipboard() {
147
+ const tab = this.tabs[this.activeTabIndex];
148
+ if (tab.kind !== 'code')
149
+ return;
150
+ // Get current source (modified or original)
151
+ const source = this.editorInstance
152
+ ? this.editorInstance.getSource()
153
+ : (this.modifiedSources.get(tab.passName) ?? tab.source);
154
+ try {
155
+ await navigator.clipboard.writeText(source);
156
+ // Show checkmark feedback
157
+ const originalHTML = this.copyButton.innerHTML;
158
+ this.copyButton.innerHTML = `
159
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
160
+ <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"/>
161
+ </svg>
162
+ `;
163
+ this.copyButton.classList.add('copied');
164
+ setTimeout(() => {
165
+ this.copyButton.innerHTML = originalHTML;
166
+ this.copyButton.classList.remove('copied');
167
+ }, 1500);
168
+ }
169
+ catch (err) {
170
+ console.error('Failed to copy:', err);
171
+ }
172
+ }
173
+ buildTabBar() {
174
+ // Create toolbar container (holds tabs + buttons)
175
+ const toolbar = document.createElement('div');
176
+ toolbar.className = 'tabbed-toolbar';
177
+ // Create tab bar
178
+ const tabBar = document.createElement('div');
179
+ tabBar.className = 'tabbed-tab-bar';
180
+ // Build tabs: Shader first, then code files, then textures
181
+ this.tabs = [];
182
+ // 1. Shader output tab
183
+ this.tabs.push({ kind: 'shader', name: 'Shader' });
184
+ // 2. Common (if exists)
185
+ if (this.project.commonSource) {
186
+ this.tabs.push({
187
+ kind: 'code',
188
+ name: 'common.glsl',
189
+ passName: 'common',
190
+ source: this.project.commonSource,
191
+ });
192
+ }
193
+ // 3. Buffers in order
194
+ const bufferOrder = [
195
+ 'BufferA', 'BufferB', 'BufferC', 'BufferD',
196
+ ];
197
+ for (const bufferName of bufferOrder) {
198
+ const pass = this.project.passes[bufferName];
199
+ if (pass) {
200
+ this.tabs.push({
201
+ kind: 'code',
202
+ name: `${bufferName.toLowerCase()}.glsl`,
203
+ passName: bufferName,
204
+ source: pass.glslSource,
205
+ });
206
+ }
207
+ }
208
+ // 4. Image pass
209
+ const imagePass = this.project.passes.Image;
210
+ this.tabs.push({
211
+ kind: 'code',
212
+ name: 'image.glsl',
213
+ passName: 'Image',
214
+ source: imagePass.glslSource,
215
+ });
216
+ // 5. Textures (images)
217
+ for (const texture of this.project.textures) {
218
+ this.tabs.push({
219
+ kind: 'image',
220
+ name: texture.filename || texture.name,
221
+ url: texture.source,
222
+ });
223
+ }
224
+ // Function to show a specific tab
225
+ const showTab = async (tabIndex) => {
226
+ // Save current editor content before switching
227
+ this.saveCurrentEditorContent();
228
+ const tab = this.tabs[tabIndex];
229
+ this.activeTabIndex = tabIndex;
230
+ // Update active tab styling
231
+ tabBar.querySelectorAll('.tabbed-tab-button').forEach((b, i) => {
232
+ b.classList.toggle('active', i === tabIndex);
233
+ });
234
+ // Hide all content first
235
+ this.canvasContainer.style.visibility = 'hidden';
236
+ this.imageViewer.style.visibility = 'hidden';
237
+ this.editorContainer.style.visibility = 'hidden';
238
+ this.buttonContainer.style.display = 'none';
239
+ // Destroy previous editor instance
240
+ if (this.editorInstance) {
241
+ this.editorInstance.destroy();
242
+ this.editorInstance = null;
243
+ }
244
+ if (tab.kind === 'shader') {
245
+ // Show shader canvas
246
+ this.canvasContainer.style.visibility = 'visible';
247
+ }
248
+ else if (tab.kind === 'code') {
249
+ // Show editor and buttons
250
+ this.editorContainer.style.visibility = 'visible';
251
+ this.buttonContainer.style.display = 'flex';
252
+ // Get source (use modified if available)
253
+ const source = this.modifiedSources.get(tab.passName) ?? tab.source;
254
+ // Clear and load editor
255
+ this.editorContainer.innerHTML = '';
256
+ try {
257
+ const { createEditor } = await import('../editor/prism-editor');
258
+ this.editorInstance = createEditor(this.editorContainer, source, (newSource) => {
259
+ this.modifiedSources.set(tab.passName, newSource);
260
+ });
261
+ }
262
+ catch (err) {
263
+ console.error('Failed to load editor:', err);
264
+ // Fallback to textarea
265
+ const textarea = document.createElement('textarea');
266
+ textarea.className = 'tabbed-fallback-textarea';
267
+ textarea.value = source;
268
+ textarea.addEventListener('input', () => {
269
+ this.modifiedSources.set(tab.passName, textarea.value);
270
+ });
271
+ this.editorContainer.appendChild(textarea);
272
+ }
273
+ }
274
+ else {
275
+ // Show image
276
+ this.imageViewer.style.visibility = 'visible';
277
+ const img = document.createElement('img');
278
+ img.src = tab.url;
279
+ img.alt = tab.name;
280
+ this.imageViewer.innerHTML = '';
281
+ this.imageViewer.appendChild(img);
282
+ }
283
+ };
284
+ // Create tab buttons
285
+ this.tabs.forEach((tab, index) => {
286
+ const tabButton = document.createElement('button');
287
+ tabButton.className = 'tabbed-tab-button';
288
+ if (tab.kind === 'shader') {
289
+ tabButton.classList.add('shader-tab');
290
+ }
291
+ else if (tab.kind === 'image') {
292
+ tabButton.classList.add('image-tab');
293
+ }
294
+ tabButton.textContent = tab.name;
295
+ if (index === 0)
296
+ tabButton.classList.add('active');
297
+ tabButton.addEventListener('click', () => showTab(index));
298
+ tabBar.appendChild(tabButton);
299
+ });
300
+ // Assemble toolbar: tabs + buttons
301
+ toolbar.appendChild(tabBar);
302
+ toolbar.appendChild(this.buttonContainer);
303
+ return toolbar;
304
+ }
305
+ }
@@ -0,0 +1,24 @@
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
+ export { FullscreenLayout } from './FullscreenLayout';
11
+ export { DefaultLayout } from './DefaultLayout';
12
+ export { SplitLayout } from './SplitLayout';
13
+ export { TabbedLayout } from './TabbedLayout';
14
+ export type { BaseLayout, LayoutOptions, LayoutMode } from './types';
15
+ import { BaseLayout, LayoutOptions, LayoutMode } from './types';
16
+ /**
17
+ * Factory function to create the appropriate layout based on mode.
18
+ *
19
+ * @param mode - Layout mode to create
20
+ * @param options - Layout options
21
+ * @returns Layout instance implementing BaseLayout interface
22
+ */
23
+ export declare function createLayout(mode: LayoutMode, options: LayoutOptions): BaseLayout;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layouts/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAMrE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEhE;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,aAAa,GACrB,UAAU,CAWZ"}
@@ -0,0 +1,36 @@
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
+ export { FullscreenLayout } from './FullscreenLayout';
11
+ export { DefaultLayout } from './DefaultLayout';
12
+ export { SplitLayout } from './SplitLayout';
13
+ export { TabbedLayout } from './TabbedLayout';
14
+ import { FullscreenLayout } from './FullscreenLayout';
15
+ import { DefaultLayout } from './DefaultLayout';
16
+ import { SplitLayout } from './SplitLayout';
17
+ import { TabbedLayout } from './TabbedLayout';
18
+ /**
19
+ * Factory function to create the appropriate layout based on mode.
20
+ *
21
+ * @param mode - Layout mode to create
22
+ * @param options - Layout options
23
+ * @returns Layout instance implementing BaseLayout interface
24
+ */
25
+ export function createLayout(mode, options) {
26
+ switch (mode) {
27
+ case 'fullscreen':
28
+ return new FullscreenLayout(options);
29
+ case 'default':
30
+ return new DefaultLayout(options);
31
+ case 'split':
32
+ return new SplitLayout(options);
33
+ case 'tabbed':
34
+ return new TabbedLayout(options);
35
+ }
36
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Split Layout Styles
3
+ * Includes code panel, tabs, and Prism.js syntax highlighting theme
4
+ */
5
+
6
+ /* ===== Split Layout ===== */
7
+ .layout-split {
8
+ width: 100%;
9
+ height: 100%;
10
+ display: flex;
11
+ gap: 40px;
12
+ /* Very generous margins on large screens */
13
+ padding: 120px 140px;
14
+ }
15
+
16
+ .layout-split .canvas-container {
17
+ position: relative;
18
+ flex: 1;
19
+ background: #000;
20
+ border-radius: 8px;
21
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2), 0 3px 8px rgba(0, 0, 0, 0.12);
22
+ overflow: hidden;
23
+ }
24
+
25
+ .layout-split .code-panel {
26
+ position: relative;
27
+ flex: 1;
28
+ display: flex;
29
+ flex-direction: column;
30
+ background: white;
31
+ border-radius: 8px;
32
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2), 0 3px 8px rgba(0, 0, 0, 0.12);
33
+ overflow: hidden;
34
+ }
35
+
36
+ /* ===== Code Panel ===== */
37
+ .tab-bar {
38
+ display: flex;
39
+ background: #f8f8f8;
40
+ border-bottom: 1px solid #e0e0e0;
41
+ padding: 8px 8px 0 8px;
42
+ gap: 4px;
43
+ }
44
+
45
+ .tab-button {
46
+ padding: 8px 16px;
47
+ background: transparent;
48
+ border: none;
49
+ border-radius: 6px 6px 0 0;
50
+ font-size: 13px;
51
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
52
+ cursor: pointer;
53
+ transition: background 0.2s;
54
+ color: #666;
55
+ }
56
+
57
+ .tab-button:hover {
58
+ background: #e8e8e8;
59
+ }
60
+
61
+ .tab-button.active {
62
+ background: white;
63
+ color: #000;
64
+ font-weight: 500;
65
+ }
66
+
67
+ .copy-button {
68
+ position: absolute;
69
+ top: 12px;
70
+ right: 12px;
71
+ padding: 6px;
72
+ background: transparent;
73
+ border: none;
74
+ border-radius: 4px;
75
+ color: #666;
76
+ cursor: pointer;
77
+ transition: all 0.2s;
78
+ z-index: 10;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ }
83
+
84
+ .copy-button:hover {
85
+ background: rgba(0, 0, 0, 0.05);
86
+ color: #333;
87
+ }
88
+
89
+ .copy-button:active {
90
+ transform: scale(0.9);
91
+ }
92
+
93
+ .copy-button.copied {
94
+ color: #4caf50;
95
+ }
96
+
97
+ .code-viewer {
98
+ flex: 1;
99
+ min-height: 0; /* Allow shrinking below content size in flexbox */
100
+ overflow: auto;
101
+ position: relative;
102
+ background: white;
103
+ }
104
+
105
+ /* Prism.js syntax highlighting */
106
+ .code-viewer pre {
107
+ margin: 0;
108
+ padding: 16px;
109
+ font-size: 13px;
110
+ line-height: 1.5;
111
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
112
+ background: white;
113
+ }
114
+
115
+ .code-viewer code {
116
+ font-family: inherit;
117
+ font-size: inherit;
118
+ }
119
+
120
+ /* Prism theme - simple light theme */
121
+ .token.comment { color: #6a9955; }
122
+ .token.keyword { color: #0000ff; }
123
+ .token.string { color: #a31515; }
124
+ .token.number { color: #098658; }
125
+ .token.operator { color: #000000; }
126
+ .token.function { color: #795e26; }
127
+ .token.class-name { color: #267f99; }
128
+ .token.punctuation { color: #000000; }
129
+
130
+ /* Image tab styling */
131
+ .tab-button.image-tab {
132
+ color: #7c4dff;
133
+ }
134
+
135
+ .tab-button.image-tab.active {
136
+ color: #7c4dff;
137
+ }
138
+
139
+ /* Image viewer */
140
+ .image-viewer {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ height: 100%;
145
+ padding: 16px;
146
+ background: #f5f5f5;
147
+ }
148
+
149
+ .image-viewer img {
150
+ max-width: 100%;
151
+ max-height: 100%;
152
+ object-fit: contain;
153
+ border-radius: 4px;
154
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
155
+ }
156
+
157
+ /* ===== Responsive adjustments ===== */
158
+
159
+ /* Progressively shrink margins on smaller screens */
160
+ @media (max-width: 1800px) {
161
+ .layout-split {
162
+ padding: 100px 120px;
163
+ }
164
+ }
165
+
166
+ @media (max-width: 1600px) {
167
+ .layout-split {
168
+ padding: 80px 100px;
169
+ }
170
+ }
171
+
172
+ @media (max-width: 1400px) {
173
+ .layout-split {
174
+ padding: 60px 80px;
175
+ }
176
+ }
177
+
178
+ @media (max-width: 1200px) {
179
+ .layout-split {
180
+ padding: 50px 60px;
181
+ }
182
+ }
183
+
184
+ @media (max-width: 1000px) {
185
+ .layout-split {
186
+ padding: 40px 50px;
187
+ }
188
+ }
189
+
190
+ @media (max-width: 800px) {
191
+ .layout-split {
192
+ flex-direction: column;
193
+ padding: 30px;
194
+ gap: 30px;
195
+ }
196
+ }