@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,570 @@
1
+ /**
2
+ * App Layer - Browser Runtime Coordinator
3
+ *
4
+ * Responsibilities:
5
+ * - Create and manage canvas
6
+ * - Initialize ShadertoyEngine
7
+ * - Run animation loop (requestAnimationFrame)
8
+ * - Handle resize and mouse events
9
+ * - Present Image pass output to screen
10
+ */
11
+ import './app.css';
12
+ import { ShadertoyEngine } from '../engine/ShadertoyEngine';
13
+ export class App {
14
+ constructor(opts) {
15
+ this.animationId = null;
16
+ this.startTime = 0;
17
+ // Mouse state for iMouse uniform
18
+ this.mouse = [0, 0, -1, -1];
19
+ this.frameCount = 0;
20
+ this.lastFpsUpdate = 0;
21
+ this.currentFps = 0;
22
+ // Playback controls
23
+ this.controlsContainer = null;
24
+ this.playPauseButton = null;
25
+ this.isPaused = false;
26
+ // Error overlay
27
+ this.errorOverlay = null;
28
+ this.isVisible = true;
29
+ // ===========================================================================
30
+ // Animation Loop
31
+ // ===========================================================================
32
+ this.animate = (currentTimeMs) => {
33
+ // Schedule next frame first (even if paused or invisible)
34
+ this.animationId = requestAnimationFrame(this.animate);
35
+ // Skip rendering if paused or off-screen
36
+ if (this.isPaused || !this.isVisible) {
37
+ return;
38
+ }
39
+ const currentTimeSec = currentTimeMs / 1000;
40
+ const elapsedTime = currentTimeSec - this.startTime;
41
+ // Update FPS counter
42
+ this.updateFps(currentTimeSec);
43
+ // Update keyboard texture with current key states
44
+ this.engine.updateKeyboardTexture();
45
+ // Run engine step
46
+ this.engine.step(elapsedTime, this.mouse);
47
+ // Present Image pass output to screen
48
+ this.presentToScreen();
49
+ };
50
+ this.container = opts.container;
51
+ this.project = opts.project;
52
+ this.pixelRatio = opts.pixelRatio ?? window.devicePixelRatio;
53
+ // Create canvas
54
+ this.canvas = document.createElement('canvas');
55
+ this.canvas.style.width = '100%';
56
+ this.canvas.style.height = '100%';
57
+ this.canvas.style.display = 'block';
58
+ this.container.appendChild(this.canvas);
59
+ // Create FPS display overlay
60
+ this.fpsDisplay = document.createElement('div');
61
+ this.fpsDisplay.className = 'fps-counter';
62
+ this.fpsDisplay.textContent = '0 FPS';
63
+ this.container.appendChild(this.fpsDisplay);
64
+ // Create playback controls if enabled
65
+ if (opts.project.controls) {
66
+ this.createControls();
67
+ }
68
+ // Get WebGL2 context
69
+ const gl = this.canvas.getContext('webgl2', {
70
+ alpha: false,
71
+ antialias: false,
72
+ depth: false,
73
+ stencil: false,
74
+ preserveDrawingBuffer: true, // Required for screenshots
75
+ powerPreference: 'high-performance',
76
+ });
77
+ if (!gl) {
78
+ throw new Error('WebGL2 not supported');
79
+ }
80
+ this.gl = gl;
81
+ // Initialize canvas size
82
+ this.updateCanvasSize();
83
+ // Create engine
84
+ this.engine = new ShadertoyEngine({
85
+ gl: this.gl,
86
+ project: opts.project,
87
+ });
88
+ // Check for compilation errors and show overlay if needed
89
+ if (this.engine.hasErrors()) {
90
+ this.showErrorOverlay(this.engine.getCompilationErrors());
91
+ }
92
+ // Set up resize observer
93
+ this.resizeObserver = new ResizeObserver(() => {
94
+ this.updateCanvasSize();
95
+ this.engine.resize(this.canvas.width, this.canvas.height);
96
+ // Reset frame counter so shaders can reinitialize (important for accumulators)
97
+ this.startTime = performance.now() / 1000;
98
+ this.engine.reset();
99
+ });
100
+ this.resizeObserver.observe(this.container);
101
+ // Set up intersection observer for auto-pause when off-screen
102
+ this.intersectionObserver = new IntersectionObserver((entries) => {
103
+ const entry = entries[0];
104
+ this.isVisible = entry.isIntersecting;
105
+ }, { threshold: 0.1 } // Trigger when 10% visible
106
+ );
107
+ this.intersectionObserver.observe(this.container);
108
+ // Set up mouse tracking
109
+ this.setupMouseTracking();
110
+ // Set up keyboard tracking for shader keyboard texture
111
+ this.setupKeyboardTracking();
112
+ // Set up global keyboard shortcuts (always available)
113
+ this.setupGlobalShortcuts();
114
+ // Set up keyboard shortcuts if controls are enabled
115
+ if (opts.project.controls) {
116
+ this.setupKeyboardShortcuts();
117
+ }
118
+ }
119
+ // ===========================================================================
120
+ // Public API
121
+ // ===========================================================================
122
+ /**
123
+ * Check if there were any shader compilation errors.
124
+ * Returns true if the engine has errors and should not be started.
125
+ */
126
+ hasErrors() {
127
+ return this.engine.hasErrors();
128
+ }
129
+ /**
130
+ * Get the underlying engine instance.
131
+ * Used for live recompilation in editor mode.
132
+ */
133
+ getEngine() {
134
+ return this.engine;
135
+ }
136
+ /**
137
+ * Start the animation loop.
138
+ */
139
+ start() {
140
+ if (this.animationId !== null) {
141
+ return; // Already running
142
+ }
143
+ this.startTime = performance.now() / 1000;
144
+ this.animate(this.startTime);
145
+ }
146
+ /**
147
+ * Stop the animation loop.
148
+ */
149
+ stop() {
150
+ if (this.animationId !== null) {
151
+ cancelAnimationFrame(this.animationId);
152
+ this.animationId = null;
153
+ }
154
+ }
155
+ /**
156
+ * Clean up all resources.
157
+ */
158
+ dispose() {
159
+ this.stop();
160
+ this.resizeObserver.disconnect();
161
+ this.intersectionObserver.disconnect();
162
+ this.engine.dispose();
163
+ this.container.removeChild(this.canvas);
164
+ this.container.removeChild(this.fpsDisplay);
165
+ }
166
+ /**
167
+ * Update FPS counter.
168
+ * Updates the display roughly once per second.
169
+ */
170
+ updateFps(currentTimeSec) {
171
+ this.frameCount++;
172
+ // Update FPS display once per second
173
+ if (currentTimeSec - this.lastFpsUpdate >= 1.0) {
174
+ this.currentFps = this.frameCount / (currentTimeSec - this.lastFpsUpdate);
175
+ this.fpsDisplay.textContent = `${Math.round(this.currentFps)} FPS`;
176
+ this.frameCount = 0;
177
+ this.lastFpsUpdate = currentTimeSec;
178
+ }
179
+ }
180
+ /**
181
+ * Present the Image pass output to the screen.
182
+ *
183
+ * Since Image is the final pass and we execute all passes to their FBOs,
184
+ * we need to blit the Image pass output to the default framebuffer.
185
+ */
186
+ presentToScreen() {
187
+ const gl = this.gl;
188
+ const imageFramebuffer = this.engine.getImageFramebuffer();
189
+ if (!imageFramebuffer) {
190
+ console.warn('No Image pass found');
191
+ return;
192
+ }
193
+ // Bind default framebuffer (screen)
194
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
195
+ // Clear screen
196
+ gl.clearColor(0, 0, 0, 1);
197
+ gl.clear(gl.COLOR_BUFFER_BIT);
198
+ // Blit Image pass FBO to screen
199
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, imageFramebuffer);
200
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
201
+ gl.blitFramebuffer(0, 0, this.canvas.width, this.canvas.height, // src
202
+ 0, 0, this.canvas.width, this.canvas.height, // dst
203
+ gl.COLOR_BUFFER_BIT, gl.NEAREST);
204
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
205
+ }
206
+ // ===========================================================================
207
+ // Resize Handling
208
+ // ===========================================================================
209
+ updateCanvasSize() {
210
+ const rect = this.container.getBoundingClientRect();
211
+ const width = Math.floor(rect.width * this.pixelRatio);
212
+ const height = Math.floor(rect.height * this.pixelRatio);
213
+ if (this.canvas.width !== width || this.canvas.height !== height) {
214
+ this.canvas.width = width;
215
+ this.canvas.height = height;
216
+ }
217
+ }
218
+ // ===========================================================================
219
+ // Mouse Tracking
220
+ // ===========================================================================
221
+ setupMouseTracking() {
222
+ const updateMouse = (e) => {
223
+ const rect = this.canvas.getBoundingClientRect();
224
+ const x = (e.clientX - rect.left) * this.pixelRatio;
225
+ const y = (rect.height - (e.clientY - rect.top)) * this.pixelRatio; // Flip Y
226
+ this.mouse[0] = x;
227
+ this.mouse[1] = y;
228
+ };
229
+ const handleClick = (e) => {
230
+ const rect = this.canvas.getBoundingClientRect();
231
+ const x = (e.clientX - rect.left) * this.pixelRatio;
232
+ const y = (rect.height - (e.clientY - rect.top)) * this.pixelRatio; // Flip Y
233
+ this.mouse[2] = x;
234
+ this.mouse[3] = y;
235
+ };
236
+ this.canvas.addEventListener('mousemove', updateMouse);
237
+ this.canvas.addEventListener('click', handleClick);
238
+ }
239
+ // ===========================================================================
240
+ // Playback Controls
241
+ // ===========================================================================
242
+ /**
243
+ * Create playback control buttons (play/pause and reset).
244
+ */
245
+ createControls() {
246
+ // Create container
247
+ this.controlsContainer = document.createElement('div');
248
+ this.controlsContainer.className = 'playback-controls';
249
+ // Play/Pause button (starts showing pause icon since we're playing)
250
+ this.playPauseButton = document.createElement('button');
251
+ this.playPauseButton.className = 'control-button';
252
+ this.playPauseButton.title = 'Play/Pause (Space)';
253
+ this.playPauseButton.innerHTML = `
254
+ <svg viewBox="0 0 16 16">
255
+ <path d="M5 3h2v10H5V3zm4 0h2v10H9V3z"/>
256
+ </svg>
257
+ `;
258
+ this.playPauseButton.addEventListener('click', () => this.togglePlayPause());
259
+ // Reset button
260
+ const resetButton = document.createElement('button');
261
+ resetButton.className = 'control-button';
262
+ resetButton.title = 'Reset (R)';
263
+ resetButton.innerHTML = `
264
+ <svg viewBox="0 0 16 16">
265
+ <path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
266
+ <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
267
+ </svg>
268
+ `;
269
+ resetButton.addEventListener('click', () => this.reset());
270
+ // Screenshot button
271
+ const screenshotButton = document.createElement('button');
272
+ screenshotButton.className = 'control-button';
273
+ screenshotButton.title = 'Screenshot (S)';
274
+ screenshotButton.innerHTML = `
275
+ <svg viewBox="0 0 16 16">
276
+ <path d="M10.5 8.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/>
277
+ <path d="M2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2zm.5 2a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 2.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0z"/>
278
+ </svg>
279
+ `;
280
+ screenshotButton.addEventListener('click', () => this.screenshot());
281
+ // Add to container
282
+ this.controlsContainer.appendChild(this.playPauseButton);
283
+ this.controlsContainer.appendChild(resetButton);
284
+ this.controlsContainer.appendChild(screenshotButton);
285
+ this.container.appendChild(this.controlsContainer);
286
+ }
287
+ /**
288
+ * Set up keyboard tracking for shader keyboard texture.
289
+ * Tracks all key presses/releases and forwards to engine.
290
+ */
291
+ setupKeyboardTracking() {
292
+ // Track keydown events
293
+ document.addEventListener('keydown', (e) => {
294
+ // Get keycode - use e.keyCode which is the ASCII code
295
+ const keycode = e.keyCode;
296
+ if (keycode >= 0 && keycode < 256) {
297
+ this.engine.updateKeyState(keycode, true);
298
+ }
299
+ });
300
+ // Track keyup events
301
+ document.addEventListener('keyup', (e) => {
302
+ const keycode = e.keyCode;
303
+ if (keycode >= 0 && keycode < 256) {
304
+ this.engine.updateKeyState(keycode, false);
305
+ }
306
+ });
307
+ }
308
+ /**
309
+ * Set up global keyboard shortcuts (always available).
310
+ */
311
+ setupGlobalShortcuts() {
312
+ document.addEventListener('keydown', (e) => {
313
+ // S - Screenshot
314
+ if (e.code === 'KeyS' && !e.repeat) {
315
+ e.preventDefault();
316
+ this.screenshot();
317
+ }
318
+ });
319
+ }
320
+ /**
321
+ * Set up keyboard shortcuts for playback control.
322
+ */
323
+ setupKeyboardShortcuts() {
324
+ document.addEventListener('keydown', (e) => {
325
+ // Space - Play/Pause
326
+ if (e.code === 'Space' && !e.repeat) {
327
+ e.preventDefault();
328
+ this.togglePlayPause();
329
+ }
330
+ // R - Reset
331
+ if (e.code === 'KeyR' && !e.repeat) {
332
+ e.preventDefault();
333
+ this.reset();
334
+ }
335
+ });
336
+ }
337
+ /**
338
+ * Toggle between play and pause states.
339
+ */
340
+ togglePlayPause() {
341
+ this.isPaused = !this.isPaused;
342
+ this.updatePlayPauseButton();
343
+ }
344
+ /**
345
+ * Reset the shader to frame 0.
346
+ */
347
+ reset() {
348
+ this.startTime = performance.now() / 1000;
349
+ this.frameCount = 0;
350
+ this.lastFpsUpdate = 0;
351
+ this.engine.reset();
352
+ }
353
+ /**
354
+ * Capture and download a screenshot of the current canvas as PNG.
355
+ * Filename format: shadertoy-{folderName}-{timestamp}.png
356
+ */
357
+ screenshot() {
358
+ // Extract folder name from project root (e.g., "/demos/keyboard-test" -> "keyboard-test")
359
+ const folderName = this.project.root.split('/').pop() || 'shader';
360
+ // Generate timestamp (YYYYMMDD-HHMMSS)
361
+ const now = new Date();
362
+ const timestamp = now.getFullYear().toString() +
363
+ (now.getMonth() + 1).toString().padStart(2, '0') +
364
+ now.getDate().toString().padStart(2, '0') + '-' +
365
+ now.getHours().toString().padStart(2, '0') +
366
+ now.getMinutes().toString().padStart(2, '0') +
367
+ now.getSeconds().toString().padStart(2, '0');
368
+ const filename = `shadertoy-${folderName}-${timestamp}.png`;
369
+ // Capture canvas as PNG blob
370
+ this.canvas.toBlob((blob) => {
371
+ if (!blob) {
372
+ console.error('Failed to create screenshot blob');
373
+ return;
374
+ }
375
+ // Create download link
376
+ const url = URL.createObjectURL(blob);
377
+ const link = document.createElement('a');
378
+ link.href = url;
379
+ link.download = filename;
380
+ link.click();
381
+ // Clean up
382
+ URL.revokeObjectURL(url);
383
+ console.log(`Screenshot saved: ${filename}`);
384
+ }, 'image/png');
385
+ }
386
+ /**
387
+ * Update play/pause button icon based on current state.
388
+ */
389
+ updatePlayPauseButton() {
390
+ if (!this.playPauseButton)
391
+ return;
392
+ if (this.isPaused) {
393
+ // Show play icon
394
+ this.playPauseButton.innerHTML = `
395
+ <svg viewBox="0 0 16 16">
396
+ <path d="M4 3v10l8-5-8-5z"/>
397
+ </svg>
398
+ `;
399
+ }
400
+ else {
401
+ // Show pause icon
402
+ this.playPauseButton.innerHTML = `
403
+ <svg viewBox="0 0 16 16">
404
+ <path d="M5 3h2v10H5V3zm4 0h2v10H9V3z"/>
405
+ </svg>
406
+ `;
407
+ }
408
+ }
409
+ // ===========================================================================
410
+ // Error Handling
411
+ // ===========================================================================
412
+ /**
413
+ * Display shader compilation errors in an overlay.
414
+ */
415
+ showErrorOverlay(errors) {
416
+ // Create overlay if it doesn't exist
417
+ if (!this.errorOverlay) {
418
+ this.errorOverlay = document.createElement('div');
419
+ this.errorOverlay.className = 'shader-error-overlay';
420
+ this.container.appendChild(this.errorOverlay);
421
+ }
422
+ // Group errors: separate common.glsl errors from pass-specific errors
423
+ const commonErrors = errors.filter(e => e.isFromCommon);
424
+ const passErrors = errors.filter(e => !e.isFromCommon);
425
+ // Deduplicate common errors (same error reported for multiple passes)
426
+ const uniqueCommonErrors = commonErrors.length > 0 ? [commonErrors[0]] : [];
427
+ // Combine: show common errors first, then pass-specific errors
428
+ const allErrors = [...uniqueCommonErrors, ...passErrors];
429
+ // Parse and format errors with source context
430
+ const formattedErrors = allErrors.map(({ passName, error, source, isFromCommon, originalLine }) => {
431
+ // Extract the actual GLSL error from the thrown error message
432
+ const glslError = error.replace('Shader compilation failed:\n', '');
433
+ // For common errors, adjust line number in error message
434
+ let adjustedError = glslError;
435
+ if (isFromCommon && originalLine !== null) {
436
+ adjustedError = glslError.replace(/Line \d+:/, `Line ${originalLine}:`);
437
+ adjustedError = adjustedError.replace(/ERROR:\s*\d+:(\d+):/, `ERROR: 0:${originalLine}:`);
438
+ }
439
+ return {
440
+ passName: isFromCommon ? 'common.glsl' : passName,
441
+ error: this.parseShaderError(adjustedError),
442
+ codeContext: isFromCommon
443
+ ? this.extractCodeContextFromCommon(originalLine)
444
+ : this.extractCodeContext(adjustedError, source),
445
+ };
446
+ });
447
+ // Build error HTML
448
+ const errorHTML = formattedErrors.map(({ passName, error, codeContext }) => `
449
+ <div class="error-section">
450
+ <div class="error-pass-name">${passName}</div>
451
+ <pre class="error-content">${this.escapeHTML(error)}</pre>
452
+ ${codeContext ? `<pre class="error-code-context">${codeContext}</pre>` : ''}
453
+ </div>
454
+ `).join('');
455
+ this.errorOverlay.innerHTML = `
456
+ <div class="error-overlay-content">
457
+ <div class="error-header">
458
+ <span class="error-title">
459
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="vertical-align: text-bottom;">
460
+ <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM3.5 7.5a.75.75 0 0 0 0 1.5h9a.75.75 0 0 0 0-1.5h-9z"/>
461
+ </svg>
462
+ Shader Compilation Failed
463
+ </span>
464
+ <button class="error-close" title="Dismiss">×</button>
465
+ </div>
466
+ <div class="error-body">
467
+ ${errorHTML}
468
+ </div>
469
+ </div>
470
+ `;
471
+ // Add close button handler
472
+ const closeButton = this.errorOverlay.querySelector('.error-close');
473
+ if (closeButton) {
474
+ closeButton.addEventListener('click', () => this.hideErrorOverlay());
475
+ }
476
+ }
477
+ /**
478
+ * Parse and improve WebGL shader error messages.
479
+ */
480
+ parseShaderError(error) {
481
+ // WebGL errors typically look like: "ERROR: 0:45: 'texure' : no matching overloaded function found"
482
+ // Let's make them more readable by highlighting line numbers and adding context
483
+ return error.split('\n').map(line => {
484
+ // Match pattern: ERROR: 0:lineNumber: message
485
+ const match = line.match(/^ERROR:\s*(\d+):(\d+):\s*(.+)$/);
486
+ if (match) {
487
+ const [, , lineNum, message] = match;
488
+ return `Line ${lineNum}: ${message}`;
489
+ }
490
+ return line;
491
+ }).join('\n');
492
+ }
493
+ /**
494
+ * Extract code context around error line (±3 lines).
495
+ * Returns HTML with the error line highlighted.
496
+ */
497
+ extractCodeContext(error, source) {
498
+ // Extract line number from error
499
+ const match = error.match(/ERROR:\s*\d+:(\d+):/);
500
+ if (!match)
501
+ return null;
502
+ const errorLine = parseInt(match[1], 10);
503
+ const lines = source.split('\n');
504
+ // Extract context (3 lines before and after)
505
+ const contextRange = 3;
506
+ const startLine = Math.max(0, errorLine - contextRange - 1);
507
+ const endLine = Math.min(lines.length, errorLine + contextRange);
508
+ const contextLines = lines.slice(startLine, endLine);
509
+ // Build HTML with line numbers and highlighting
510
+ const html = contextLines.map((line, idx) => {
511
+ const lineNum = startLine + idx + 1;
512
+ const isErrorLine = lineNum === errorLine;
513
+ const lineNumPadded = String(lineNum).padStart(4, ' ');
514
+ const escapedLine = this.escapeHTML(line);
515
+ if (isErrorLine) {
516
+ return `<span class="error-line-highlight">${lineNumPadded} │ ${escapedLine}</span>`;
517
+ }
518
+ else {
519
+ return `<span class="context-line">${lineNumPadded} │ ${escapedLine}</span>`;
520
+ }
521
+ }).join(''); // No newline - spans already have display:block
522
+ return html;
523
+ }
524
+ /**
525
+ * Extract code context from common.glsl file.
526
+ * Similar to extractCodeContext but uses the original common source.
527
+ */
528
+ extractCodeContextFromCommon(errorLine) {
529
+ const commonSource = this.engine.project.commonSource;
530
+ if (!commonSource)
531
+ return null;
532
+ const lines = commonSource.split('\n');
533
+ // Extract context (3 lines before and after)
534
+ const contextRange = 3;
535
+ const startLine = Math.max(0, errorLine - contextRange - 1);
536
+ const endLine = Math.min(lines.length, errorLine + contextRange);
537
+ const contextLines = lines.slice(startLine, endLine);
538
+ // Build HTML with line numbers and highlighting
539
+ const html = contextLines.map((line, idx) => {
540
+ const lineNum = startLine + idx + 1;
541
+ const isErrorLine = lineNum === errorLine;
542
+ const lineNumPadded = String(lineNum).padStart(4, ' ');
543
+ const escapedLine = this.escapeHTML(line);
544
+ if (isErrorLine) {
545
+ return `<span class="error-line-highlight">${lineNumPadded} │ ${escapedLine}</span>`;
546
+ }
547
+ else {
548
+ return `<span class="context-line">${lineNumPadded} │ ${escapedLine}</span>`;
549
+ }
550
+ }).join(''); // No newline - spans already have display:block
551
+ return html;
552
+ }
553
+ /**
554
+ * Escape HTML to prevent XSS.
555
+ */
556
+ escapeHTML(text) {
557
+ const div = document.createElement('div');
558
+ div.textContent = text;
559
+ return div.innerHTML;
560
+ }
561
+ /**
562
+ * Hide the error overlay.
563
+ */
564
+ hideErrorOverlay() {
565
+ if (this.errorOverlay) {
566
+ this.errorOverlay.remove();
567
+ this.errorOverlay = null;
568
+ }
569
+ }
570
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * App Layer - Type Definitions
3
+ *
4
+ * Types for the browser runtime coordinator.
5
+ */
6
+ import type { ShadertoyProject } from '../project/types';
7
+ /**
8
+ * Options for creating the App.
9
+ */
10
+ export interface AppOptions {
11
+ /**
12
+ * HTML container element (App will create canvas inside).
13
+ */
14
+ container: HTMLElement;
15
+ /**
16
+ * Loaded Shadertoy project.
17
+ */
18
+ project: ShadertoyProject;
19
+ /**
20
+ * Canvas pixel ratio (default: window.devicePixelRatio).
21
+ * Set to 1 for performance, or higher for retina displays.
22
+ */
23
+ pixelRatio?: number;
24
+ }
25
+ /**
26
+ * Mouse state for iMouse uniform.
27
+ * Format: [x, y, clickX, clickY]
28
+ * - x, y: current mouse position in pixels
29
+ * - clickX, clickY: position of last click (or -1, -1 if no click)
30
+ */
31
+ export type MouseState = [number, number, number, number];
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/app/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,SAAS,EAAE,WAAW,CAAC;IAEvB;;OAEG;IACH,OAAO,EAAE,gBAAgB,CAAC;IAE1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * App Layer - Type Definitions
3
+ *
4
+ * Types for the browser runtime coordinator.
5
+ */
6
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Editor Panel - Shared component for code editing in layouts
3
+ *
4
+ * Provides:
5
+ * - CodeMirror editor (dynamically loaded)
6
+ * - Recompile button with keyboard shortcut
7
+ * - Error display
8
+ * - Tab management for multiple passes
9
+ */
10
+ import { ShadertoyProject } from '../project/types';
11
+ import { RecompileHandler } from '../layouts/types';
12
+ import './editor-panel.css';
13
+ export declare class EditorPanel {
14
+ private container;
15
+ private project;
16
+ private recompileHandler;
17
+ private tabBar;
18
+ private contentArea;
19
+ private copyButton;
20
+ private recompileButton;
21
+ private errorDisplay;
22
+ private tabs;
23
+ private activeTabIndex;
24
+ private editorInstance;
25
+ private modifiedSources;
26
+ constructor(container: HTMLElement, project: ShadertoyProject);
27
+ setRecompileHandler(handler: RecompileHandler): void;
28
+ dispose(): void;
29
+ private buildTabs;
30
+ private buildTabBar;
31
+ private showTab;
32
+ private saveCurrentEditorContent;
33
+ private recompile;
34
+ private showError;
35
+ private hideError;
36
+ private copyToClipboard;
37
+ private setupKeyboardShortcut;
38
+ }
39
+ //# sourceMappingURL=EditorPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditorPanel.d.ts","sourceRoot":"","sources":["../../src/editor/EditorPanel.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,gBAAgB,EAAY,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,oBAAoB,CAAC;AAY5B,qBAAa,WAAW;IACtB,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,gBAAgB,CAAiC;IAEzD,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,YAAY,CAAc;IAElC,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,cAAc,CAAa;IAGnC,OAAO,CAAC,cAAc,CAA+B;IAGrD,OAAO,CAAC,eAAe,CAAkC;gBAE7C,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB;IA+D7D,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAIpD,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,SAAS;IAgDjB,OAAO,CAAC,WAAW;YAmBL,OAAO;IAsErB,OAAO,CAAC,wBAAwB;IAUhC,OAAO,CAAC,SAAS;IA0BjB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,SAAS;YAIH,eAAe;IA4B7B,OAAO,CAAC,qBAAqB;CAS9B"}