Update uniform method to take an option object

This commit is contained in:
Tristan de Cacqueray
2024-10-20 19:54:16 -04:00
parent 9e0b840afd
commit 1baa6da370
6 changed files with 91 additions and 149 deletions
-1
View File
@@ -8,7 +8,6 @@
"main": "dist/index.mjs"
},
"scripts": {
"test": "vitest run",
"build": "vite build",
"prepublishOnly": "npm run build"
},
-3
View File
@@ -1,3 +0,0 @@
// Fix `ReferenceError: self is not defined`
// when importing picogl in tests
globalThis.self = {};
+76 -104
View File
@@ -4,122 +4,94 @@ 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 <https://www.gnu.org/licenses/>.
*/
import { register, logger } from '@strudel/core';
import { Pattern, reify, register, logger } from '@strudel/core';
import { setUniform } from './shader.mjs';
// Parse a destination from the mini notation, e.g. `name` or `name:attr:value`
export function parseUniformDest(dest) {
let result = {};
if (typeof dest === 'string') result.name = dest;
else if (dest.length >= 2) {
result.name = dest[0];
// Parse the attr:value pairs
for (let i = 1; i < dest.length; i += 2) {
const k = dest[i];
const v = dest[i + 1];
const isNum = typeof v === 'number';
if (k == 'index' && isNum) result.position = v;
else if (k == 'index' && v == 'random') result.position = Math.floor(Math.random() * 1024);
else if (k == 'index' && v == 'seq') result.position = null;
else if (k == 'gain' && isNum) result.gain = v;
else if (k == 'slow' && isNum) result.slow = v;
else throw 'Bad uniform param ' + k + ':' + v;
}
}
return result;
}
// Keep track of the last uniforms' array position
let _uniforms = {};
function getNextPosition(name, value) {
// Initialize uniform state
if (!_uniforms[name]) _uniforms[name] = { _count: 0 };
const uniform = _uniforms[name];
// Set a new position when the value changes
if (uniform._last != value) {
uniform._last = value;
uniform._count++;
}
return uniform._count;
}
/**
* Update a shader. The destination name consist of
*
* - the uniform name
* - optional 'index' to set array position, either a number or an assignment mode ('seq' or 'random')
* - optional 'gain' to adjust the value: 0 to silence, 2 to double
* - optional 'slow' to adjust the change speed: 1 for instant, 50 for slow changes, default to 10
* Update a shader.
*
* @name uniform
* @example
* pan(sine.uniform("iColor"))
* @example
* gain("<.5 .3>".uniform("rotations:index:seq"))
* gain("<.5 .3>".uniform("rotations:seq"))
* @example
* s("bd sd").uniform({onTrigger: true, dest: "moveFWD"})
*/
export const uniform = register('uniform', (target, pat) => {
// TODO: support multiple shader instance
const instance = 'default';
export const uniform = register('uniform', (options, pat) => {
// The shader instance name
const instance = options.instance || 'default';
// Decode the uniform target defintion
const uniformDest = parseUniformDest(target);
// Set the first value by default
if (uniformDest.position === undefined) uniformDest.position = 0;
// Are the uniform updated on trigger
const onTrigger = options.onTrigger || false;
return pat.withValue((v) => {
// TODO: figure out why this is called repeatedly when changing values. For example, on first call, the last value is passed.
if (typeof v === 'number') {
const position = uniformDest.position === null ? getNextPosition(uniformDest.name, v) : uniformDest.position;
const value = v * (uniformDest.gain || 1);
setUniform(instance, uniformDest.name, value, false, position, uniformDest.slow || 10);
const setCtx = (uniformParam) => (ctx) => ({
uniformParam,
onTrigger: () => {},
dominantTrigger: true,
...ctx,
});
const pitches = { _count: 0 };
const getPosition = (value, dest) => {
if (typeof dest === 'number') return dest;
else if (dest == 'seq') return pitches._count++;
else if (dest == 'random') return Math.floor(Math.random() * 1024);
else if (onTrigger) {
const note = value.note || value.n || value.sound || value.s;
if (pitches[note] === undefined) {
// Assign new value, the first note gets 0, then 1, then 2, ...
pitches[note] = Object.keys(pitches).length - 1;
}
return pitches[note];
} else {
console.error('Uniform applied to a non number pattern');
throw 'Invalid position' + dest;
}
return v;
};
const getUniformPosition = (value, dest) => {
if (typeof dest === 'string') {
return [dest, 0];
} else {
return [dest[0], getPosition(value, dest[1])];
}
};
const optionsPats = [];
if (Array.isArray(options) || typeof options === 'string')
optionsPats.push(reify(options).withContext(setCtx('dest')));
else {
if (options.dest) optionsPats.push(reify(options.dest).withContext(setCtx('dest')));
if (options.gain) optionsPats.push(reify(options.gain).withContext(setCtx('gain')));
if (options.slow) optionsPats.push(reify(options.slow).withContext(setCtx('slow')));
}
return stack(pat, ...optionsPats).withHaps((haps) => {
let dest;
let gain = 1;
let slow = 10;
let source;
haps.forEach((hap) => {
if (hap.context.uniformParam == 'dest') {
dest = hap.value;
} else if (hap.context.uniformParam == 'gain') {
gain = hap.value;
} else if (hap.context.uniformParam == 'slow') {
slow = hap.value;
} else {
source = hap;
}
});
if (dest && source) {
if (onTrigger) {
source.context.onTrigger = (_, hap) => {
const [uniform, position] = getUniformPosition(hap.value, dest);
setUniform(instance, uniform, (hap.value.gain || 1) * gain, true, position, slow);
};
source.context.dominantTrigger = false;
} else {
const [uniform, position] = getUniformPosition(source.value, dest);
setUniform(instance, uniform, source.value * gain, false, position, slow);
}
}
return haps;
});
});
function getNotePosition(name, value) {
// Initialize uniform state
if (!_uniforms[name]) _uniforms[name] = {};
const uniform = _uniforms[name];
const note = value.note || value.n || value.sound || value.s;
if (uniform[note] === undefined) {
// Assign new value, the first note gets 0, then 1, then 2, ...
uniform[note] = Object.keys(uniform).length;
}
return uniform[note];
}
/**
* Update a shader with note-on event. See the 'uniform' doc.
*
* @name uniformTrigger
* @example
* s("bd sd").uniformTrigger("iColors:gain:2"))
*/
export const uniformTrigger = register('uniformTrigger', (target, pat) => {
// TODO: support multiple shader instance
const instance = 'default';
// Decode the uniform target defintion
const uniformDest = parseUniformDest(target);
return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
const position =
uniformDest.position === undefined
? // Set the position based on the note by default
getNotePosition(uniformDest.name, hap.value)
: uniformDest.position === null
? // The index is set to `seq`
getNextPosition(uniformDest.name, currentTime)
: uniformDest.position;
const value = (hap.value.gain || 1) * (uniformDest.gain || 1);
// Update the uniform
setUniform(instance, uniformDest.name, value, true, position, uniformDest.slow || 10);
}, false);
});
-22
View File
@@ -1,22 +0,0 @@
/*
uniform.test.mjs - <short description TODO>
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/pattern.test.mjs>
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 <https://www.gnu.org/licenses/>.
*/
import { describe, it, expect } from 'vitest';
import { parseUniformDest } from './uniform.mjs';
describe('Uniform', () => {
it('Parse simple', () => {
expect(parseUniformDest('iColor')).toStrictEqual({ name: 'iColor' });
});
it('Parse param', () => {
expect(parseUniformDest(['iColor', 'index', 2])).toStrictEqual({ name: 'iColor', position: 2 });
expect(parseUniformDest(['iColor', 'index', 'seq'])).toStrictEqual({ name: 'iColor', position: null });
expect(parseUniformDest(['iColor', 'gain', 3])).toStrictEqual({ name: 'iColor', gain: 3 });
});
it('Parse multi param', () => {
expect(parseUniformDest(['iColor', 'index', 2, 'gain', 3])).toStrictEqual({ name: 'iColor', position: 2, gain: 3 });
});
});
-3
View File
@@ -5,9 +5,6 @@ import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
test: {
setupFiles: './testSetup.mjs',
},
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
+15 -16
View File
@@ -8345,32 +8345,31 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = `
exports[`runs examples > example "uniform" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bd ]",
"[ 1/1 → 2/1 | s:bd ]",
"[ 2/1 → 3/1 | s:bd ]",
"[ 3/1 → 4/1 | s:bd ]",
"[ ~show() {
return this.begin.show() + ' → ' + this.end.show();
} | pan:0.4999999999999998 ]",
]
`;
exports[`runs examples > example "uniform" example index 1 1`] = `
[
"[ 0/1 → 1/1 | s:bd ]",
"[ 1/1 → 2/1 | s:bd ]",
"[ 2/1 → 3/1 | s:bd ]",
"[ 3/1 → 4/1 | s:bd ]",
"[ 0/1 → 1/1 | gain:0.5 ]",
"[ 1/1 → 2/1 | gain:0.3 ]",
"[ 2/1 → 3/1 | gain:0.5 ]",
"[ 3/1 → 4/1 | gain:0.3 ]",
]
`;
exports[`runs examples > example "uniform" example index 2 1`] = `
[
"[ 0/1 → 1/2 | note:c3 ]",
"[ 1/2 → 1/1 | note:e3 ]",
"[ 1/1 → 3/2 | note:c3 ]",
"[ 3/2 → 2/1 | note:e3 ]",
"[ 2/1 → 5/2 | note:c3 ]",
"[ 5/2 → 3/1 | note:e3 ]",
"[ 3/1 → 7/2 | note:c3 ]",
"[ 7/2 → 4/1 | note:e3 ]",
"[ 0/1 → 1/2 | s:bd ]",
"[ 1/2 → 1/1 | s:sd ]",
"[ 1/1 → 3/2 | s:bd ]",
"[ 3/2 → 2/1 | s:sd ]",
"[ 2/1 → 5/2 | s:bd ]",
"[ 5/2 → 3/1 | s:sd ]",
"[ 3/1 → 7/2 | s:bd ]",
"[ 7/2 → 4/1 | s:sd ]",
]
`;