initial release

This commit is contained in:
Michaël Lemaire 2024-12-17 15:38:56 +01:00
commit 51a539b73b
4 changed files with 276 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
web/*.js

1
mod.ts Normal file
View file

@ -0,0 +1 @@
export { ShaderView } from "./src/shaderview.ts";

245
src/shaderview.ts Normal file
View file

@ -0,0 +1,245 @@
export class ShaderView {
uniforms: Record<string, any> = {};
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<T>(val: T | null): T {
if (val === null) {
throw new Error("Unexpected null value");
} else {
return val;
}
}

27
web/index.html Normal file
View file

@ -0,0 +1,27 @@
<html>
<head>
<meta charset="utf-8">
<script type="module">
import { ShaderView } from "./mod.js";
const shader = `
void main()
{
vec2 uv = (gl_FragCoord.xy * 2. - iResolution.xy) / iResolution.y;
float d = length(uv);
d = abs(sin(d * 10. + iTime)) / 10.;
d = smoothstep(0.95, 1., 1. - d);
vec3 color = d * vec3(1., 2., 3.);
gl_FragColor = vec4(color, 1.0);
}
`.trim();
new ShaderView(shader, {
parent: document.getElementById("container"),
size: { width: 512, height: 512 },
});
</script>
</head>
<body>
<div id="container"></div>
</body>
</html>