diff --git a/packages/shader/index.mjs b/packages/shader/index.mjs
index 541e86fde..41aa0e1de 100644
--- a/packages/shader/index.mjs
+++ b/packages/shader/index.mjs
@@ -1,234 +1,2 @@
-/*
-shader.mjs - implements the `loadShader` helper and `shader` pattern function
-Copyright (C) 2024 Strudel contributors
-This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
-*/
-
-import { PicoGL } from 'picogl';
-import { register, logger } from '@strudel/core';
-
-// The standard fullscreen vertex shader.
-const vertexShader = `#version 300 es
-precision highp float;
-layout(location=0) in vec2 position;
-void main() {
- gl_Position = vec4(position, 1, 1);
-}
-`;
-
-// Make the fragment source, similar to the one from shadertoy.
-function mkFragmentShader(code) {
- return `#version 300 es
-precision highp float;
-out vec4 oColor;
-uniform float iTime;
-uniform vec2 iResolution;
-
-${code}
-
-void main(void) {
- mainImage(oColor, gl_FragCoord.xy);
-}
-`;
-}
-
-// Modulation helpers.
-const hardModulation = () => {
- let val = 0;
- return {
- get: () => val,
- set: (v) => {
- val = v;
- },
- };
-};
-
-const decayModulation = (decay) => {
- let val = 0;
- let desired = 0;
- return {
- get: (ts) => {
- val += (desired - val) / decay;
- return val;
- },
- set: (v) => {
- desired = val + v;
- },
- };
-};
-
-// Set an uniform value (from a pattern).
-function setUniform(instance, name, value, position) {
- const uniform = instance.uniforms[name];
- if (uniform) {
- if (uniform.count == 0) {
- // This is a single value
- uniform.mod.set(value);
- } else {
- // This is an array
- const idx = position % uniform.mod.length;
- uniform.mod[idx].set(value);
- }
- } else {
- logger('[shader] unknown uniform: ' + name);
- }
-
- // Ensure the instance is drawn
- instance.age = 0;
- if (!instance.drawing) {
- instance.drawing = requestAnimationFrame(instance.update);
- }
-}
-
-// Update the uniforms for a given drawFrame call.
-function updateUniforms(drawFrame, elapsed, uniforms) {
- Object.values(uniforms).forEach((uniform) => {
- const value =
- uniform.count == 0 ? uniform.mod.get(elapsed) : uniform.value.map((_, i) => uniform.mod[i].get(elapsed));
- // Send the value to the GPU
- drawFrame.uniform(uniform.name, value);
- });
-}
-
-// Setup the instance's uniform after shader compilation.
-function setupUniforms(uniforms, program) {
- Object.entries(program.uniforms).forEach(([name, uniform]) => {
- if (name != 'iTime' && name != 'iResolution') {
- // remove array suffix
- const uname = name.replace('[0]', '');
- const count = uniform.count | 0;
- if (!uniforms[uname] || uniforms[uname].count != count) {
- // TODO: keep the previous value when the count change...
- uniforms[uname] = {
- name,
- count,
- value: count == 0 ? 0 : new Float32Array(count),
- mod: count == 0 ? decayModulation(50) : new Array(count).fill().map(() => decayModulation(50)),
- };
- }
- }
- });
- // TODO: remove previous uniform that are no longer used...
- return uniforms;
-}
-
-// Setup the canvas and return the WebGL context.
-function setupCanvas(name) {
- // TODO: support custom size
- const width = 400;
- const height = 300;
- const canvas = document.createElement('canvas');
- canvas.id = 'cnv-' + name;
- canvas.width = width;
- canvas.height = height;
- const top = 60 + Object.keys(_instances).length * height;
- canvas.style = `pointer-events:none;width:${width}px;height:${height}px;position:fixed;top:${top}px;right:23px`;
- document.body.append(canvas);
- return canvas.getContext('webgl2');
-}
-
-// Setup the shader instance
-async function initializeShaderInstance(name, code) {
- // Setup PicoGL app
- const ctx = setupCanvas(name);
- console.log(ctx);
- const app = PicoGL.createApp(ctx);
- app.resize(400, 300);
-
- // Setup buffers
- const resolution = new Float32Array([ctx.canvas.width, ctx.canvas.height]);
-
- // Two triangle to cover the whole canvas
- const positionBuffer = app.createVertexBuffer(
- PicoGL.FLOAT,
- 2,
- new Float32Array([-1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1]),
- );
-
- // Setup the arrays
- const arrays = app.createVertexArray().vertexAttributeBuffer(0, positionBuffer);
-
- return app
- .createPrograms([vertexShader, code])
- .then(([program]) => {
- const drawFrame = app.createDrawCall(program, arrays);
- const instance = { app, code, program, arrays, drawFrame, uniforms: setupUniforms({}, program) };
-
- // Render frame logic
- let prev = performance.now() / 1000;
- instance.age = 0;
- instance.update = () => {
- const now = performance.now() / 1000;
- const elapsed = now - prev;
- prev = now;
- // console.log("drawing!")
- app.clear();
- instance.drawFrame.uniform('iResolution', resolution).uniform('iTime', now);
-
- updateUniforms(instance.drawFrame, elapsed, instance.uniforms);
-
- instance.drawFrame.draw();
- if (instance.age++ < 100) requestAnimationFrame(instance.update);
- else instance.drawing = false;
- };
- return instance;
- })
- .catch((err) => {
- ctx.canvas.remove();
- throw err;
- });
-}
-
-// Update the instance program
-async function reloadShaderInstanceCode(instance, code) {
- return instance.app.createPrograms([vertexShader, code]).then(([program]) => {
- instance.program.delete();
- instance.program = program;
- instance.uniforms = setupUniforms(instance.uniforms, program);
- instance.drawFrame = instance.app.createDrawCall(program, instance.arrays);
- });
-}
-
-// Keep track of the running shader instances
-let _instances = {};
-export async function loadShader(code = '', name = 'default') {
- if (code) {
- code = mkFragmentShader(code);
- }
- if (!_instances[name]) {
- _instances[name] = await initializeShaderInstance(name, code);
- logger('[shader] ready');
- } else if (_instances[name].code != code) {
- await reloadShaderInstanceCode(_instances[name], code);
- logger('[shader] reloaded');
- }
-}
-
-export const shader = register('shader', (options, pat) => {
- // Keep track of the pitches value: Map String Int
- const pitches = { _count: 0 };
-
- return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
- const instance = _instances[options.instance || 'default'];
- if (!instance) {
- logger('[shader] not loaded yet', 'warning');
- return;
- }
-
- const value = options.gain || 1.0;
- if (options.pitch !== undefined) {
- const note = hap.value.note || hap.value.s;
- if (pitches[note] === undefined) {
- // Assign new value, the first note gets 0, then 1, then 2, ...
- pitches[note] = Object.keys(pitches).length;
- }
- setUniform(instance, options.pitch, value, pitches[note]);
- } else if (options.seq !== undefined) {
- setUniform(instance, options.seq, value, pitches._count++);
- } else if (options.uniform !== undefined) {
- setUniform(instance, options.uniform, value);
- } else {
- console.error('Unknown shader options, need either pitch or uniform', options);
- }
- }, false);
-});
+export {loadShader} from './shader.mjs';
+export * from './uniform.mjs';
diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs
new file mode 100644
index 000000000..e81a0cf50
--- /dev/null
+++ b/packages/shader/shader.mjs
@@ -0,0 +1,211 @@
+/*
+shader.mjs - implements the `loadShader` function
+Copyright (C) 2024 Strudel contributors
+This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
+*/
+
+import { PicoGL } from 'picogl';
+import { logger } from '@strudel/core';
+
+// The standard fullscreen vertex shader.
+const vertexShader = `#version 300 es
+precision highp float;
+layout(location=0) in vec2 position;
+void main() {
+ gl_Position = vec4(position, 1, 1);
+}
+`;
+
+// Make the fragment source, similar to the one from shadertoy.
+function mkFragmentShader(code) {
+ return `#version 300 es
+precision highp float;
+out vec4 oColor;
+uniform float iTime;
+uniform vec2 iResolution;
+
+${code}
+
+void main(void) {
+ mainImage(oColor, gl_FragCoord.xy);
+}
+`;
+}
+
+// Modulation helpers.
+const hardModulation = () => {
+ let val = 0;
+ return {
+ get: () => val,
+ set: (v) => {
+ val = v;
+ },
+ };
+};
+
+const decayModulation = (decay) => {
+ let val = 0;
+ let desired = 0;
+ return {
+ get: (ts) => {
+ val += (desired - val) / decay;
+ return val;
+ },
+ set: (v) => {
+ desired = val + v;
+ },
+ };
+};
+
+// Set an uniform value (from a pattern).
+export function setUniform(instanceName, name, value, position) {
+ const instance = _instances[instanceName || 'default'];
+ if (!instance) {
+ logger('[shader] not loaded yet', 'warning');
+ return;
+ }
+
+ const uniform = instance.uniforms[name];
+ if (uniform) {
+ if (uniform.count == 0) {
+ // This is a single value
+ uniform.mod.set(value);
+ } else {
+ // This is an array
+ const idx = position % uniform.mod.length;
+ uniform.mod[idx].set(value);
+ }
+ } else {
+ logger('[shader] unknown uniform: ' + name);
+ }
+
+ // Ensure the instance is drawn
+ instance.age = 0;
+ if (!instance.drawing) {
+ instance.drawing = requestAnimationFrame(instance.update);
+ }
+}
+
+// Update the uniforms for a given drawFrame call.
+function updateUniforms(drawFrame, elapsed, uniforms) {
+ Object.values(uniforms).forEach((uniform) => {
+ const value =
+ uniform.count == 0 ? uniform.mod.get(elapsed) : uniform.value.map((_, i) => uniform.mod[i].get(elapsed));
+ // Send the value to the GPU
+ drawFrame.uniform(uniform.name, value);
+ });
+}
+
+// Setup the instance's uniform after shader compilation.
+function setupUniforms(uniforms, program) {
+ Object.entries(program.uniforms).forEach(([name, uniform]) => {
+ if (name != 'iTime' && name != 'iResolution') {
+ // remove array suffix
+ const uname = name.replace('[0]', '');
+ const count = uniform.count | 0;
+ if (!uniforms[uname] || uniforms[uname].count != count) {
+ // TODO: keep the previous value when the count change...
+ uniforms[uname] = {
+ name,
+ count,
+ value: count == 0 ? 0 : new Float32Array(count),
+ mod: count == 0 ? decayModulation(50) : new Array(count).fill().map(() => decayModulation(50)),
+ };
+ }
+ }
+ });
+ // TODO: remove previous uniform that are no longer used...
+ return uniforms;
+}
+
+// Setup the canvas and return the WebGL context.
+function setupCanvas(name) {
+ // TODO: support custom size
+ const width = 400;
+ const height = 300;
+ const canvas = document.createElement('canvas');
+ canvas.id = 'cnv-' + name;
+ canvas.width = width;
+ canvas.height = height;
+ const top = 60 + Object.keys(_instances).length * height;
+ canvas.style = `pointer-events:none;width:${width}px;height:${height}px;position:fixed;top:${top}px;right:23px`;
+ document.body.append(canvas);
+ return canvas.getContext('webgl2');
+}
+
+// Setup the shader instance
+async function initializeShaderInstance(name, code) {
+ // Setup PicoGL app
+ const ctx = setupCanvas(name);
+ console.log(ctx);
+ const app = PicoGL.createApp(ctx);
+ app.resize(400, 300);
+
+ // Setup buffers
+ const resolution = new Float32Array([ctx.canvas.width, ctx.canvas.height]);
+
+ // Two triangle to cover the whole canvas
+ const positionBuffer = app.createVertexBuffer(
+ PicoGL.FLOAT,
+ 2,
+ new Float32Array([-1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1]),
+ );
+
+ // Setup the arrays
+ const arrays = app.createVertexArray().vertexAttributeBuffer(0, positionBuffer);
+
+ return app
+ .createPrograms([vertexShader, code])
+ .then(([program]) => {
+ const drawFrame = app.createDrawCall(program, arrays);
+ const instance = { app, code, program, arrays, drawFrame, uniforms: setupUniforms({}, program) };
+
+ // Render frame logic
+ let prev = performance.now() / 1000;
+ instance.age = 0;
+ instance.update = () => {
+ const now = performance.now() / 1000;
+ const elapsed = now - prev;
+ prev = now;
+ // console.log("drawing!")
+ app.clear();
+ instance.drawFrame.uniform('iResolution', resolution).uniform('iTime', now);
+
+ updateUniforms(instance.drawFrame, elapsed, instance.uniforms);
+
+ instance.drawFrame.draw();
+ if (instance.age++ < 100) requestAnimationFrame(instance.update);
+ else instance.drawing = false;
+ };
+ return instance;
+ })
+ .catch((err) => {
+ ctx.canvas.remove();
+ throw err;
+ });
+}
+
+// Update the instance program
+async function reloadShaderInstanceCode(instance, code) {
+ return instance.app.createPrograms([vertexShader, code]).then(([program]) => {
+ instance.program.delete();
+ instance.program = program;
+ instance.uniforms = setupUniforms(instance.uniforms, program);
+ instance.drawFrame = instance.app.createDrawCall(program, instance.arrays);
+ });
+}
+
+// Keep track of the running shader instances
+let _instances = {};
+export async function loadShader(code = '', name = 'default') {
+ if (code) {
+ code = mkFragmentShader(code);
+ }
+ if (!_instances[name]) {
+ _instances[name] = await initializeShaderInstance(name, code);
+ logger('[shader] ready');
+ } else if (_instances[name].code != code) {
+ await reloadShaderInstanceCode(_instances[name], code);
+ logger('[shader] reloaded');
+ }
+}
diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs
new file mode 100644
index 000000000..e1ae7f732
--- /dev/null
+++ b/packages/shader/uniform.mjs
@@ -0,0 +1,33 @@
+/*
+uniform.mjs - implements the `uniform` pattern function
+Copyright (C) 2024 Strudel contributors
+This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
+*/
+
+import { register, logger } from '@strudel/core';
+import { setUniform } from './shader.mjs';
+
+export const uniform = register('uniform', (options, pat) => {
+ // Keep track of the pitches value: Map String Int
+ const pitches = { _count: 0 };
+
+ return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
+ const instance = options.instance;
+
+ const value = options.gain || 1.0;
+ if (options.pitch !== undefined) {
+ const note = hap.value.note || hap.value.s;
+ if (pitches[note] === undefined) {
+ // Assign new value, the first note gets 0, then 1, then 2, ...
+ pitches[note] = Object.keys(pitches).length;
+ }
+ setUniform(instance, options.pitch, value, pitches[note]);
+ } else if (options.seq !== undefined) {
+ setUniform(instance, options.seq, value, pitches._count++);
+ } else if (options.uniform !== undefined) {
+ setUniform(instance, options.uniform, value);
+ } else {
+ console.error('Unknown shader options, need either pitch or uniform', options);
+ }
+ }, false);
+});