commit a1608ff5dd04a4e657e7a0ddb3b0df371b7e7d6e
parent 43d0c885326b39c79356904c7cfd99cb247d1889
Author: massi <mdsiboldi@gmail.com>
Date: Wed, 5 Jul 2023 13:06:05 -0700
better sliders and a webworker
Diffstat:
6 files changed, 362 insertions(+), 179 deletions(-)
diff --git a/src/lib/Slider.svelte b/src/lib/Slider.svelte
@@ -0,0 +1,90 @@
+<script lang="ts">
+ import type { Unit, UnitId, UnitMap } from '$lib/types';
+ import { INPUT_RANGE, getUnit as _getUnit } from '$lib/types';
+ export let id: UnitId;
+ export let units: UnitMap;
+ export let handleInput: (id: string, coarseVal: number | null, fineVal: number | null) => void;
+ export let handleChange: () => void;
+
+ $: props = { units, handleInput, handleChange };
+ $: unit = getUnit(id);
+
+ const _handleInput = (...args) => {
+ handleInput(...args);
+ unit = unit;
+ };
+
+ $: getUnit = _getUnit.bind(null, units);
+ $: coarseUnit = Math.floor(unit.value / 100_000) * 100_000;
+ $: fineUnit = unit.value % 100_000;
+</script>
+
+<div class="unit-container">
+ {#if unit.kind === 'const'}
+ <h3>{unit.kind}</h3>
+ <div class="val">
+ {unit.value}
+ </div>
+ <input
+ name={id}
+ type="range"
+ min="0"
+ max={INPUT_RANGE}
+ 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}
+ step={1}
+ value={fineUnit}
+ on:input={(e) => _handleInput(id, null, Number(e.target.value))}
+ on:change={handleChange}
+ />
+ {:else if unit.kind === 'combinator'}
+ <h3>{unit.kind}</h3>
+ <div class="combinator-container">
+ {#each unit.sources as sourceId}
+ <svelte:self id={sourceId} {...props} />
+ {/each}
+ </div>
+ {:else if unit.kind === 'rescale'}
+ <svelte:self id={unit.input} {...props} />
+ {:else if unit.kind === 'noise'}
+ <h3>{unit.kind}</h3>
+ <svelte:self id={unit.amount} {...props} />
+ {:else if unit.kind === 'osc'}
+ <h3>{unit.kind}</h3>
+ <h4>rate</h4>
+ <svelte:self id={unit.rate} {...props} />
+ <h4>amount</h4>
+ <svelte:self id={unit.amount} {...props} />
+ {:else}
+ <h3>{unit.kind} nyi</h3>
+ {/if}
+</div>
+
+<style>
+ .combinator-container {
+ display: flex;
+ flex-direction: row;
+ }
+ .unit-container {
+ background: rgba(255, 255, 255, 0.3);
+ padding: 10px;
+ display: flexbox;
+ }
+ h1,
+ h2,
+ h3,
+ h4 {
+ padding: 0;
+ margin: 0;
+ }
+ .val {
+ height: 16px;
+ }
+</style>
diff --git a/src/lib/UnitMap.ts b/src/lib/UnitMap.ts
diff --git a/src/lib/engine.worker.ts b/src/lib/engine.worker.ts
@@ -0,0 +1,137 @@
+import type {
+ EngineMessage,
+ SynthConfig,
+ Unit,
+ UnitId,
+ UnitState,
+ UnitStateMap,
+} from "$lib/types";
+import { getUnit, INPUT_RANGE, LOOP_CYCLES } from "$lib/types";
+
+let config: SynthConfig | undefined = undefined;
+
+let unitState: UnitStateMap = new Map();
+
+let canvas: OffscreenCanvas | undefined;
+
+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;
+ }
+ }
+};
+
+function getUnitState(id: UnitId): UnitState {
+ const unit = getUnit(config.units, id);
+ const theState = unitState.get(id);
+ switch (unit.kind) {
+ case "osc": {
+ if (theState === undefined) {
+ return 0;
+ }
+ if (typeof theState !== "number" || Number.isNaN(theState)) {
+ throw new Error(`invalid state for osc unit ${id}: ${theState}`);
+ }
+ break;
+ }
+ default: {
+ throw new Error("state for this invalid or NYI");
+ }
+ }
+ return theState;
+}
+
+function setUnitState(id: UnitId, state: UnitState) {
+ // TODO: type safety
+ unitState.set(id, state);
+}
+
+function v(id: UnitId): number {
+ const unit: Unit = getUnit(config.units, id);
+ switch (unit.kind) {
+ case "osc": {
+ const position = getUnitState(id);
+ return (Math.sin(2 * Math.PI * (position / LOOP_CYCLES)) - 0.5) *
+ v(unit.amount);
+ }
+ case "const": {
+ 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);
+ }
+ default: {
+ throw new Error(`${unit.kind} unsupported`);
+ }
+ }
+}
+
+function update() {
+ // update all unit states
+ const ids = config.units.keys();
+ for (let id of ids) {
+ const unit = getUnit(config.units, id);
+ switch (unit.kind) {
+ case "osc": {
+ const position = getUnitState(id);
+ const rate = v(unit.rate);
+ setUnitState(id, (position + rate) % LOOP_CYCLES);
+ break;
+ }
+ }
+ }
+}
+
+function drawSquares() {
+ if (config && canvas) {
+ const ctx = canvas.getContext("2d");
+ const cols = 300;
+ const rows = 100;
+ let color = [0, 0, 0];
+ let width = canvas.width;
+ let height = canvas.height;
+ let paneWidth = Math.ceil(width / cols);
+ let paneHeight = Math.ceil(height / rows);
+
+ 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;
+ ctx.fillRect(x, y, paneWidth, paneHeight);
+ update();
+ }
+ }
+ }
+ requestAnimationFrame(drawSquares);
+}
+drawSquares();
+
+export {};
diff --git a/src/lib/types.ts b/src/lib/types.ts
@@ -0,0 +1,74 @@
+export type SynthConfig = {
+ sinks: Sinks;
+ units: UnitMap;
+};
+export type EngineMessage = {
+ kind: "config";
+ content: SynthConfig;
+} | {
+ kind: "window";
+ content: {
+ height: number;
+ width: number;
+ };
+} | {
+ kind: "canvas";
+ content: OffscreenCanvas;
+};
+
+export type Unit =
+ | OscUnit
+ | ConstUnit
+ | CombinatorUnit
+ | RescaleUnit
+ | NoiseUnit;
+
+export type UnitId = string;
+
+export type RescaleUnit = {
+ kind: "rescale";
+ input: UnitId;
+ min: UnitId;
+ max: UnitId;
+};
+
+// 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: UnitId;
+ amount: UnitId;
+};
+
+export type NoiseUnit = {
+ kind: "noise";
+ amount: UnitId;
+};
+
+// outputs number as it is
+export type ConstUnit = {
+ kind: "const";
+ value: number;
+};
+
+export type CombinatorUnit = {
+ kind: "combinator";
+ sources: UnitId[];
+};
+
+export type UnitMap = Map<UnitId, Unit>;
+export type UnitStateMap = Map<UnitId, UnitState>;
+export type UnitState = any;
+export type Sinks = {
+ red?: UnitId;
+ green?: UnitId;
+ blue?: UnitId;
+};
+
+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 const LOOP_CYCLES = 100_000_000;
+export const INPUT_RANGE = 100_000_000;
diff --git a/src/routes/+page.js b/src/routes/+layout.server.js
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
@@ -11,33 +11,57 @@
NoiseUnit,
ConstUnit,
CombinatorUnit,
+ PageMessage,
UnitMap,
UnitStateMap,
UnitState,
Sinks
} from '$lib/types';
- let cvs: HTMLCanvasElement;
+
+ let cvs: HTMLCanvasElement | undefined;
+ let offscreenCanvas: OffscreenCanvas | undefined;
+
+ // initialize worker
+ let engineWorker: Worker | undefined = undefined;
+ const loadWorker = async () => {
+ const EngineWorker = await import('$lib/engine.worker?worker');
+ engineWorker = new EngineWorker.default();
+ engineWorker.postMessage({ kind: 'config', content: { units, sinks } });
+ };
+
+ 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]);
+ }
+ }
onMount(() => {
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === cvs);
- if (entry) {
- cvs.width = entry.devicePixelContentBoxSize[0].inlineSize;
- cvs.height = entry.devicePixelContentBoxSize[0].blockSize;
+ if (entry && cvs) {
+ const width = entry.devicePixelContentBoxSize[0].inlineSize;
+ const height = entry.devicePixelContentBoxSize[0].blockSize;
+ engineWorker && engineWorker.postMessage({ kind: 'window', content: { width, height } });
}
});
- resizeObserver.observe(cvs);
if (cvs) resizeObserver.observe(cvs, { box: 'device-pixel-content-box' });
// This callback cleans up the observer
- return () => resizeObserver.unobserve(cvs);
+ return () => {
+ if (cvs) resizeObserver.unobserve(cvs);
+ };
});
$: getUnit = _getUnit.bind(null, units);
let id = 0;
let units: UnitMap = new Map();
- let unitState: UnitStateMap = new Map();
let sinks: Sinks = {};
onMount(() => {
@@ -77,115 +101,13 @@
console.log(fromUrl());
}
- function initUnits() {
- const doc = decodeURIComponent(
- new URL(document.location.toString()).searchParams.get('z') || ''
- );
- if (doc) {
- } else {
- return;
- }
- }
-
function addUnit(unit: Unit) {
const _id = String(id++);
units.set(_id, unit);
- switch (unit.kind) {
- case 'osc': {
- unitState.set(_id, 0);
- }
- }
units = units;
return _id;
}
- function getUnitState(id: UnitId): UnitState {
- let unit = getUnit(id);
- let goal = unitState.get(id);
- switch (unit.kind) {
- case 'osc': {
- if (typeof goal !== 'number') {
- throw new Error('invalid state for osc unit: ' + id);
- }
- break;
- }
- default: {
- throw new Error('state for this invalid or NYI');
- }
- }
- return goal;
- }
-
- function setUnitState(id: UnitId, state: UnitState) {
- // TODO: type safety
- unitState.set(id, state);
- }
-
- function v(id: UnitId): number {
- const unit: Unit = getUnit(id);
- switch (unit.kind) {
- case 'osc': {
- const position = getUnitState(id);
- return (Math.sin(2 * Math.PI * (position / LOOP_CYCLES)) - 0.5) * v(unit.amount);
- }
- case 'const': {
- 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);
- }
- }
- }
-
- function saveState() {
- const saved = {};
- const ids = units.keys();
- for (let id of ids) {
- const unit = getUnit(id);
- switch (unit.kind) {
- case 'osc':
- const position = getUnitState(id);
- saved[id] = position;
- }
- }
- return saved;
- }
- function restoreState(saved) {
- const ids = units.keys();
- for (let id of ids) {
- const unit = getUnit(id);
- switch (unit.kind) {
- case 'osc':
- setUnitState(id, saved[id]);
- }
- }
- }
-
- function update() {
- // update all unit states
- const ids = units.keys();
- for (let id of ids) {
- const unit = getUnit(id);
- switch (unit.kind) {
- case 'osc':
- const position = getUnitState(id);
- const rate = v(unit.rate);
- // console.log({ id, units, position, rate });
- setUnitState(id, (position + rate) % LOOP_CYCLES);
- }
- }
- }
-
type EzUnit = UnitId | number | undefined;
// make a const unit or use the supplied one
function ez(input: EzUnit): UnitId {
@@ -266,71 +188,37 @@
}
};
+ const CELLS = 5000;
+
const red = mk.re_color(
mk.add(
mk.c(INPUT_RANGE / 2), //
- mk.n(INPUT_RANGE / 3),
- mk.osc(mk.add(10, mk.osc()), INPUT_RANGE / 2) //
- )
- );
- const green = mk.re_color(
- mk.add(
- mk.c(100), //
- mk.osc(mk.add(mk.osc(0, 1), mk.osc(0, 1)), 10) //
- )
- );
- const blue = mk.re_color(
- mk.add(
- mk.c(100), //
- mk.osc(mk.add(mk.osc(0, 1), mk.osc(0, 1)), 10) //
+ mk.n(INPUT_RANGE / 2),
+ mk.osc(
+ mk.add(mk.c(0), mk.osc(CELLS * 4 * 10), mk.osc(CELLS * 2 * 10, INPUT_RANGE / 2)),
+ INPUT_RANGE / 2
+ ) //
)
);
+ const green = mk.re_color();
+ const blue = mk.re_color();
sinks = { red, green, blue };
- function drawSquares(ctx: CanvasRenderingContext2D) {
- if (!cvs) {
- return;
+ function updateUnit(k: UnitId, vCoarse: number | null, vFine: number | null) {
+ const unit = get.constUnit(k);
+ const oldVal = unit.value;
+ if (typeof vCoarse === typeof vFine) {
+ throw new Error('nyi: coarse and fine simul adjust');
}
- const cols = 50;
- const rows = 50;
- let color = [50, 0, 100];
- let width = cvs.width;
- let height = cvs.height;
- let paneWidth = Math.ceil(width / cols);
- let paneHeight = Math.ceil(height / rows);
+ console.log({ oldVal, vCoarse, vFine });
- update();
- let prevState = saveState();
- for (let row = 0; row < rows; row++) {
- for (let col = 0; col < cols; col++) {
- color[0] = v(red);
- color[1] = v(green);
- color[2] = v(blue);
- update();
- ctx.fillStyle = `rgb(${color.join(', ')})`;
- let x = col * paneWidth;
- let y = row * paneHeight;
- ctx.fillRect(x, y, paneWidth, paneHeight);
- }
+ if (vCoarse !== null) {
+ unit.value = (oldVal % 100_000) + vCoarse;
+ } else if (vFine !== null) {
+ unit.value = Math.floor(oldVal / 100_000) * 100_000 + vFine;
}
- //restoreState(prevState);
- }
- function run() {
- requestAnimationFrame((t) => {
- drawSquares(cvs.getContext('2d'));
- run();
- });
- }
-
- function updateUnit(k: UnitId, v: number) {
- const unit = get.constUnit(k);
- unit.value = Number(v);
- }
-
- console.log(new Set(Object.values(sinks)));
-
- $: if (cvs) {
- run();
+ console.log({ value: unit.value });
+ units = units;
}
</script>
@@ -338,24 +226,18 @@
<div id="sliders">
{#each [...Object.entries(sinks)] as [label, unitId]}
- <div>
- <h2>{label}</h2>
- <Slider
- id={unitId}
- units={units}
- handleInput={updateUnit}
- handleChange={updateUrl}
- />
- </div>
+ <div>
+ <h2>{label}</h2>
+ <Slider id={unitId} {units} handleInput={updateUnit} handleChange={updateUrl} />
+ </div>
{/each}
</div>
-<button on:click={run}>step </button>
<style>
- h2 {
- margin: 0 5px;
- background: white;
- }
+ h2 {
+ margin: 0 5px;
+ background: white;
+ }
canvas {
width: 100vw;
height: 100vh;
@@ -365,6 +247,6 @@
position: absolute;
left: 10px;
top: 10px;
- display: flex;
+ display: flex;
}
</style>