From 51a539b73bc4f6a38e3c7fbe2526935e88279e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 17 Dec 2024 15:38:56 +0100 Subject: [PATCH] initial release --- .gitignore | 3 + mod.ts | 1 + src/shaderview.ts | 245 ++++++++++++++++++++++++++++++++++++++++++++++ web/index.html | 27 +++++ 4 files changed, 276 insertions(+) create mode 100644 .gitignore create mode 100644 mod.ts create mode 100644 src/shaderview.ts create mode 100644 web/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1252086 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +web/*.js diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..45df2b7 --- /dev/null +++ b/mod.ts @@ -0,0 +1 @@ +export { ShaderView } from "./src/shaderview.ts"; diff --git a/src/shaderview.ts b/src/shaderview.ts new file mode 100644 index 0000000..8037f62 --- /dev/null +++ b/src/shaderview.ts @@ -0,0 +1,245 @@ +export class ShaderView { + uniforms: Record = {}; + canvas: HTMLCanvasElement; + gl: WebGLRenderingContext; + vertexShader: WebGLShader; + fragmentShader: WebGLShader; + program: WebGLProgram; + vertices: Float32Array; + buffer: WebGLBuffer; + mousedown = false; + lastTime = 0; + + constructor( + shaderString: string, + options?: { + parent?: HTMLElement; + noAutoStart?: boolean; + size?: { width: number; height: number }; + }, + ) { + // shadertoy differences + const ioTest = /\(\s*out\s+vec4\s+(\S+)\s*,\s*in\s+vec2\s+(\S+)\s*\)/; + const io = shaderString.match(ioTest); + shaderString = shaderString.replace("mainImage", "main"); + shaderString = shaderString.replace(ioTest, "()"); + + // shadertoy built in uniforms + const uniforms = this.uniforms = { + iResolution: { + type: "vec3", + value: [window.innerWidth, window.innerHeight, 0], + }, + iTime: { + type: "float", + value: 0, + }, + iTimeDelta: { + type: "float", + value: 0, + }, + iFrame: { + type: "int", + value: 0, + }, + iMouse: { + type: "vec4", + value: [0, 0, 0, 0], + }, + }; + + // create default string values + shaderString = + (io + ? `#define ${io[1]} gl_FragColor\n#define ${io[2]} gl_FragCoord.xy\n` + : "") + shaderString; + shaderString = Object.keys(uniforms) + .map((key) => ({ + name: key, + type: uniforms[key].type, + })) + .reduce((a, uniform) => ( + a + `uniform ${uniform.type} ${uniform.name};\n` + ), "") + shaderString; + shaderString = "precision highp float;\n" + shaderString; + + // create canvas + const canvas = this.canvas = document.createElement("canvas"); + + // get webgl context and set clearColor + const gl = this.gl = check(canvas.getContext("webgl")); + gl.clearColor(0, 0, 0, 0); + + // compile basic vertex shader to make rect fill screen + const vertexShader = this.vertexShader = check( + gl.createShader(gl.VERTEX_SHADER), + ); + gl.shaderSource( + vertexShader, + ` + attribute vec2 position; + void main() { + gl_Position = vec4(position, 0.0, 1.0); + } + `, + ); + gl.compileShader(vertexShader); + + // compile fragment shader from string passed in + const fragmentShader = this.fragmentShader = check(gl.createShader( + gl.FRAGMENT_SHADER, + )); + gl.shaderSource(fragmentShader, shaderString); + gl.compileShader(fragmentShader); + + // make program from shaders + const program = this.program = check(gl.createProgram()); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + // vertices for basic rectangle to fill screen + const vertices = this.vertices = new Float32Array([ + -1, + 1, + 1, + 1, + 1, + -1, + -1, + 1, + 1, + -1, + -1, + -1, + ]); + + const buffer = this.buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); + + gl.useProgram(program); + + const position = gl.getAttribLocation(program, "position"); + gl.enableVertexAttribArray(position); + gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); + + // get all uniform locations from shaders + Object.keys(uniforms).forEach((key, i) => { + uniforms[key].location = gl.getUniformLocation(program, key); + }); + + // report webgl errors + this.reportErrors(); + + // add event listeners + canvas.addEventListener("mousedown", (ev) => this.mouseDown(ev)); + canvas.addEventListener("mousemove", (ev) => this.mouseDown(ev)); + canvas.addEventListener("mouseup", (ev) => this.mouseUp(ev)); + + // attach to parent + if (options?.parent) { + this.setParent(options.parent); + } + + // initial size + if (options?.size) { + this.resize(options.size.width, options.size.height); + } + + // auto render unless otherwise specified + if (!options?.noAutoStart) { + this.start(); + } + } + + setParent(parent: HTMLElement): void { + parent.appendChild(this.canvas); + } + + mouseDown(e) { + this.mousedown = true; + this.uniforms.iMouse.value[2] = e.clientX; + this.uniforms.iMouse.value[3] = e.clientY; + } + + mouseMove(e) { + if (this.mousedown) { + this.uniforms.iMouse.value[0] = e.clientX; + this.uniforms.iMouse.value[1] = e.clientY; + } + } + + mouseUp(e) { + this.mousedown = false; + this.uniforms.iMouse.value[2] = 0; + this.uniforms.iMouse.value[3] = 0; + } + + start(): void { + this.queueRender(true); + } + + queueRender(loop = true) { + requestAnimationFrame((time) => this.render(time, loop)); + } + + render(timestamp: number, loop: boolean) { + const gl = this.gl; + + let delta = this.lastTime ? ((timestamp - this.lastTime) / 1000) : 0; + this.lastTime = timestamp; + + this.uniforms.iTime.value += delta; + this.uniforms.iTimeDelta.value = delta; + this.uniforms.iFrame.value++; + + gl.clear(gl.COLOR_BUFFER_BIT); + + Object.keys(this.uniforms).forEach((key) => { + const t = this.uniforms[key].type; + const method = t.match(/vec/) ? `${t[t.length - 1]}fv` : `1${t[0]}`; + gl[`uniform${method}`]( + this.uniforms[key].location, + this.uniforms[key].value, + ); + }); + + gl.drawArrays(gl.TRIANGLES, 0, this.vertices.length / 2); + + if (loop) { + this.queueRender(loop); + } + } + + reportErrors() { + const gl = this.gl; + + if (!gl.getShaderParameter(this.vertexShader, gl.COMPILE_STATUS)) { + console.log(gl.getShaderInfoLog(this.vertexShader)); + } + + if (!gl.getShaderParameter(this.fragmentShader, gl.COMPILE_STATUS)) { + console.log(gl.getShaderInfoLog(this.fragmentShader)); + } + + if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { + console.log(gl.getProgramInfoLog(this.program)); + } + } + + resize(width: number, height: number) { + this.canvas.width = this.uniforms.iResolution.value[0] = width; + this.canvas.height = this.uniforms.iResolution.value[1] = height; + + this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); + } +} + +function check(val: T | null): T { + if (val === null) { + throw new Error("Unexpected null value"); + } else { + return val; + } +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..2325388 --- /dev/null +++ b/web/index.html @@ -0,0 +1,27 @@ + + + + + + + +
+ +