@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
package/src/app/app.css
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Styles - FPS counter and shader error overlay
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* ===== FPS Counter ===== */
|
|
6
|
+
.fps-counter {
|
|
7
|
+
position: absolute;
|
|
8
|
+
bottom: 8px;
|
|
9
|
+
left: 8px;
|
|
10
|
+
padding: 6px 10px;
|
|
11
|
+
background: rgba(0, 0, 0, 0.75);
|
|
12
|
+
color: white;
|
|
13
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
14
|
+
font-size: 12px;
|
|
15
|
+
font-weight: 500;
|
|
16
|
+
border-radius: 4px;
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
user-select: none;
|
|
19
|
+
z-index: 1000;
|
|
20
|
+
backdrop-filter: blur(4px);
|
|
21
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ===== Playback Controls ===== */
|
|
25
|
+
.playback-controls {
|
|
26
|
+
position: absolute;
|
|
27
|
+
bottom: 8px;
|
|
28
|
+
right: 8px;
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 8px;
|
|
31
|
+
z-index: 1000;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.control-button {
|
|
35
|
+
padding: 6px 8px;
|
|
36
|
+
background: rgba(0, 0, 0, 0.75);
|
|
37
|
+
color: white;
|
|
38
|
+
border: none;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
backdrop-filter: blur(4px);
|
|
42
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
43
|
+
transition: all 0.2s ease;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
width: 32px;
|
|
48
|
+
height: 32px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.control-button:hover {
|
|
52
|
+
background: rgba(0, 0, 0, 0.85);
|
|
53
|
+
transform: scale(1.05);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.control-button:active {
|
|
57
|
+
transform: scale(0.95);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.control-button svg {
|
|
61
|
+
width: 16px;
|
|
62
|
+
height: 16px;
|
|
63
|
+
fill: currentColor;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ===== Shader Error Overlay ===== */
|
|
67
|
+
.shader-error-overlay {
|
|
68
|
+
position: absolute;
|
|
69
|
+
top: 0;
|
|
70
|
+
left: 0;
|
|
71
|
+
right: 0;
|
|
72
|
+
bottom: 0;
|
|
73
|
+
background: rgba(0, 0, 0, 0.95);
|
|
74
|
+
backdrop-filter: blur(8px);
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
z-index: 2000;
|
|
79
|
+
padding: 60px;
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.error-overlay-content {
|
|
84
|
+
background: #1a1a1a;
|
|
85
|
+
border-radius: 6px;
|
|
86
|
+
max-width: 900px;
|
|
87
|
+
width: 100%;
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8), 0 0 1px rgba(255, 255, 255, 0.1);
|
|
91
|
+
border: 1px solid #2a2a2a;
|
|
92
|
+
max-height: calc(100vh - 120px);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.error-header {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: space-between;
|
|
99
|
+
padding: 18px 24px;
|
|
100
|
+
background: linear-gradient(135deg, #c62828 0%, #b71c1c 100%);
|
|
101
|
+
color: white;
|
|
102
|
+
border-radius: 6px 6px 0 0;
|
|
103
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
|
|
104
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.error-title {
|
|
108
|
+
font-size: 15px;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
letter-spacing: -0.01em;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.error-close {
|
|
118
|
+
background: transparent;
|
|
119
|
+
border: none;
|
|
120
|
+
color: rgba(255, 255, 255, 0.9);
|
|
121
|
+
font-size: 24px;
|
|
122
|
+
line-height: 1;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
padding: 0;
|
|
125
|
+
width: 32px;
|
|
126
|
+
height: 32px;
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
transition: all 0.2s ease;
|
|
132
|
+
opacity: 0.8;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.error-close:hover {
|
|
136
|
+
background: rgba(255, 255, 255, 0.15);
|
|
137
|
+
opacity: 1;
|
|
138
|
+
transform: scale(1.05);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.error-body {
|
|
142
|
+
padding: 24px;
|
|
143
|
+
overflow-y: auto;
|
|
144
|
+
flex: 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.error-section {
|
|
148
|
+
margin-bottom: 24px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.error-section:last-child {
|
|
152
|
+
margin-bottom: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.error-pass-name {
|
|
156
|
+
font-size: 13px;
|
|
157
|
+
font-weight: 600;
|
|
158
|
+
color: #ffa726;
|
|
159
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
160
|
+
margin-bottom: 10px;
|
|
161
|
+
padding-bottom: 6px;
|
|
162
|
+
border-bottom: 1px solid #2a2a2a;
|
|
163
|
+
letter-spacing: 0.02em;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.error-content {
|
|
167
|
+
margin: 0;
|
|
168
|
+
padding: 14px 16px;
|
|
169
|
+
background: #0f0f0f;
|
|
170
|
+
border-radius: 4px;
|
|
171
|
+
color: #ff6b6b;
|
|
172
|
+
font-size: 13px;
|
|
173
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
174
|
+
line-height: 1.6;
|
|
175
|
+
overflow-x: auto;
|
|
176
|
+
border: 1px solid #2a2a2a;
|
|
177
|
+
white-space: pre-wrap;
|
|
178
|
+
word-break: break-word;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.error-code-context {
|
|
182
|
+
margin: 12px 0 0 0;
|
|
183
|
+
padding: 14px 16px;
|
|
184
|
+
background: #0d0d0d;
|
|
185
|
+
border-radius: 4px;
|
|
186
|
+
color: #b0b0b0;
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
189
|
+
line-height: 1.6;
|
|
190
|
+
overflow-x: auto;
|
|
191
|
+
border: 1px solid #2a2a2a;
|
|
192
|
+
white-space: pre;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.error-code-context .context-line {
|
|
196
|
+
color: #666;
|
|
197
|
+
display: block;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.error-code-context .error-line-highlight {
|
|
201
|
+
color: #ffffff;
|
|
202
|
+
background: rgba(198, 40, 40, 0.25);
|
|
203
|
+
display: block;
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
border-left: 3px solid #c62828;
|
|
206
|
+
margin-left: -16px;
|
|
207
|
+
padding-left: 13px;
|
|
208
|
+
}
|
package/src/app/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Layer - Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Types for the browser runtime coordinator.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ShadertoyProject } from '../project/types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for creating the App.
|
|
11
|
+
*/
|
|
12
|
+
export interface AppOptions {
|
|
13
|
+
/**
|
|
14
|
+
* HTML container element (App will create canvas inside).
|
|
15
|
+
*/
|
|
16
|
+
container: HTMLElement;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loaded Shadertoy project.
|
|
20
|
+
*/
|
|
21
|
+
project: ShadertoyProject;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Canvas pixel ratio (default: window.devicePixelRatio).
|
|
25
|
+
* Set to 1 for performance, or higher for retina displays.
|
|
26
|
+
*/
|
|
27
|
+
pixelRatio?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Mouse state for iMouse uniform.
|
|
32
|
+
* Format: [x, y, clickX, clickY]
|
|
33
|
+
* - x, y: current mouse position in pixels
|
|
34
|
+
* - clickX, clickY: position of last click (or -1, -1 if no click)
|
|
35
|
+
*/
|
|
36
|
+
export type MouseState = [number, number, number, number];
|
|
@@ -0,0 +1,340 @@
|
|
|
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
|
+
|
|
11
|
+
import { ShadertoyProject, PassName } from '../project/types';
|
|
12
|
+
import { RecompileHandler } from '../layouts/types';
|
|
13
|
+
|
|
14
|
+
import './editor-panel.css';
|
|
15
|
+
|
|
16
|
+
type CodeTab = { kind: 'code'; name: string; passName: 'common' | PassName; source: string };
|
|
17
|
+
type ImageTab = { kind: 'image'; name: string; url: string };
|
|
18
|
+
type Tab = CodeTab | ImageTab;
|
|
19
|
+
|
|
20
|
+
interface EditorInstance {
|
|
21
|
+
getSource: () => string;
|
|
22
|
+
setSource: (source: string) => void;
|
|
23
|
+
destroy: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class EditorPanel {
|
|
27
|
+
private container: HTMLElement;
|
|
28
|
+
private project: ShadertoyProject;
|
|
29
|
+
private recompileHandler: RecompileHandler | null = null;
|
|
30
|
+
|
|
31
|
+
private tabBar: HTMLElement;
|
|
32
|
+
private contentArea: HTMLElement;
|
|
33
|
+
private copyButton: HTMLElement;
|
|
34
|
+
private recompileButton: HTMLElement;
|
|
35
|
+
private errorDisplay: HTMLElement;
|
|
36
|
+
|
|
37
|
+
private tabs: Tab[] = [];
|
|
38
|
+
private activeTabIndex: number = 0;
|
|
39
|
+
|
|
40
|
+
// Editor instance (null if not in editor mode or viewing image)
|
|
41
|
+
private editorInstance: EditorInstance | null = null;
|
|
42
|
+
|
|
43
|
+
// Track modified sources (passName -> modified source)
|
|
44
|
+
private modifiedSources: Map<string, string> = new Map();
|
|
45
|
+
|
|
46
|
+
constructor(container: HTMLElement, project: ShadertoyProject) {
|
|
47
|
+
this.container = container;
|
|
48
|
+
this.project = project;
|
|
49
|
+
|
|
50
|
+
// Build tabs
|
|
51
|
+
this.buildTabs();
|
|
52
|
+
|
|
53
|
+
// Create tab bar
|
|
54
|
+
this.tabBar = document.createElement('div');
|
|
55
|
+
this.tabBar.className = 'editor-tab-bar';
|
|
56
|
+
this.buildTabBar();
|
|
57
|
+
|
|
58
|
+
// Create content area
|
|
59
|
+
this.contentArea = document.createElement('div');
|
|
60
|
+
this.contentArea.className = 'editor-content-area';
|
|
61
|
+
|
|
62
|
+
// Create copy button (icon only)
|
|
63
|
+
this.copyButton = document.createElement('button');
|
|
64
|
+
this.copyButton.className = 'editor-copy-button';
|
|
65
|
+
this.copyButton.innerHTML = `
|
|
66
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
67
|
+
<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"/>
|
|
68
|
+
<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"/>
|
|
69
|
+
</svg>
|
|
70
|
+
`;
|
|
71
|
+
this.copyButton.title = 'Copy code to clipboard';
|
|
72
|
+
this.copyButton.addEventListener('click', () => this.copyToClipboard());
|
|
73
|
+
|
|
74
|
+
// Create recompile button
|
|
75
|
+
this.recompileButton = document.createElement('button');
|
|
76
|
+
this.recompileButton.className = 'editor-recompile-button';
|
|
77
|
+
this.recompileButton.innerHTML = `
|
|
78
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
79
|
+
<path d="M4 3v10l8-5-8-5z"/>
|
|
80
|
+
</svg>
|
|
81
|
+
Recompile
|
|
82
|
+
`;
|
|
83
|
+
this.recompileButton.title = 'Recompile shader (Ctrl+Enter)';
|
|
84
|
+
this.recompileButton.addEventListener('click', () => this.recompile());
|
|
85
|
+
|
|
86
|
+
// Create error display
|
|
87
|
+
this.errorDisplay = document.createElement('div');
|
|
88
|
+
this.errorDisplay.className = 'editor-error-display';
|
|
89
|
+
this.errorDisplay.style.display = 'none';
|
|
90
|
+
|
|
91
|
+
// Assemble panel
|
|
92
|
+
const toolbar = document.createElement('div');
|
|
93
|
+
toolbar.className = 'editor-toolbar';
|
|
94
|
+
toolbar.appendChild(this.tabBar);
|
|
95
|
+
toolbar.appendChild(this.copyButton);
|
|
96
|
+
toolbar.appendChild(this.recompileButton);
|
|
97
|
+
|
|
98
|
+
this.container.appendChild(toolbar);
|
|
99
|
+
this.container.appendChild(this.contentArea);
|
|
100
|
+
this.container.appendChild(this.errorDisplay);
|
|
101
|
+
|
|
102
|
+
// Set up keyboard shortcut
|
|
103
|
+
this.setupKeyboardShortcut();
|
|
104
|
+
|
|
105
|
+
// Load editor for first tab
|
|
106
|
+
this.showTab(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setRecompileHandler(handler: RecompileHandler): void {
|
|
110
|
+
this.recompileHandler = handler;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dispose(): void {
|
|
114
|
+
if (this.editorInstance) {
|
|
115
|
+
this.editorInstance.destroy();
|
|
116
|
+
this.editorInstance = null;
|
|
117
|
+
}
|
|
118
|
+
this.container.innerHTML = '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private buildTabs(): void {
|
|
122
|
+
this.tabs = [];
|
|
123
|
+
|
|
124
|
+
// 1. Common first (if exists)
|
|
125
|
+
if (this.project.commonSource) {
|
|
126
|
+
this.tabs.push({
|
|
127
|
+
kind: 'code',
|
|
128
|
+
name: 'common.glsl',
|
|
129
|
+
passName: 'common',
|
|
130
|
+
source: this.project.commonSource,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 2. Buffers in order (A, B, C, D)
|
|
135
|
+
const bufferOrder: ('BufferA' | 'BufferB' | 'BufferC' | 'BufferD')[] = [
|
|
136
|
+
'BufferA', 'BufferB', 'BufferC', 'BufferD',
|
|
137
|
+
];
|
|
138
|
+
for (const bufferName of bufferOrder) {
|
|
139
|
+
const pass = this.project.passes[bufferName];
|
|
140
|
+
if (pass) {
|
|
141
|
+
this.tabs.push({
|
|
142
|
+
kind: 'code',
|
|
143
|
+
name: `${bufferName.toLowerCase()}.glsl`,
|
|
144
|
+
passName: bufferName,
|
|
145
|
+
source: pass.glslSource,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Image pass
|
|
151
|
+
const imagePass = this.project.passes.Image;
|
|
152
|
+
this.tabs.push({
|
|
153
|
+
kind: 'code',
|
|
154
|
+
name: 'image.glsl',
|
|
155
|
+
passName: 'Image',
|
|
156
|
+
source: imagePass.glslSource,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// 4. Textures (images) - not editable
|
|
160
|
+
for (const texture of this.project.textures) {
|
|
161
|
+
this.tabs.push({
|
|
162
|
+
kind: 'image',
|
|
163
|
+
name: texture.filename || texture.name,
|
|
164
|
+
url: texture.source,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private buildTabBar(): void {
|
|
170
|
+
this.tabBar.innerHTML = '';
|
|
171
|
+
|
|
172
|
+
this.tabs.forEach((tab, index) => {
|
|
173
|
+
const tabButton = document.createElement('button');
|
|
174
|
+
tabButton.className = 'editor-tab-button';
|
|
175
|
+
if (tab.kind === 'image') {
|
|
176
|
+
tabButton.classList.add('image-tab');
|
|
177
|
+
}
|
|
178
|
+
tabButton.textContent = tab.name;
|
|
179
|
+
if (index === this.activeTabIndex) {
|
|
180
|
+
tabButton.classList.add('active');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
tabButton.addEventListener('click', () => this.showTab(index));
|
|
184
|
+
this.tabBar.appendChild(tabButton);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async showTab(index: number): Promise<void> {
|
|
189
|
+
// Save current editor content before switching
|
|
190
|
+
this.saveCurrentEditorContent();
|
|
191
|
+
|
|
192
|
+
this.activeTabIndex = index;
|
|
193
|
+
const tab = this.tabs[index];
|
|
194
|
+
|
|
195
|
+
// Update tab bar active state
|
|
196
|
+
this.tabBar.querySelectorAll('.editor-tab-button').forEach((btn, i) => {
|
|
197
|
+
btn.classList.toggle('active', i === index);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Clear content area
|
|
201
|
+
this.contentArea.innerHTML = '';
|
|
202
|
+
|
|
203
|
+
// Destroy previous editor instance
|
|
204
|
+
if (this.editorInstance) {
|
|
205
|
+
this.editorInstance.destroy();
|
|
206
|
+
this.editorInstance = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (tab.kind === 'code') {
|
|
210
|
+
// Show buttons
|
|
211
|
+
this.copyButton.style.display = '';
|
|
212
|
+
this.recompileButton.style.display = '';
|
|
213
|
+
|
|
214
|
+
// Get source (use modified if available, otherwise original)
|
|
215
|
+
const source = this.modifiedSources.get(tab.passName) ?? tab.source;
|
|
216
|
+
|
|
217
|
+
// Create editor container
|
|
218
|
+
const editorContainer = document.createElement('div');
|
|
219
|
+
editorContainer.className = 'editor-prism-container';
|
|
220
|
+
this.contentArea.appendChild(editorContainer);
|
|
221
|
+
|
|
222
|
+
// Dynamically load editor and create instance
|
|
223
|
+
try {
|
|
224
|
+
const { createEditor } = await import('./prism-editor');
|
|
225
|
+
this.editorInstance = createEditor(editorContainer, source, (newSource) => {
|
|
226
|
+
// Track modifications
|
|
227
|
+
this.modifiedSources.set(tab.passName, newSource);
|
|
228
|
+
});
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.error('Failed to load editor:', err);
|
|
231
|
+
// Fallback to textarea
|
|
232
|
+
const textarea = document.createElement('textarea');
|
|
233
|
+
textarea.className = 'editor-fallback-textarea';
|
|
234
|
+
textarea.value = source;
|
|
235
|
+
textarea.addEventListener('input', () => {
|
|
236
|
+
this.modifiedSources.set(tab.passName, textarea.value);
|
|
237
|
+
});
|
|
238
|
+
editorContainer.appendChild(textarea);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// Hide buttons for image tabs
|
|
242
|
+
this.copyButton.style.display = 'none';
|
|
243
|
+
this.recompileButton.style.display = 'none';
|
|
244
|
+
|
|
245
|
+
// Show image
|
|
246
|
+
const imgContainer = document.createElement('div');
|
|
247
|
+
imgContainer.className = 'editor-image-viewer';
|
|
248
|
+
|
|
249
|
+
const img = document.createElement('img');
|
|
250
|
+
img.src = tab.url;
|
|
251
|
+
img.alt = tab.name;
|
|
252
|
+
|
|
253
|
+
imgContainer.appendChild(img);
|
|
254
|
+
this.contentArea.appendChild(imgContainer);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private saveCurrentEditorContent(): void {
|
|
259
|
+
if (this.editorInstance) {
|
|
260
|
+
const tab = this.tabs[this.activeTabIndex];
|
|
261
|
+
if (tab.kind === 'code') {
|
|
262
|
+
const source = this.editorInstance.getSource();
|
|
263
|
+
this.modifiedSources.set(tab.passName, source);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private recompile(): void {
|
|
269
|
+
if (!this.recompileHandler) {
|
|
270
|
+
console.warn('No recompile handler set');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Save current content first
|
|
275
|
+
this.saveCurrentEditorContent();
|
|
276
|
+
|
|
277
|
+
const tab = this.tabs[this.activeTabIndex];
|
|
278
|
+
if (tab.kind !== 'code') {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const source = this.modifiedSources.get(tab.passName) ?? tab.source;
|
|
283
|
+
const result = this.recompileHandler(tab.passName, source);
|
|
284
|
+
|
|
285
|
+
if (result.success) {
|
|
286
|
+
this.hideError();
|
|
287
|
+
// Update the original source in the tab
|
|
288
|
+
tab.source = source;
|
|
289
|
+
} else {
|
|
290
|
+
this.showError(result.error || 'Compilation failed');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private showError(message: string): void {
|
|
295
|
+
this.errorDisplay.textContent = message;
|
|
296
|
+
this.errorDisplay.style.display = 'block';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private hideError(): void {
|
|
300
|
+
this.errorDisplay.style.display = 'none';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async copyToClipboard(): Promise<void> {
|
|
304
|
+
const tab = this.tabs[this.activeTabIndex];
|
|
305
|
+
if (tab.kind !== 'code') return;
|
|
306
|
+
|
|
307
|
+
// Get current source (modified or original)
|
|
308
|
+
const source = this.editorInstance
|
|
309
|
+
? this.editorInstance.getSource()
|
|
310
|
+
: (this.modifiedSources.get(tab.passName) ?? tab.source);
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await navigator.clipboard.writeText(source);
|
|
314
|
+
// Show checkmark feedback
|
|
315
|
+
const originalHTML = this.copyButton.innerHTML;
|
|
316
|
+
this.copyButton.innerHTML = `
|
|
317
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
318
|
+
<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"/>
|
|
319
|
+
</svg>
|
|
320
|
+
`;
|
|
321
|
+
this.copyButton.classList.add('copied');
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
this.copyButton.innerHTML = originalHTML;
|
|
324
|
+
this.copyButton.classList.remove('copied');
|
|
325
|
+
}, 1500);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.error('Failed to copy:', err);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private setupKeyboardShortcut(): void {
|
|
332
|
+
// Listen for Ctrl+Enter / Cmd+Enter
|
|
333
|
+
this.container.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
334
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
this.recompile();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|