commit bc65021938b86b284cf07a62f69cdeccc401f005
parent ad431becdd17df5a1f5afd0405e431be35131591
Author: massi <mdsiboldi@gmail.com>
Date: Tue, 18 Jul 2023 09:28:06 -0700
better sliders, ranges, math, etc
Diffstat:
7 files changed, 403 insertions(+), 132 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@sveltejs/adapter-static": "^2.0.2",
+ "@types/offscreencanvas": "^2019.7.0",
"lodash": "^4.17.21"
},
"devDependencies": {
@@ -615,6 +616,11 @@
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.0",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz",
+ "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg=="
+ },
"node_modules/@types/pug": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz",
@@ -3299,6 +3305,11 @@
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
+ "@types/offscreencanvas": {
+ "version": "2019.7.0",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz",
+ "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg=="
+ },
"@types/pug": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz",
diff --git a/package.json b/package.json
@@ -30,6 +30,7 @@
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "^2.0.2",
+ "@types/offscreencanvas": "^2019.7.0",
"lodash": "^4.17.21"
}
}
diff --git a/src/lib/NumberSelector.svelte b/src/lib/NumberSelector.svelte
@@ -0,0 +1,133 @@
+<script lang="ts">
+ import { range, clamp } from '$lib/types';
+ export let value: number;
+ export let updateValue: (n: number) => void;
+
+ const MIN = range.slider.min;
+ const MAX = range.slider.max;
+
+ let pos:
+ | {
+ start: { x: number; y: number };
+ delta: { x: number; y: number };
+ mouse: { x: number; y: number };
+ }
+ | undefined;
+ let el: HTMLDivElement | undefined;
+
+ const handleMove = (evt: MouseEvent) => {
+ if (!pos) return;
+ pos.delta.x += evt.movementX;
+ //pos.delta.y += evt.movementY;
+ let mouseX = (pos.start.x + pos.delta.x / 2) % window.innerWidth;
+
+ if (mouseX < 0) {
+ mouseX += window.innerWidth;
+ }
+ pos.mouse.x = mouseX;
+
+ //let mouseY = (pos.start.y + pos.delta.y) % window.innerHeight;
+ //if (mouseY < 0) {
+ // mouseY += window.innerHeight;
+ //}
+ //pos.mouse.y = mouseY;
+ };
+
+ type Ticker = { start: () => void; stop: () => void };
+ const Ticker = (): Ticker => {
+ let c = 0;
+ let tDelta: number | undefined;
+ let cont = true;
+ const run = (ts: number) => {
+ if (!cont || !pos || !pos.delta) {
+ return;
+ }
+ if (tDelta === undefined) {
+ tDelta = ts;
+ } else {
+ tDelta = ts - tDelta;
+ const rate = Math.pow(pos.delta.x / 200, 3);
+ c = c + rate * tDelta;
+ if (Math.abs(c) > 10000) {
+ let n = Math.floor(c / 10000);
+ c -= n * 10000;
+ const truncValue = clamp(value + n, { min: -500_000, max: 500_000 });
+ updateValue(truncValue);
+ }
+ }
+ requestAnimationFrame(run);
+ };
+ const stop = () => {
+ cont = false;
+ };
+ const start = () => {
+ cont = true;
+ requestAnimationFrame(run);
+ };
+ return { start, stop };
+ };
+
+ let ticker: Ticker | undefined;
+
+ $: distance = pos ? Math.sqrt(pos.delta.x * pos.delta.x + pos.delta.y * pos.delta.y) : 0;
+
+ const handleMouseDown = async (evt: MouseEvent) => {
+ await el?.requestPointerLock();
+ pos = {
+ start: {
+ x: evt.pageX,
+ y: evt.pageY
+ },
+ delta: {
+ x: 0,
+ y: 0
+ },
+ mouse: {
+ x: evt.pageX,
+ y: evt.pageY
+ }
+ };
+ ticker = Ticker();
+ ticker.start();
+ document.addEventListener('mousemove', handleMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ };
+
+ const handleMouseUp = () => {
+ ticker?.stop();
+ ticker = undefined;
+ pos = undefined;
+ document.exitPointerLock();
+ document.removeEventListener('mousemove', handleMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+
+ const handleDbl = () => {
+ console.log('dbl');
+ updateValue(0);
+ };
+
+ $: if (el) {
+ el.removeEventListener('dblclick', handleDbl);
+ el.addEventListener('dblclick', handleDbl);
+ el.removeEventListener('mousedown', handleMouseDown);
+ //el.addEventListener('mousedown', handleMouseDown);
+ }
+</script>
+
+<div bind:this={el} class="sel">{value} {pos?.delta.x}</div>
+{#if pos}
+ <div class="tmp-pointer" style={`top:${pos.mouse.y}px; left:${pos.mouse.x}px`} />
+{/if}
+
+<style>
+ .sel {
+ font-family: monospace;
+ }
+ .tmp-pointer {
+ position: fixed;
+ height: 10px;
+ width: 10px;
+ background: red;
+ }
+</style>
diff --git a/src/lib/Slider.svelte b/src/lib/Slider.svelte
@@ -1,20 +1,21 @@
<script lang="ts">
import type { Unit, UnitId, UnitMap } from '$lib/types';
- import { INPUT_RANGE, getUnit as _getUnit, is } from '$lib/types';
+ import { rescale, range, INPUT_RANGE, getUnit as _getUnit, is } from '$lib/types';
+ import NumberSelector from '$lib/NumberSelector.svelte';
+
export let id: UnitId;
export let units: UnitMap;
- export let handleInput: (id: string, coarseVal: number | null, fineVal: number | null) => void;
+ export let handleInput: (id: string, value: number) => void;
export let handleChange: () => void;
$: props = { units, handleInput, handleChange };
$: getUnit = _getUnit.bind(null, units);
$: unit = getUnit(id);
- $: coarseUnit = !is.unit.const(unit) ? 0 : Math.floor(unit.value / 100_000) * 100_000;
- $: fineUnit = !is.unit.const(unit) ? 0 : unit.value % 100_000;
+ $: sliderValue = Math.round(rescale(unit.value, range.signal, range.slider));
- const _handleInput = (...args) => {
- handleInput(...args);
+ const _handleInput = (val: number) => {
+ handleInput(id, Math.round(rescale(val, range.slider, range.signal)));
unit = unit;
};
</script>
@@ -22,26 +23,15 @@
<div class="unit-container">
{#if unit.kind === 'const'}
<h3>{unit.kind}</h3>
- <div class="val">
- {unit.value}
- </div>
+ <NumberSelector value={sliderValue} updateValue={_handleInput} />
<input
name={id}
type="range"
- min={(-1 * INPUT_RANGE) / 2}
- max={INPUT_RANGE / 2}
- step={100_000}
- value={coarseUnit}
- on:input={(e) => _handleInput(id, Number(e.target?.value), null)}
- on:change={handleChange}
- />
- <input
- type="range"
- min={0}
- max={100_000 - 1}
+ min={range.slider.min}
+ max={range.slider.max}
step={1}
- value={fineUnit}
- on:input={(e) => _handleInput(id, null, Number(e.target?.value))}
+ value={sliderValue}
+ on:input={(e) => _handleInput(Number(e.target?.value))}
on:change={handleChange}
/>
{:else if unit.kind === 'combinator'}
@@ -59,7 +49,12 @@
{:else if unit.kind === 'osc'}
<h3>{unit.kind}</h3>
<h4>rate</h4>
- <svelte:self id={unit.rate.id} {...props} />
+ coarse
+ <svelte:self id={unit.coarse.id} {...props} />
+ fine
+ <svelte:self id={unit.fine.id} {...props} />
+ superfine
+ <svelte:self id={unit.superfine.id} {...props} />
<h4>amount</h4>
<svelte:self id={unit.amount.id} {...props} />
{:else}
diff --git a/src/lib/engine.worker.ts b/src/lib/engine.worker.ts
@@ -7,31 +7,33 @@ import type {
UnitState,
UnitStateMap,
} from "$lib/types";
-import { getUnit, INPUT_RANGE, LOOP_CYCLES } from "$lib/types";
+import {
+ CELLS,
+ clamp,
+ getUnit,
+ INPUT_RANGE,
+ LOOP_CYCLES,
+ range,
+ rescale,
+ wrangle,
+} from "$lib/types";
let config: SynthConfig | undefined = undefined;
let unitState: UnitStateMap = new Map();
-let canvas: OffscreenCanvas | undefined;
+let canvas: OffscreenCanvas = new OffscreenCanvas(1, 1);
onmessage = (message: { data: EngineMessage }) => {
const { data, data: { kind, content } } = message;
switch (kind) {
- case "canvas": {
- canvas = content;
- break;
- }
case "config": {
config = content;
break;
}
case "window": {
- if (canvas) {
- canvas.height = content.height;
- canvas.width = content.width;
- }
- break;
+ console.log(content);
+ canvas = new OffscreenCanvas(content.width, content.height);
}
}
};
@@ -68,21 +70,23 @@ function v(input: Input): number {
case "osc": {
const position = getUnitState(id);
return Math.sin(2 * Math.PI * (position / LOOP_CYCLES)) *
- v(unit.amount) - 0.5;
+ v(unit.amount);
+ //if (id % 7 === 0) {
+ // return (position < LOOP_CYCLES / 2 ? -1 : 1) * v(unit.amount);
+ //} else if (id % 5 === 0) {
+ // return (position / LOOP_CYCLES) * v(unit.amount);
+ //} else {
+ // return Math.sin(2 * Math.PI * (position / LOOP_CYCLES)) *
+ // v(unit.amount);
+ //}
}
case "const": {
+ // range is whatever the control for it is???
return unit.value;
}
case "combinator": {
return unit.sources.reduce((a, b) => a + v(b), 0);
}
- case "rescale": {
- const min = v(unit.min);
- const max = v(unit.max);
- const input = v(unit.input);
-
- return (max - min) * (input / INPUT_RANGE) + min;
- }
case "noise": {
return Math.random() * v(unit.amount);
}
@@ -100,8 +104,19 @@ function update() {
switch (unit.kind) {
case "osc": {
const position = getUnitState(id);
- const rate = v(unit.rate);
- setUnitState(id, (position + rate) % LOOP_CYCLES);
+ const coarse = rescale(v(unit.coarse), range.signal, range.osc.coarse);
+ const fine = rescale(v(unit.fine), range.signal, range.osc.fine);
+ const superfine = rescale(
+ v(unit.superfine),
+ range.signal,
+ range.osc.superfine,
+ );
+ setUnitState(
+ id,
+ (position + coarse * LOOP_CYCLES / 100 + fine * 20000 +
+ superfine * 10) %
+ LOOP_CYCLES,
+ );
break;
}
}
@@ -109,7 +124,7 @@ function update() {
}
function drawSquares() {
- if (config && canvas) {
+ if (config) {
const ctx = canvas.getContext("2d");
const cols = 100;
const rows = 50;
@@ -121,16 +136,27 @@ function drawSquares() {
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
- color[0] = v(config.sinks.red);
- color[1] = v(config.sinks.green);
- color[2] = v(config.sinks.blue);
- ctx.fillStyle = `rgb(${color.join(", ")})`;
- let x = col * paneWidth;
- let y = row * paneHeight;
+ color[0] = wrangle(v(config.sinks.red), range.signal, {
+ min: 0,
+ max: 100,
+ });
+ color[1] = wrangle(v(config.sinks.green), range.signal, {
+ min: 0,
+ max: 0.5,
+ });
+ color[2] = wrangle(v(config.sinks.blue), range.signal, {
+ min: 0,
+ max: 360,
+ });
+ ctx.fillStyle = `oklch(${color[0]}% ${color[1]} ${color[2]}deg)`;
+ const x = col * paneWidth;
+ const y = row * paneHeight;
ctx.fillRect(x, y, paneWidth, paneHeight);
update();
}
}
+ const bmp = canvas.transferToImageBitmap();
+ postMessage({ kind: "bmp", content: bmp }, [bmp]);
}
requestAnimationFrame(drawSquares);
}
diff --git a/src/lib/types.ts b/src/lib/types.ts
@@ -1,5 +1,9 @@
+import type { OffscreenCavas } from "@types/offscreencanvas";
export const LOOP_CYCLES = 100_000_000;
-export const INPUT_RANGE = 100_000_000;
+export const INPUT_RANGE = 100;
+export const ROWS = 100;
+export const COLS = 50;
+export const CELLS = ROWS * COLS;
export type SynthConfig = {
sinks: Sinks;
@@ -19,13 +23,16 @@ export type EngineMessage = {
content: OffscreenCanvas;
};
-export type Unit =
+export type HigherUnit =
| OscUnit
- | ConstUnit
| CombinatorUnit
| RescaleUnit
| NoiseUnit;
+export type Unit =
+ | ConstUnit
+ | HigherUnit;
+
export type UnitId = string;
// To support units with multiple outputs.
@@ -41,7 +48,9 @@ export type RescaleUnit = {
// uses rate and amount to output a sine wave going that fast and loud. TBD: what units the values are, etc.
export type OscUnit = {
kind: "osc";
- rate: Input;
+ coarse: Input;
+ fine: Input;
+ superfine: Input;
amount: Input;
};
@@ -70,12 +79,120 @@ export type Sinks = {
blue?: Input;
};
+type Range = {
+ min: number;
+ max: number;
+};
+
+export const range = {
+ osc: {
+ coarse: {
+ min: -50,
+ max: 50,
+ },
+ fine: {
+ min: -50,
+ max: 50,
+ },
+ superfine: {
+ min: -50,
+ max: 50,
+ },
+ amount: {
+ min: -50,
+ max: 50,
+ },
+ output: {
+ min: -1,
+ max: 1,
+ },
+ },
+ noise: {
+ amount: {
+ min: -50,
+ max: 50,
+ },
+ },
+ color: {
+ min: 0,
+ max: 255,
+ },
+ pmone: {
+ min: -1,
+ max: 1,
+ },
+ slider: {
+ min: -50,
+ max: 50,
+ },
+ signal: {
+ min: -5_000_000,
+ max: 5_000_000,
+ },
+};
+
+//export function rangeForInput<U extends Unit>(
+// units: UnitMap,
+// id: UnitId,
+// k: keyof Omit<U, "kind">,
+//): Range {
+// let unit = getUnit(units, id);
+// switch (unit.kind) {
+// case "const": {
+// return range.slider;
+// }
+// case "osc": {
+// switch (k) {
+// case "amount": {
+// return range.osc.amount;
+// }
+// case "coarse": {
+// return range.osc.coarse;
+// }
+// case "fine": {
+// return range.osc.fine;
+// }
+// default: {
+// throw new Error(`cannot find range for ${String(k)} in ${unit.kind}`)
+// }
+// }
+// }
+// case "combinator": {
+// return unit.sources.reduce((accum, (src) => {
+// console.log('hi');
+// return
+// }))
+// }
+// default:
+// throw new Error("NYI: range for " + unit.kind);
+// }
+//}
+
+export function clamp(n: number, range: Range) {
+ return Math.max(range.min, Math.min(range.max, n));
+}
+export function rescale(n: number, origin: Range, dest: Range) {
+ return ((n - origin.min) / (origin.max - origin.min)) *
+ (dest.max - dest.min) + dest.min;
+}
+export function wrangle(n: number, origin: Range, dest: Range) {
+ return rescale(clamp(n, origin), origin, dest);
+}
+
export function getUnit(units: UnitMap, id: UnitId): Unit {
const result = units.get(id);
if (!result) throw new Error("invalid id for unit: " + id);
return result;
}
+export function getHigherUnit(units: UnitMap, id: UnitId): HigherUnit {
+ const result = getUnit(units, id);
+ if (result.kind === "const") {
+ throw new Error("lower unit found");
+ }
+ return result;
+}
+
export const is = {
input: (i: any): i is Input => {
return i === Object(i) && i.id;
@@ -84,5 +201,20 @@ export const is = {
const: (u: Unit): u is ConstUnit => {
return u.kind === "const";
},
+ osc: (u: Unit): u is OscUnit => {
+ return u.kind === "osc";
+ },
+ },
+};
+
+export const ensure = {
+ unit: {
+ osc: (u: Unit): OscUnit => {
+ if (is.unit.osc(u)) {
+ return u;
+ } else {
+ throw new Error("this is not an osc");
+ }
+ },
},
};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { debounce } from 'lodash';
import Slider from '$lib/Slider.svelte';
- import { is, getUnit as _getUnit, INPUT_RANGE } from '$lib/types';
+ import { rescale, range, is, getUnit as _getUnit, INPUT_RANGE } from '$lib/types';
import type { Input, Unit, UnitId, ConstUnit, UnitMap, Sinks } from '$lib/types';
let cvs: HTMLCanvasElement | undefined;
@@ -14,17 +14,26 @@
const EngineWorker = await import('$lib/engine.worker?worker');
engineWorker = new EngineWorker.default();
engineWorker.postMessage({ kind: 'config', content: { units, sinks } });
+ if (cvs && engineWorker && !offscreenCanvas) {
+ const { innerHeight: height, innerWidth: width } = window;
+ console.log('sending canvas', { height, width });
+ setTimeout(() => {
+ engineWorker?.postMessage({ kind: 'window', content: { width, height } });
+ }, 0);
+ }
};
onMount(loadWorker);
$: engineWorker && engineWorker.postMessage({ kind: 'config', content: { units, sinks } });
+
$: {
- if (cvs && engineWorker && !offscreenCanvas) {
- const { height, width } = cvs;
- console.log('sending canvas');
- offscreenCanvas = cvs.transferControlToOffscreen();
- engineWorker.postMessage({ kind: 'window', content: { width, height } });
- engineWorker.postMessage({ kind: 'canvas', content: offscreenCanvas }, [offscreenCanvas]);
+ if (engineWorker) {
+ engineWorker.onmessage = (msg) => {
+ if (cvs) {
+ const ctx = cvs.getContext('bitmaprenderer');
+ ctx?.transferFromImageBitmap(msg.data.content);
+ }
+ };
}
}
@@ -34,6 +43,8 @@
if (entry && cvs) {
const width = entry.devicePixelContentBoxSize[0].inlineSize;
const height = entry.devicePixelContentBoxSize[0].blockSize;
+ cvs.width = width;
+ cvs.height = height;
engineWorker && engineWorker.postMessage({ kind: 'window', content: { width, height } });
}
});
@@ -83,10 +94,6 @@
history.pushState({}, '', url);
}
const updateUrl = debounce(_updateUrl, 250);
- $: {
- console.log(location);
- console.log(fromUrl());
- }
function addUnit(unit: Unit): Input {
const id = String(uid++);
@@ -98,50 +105,32 @@
type EzUnit = Input | number | undefined;
// make a const unit or use the supplied one
function ez(input: EzUnit): Input {
- return is.input(input) ? input : mk.c(input || 0);
+ return is.input(input) ? input : mk.c(input || randConst());
}
- function gatherControls(accum: Set<Input>, ...ins: Input[]) {
- for (let input of ins) {
- const unit = getUnit(input.id);
- switch (unit.kind) {
- case 'osc': {
- gatherControls(accum, unit.rate, unit.amount);
- break;
- }
- case 'rescale': {
- gatherControls(accum, unit.input);
- break;
- }
- case 'noise': {
- gatherControls(accum, unit.amount);
- break;
- }
- case 'const': {
- accum.add(input);
- break;
- }
- case 'combinator': {
- gatherControls(accum, ...unit.sources);
- break;
- }
- }
- }
- return accum;
- }
- $: unitsToControl = gatherControls(new Set(), ...Object.values(sinks));
+ const randConst = () => {
+ const sliderVal = Math.round(rescale(Math.random(), { min: 0, max: 1 }, range.slider));
+ return rescale(sliderVal, range.slider, range.signal);
+ };
const mk = {
c: function mkConst(value?: number): Input {
return addUnit({
kind: 'const',
- value: value || 0
+ value: value == null ? randConst() : value
});
},
- osc: function mkOsc(rate?: EzUnit, amount?: EzUnit): Input {
+ osc: function mkOsc(
+ coarse?: EzUnit,
+ fine?: EzUnit,
+ superfine?: EzUnit,
+ amount?: EzUnit
+ ): Input {
return addUnit({
kind: 'osc',
- rate: mk.re(0, INPUT_RANGE / 10, ez(rate)),
+ coarse: ez(coarse),
+ fine: ez(fine),
+ superfine: superfine ? ez(superfine) : mk.c(0),
amount: ez(amount)
});
},
@@ -151,17 +140,6 @@
sources: ids.map(ez)
});
},
- re: function rescale(min?: EzUnit, max?: EzUnit, input?: EzUnit) {
- return addUnit({
- kind: 'rescale',
- min: ez(min),
- max: ez(max),
- input: ez(input)
- });
- },
- re_color: function rescaleToColorRange(input?: EzUnit) {
- return mk.re(0, 255, input);
- },
n: function noise(amt: EzUnit) {
return addUnit({ kind: 'noise', amount: ez(amt) });
}
@@ -170,36 +148,31 @@
const get = {
constUnit: (id: UnitId): ConstUnit => {
const unit = getUnit(id);
- if (unit.kind !== 'const') throw new Error('expecting a const here bro....');
+ if (!is.unit.const(unit)) throw new Error('expecting a const here bro....');
return unit;
}
};
- const red = mk.re_color(
- mk.add(
- mk.c(INPUT_RANGE / 2), //
- mk.n(0),
- mk.osc(mk.add(mk.c(0), mk.osc(), mk.osc(mk.c(), mk.osc())), INPUT_RANGE / 2) //
- )
+ const shared = new Array(10).fill(0).map(() => mk.c());
+
+ const red = mk.add(
+ mk.c(), //
+ mk.osc(mk.add(mk.c(), mk.osc()))
);
- const green = mk.re_color();
- const blue = mk.re_color();
+ const green = mk.add(
+ mk.c(), //
+ mk.osc(mk.add(mk.c(), mk.osc()))
+ );
+ const blue = mk.add(
+ mk.c(), //
+ mk.osc(mk.add(mk.c(), mk.osc()))
+ );
+
sinks = { red, green, blue };
- function updateUnit(k: UnitId, vCoarse: number | null, vFine: number | null) {
+ function updateUnit(k: UnitId, value: number) {
const unit = get.constUnit(k);
- const oldVal = unit.value;
- if (typeof vCoarse === typeof vFine) {
- throw new Error('nyi: coarse and fine simul adjust');
- }
- console.log({ oldVal, vCoarse, vFine });
-
- if (vCoarse !== null) {
- unit.value = (oldVal % 100_000) + vCoarse;
- } else if (vFine !== null) {
- unit.value = Math.floor(oldVal / 100_000) * 100_000 + Math.abs(vFine);
- }
- console.log({ value: unit.value });
+ unit.value = value;
units = units;
}
</script>