Skip to content

Commit 2b1bcad

Browse files
committed
Add shadertoy-ish example
1 parent c3c6d84 commit 2b1bcad

File tree

4 files changed

+277
-0
lines changed

4 files changed

+277
-0
lines changed

typescript/packages/lookslike-high-level/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * as CommonFileImporter from "./import-wrapper.js";
88
// export * as CommonImport from "./import.js";
99
export * as CommonAsciiLoader from "./ascii-loader.js";
1010
export * as CommonMarkdown from "./markdown.js";
11+
export * as CommonShaderLayer from "./shader-layer.js";
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { LitElement, html, css } from "lit-element";
2+
import { customElement, property } from "lit/decorators.js";
3+
import { createRef, ref } from "lit/directives/ref.js";
4+
5+
@customElement("shader-layer")
6+
export class ShaderLayer extends LitElement {
7+
@property({ type: String }) shader = '';
8+
@property({ type: Number }) width = 640;
9+
@property({ type: Number }) height = 480;
10+
11+
private canvasRef = createRef<HTMLCanvasElement>();
12+
private gl?: WebGLRenderingContext;
13+
private program?: WebGLProgram;
14+
private timeLocation?: WebGLUniformLocation;
15+
private resolutionLocation?: WebGLUniformLocation;
16+
private animationFrame?: number;
17+
private startTime = performance.now();
18+
19+
static override styles = css`
20+
:host {
21+
display: block;
22+
width: 100%;
23+
height: 100%;
24+
position: absolute;
25+
left: 0;
26+
right: 0;
27+
top: 0;
28+
bottom: 0;
29+
}
30+
canvas {
31+
width: 100%;
32+
height: 100%;
33+
position: absolute;
34+
top: 0;
35+
left: 0;
36+
mix-blend-mode: hard-light;
37+
}
38+
`;
39+
40+
private setupWebGL() {
41+
const canvas = this.canvasRef.value;
42+
if (!canvas) return;
43+
44+
canvas.width = this.width;
45+
canvas.height = this.height;
46+
47+
this.gl = canvas.getContext('webgl', {
48+
alpha: true,
49+
premultipliedAlpha: false
50+
})!;
51+
if (!this.gl) return;
52+
53+
// Enable blending for transparency
54+
this.gl.enable(this.gl.BLEND);
55+
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
56+
57+
const vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
58+
if (!vertexShader) return;
59+
60+
this.gl.shaderSource(vertexShader, `
61+
attribute vec2 position;
62+
varying vec2 v_texCoord;
63+
void main() {
64+
v_texCoord = (position + 1.0) * 0.5;
65+
gl_Position = vec4(position, 0.0, 1.0);
66+
}
67+
`);
68+
this.gl.compileShader(vertexShader);
69+
70+
const positions = new Float32Array([
71+
-1, -1,
72+
1, -1,
73+
-1, 1,
74+
-1, 1,
75+
1, -1,
76+
1, 1
77+
]);
78+
const positionBuffer = this.gl.createBuffer();
79+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
80+
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
81+
82+
if (!this.shader) return;
83+
84+
this.program = this.gl.createProgram();
85+
if (!this.program) return;
86+
87+
const fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
88+
if (!fragmentShader) return;
89+
90+
this.gl.shaderSource(fragmentShader, this.shader);
91+
this.gl.compileShader(fragmentShader);
92+
93+
if (!this.gl.getShaderParameter(fragmentShader, this.gl.COMPILE_STATUS)) {
94+
console.error('Fragment shader compilation error:', this.gl.getShaderInfoLog(fragmentShader));
95+
return;
96+
}
97+
98+
this.gl.attachShader(this.program, vertexShader);
99+
this.gl.attachShader(this.program, fragmentShader);
100+
this.gl.linkProgram(this.program);
101+
102+
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
103+
console.error('Program linking error:', this.gl.getProgramInfoLog(this.program));
104+
return;
105+
}
106+
107+
const positionLocation = this.gl.getAttribLocation(this.program, "position");
108+
this.gl.enableVertexAttribArray(positionLocation);
109+
this.gl.vertexAttribPointer(positionLocation, 2, this.gl.FLOAT, false, 0, 0);
110+
111+
this.timeLocation = this.gl.getUniformLocation(this.program, "iTime");
112+
this.resolutionLocation = this.gl.getUniformLocation(this.program, "iResolution");
113+
}
114+
115+
private renderGl() {
116+
if (!this.gl || !this.program) return;
117+
118+
const time = (performance.now() - this.startTime) / 1000;
119+
120+
this.gl.viewport(0, 0, this.width, this.height);
121+
this.gl.useProgram(this.program);
122+
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
123+
124+
if (this.timeLocation) {
125+
this.gl.uniform1f(this.timeLocation, time);
126+
}
127+
if (this.resolutionLocation) {
128+
this.gl.uniform2f(this.resolutionLocation, this.width, this.height);
129+
}
130+
131+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
132+
133+
this.animationFrame = requestAnimationFrame(() => this.renderGl());
134+
}
135+
136+
override firstUpdated() {
137+
this.setupWebGL();
138+
this.renderGl();
139+
}
140+
141+
override disconnectedCallback() {
142+
super.disconnectedCallback();
143+
if (this.animationFrame) {
144+
cancelAnimationFrame(this.animationFrame);
145+
}
146+
}
147+
148+
override render() {
149+
return html`
150+
<canvas ${ref(this.canvasRef)}></canvas>
151+
`;
152+
}
153+
}

