diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..83c1115 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{ts,json}] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore index 1252086..8b7abdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -node_modules -dist +deno.d.ts +.vscode +.local +.output web/*.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..555b0c1 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# typescript/shaderview + +[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/shaderview?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=shaderview) diff --git a/run b/run new file mode 100755 index 0000000..74d1c6d --- /dev/null +++ b/run @@ -0,0 +1,19 @@ +#!/bin/sh +# Simplified run tool for deno commands + +if test $# -eq 0 +then + echo "Usage: $0 [file or command]" + exit 1 +elif echo $1 | grep -q '.*.ts' +then + denocmd=run + denoargs=$1 + shift +else + denocmd=$1 + shift +fi + +denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@" +exec deno $denocmd $denoargs diff --git a/src/shaderview.test.ts b/src/shaderview.test.ts new file mode 100644 index 0000000..55278b2 --- /dev/null +++ b/src/shaderview.test.ts @@ -0,0 +1,54 @@ +import { assertEquals } from "jsr:@std/assert"; +import { ShaderView } from "./shaderview.ts"; + +Deno.test("list uniforms from shader code", () => { + assertEquals( + new ShaderView("void main() {}", { noRender: true }).listUniforms(), + { + iFrame: { + type: "int", + }, + iMouse: { + type: "vec4", + }, + iResolution: { + type: "vec3", + }, + iTime: { + type: "float", + }, + iTimeDelta: { + type: "float", + }, + }, + ); + assertEquals( + new ShaderView( + "uniform vec3 test1 ;\nuniform float test2; void main() {}", + { noRender: true }, + ).listUniforms(), + { + iFrame: { + type: "int", + }, + iMouse: { + type: "vec4", + }, + iResolution: { + type: "vec3", + }, + iTime: { + type: "float", + }, + iTimeDelta: { + type: "float", + }, + test1: { + type: "vec3", + }, + test2: { + type: "float", + }, + }, + ); +}); diff --git a/src/shaderview.ts b/src/shaderview.ts index 8037f62..7892242 100644 --- a/src/shaderview.ts +++ b/src/shaderview.ts @@ -1,34 +1,24 @@ export class ShaderView { + readonly finalShader: string; uniforms: Record = {}; - canvas: HTMLCanvasElement; - gl: WebGLRenderingContext; - vertexShader: WebGLShader; - fragmentShader: WebGLShader; - program: WebGLProgram; - vertices: Float32Array; - buffer: WebGLBuffer; - mousedown = false; - lastTime = 0; + render?: ShaderViewRender; constructor( shaderString: string, options?: { parent?: HTMLElement; + noRender?: boolean; 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, "()"); + const size = options?.size ?? { width: 128, height: 128 }; // shadertoy built in uniforms - const uniforms = this.uniforms = { + this.uniforms = { iResolution: { type: "vec3", - value: [window.innerWidth, window.innerHeight, 0], + value: [size.width, size.height, 0], }, iTime: { type: "float", @@ -48,21 +38,97 @@ export class ShaderView { }, }; + this.finalShader = this.preprocessShaderCode(shaderString); + + if (!options?.noRender) { + this.render = new ShaderViewRender(this.finalShader, this.uniforms); + } + + // attach to parent + if (options?.parent) { + this.setParent(options.parent); + } + + // initial size + this.resize(size.width, size.height); + + // auto render unless otherwise specified + if (this.render && !options?.noAutoStart) { + this.render.start(); + } + } + + preprocessShaderCode(shaderString: string): string { + // 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, "()"); + // create default string values shaderString = (io ? `#define ${io[1]} gl_FragColor\n#define ${io[2]} gl_FragCoord.xy\n` : "") + shaderString; - shaderString = Object.keys(uniforms) + shaderString = Object.keys(this.uniforms) .map((key) => ({ name: key, - type: uniforms[key].type, + type: this.uniforms[key].type, })) .reduce((a, uniform) => ( a + `uniform ${uniform.type} ${uniform.name};\n` ), "") + shaderString; shaderString = "precision highp float;\n" + shaderString; + return shaderString; + } + listUniforms(): Record { + const result: Record = {}; + + for ( + const uniform of this.finalShader.match(/\buniform\s+\w+\s+\w+/g) ?? [] + ) { + const [type, name] = uniform.replaceAll(/\s+/g, " ").split(" ").slice(1); + result[name] = { type }; + } + + return result; + } + + setUniform(name: string, value: any): void { + this.uniforms[name]; + } + + setParent(parent: HTMLElement): void { + if (this.render) { + parent.appendChild(this.render.canvas); + } + } + + resize(width: number, height: number) { + this.uniforms.iResolution.value[0] = width; + this.uniforms.iResolution.value[1] = height; + if (this.render) { + this.render.resize(width, height); + } + } +} + +class ShaderViewRender { + canvas: HTMLCanvasElement; + gl: WebGLRenderingContext; + vertexShader: WebGLShader; + fragmentShader: WebGLShader; + program: WebGLProgram; + vertices: Float32Array; + buffer: WebGLBuffer; + mousedown = false; + lastTime = 0; + + constructor( + readonly shaderCode: string, + readonly uniforms: Record = {}, + ) { // create canvas const canvas = this.canvas = document.createElement("canvas"); @@ -89,7 +155,7 @@ export class ShaderView { const fragmentShader = this.fragmentShader = check(gl.createShader( gl.FRAGMENT_SHADER, )); - gl.shaderSource(fragmentShader, shaderString); + gl.shaderSource(fragmentShader, this.shaderCode); gl.compileShader(fragmentShader); // make program from shaders @@ -136,25 +202,6 @@ export class ShaderView { 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) { @@ -229,10 +276,10 @@ export class ShaderView { } resize(width: number, height: number) { - this.canvas.width = this.uniforms.iResolution.value[0] = width; - this.canvas.height = this.uniforms.iResolution.value[1] = height; + this.canvas.width = width; + this.canvas.height = height; - this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); + this.gl.viewport(0, 0, width, height); } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..93f2e69 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "ESNext", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "preserveConstEnums": true, + "lib": ["dom"] + } +} diff --git a/web/index.html b/web/index.html index 2325388..e655f40 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@