function shader({
width = 640,
height = 480,
devicePixelRatio = window.devicePixelRatio,
invalidation,
visibility,
uniforms = {},
inputs = {},
iMouse = false,
iTime = false,
sources = [],
preserveDrawingBuffer = false
} = {}) {
uniforms = new Map(
Object.entries(uniforms).map(([name, type]) => [name, { type }])
);
inputs = new Map(Object.entries(inputs));
for (const { type } of uniforms.values())
if (type !== "float") throw new Error(`unknown type: ${type}`);
for (const name of inputs.keys())
if (!uniforms.has(name)) uniforms.set(name, { type: "float" });
if (iTime && !uniforms.has("iTime")) uniforms.set("iTime", { type: "float" });
if (visibility !== undefined && typeof visibility !== "function")
throw new Error("invalid visibility");
return function() {
const source = String.raw.apply(String, arguments);
const canvas = DOM.canvas(
width * devicePixelRatio,
height * devicePixelRatio
);
canvas.style = `max-width: 100%; width: ${width}px; height: auto;`;
const gl = canvas.getContext("webgl", { preserveDrawingBuffer });
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
`precision highp float;
${Array.from(uniforms, ([name, { type }]) => `uniform ${type} ${name};`).join(
"\n"
)}
const vec3 iResolution = vec3(
${(width * devicePixelRatio).toFixed(1)},
${(height * devicePixelRatio).toFixed(1)},
${devicePixelRatio.toFixed(1)}
);
`,
...sources,
source,
`
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`
);
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
`
attribute vec2 a_vertex;
void main() {
gl_Position = vec4(a_vertex, 0.0, 1.0);
}
`
);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
Float32Array.of(-1, -1, +1, -1, +1, +1, -1, +1),
gl.STATIC_DRAW
);
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
const a_vertex = gl.getAttribLocation(program, "a_vertex");
gl.enableVertexAttribArray(a_vertex);
gl.vertexAttribPointer(a_vertex, 2, gl.FLOAT, false, 0, 0);
for (const [name, u] of uniforms)
u.location = gl.getUniformLocation(program, name);
const ondispose =
invalidation === undefined ? disposal(canvas) : invalidation;
let frame;
let disposed = false;
ondispose.then(() => (disposed = true));
async function render() {
if (visibility !== undefined) await visibility();
frame = undefined;
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}
Object.assign(canvas, {
gl,
program,
update(values = {}) {
if (disposed) return false;
for (const name in values) {
const u = uniforms.get(name);
if (!u) throw new Error(`unknown uniform: ${name}`);
gl.uniform1f(u.location, values[name]);
}
frame || requestAnimationFrame(render);
return true;
}
});
for (const [name, input] of inputs) {
const u = uniforms.get(name);
if (!u) throw new Error(`unknown uniform: ${name}`);
gl.uniform1f(u.location, input.value);
const update = () => {
gl.uniform1f(u.location, input.value);
frame || requestAnimationFrame(render);
};
input.addEventListener("input", update);
ondispose.then(() => input.removeEventListener("input", update));
}
if (iTime) {
frame = true; // always rendering
const u_time = gl.getUniformLocation(program, "iTime");
let timeframe;
(async function tick() {
if (visibility !== undefined) await visibility();
gl.uniform1f(u_time, performance.now() / 1000);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
return (timeframe = requestAnimationFrame(tick));
})();
ondispose.then(() => cancelAnimationFrame(timeframe));
} else {
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}
return canvas;
};
}