initial release
This commit is contained in:
commit
51a539b73b
4 changed files with 276 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
web/*.js
|
1
mod.ts
Normal file
1
mod.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { ShaderView } from "./src/shaderview.ts";
|
245
src/shaderview.ts
Normal file
245
src/shaderview.ts
Normal 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
27
web/index.html
Normal 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>
|
Loading…
Reference in a new issue