typescript/packages/lookslike-high-level/src/components/sidebar.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ export class CommonSidebar extends LitElement {
143143
this.setField("query", JSON.parse(e.detail.state.doc.toString()));
144144
};
145145

146+
const onDataChanged = (e: CustomEvent) => {
147+
this.setField("data", JSON.parse(e.detail.state.doc.toString()));
148+
};
149+
146150
return html`
147151
<os-navstack>
148152
${when(
@@ -230,6 +234,7 @@ export class CommonSidebar extends LitElement {
230234
.source=${watchCell(data, (q) =>
231235
JSON.stringify(q, null, 2)
232236
)}
237+
@doc-change=${onDataChanged}
233238
></os-code-editor>
234239
</div>
235240
</os-sidebar-group>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
UI,
3+
NAME,
4+
lift,
5+
handler,
6+
recipe,
7+
fetchData,
8+
llm,
9+
cell,
10+
ifElse,
11+
} from "@commontools/common-builder";
12+
import * as z from "zod";
13+
import { buildTransactionRequest, schemaQuery } from "../query.js";
14+
import { h } from "@commontools/common-html";
15+
16+
export const schema = z.object({
17+
sourceCode: z.string()
18+
})
19+
20+
type ShaderItem = z.infer<typeof schema>;
21+
22+
const eid = (e: any) => (e as any)['.'];
23+
24+
const onAddItem = handler<{}, { sourceCode: string }>((e, state) => {
25+
const sourceCode = state.sourceCode;
26+
state.sourceCode = '';
27+
return fetchData(buildTransactionRequest(prepChanges({ sourceCode })));
28+
})
29+
30+
const prepChanges = lift(({ sourceCode }) => {
31+
return {
32+
changes: [
33+
{
34+
Import: {
35+
sourceCode
36+
}
37+
}
38+
]
39+
}
40+
})
41+
42+
const prepGeneration = lift(({ prompt }) => {
43+
return {
44+
messages: [prompt],
45+
system: "test"
46+
}
47+
})
48+
49+
const onGenerateShader = handler<{}, { prompt: string; triggerPrompt: string; }>((e, state) => {
50+
state.triggerPrompt = state.prompt;
51+
})
52+
53+
const buildGeneration = lift(({ prompt }) => {
54+
return {
55+
messages: [prompt, `\`\`\`glsl
56+
precision mediump float;
57+
uniform float iTime;
58+
uniform vec2 iResolution;
59+
#define UV (gl_FragCoord.xy / iResolution);
60+
61+
varying vec2 v_texCoord;
62+
63+
`],
64+
system: "return a full, plain, glsl shader",
65+
stop: "```"
66+
}
67+
})
68+
69+
const grabGLSL = lift<{ result?: string }, string | undefined>(({ result }) => {
70+
if (!result) {
71+
return;
72+
}
73+
const html = result.match(/```glsl\n([\s\S]+?)```/)?.[1];
74+
if (!html) {
75+
console.error("No GLSL found in text", result);
76+
return;
77+
}
78+
return html;
79+
});
80+
81+
82+
export const shaderQuery = recipe(
83+
z.object({ focused: z.string(), prompt: z.string(), sourceCode: z.string(), triggerPrompt: z.string() }).describe("shader query"),
84+
({ sourceCode, prompt, triggerPrompt, focused }) => {
85+
const { result: items, query } = schemaQuery(schema)
86+
87+
const onCodeChange = handler<InputEvent, { sourceCode: string }>((e, state) => {
88+
state.sourceCode = (e.target as HTMLInputElement).value;
89+
});
90+
91+
const onPromptChange = handler<InputEvent, { prompt: string }>((e, state) => {
92+
state.prompt = (e.target as HTMLInputElement).value;
93+
});
94+
95+
const { result } = llm(buildGeneration({ prompt: triggerPrompt }))
96+
const hasResult = lift(({ result }) => !!result)({ result });
97+
98+
return {
99+
[NAME]: 'Shader query',
100+
[UI]: <div style="width: 100%; height: 100%;">
101+
<div>
102+
<input type="string" value={prompt} placeholder="Prompt" oninput={onPromptChange({ prompt })}></input>
103+
<textarea value={grabGLSL({ result })} placeholder="Shader source" oninput={onCodeChange({ sourceCode })}></textarea>
104+
<button onclick={onGenerateShader({ prompt, triggerPrompt })}>Generate</button>
105+
<button onclick={onAddItem({ sourceCode: grabGLSL({ result }) })}>Add</button>
106+
</div>
107+
<div style="position: relative; width: 100%; height: 100%;">
108+
{items.map(({ sourceCode }) => {
109+
return <shader-layer width={640} height={480} shader={sourceCode}></shader-layer>
110+
})}
111+
{ifElse(hasResult, <shader-layer width={640} height={480} shader={grabGLSL({ result })}></shader-layer>, <div></div>)}
112+
</div>
113+
</div>,
114+
data: items,
115+
query
116+
};
117+
},
118+
);

0 commit comments

Comments
 (0)