Skip to content

Commit 78720f0

Browse files
committed
Iterate on shader editor
1 parent 5e1dfec commit 78720f0

File tree

3 files changed

+173
-70
lines changed

3 files changed

+173
-70
lines changed

typescript/packages/lookslike-high-level/src/components/shader-layer.ts

Lines changed: 99 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class ShaderLayer extends LitElement {
1717
private animationFrame?: number;
1818
private startTime = performance.now();
1919
private vertexShader?: WebGLShader;
20+
private errorMessage: string | null = null;
2021

2122
static override styles = css`
2223
:host {
@@ -37,38 +38,83 @@ export class ShaderLayer extends LitElement {
3738
left: 0;
3839
mix-blend-mode: var(--blend-mode, default);
3940
}
41+
.error {
42+
color: red;
43+
padding: 20px;
44+
position: absolute;
45+
top: 0;
46+
left: 0;
47+
background: rgba(0,0,0,0.8);
48+
}
4049
`;
4150

51+
private cleanup() {
52+
if (this.animationFrame) {
53+
cancelAnimationFrame(this.animationFrame);
54+
this.animationFrame = undefined;
55+
}
56+
57+
if (this.gl) {
58+
if (this.program) {
59+
this.gl.deleteProgram(this.program);
60+
this.program = undefined;
61+
}
62+
if (this.vertexShader) {
63+
this.gl.deleteShader(this.vertexShader);
64+
this.vertexShader = undefined;
65+
}
66+
this.gl = undefined;
67+
}
68+
69+
this.timeLocation = undefined;
70+
this.resolutionLocation = undefined;
71+
this.errorMessage = null;
72+
}
73+
4274
private setupWebGL() {
75+
this.cleanup();
76+
4377
const canvas = this.canvasRef.value;
4478
if (!canvas) return;
4579

4680
canvas.width = this.width;
4781
canvas.height = this.height;
4882
canvas.style.setProperty('--blend-mode', this.blendMode);
4983

50-
this.gl = canvas.getContext('webgl', {
84+
const gl = canvas.getContext('webgl', {
5185
alpha: true,
5286
premultipliedAlpha: false
53-
})!;
54-
if (!this.gl) return;
87+
});
88+
if (!gl) {
89+
this.errorMessage = "WebGL not supported";
90+
return;
91+
}
92+
this.gl = gl;
5593

56-
// Enable blending for transparency
57-
this.gl.enable(this.gl.BLEND);
58-
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
94+
gl.enable(gl.BLEND);
95+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5996

60-
this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
61-
if (!this.vertexShader) return;
97+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
98+
if (!vertexShader) {
99+
this.errorMessage = "Failed to create vertex shader";
100+
return;
101+
}
102+
this.vertexShader = vertexShader;
62103

63-
this.gl.shaderSource(this.vertexShader, `
104+
gl.shaderSource(vertexShader, `
64105
attribute vec2 position;
65106
varying vec2 v_texCoord;
66107
void main() {
67108
v_texCoord = (position + 1.0) * 0.5;
68109
gl_Position = vec4(position, 0.0, 1.0);
69110
}
70111
`);
71-
this.gl.compileShader(this.vertexShader);
112+
gl.compileShader(vertexShader);
113+
114+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
115+
this.errorMessage = `Vertex shader error: ${gl.getShaderInfoLog(vertexShader)}`;
116+
return;
117+
}
72118

73119
const positions = new Float32Array([
74120
-1, -1,
@@ -78,53 +124,62 @@ export class ShaderLayer extends LitElement {
78124
1, -1,
79125
1, 1
80126
]);
81-
const positionBuffer = this.gl.createBuffer();
82-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
83-
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
127+
const positionBuffer = gl.createBuffer();
128+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
129+
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
84130

85131
this.initShaderProgram();
86132
}
87133

88134
private initShaderProgram() {
89135
if (!this.gl || !this.vertexShader || !this.shader) return;
136+
const gl = this.gl;
90137

91-
// Clean up existing program
92-
if (this.program) {
93-
this.gl.deleteProgram(this.program);
138+
const program = gl.createProgram();
139+
if (!program) {
140+
this.errorMessage = "Failed to create program";
141+
return;
94142
}
95143

96-
this.program = this.gl.createProgram();
97-
if (!this.program) return;
98-
99-
const fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
100-
if (!fragmentShader) return;
144+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
145+
if (!fragmentShader) {
146+
this.errorMessage = "Failed to create fragment shader";
147+
return;
148+
}
101149

102-
this.gl.shaderSource(fragmentShader, this.shader);
103-
this.gl.compileShader(fragmentShader);
150+
gl.shaderSource(fragmentShader, this.shader);
151+
gl.compileShader(fragmentShader);
104152

105-
if (!this.gl.getShaderParameter(fragmentShader, this.gl.COMPILE_STATUS)) {
106-
console.error('Fragment shader compilation error:', this.gl.getShaderInfoLog(fragmentShader));
153+
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
154+
this.errorMessage = `Fragment shader error: ${gl.getShaderInfoLog(fragmentShader)}`;
155+
gl.deleteShader(fragmentShader);
107156
return;
108157
}
109158

110-
this.gl.attachShader(this.program, this.vertexShader);
111-
this.gl.attachShader(this.program, fragmentShader);
112-
this.gl.linkProgram(this.program);
159+
gl.attachShader(program, this.vertexShader);
160+
gl.attachShader(program, fragmentShader);
161+
gl.linkProgram(program);
113162

114-
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
115-
console.error('Program linking error:', this.gl.getProgramInfoLog(this.program));
163+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
164+
this.errorMessage = `Program linking error: ${gl.getProgramInfoLog(program)}`;
165+
gl.deleteShader(fragmentShader);
166+
gl.deleteProgram(program);
116167
return;
117168
}
118169

119-
const positionLocation = this.gl.getAttribLocation(this.program, "position");
120-
this.gl.enableVertexAttribArray(positionLocation);
121-
this.gl.vertexAttribPointer(positionLocation, 2, this.gl.FLOAT, false, 0, 0);
170+
this.program = program;
171+
172+
const positionLocation = gl.getAttribLocation(program, "position");
173+
gl.enableVertexAttribArray(positionLocation);
174+
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
122175

123-
this.timeLocation = this.gl.getUniformLocation(this.program, "iTime");
124-
this.resolutionLocation = this.gl.getUniformLocation(this.program, "iResolution");
176+
this.timeLocation = gl.getUniformLocation(program, "iTime");
177+
this.resolutionLocation = gl.getUniformLocation(program, "iResolution");
125178

126-
// Clean up fragment shader
127-
this.gl.deleteShader(fragmentShader);
179+
gl.deleteShader(fragmentShader);
180+
181+
this.startTime = performance.now();
182+
this.renderGl();
128183
}
129184

130185
private renderGl() {
@@ -149,32 +204,27 @@ export class ShaderLayer extends LitElement {
149204
}
150205

151206
override updated(changedProperties: Map<string, any>) {
152-
if (changedProperties.has('shader')) {
153-
this.initShaderProgram();
207+
if (changedProperties.has('shader') ||
208+
changedProperties.has('width') ||
209+
changedProperties.has('height') ||
210+
changedProperties.has('blendMode')) {
211+
this.setupWebGL();
154212
}
155213
}
156214

157215
override firstUpdated() {
158216
this.setupWebGL();
159-
this.renderGl();
160217
}
161218

162219
override disconnectedCallback() {
163220
super.disconnectedCallback();
164-
if (this.animationFrame) {
165-
cancelAnimationFrame(this.animationFrame);
166-
}
167-
if (this.gl && this.program) {
168-
this.gl.deleteProgram(this.program);
169-
}
170-
if (this.gl && this.vertexShader) {
171-
this.gl.deleteShader(this.vertexShader);
172-
}
221+
this.cleanup();
173222
}
174223

175224
override render() {
176225
return html`
177226
<canvas ${ref(this.canvasRef)}></canvas>
227+
${this.errorMessage ? html`<div class="error">${this.errorMessage}</div>` : null}
178228
`;
179229
}
180230
}

typescript/packages/lookslike-high-level/src/spells/20_shader_editor.tsx

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ const Shader = z.object({
5454
const ShaderEditor = z.object({
5555
editingShader: Ref.describe("The shader currently being edited"),
5656
shaders: z.array(Shader).describe("All shaders in the system"),
57-
'~/common/ui/shader-list': UiFragment.describe("The UI fragment for the shaders list")
57+
'~/common/ui/shader-list': UiFragment.describe("The UI fragment for the shaders list"),
58+
'~/common/ui/navigation': UiFragment.describe("The UI fragment for navigation")
5859
});
5960

6061
const SourceModificationPrompt = z.object({
@@ -73,18 +74,21 @@ type EditEvent = {
7374
const shaderEditor = typedBehavior(Shader, {
7475
render: ({ self, name, sourceCode, notes }) => (
7576
<div entity={self}>
76-
<common-form
77-
schema={Shader}
78-
value={{ name, sourceCode: sourceCode || SHADER_TEMPLATE, notes }}
79-
onsubmit="~/on/save"
80-
/>
81-
<h4>Modify with AI</h4>
82-
<common-form
83-
schema={SourceModificationPrompt}
84-
value={{ sourceId: self }}
85-
reset
86-
onsubmit="~/on/modify-with-ai"
87-
/>
77+
<details>
78+
<summary>Edit</summary>
79+
<common-form
80+
schema={Shader}
81+
value={{ name, sourceCode: sourceCode || SHADER_TEMPLATE, notes }}
82+
onsubmit="~/on/save"
83+
/>
84+
<h4>Modify with AI</h4>
85+
<common-form
86+
schema={SourceModificationPrompt}
87+
value={{ sourceId: self }}
88+
reset
89+
onsubmit="~/on/modify-with-ai"
90+
/>
91+
</details>
8892
<div style="position: relative; width: 500px; height: 500px;">
8993
<shader-layer
9094
width={500}
@@ -147,9 +151,11 @@ const shaderEditor = typedBehavior(Shader, {
147151
export const shaderManager = typedBehavior(
148152
ShaderEditor.pick({
149153
editingShader: true,
150-
'~/common/ui/shader-list': true
154+
shaders: true,
155+
'~/common/ui/shader-list': true,
156+
'~/common/ui/navigation': true
151157
}), {
152-
render: ({ self, editingShader, '~/common/ui/shader-list': shaderList }) => (
158+
render: ({ self, editingShader, '~/common/ui/shader-list': shaderList, '~/common/ui/navigation': navigation }) => (
153159
<div entity={self}>
154160
<div>
155161
<details>
@@ -168,6 +174,7 @@ export const shaderManager = typedBehavior(
168174
<h3>Edit Shader</h3>
169175
<button onclick="~/on/close-shader-editor">Close</button>
170176
<Charm self={editingShader} spell={shaderEditor as any} />
177+
{subview(navigation)}
171178
</div>
172179
)}
173180

@@ -214,6 +221,49 @@ export const shaderManager = typedBehavior(
214221
}]
215222
}).commit(),
216223

224+
renderNavigation: resolve(ShaderEditor.pick({ shaders: true, editingShader: true }))
225+
.update(({ self, shaders, editingShader }) => {
226+
const currentIndex = shaders.findIndex(s => (s as any).self.toString() === editingShader?.toString());
227+
const shader = shaders[currentIndex];
228+
if (!shader) return [];
229+
230+
return [{
231+
Upsert: [self, '~/common/ui/navigation',
232+
<div style="display: flex; align-items: center; gap: 1rem; margin: 1rem 0;">
233+
<button
234+
onclick={`~/on/prev-shader`}
235+
disabled={currentIndex === 0}
236+
>Previous</button>
237+
<span>{shader.name}</span>
238+
<button
239+
onclick={`~/on/next-shader`}
240+
disabled={currentIndex === shaders.length - 1}
241+
>Next</button>
242+
</div> as any
243+
]
244+
}]
245+
}).commit(),
246+
247+
onPrevShader:
248+
resolve(ShaderEditor.pick({ editingShader: true, shaders: true }))
249+
.with(event("~/on/prev-shader"))
250+
.transact(({ self, shaders, editingShader }, cmd) => {
251+
const currentIndex = shaders.findIndex(s => (s as any).self.toString() === editingShader?.toString());
252+
if (currentIndex > 0) {
253+
cmd.add(...Transact.set(self, { editingShader: (shaders[currentIndex - 1] as any).self }));
254+
}
255+
}),
256+
257+
onNextShader:
258+
resolve(ShaderEditor.pick({ editingShader: true, shaders: true }))
259+
.with(event("~/on/next-shader"))
260+
.transact(({ self, shaders, editingShader }, cmd) => {
261+
const currentIndex = shaders.findIndex(s => (s as any).self.toString() === editingShader?.toString());
262+
if (currentIndex < shaders.length - 1) {
263+
cmd.add(...Transact.set(self, { editingShader: (shaders[currentIndex + 1] as any).self }));
264+
}
265+
}),
266+
217267
onDeleteShader: event("~/on/delete-shader")
218268
.transact(({ self, event }, cmd) => {
219269
const ev = Session.resolve<EditEvent>(event);

typescript/packages/lookslike-high-level/src/spells/spell.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,15 @@ export function Charm<T extends Record<string, Rule<Selector>>>({
137137
self: Reference;
138138
}) {
139139
return (
140-
<common-charm
141-
id={self.toString()}
142-
key={self.toString()}
143-
spell={() => spell}
144-
entity={() => self}
145-
/>
140+
<div>
141+
{/* stupid workaround because keyed nodes only work all nodes are keyed within a parent... we should fix our JSX bindings */}
142+
<common-charm
143+
id={self.toString()}
144+
key={self.toString()}
145+
spell={() => spell}
146+
entity={() => self}
147+
/>
148+
</div>
146149
);
147150
}
148151

0 commit comments

Comments
 (0)