commit 9a1882ee9f108571b74e38371612467f7492a3f7
parent 80180c90b4fe7cb7f5870cc65e9c818ace445ebc
Author: massi <mdsiboldi@gmail.com>
Date: Fri, 15 Mar 2024 11:12:23 -0700
lots of things
Diffstat:
22 files changed, 560 insertions(+), 188 deletions(-)
diff --git a/package.json b/package.json
@@ -3,7 +3,7 @@
"version": "0.0.1",
"private": true,
"scripts": {
- "dev": "vite dev",
+ "dev": "vite dev && docker run -p 6379:6379 -it redis/redis-stack-server:latest",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
diff --git a/src/lib/ConstUnit.svelte b/src/lib/ConstUnit.svelte
@@ -1,18 +1,18 @@
<script lang="ts">
- import Control from '$lib/Control.svelte';
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
import type { UnitId } from '$lib/types';
export let id: UnitId;
const kind = 'const';
- const KControl = Control<typeof kind>;
+ const UnitControl = _UnitControl<typeof kind>;
$: common = { kind, id } as const;
</script>
<div>
<h1>const</h1>
- <KControl {...common} controlName="value" />
+ <UnitControl {...common} controlName="value" />
</div>
<style>
diff --git a/src/lib/Control.svelte b/src/lib/Control.svelte
@@ -1,56 +1,27 @@
-<script lang="ts" generics="TKind extends UnitKind">
- import { unitStore, unitToConnect } from '$lib/stores';
- import {
- unitRange,
- getUnit,
- inp,
- RANGE,
- rescale,
- type ControlName,
- type Controls,
- type UnitId,
- type UnitKind,
- wrangle
- } from '$lib/types';
+<script lang="ts">
+ import { RANGE, inp, rescale, wrangle, type Input, type NumberRange } from '$lib/types';
import DumbSlider from './DumbSlider.svelte';
- import InputDragger from './InputDragger.svelte';
- export let id: UnitId;
- export let kind: TKind;
- export let controlName: ControlName<TKind>;
+ export let input: Input;
+ export let controlRange: NumberRange;
+ export let setInput: (i: Input) => void;
- $: unit = getUnit(kind, $unitStore, id);
-
- const controlRange = unitRange[kind][controlName];
- $: input = (unit.controls as Controls<TKind>)[controlName];
const updateValue = (controlValue: number) => {
const n = Math.round(rescale(controlValue, controlRange, RANGE.signal));
- unitStore.setControl(kind, id, controlName, n);
+ setInput(inp.setValue(input, n));
};
- let connection: { connected: boolean; onConnect: () => void } | null;
- $: {
- if ($unitToConnect) {
- connection = {
- connected: Array.isArray(input) && inp.connected(input, $unitToConnect),
- onConnect: () => {
- if (!$unitToConnect) return;
- unitStore.setControl(kind, id, controlName, inp.toggle(input, $unitToConnect));
- }
- };
- } else {
- connection = null;
- }
- }
+
let value: number | null;
- $: value = typeof input === 'number' ? wrangle(input, RANGE.signal, controlRange) : null;
+ $: value = Object.hasOwn(input, 'value')
+ ? Math.round(wrangle(input.value!, RANGE.signal, controlRange))
+ : null;
$: slotProps = { range: controlRange, value: value || 0, update: updateValue };
</script>
-<div class="control-wrapper">
- <h3>{controlName}</h3>
- <InputDragger {connection} />
+<div>
{#if value !== null}
+ <input type="number" {value} on:input={(e) => updateValue(Number(e.currentTarget?.value))} />
{#if $$slots.default}
<slot props={{ range: controlRange, value: value || 0, update: updateValue }} />
{:else}
@@ -58,18 +29,3 @@
{/if}
{/if}
</div>
-
-<style>
- h3 {
- margin: 0;
- padding: 3px 3px 3px 24px;
- }
- .control-wrapper {
- position: relative;
- width: 100%;
- height: 80px;
- background: rgba(255, 255, 255, 0.3);
- padding: 10px;
- display: flexbox;
- }
-</style>
diff --git a/src/lib/DumbSlider.svelte b/src/lib/DumbSlider.svelte
@@ -13,4 +13,5 @@
step={1}
{value}
on:input={(e) => update(Number(e.currentTarget?.value))}
+ on:dblclick={(e) => update(0)}
/>
diff --git a/src/lib/ImgUnit.svelte b/src/lib/ImgUnit.svelte
@@ -0,0 +1,21 @@
+<script lang="ts">
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
+ import type { UnitId } from '$lib/types';
+
+ export let id: UnitId;
+
+ const kind = 'img';
+ const UnitControl = _UnitControl<typeof kind>;
+
+ $: common = { kind, id } as const;
+</script>
+
+<div>
+ <h1>image of a turtle</h1>
+</div>
+
+<style>
+ h1 {
+ color: blue;
+ }
+</style>
diff --git a/src/lib/InputDragger.svelte b/src/lib/InputDragger.svelte
@@ -1,15 +1,14 @@
<script lang="ts">
export let connection: { connected: Boolean; onConnect: () => void } | null;
+ export let controlName: string;
</script>
-<div class="input-dragger">
+<div class="input-dragger" data-control-name={controlName}>
{#if connection}
<div
class={['connect', connection.connected && 'connected'].filter(Boolean).join(' ')}
on:mouseup={connection.onConnect}
- >
- hi
- </div>
+ />
{/if}
</div>
diff --git a/src/lib/LagUnit.svelte b/src/lib/LagUnit.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
+ import type { UnitId } from '$lib/types';
+
+ export let id: UnitId;
+
+ const kind = 'lag';
+ const UnitControl = _UnitControl<typeof kind>;
+
+ $: common = { kind, id } as const;
+</script>
+
+<div>
+ <h1>lag</h1>
+ <UnitControl {...common} controlName="signal" />
+ <UnitControl {...common} controlName="amount" />
+</div>
+
+<style>
+ h1 {
+ padding: 0;
+ font-size: 24px;
+ margin: 0;
+ color: pink;
+ }
+</style>
diff --git a/src/lib/MathUnit.svelte b/src/lib/MathUnit.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
+ import type { UnitId } from '$lib/types';
+
+ export let id: UnitId;
+
+ const kind = 'math';
+ const UnitControl = _UnitControl<typeof kind>;
+
+ $: common = { kind, id } as const;
+</script>
+
+<div>
+ <h1>math</h1>
+ <UnitControl {...common} controlName="a" />
+ <UnitControl {...common} controlName="op" />
+ <UnitControl {...common} controlName="b" />
+</div>
+
+<style>
+ h1 {
+ padding: 0;
+ font-size: 24px;
+ margin: 0;
+ color: grey;
+ }
+</style>
diff --git a/src/lib/MathUnit.ts b/src/lib/MathUnit.ts
@@ -0,0 +1,22 @@
+<script lang="ts">
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
+ import type { UnitId } from '$lib/types';
+
+ export let id: UnitId;
+
+ const kind = 'const';
+ const UnitControl = _UnitControl<typeof kind>;
+
+ $: common = { kind, id } as const;
+</script>
+
+<div>
+ <h1>const</h1>
+ <UnitControl {...common} controlName="value" />
+</div>
+
+<style>
+ h1 {
+ color: blue;
+ }
+</style>
diff --git a/src/lib/NoiseUnit.svelte b/src/lib/NoiseUnit.svelte
@@ -1,18 +1,18 @@
<script lang="ts">
- import Control from '$lib/Control.svelte';
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
import type { UnitId } from '$lib/types';
export let id: UnitId;
const kind = 'noise';
- const KControl = Control<typeof kind>;
+ const UnitControl = _UnitControl<typeof kind>;
$: common = { kind, id } as const;
</script>
<div>
<h1>noiseyboi</h1>
- <KControl {...common} controlName="amount" />
+ <UnitControl {...common} controlName="amount" />
</div>
<style>
diff --git a/src/lib/OscUnit.svelte b/src/lib/OscUnit.svelte
@@ -1,21 +1,22 @@
<script lang="ts">
- import Control from '$lib/Control.svelte';
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
import type { UnitId } from '$lib/types';
export let id: UnitId;
const kind = 'osc';
- const KControl = Control<typeof kind>;
+ const UnitControl = _UnitControl<typeof kind>;
$: common = { kind, id } as const;
</script>
<div>
<h1>os-kill-8r</h1>
- <KControl {...common} controlName="coarse" />
- <KControl {...common} controlName="fine" />
- <KControl {...common} controlName="superfine" />
- <KControl {...common} controlName="amount" />
+ <UnitControl {...common} controlName="coarse" />
+ <UnitControl {...common} controlName="fine" />
+ <UnitControl {...common} controlName="superfine" />
+ <UnitControl {...common} controlName="amount" />
+ <UnitControl {...common} controlName="waveshape" />
</div>
<style>
diff --git a/src/lib/Sink.svelte b/src/lib/Sink.svelte
@@ -1,9 +1,11 @@
<script lang="ts">
- import type { Input } from '$lib/types';
+ import { RANGE, type Input } from '$lib/types';
+ import Control from '$lib/Control.svelte';
export let channel: 'l' | 'c' | 'h';
export let onSinkConnect: null | ((ch: 'l' | 'c' | 'h') => void);
+ export let setInput: (input: Input) => void;
export let connected: boolean;
- export let input: Input | null;
+ export let input: Input;
let _onSinkConnect: null | (() => void) = null;
$: _onSinkConnect = onSinkConnect ? onSinkConnect.bind(null, channel) : null;
@@ -12,12 +14,13 @@
.join(' ');
</script>
-<div class="sink">
+<div class="sink" data-sink-ch={channel}>
{#if onSinkConnect}
<div class={classes} on:mouseup={_onSinkConnect} />
{/if}
<button class={Boolean(_onSinkConnect) ? 'hl' : ''}>
- {channel}: {Array.isArray(input) ? input.map((o) => o.id).join(' + ') : 'none'}
+ {channel}: {Array.isArray(input) ? input.map((o) => o.id.substring(0, 5)).join(' + ') : 'none'}
+ <Control {input} controlRange={RANGE.signal} {setInput} />
</button>
</div>
diff --git a/src/lib/SmoothUnit.svelte b/src/lib/SmoothUnit.svelte
@@ -1,19 +1,19 @@
<script lang="ts">
- import Control from '$lib/Control.svelte';
+ import { default as _UnitControl } from '$lib/UnitControl.svelte';
import type { UnitId } from '$lib/types';
export let id: UnitId;
const kind = 'smooth';
- const KControl = Control<typeof kind>;
+ const UnitControl = _UnitControl<typeof kind>;
$: common = { kind, id } as const;
</script>
<div>
<h1>smoother</h1>
- <KControl {...common} controlName="signal"><div>cant control this one</div></KControl>
- <KControl {...common} controlName="frames" />
+ <UnitControl {...common} controlName="signal" />
+ <UnitControl {...common} controlName="frames" />
</div>
<style>
diff --git a/src/lib/UnitControl.svelte b/src/lib/UnitControl.svelte
@@ -0,0 +1,73 @@
+<script lang="ts" generics="TKind extends UnitKind">
+ import { unitStore, unitToConnect } from '$lib/stores';
+ import {
+ RANGE,
+ getUnit,
+ inp,
+ rescale,
+ unitRange,
+ type ControlName,
+ type Controls,
+ type Input,
+ type UnitId,
+ type UnitKind
+ } from '$lib/types';
+ import Control from './Control.svelte';
+ import InputDragger from './InputDragger.svelte';
+
+ export let id: UnitId;
+ export let kind: TKind;
+ export let controlName: ControlName<TKind>;
+
+ const controlRange = unitRange[kind][controlName];
+
+ $: unit = getUnit(kind, $unitStore, id);
+
+ $: input = (unit.controls as Controls<TKind>)[controlName];
+ const updateValue = (controlValue: number) => {
+ const n = Math.round(rescale(controlValue, controlRange, RANGE.signal));
+ unitStore.setControl(kind, id, controlName, inp.setValue(input, n));
+ };
+ let connection: { connected: boolean; onConnect: () => void } | null;
+ $: {
+ if ($unitToConnect) {
+ connection = {
+ connected: inp.connected(input, $unitToConnect),
+ onConnect: () => {
+ if (!$unitToConnect) return;
+ unitStore.setControl(kind, id, controlName, inp.toggle(input, $unitToConnect));
+ }
+ };
+ } else {
+ connection = null;
+ }
+ }
+
+ const setInput = (i: Input) => {
+ unitStore.setControl(kind, id, controlName, i);
+ };
+</script>
+
+<div class="control-wrapper">
+ <h3>{controlName}</h3>
+ <InputDragger {controlName} {connection} />
+ <Control {input} {controlRange} {setInput} />
+</div>
+
+<style>
+ input[type='number'] {
+ width: 80px;
+ }
+ h3 {
+ margin: 0;
+ padding: 3px 3px 3px 24px;
+ }
+ .control-wrapper {
+ position: relative;
+ width: 100%;
+ height: 80px;
+ background: rgba(255, 255, 255, 0.3);
+ padding: 10px;
+ display: flexbox;
+ }
+</style>
diff --git a/src/lib/Wires.svelte b/src/lib/Wires.svelte
@@ -0,0 +1,112 @@
+<script lang="ts">
+ import { onMount } from 'svelte';
+ import { sinksStore, unitStore } from './stores';
+ import type { Input, NumberRange, Pos } from './types';
+
+ let cvs: HTMLCanvasElement | null = null;
+
+ type VisitCb = (start: Pos, end: Pos) => void;
+ const visitInput = (ownerQuery: string, input: Input, cb: VisitCb) => {
+ const ownerEl = document.querySelector(ownerQuery);
+ if (ownerEl === null) {
+ return;
+ } else {
+ for (let src of input.sources) {
+ const id = src.id;
+ const el = document.querySelector(`div[data-unit-id='${id}'] .output-dragger`);
+
+ if (el) {
+ cb(ownerEl.getBoundingClientRect(), el.getBoundingClientRect());
+ }
+ }
+ }
+ };
+
+ const visitConnections = (cb: VisitCb) => {
+ // just use query selectors to find the elements to connect.
+
+ // controls
+ for (let [id, unit] of Object.entries($unitStore)) {
+ const unitQuery = `div[data-unit-id='${id}']`;
+ for (let [controlName, control] of Object.entries(unit.controls)) {
+ const query = `${unitQuery} div[data-control-name='${controlName}']`;
+ visitInput(query, control, cb);
+ }
+ }
+ // sinks
+ for (let ch of ['l', 'c', 'h'] as const) {
+ // console.log($sinksStore[ch]);
+ visitInput(`div[data-sink-ch='${ch}']`, $sinksStore[ch], cb);
+ }
+ };
+ const draw = () => {
+ const ctx = cvs?.getContext('2d');
+ const OFFSET = 8;
+
+ if (ctx) {
+ ctx.reset();
+ visitConnections((start, end) => {
+ if (ctx) {
+ ctx.strokeStyle = ctx.fillStyle = 'red';
+ ctx.lineWidth = 5;
+
+ ctx.beginPath();
+ ctx.ellipse(start.x + OFFSET, start.y + OFFSET, 6, 6, 0, 0, 2 * Math.PI);
+ ctx.closePath();
+ ctx.fill();
+
+ ctx.beginPath();
+ ctx.ellipse(end.x + OFFSET, end.y + OFFSET, 6, 6, 0, 0, 2 * Math.PI);
+ ctx.closePath();
+ ctx.fill();
+
+ ctx.beginPath();
+ ctx.moveTo(start.x + OFFSET, start.y + OFFSET);
+ ctx.lineTo(end.x + OFFSET, end.y + OFFSET);
+ ctx.closePath();
+ ctx.stroke();
+ }
+ });
+ }
+ };
+ const loop = () => {
+ // setInterval(draw, 1000);
+ draw();
+ requestAnimationFrame(loop);
+ };
+ loop();
+
+ onMount(() => {
+ const resizeObserver = new ResizeObserver((entries) => {
+ const entry = entries.find((entry) => entry.target === cvs);
+ if (entry && cvs) {
+ const width = entry.devicePixelContentBoxSize[0].inlineSize;
+ const height = entry.devicePixelContentBoxSize[0].blockSize;
+ cvs.width = width;
+ cvs.height = height;
+ }
+ });
+
+ if (cvs) resizeObserver.observe(cvs, { box: 'device-pixel-content-box' });
+
+ // This callback cleans up the observer
+ return () => {
+ if (cvs) resizeObserver.unobserve(cvs);
+ };
+ });
+</script>
+
+<canvas bind:this={cvs} />
+
+<style>
+ canvas {
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 100vw;
+ opacity: 0.5;
+ pointer-events: none;
+ }
+</style>
diff --git a/src/lib/color.ts b/src/lib/color.ts
@@ -1,6 +1,6 @@
import type { Color } from '$lib/types';
-// from ttps://observablehq.com/@shan/oklab-color-wheel
+// from https://observablehq.com/@shan/oklab-color-wheel
const gamma = (x: number) => (x >= 0.0031308 ? 1.055 * Math.pow(x, 1 / 2.4) - 0.055 : 12.92 * x)
diff --git a/src/lib/engine.worker.ts b/src/lib/engine.worker.ts
@@ -17,8 +17,10 @@ import {
rescale,
wrangle,
unitRange,
- oscShapes
+ oscShapes,
+ mathOps
} from '$lib/types';
+import _ from 'lodash';
let config: SynthConfig | undefined = undefined;
let unitState: UnitStateMap = new Map();
@@ -57,6 +59,9 @@ function getUnitState(id: UnitId): UnitState {
return theState === undefined ? [] : theState;
break;
}
+ case 'lag': {
+ return theState === undefined ? [] : theState;
+ }
default: {
throw new Error('state for this invalid or NYI');
}
@@ -75,7 +80,10 @@ function vUnit(x: { id: UnitId }): number {
switch (unit.kind) {
case 'osc': {
const position = getUnitState(id);
- return oscShapes.sine(position / LOOP_CYCLES, v(unit.controls.amount));
+ const shapeIdx = Math.round(
+ wrangle(v(unit.controls.waveshape), RANGE.signal, unitRange.osc.waveshape)
+ );
+ return oscShapes[shapeIdx](position / LOOP_CYCLES, v(unit.controls.amount));
}
case 'const': {
return v(unit.controls.value);
@@ -87,6 +95,20 @@ function vUnit(x: { id: UnitId }): number {
const frames: number[] = getUnitState(id);
return frames.reduce((acc, item) => item + acc, 0) / frames.length;
}
+ case 'math': {
+ const opIdx = Math.round(wrangle(v(unit.controls.op), RANGE.signal, unitRange.math.op));
+ if (opIdx === 2) {
+ //mult
+ return (
+ 5_000_000 * mathOps[opIdx](v(unit.controls.a) / 5_000_000, v(unit.controls.b) / 5_000_000)
+ );
+ } else {
+ return mathOps[opIdx](v(unit.controls.a), v(unit.controls.b));
+ }
+ }
+ case 'lag': {
+ return getUnitState(id)[0];
+ }
}
}
@@ -117,10 +139,9 @@ function v(input: Input): number {
if (typeof input === 'number') {
return input;
}
- const result = input.reduce((a, b) => a + vUnit(b), 0);
+ const result = (input.value || 0) + input.sources.reduce((a, b) => a + vUnit(b), 0);
return result;
}
-
function update() {
// update all unit states
if (!config) {
@@ -139,10 +160,17 @@ function update() {
RANGE.signal,
unitRange.osc.superfine
);
- setUnitState(
- id,
- (position + (coarse * LOOP_CYCLES) / 100 + fine * 20000 + superfine) % LOOP_CYCLES
- );
+ // i'm assuming that when these all get set to 0, the position should also reset
+ // that way it isn't stuck at some constant value that effects other units down the line
+ if (coarse === 0 && fine === 0 && superfine === 0) {
+ if (position !== 0) console.log(`Resetting position (${position}) of ${id} to 0.`);
+ setUnitState(id, 0);
+ } else {
+ setUnitState(
+ id,
+ (position + (coarse * LOOP_CYCLES) / 100 + fine * 20000 + superfine) % LOOP_CYCLES
+ );
+ }
break;
}
case 'smooth': {
@@ -154,10 +182,39 @@ function update() {
frames = frames.slice(0, n);
}
setUnitState(id, frames);
+ break;
+ }
+ case 'lag': {
+ const amt = wrangle(v(unit.controls.amount), RANGE.signal, unitRange.lag.amount);
+ if (amt === 0) {
+ setUnitState(id, [v(unit.controls.signal)]);
+ } else {
+ let arr = getUnitState(id) as number[];
+ arr.push(v(unit.controls.signal));
+ if (arr.length > amt) {
+ if (arr.length - 1 === amt) {
+ arr.pop();
+ } else {
+ arr = arr.slice(arr.length - amt);
+ }
+ arr.unshift();
+ }
+ setUnitState(id, arr);
+ }
+ break;
}
}
}
}
+function throttleLog(k: string, msg: any) {
+ logMap[k] = msg;
+}
+let logMap: Record<string, any> = {};
+function logSometimes() {
+ Object.keys(logMap).map((k) => console.log(k, logMap[k]));
+ setTimeout(logSometimes, 250);
+}
+logSometimes();
function drawSquares() {
if (config) {
@@ -180,18 +237,9 @@ function drawSquares() {
const c = config.sinks.c == null ? 0 : v(config.sinks.c);
const h = config.sinks.h == null ? 0 : v(config.sinks.h);
const rgb = oklch(
- wrangle(l, RANGE.signal, {
- min: 0,
- max: 1
- }),
- wrangle(c, RANGE.signal, {
- min: 0,
- max: 0.5
- }),
- wrangle(h, RANGE.signal, {
- min: 0,
- max: 360
- })
+ wrangle(l, RANGE.signal, RANGE.lch.l),
+ wrangle(c, RANGE.signal, RANGE.lch.c),
+ wrangle(h, RANGE.signal, RANGE.lch.h)
);
data[di++] = rgb.r;
data[di++] = rgb.g;
diff --git a/src/lib/stores.ts b/src/lib/stores.ts
@@ -11,7 +11,8 @@ import {
RANGE,
wrangle,
unitRange,
- type Pos
+ type Pos,
+ type Sinks
} from '$lib/types';
import { v4 as uuidv4 } from 'uuid';
@@ -22,6 +23,14 @@ const randConst = () => {
return rescale(sliderVal, RANGE.slider, RANGE.signal);
};
+const mkInput = (value?: number) => {
+ const ret: Input = { sources: [] };
+ if (value !== undefined) {
+ ret.value = value;
+ }
+ return ret;
+};
+
const mkUnitStore = () => {
const { subscribe, set, update } = writable<Units>({});
const setControl = <K extends UnitKind>(
@@ -47,30 +56,61 @@ const mkUnitStore = () => {
let unit: Unit;
switch (kind) {
case 'const': {
- unit = { kind: 'const', controls: { value: 0 }, pos: { x: 0, y: 0 } };
+ unit = { kind: 'const', controls: { value: mkInput(0) }, pos: { x: 0, y: 0 } };
break;
}
case 'osc': {
unit = {
kind: 'osc',
controls: {
- coarse: wrangle(1, unitRange.osc.coarse, RANGE.signal),
- fine: wrangle(1, unitRange.osc.coarse, RANGE.signal),
- superfine: 0,
- amount: RANGE.signal.max
+ coarse: mkInput(wrangle(1, unitRange.osc.coarse, RANGE.signal)),
+ fine: mkInput(wrangle(1, unitRange.osc.fine, RANGE.signal)),
+ superfine: mkInput(0),
+ amount: mkInput(RANGE.signal.max),
+ waveshape: mkInput(RANGE.signal.min)
},
pos: { x: 0, y: 0 }
};
break;
}
case 'noise': {
- unit = { kind: 'noise', controls: { amount: RANGE.signal.max / 2 }, pos: { x: 0, y: 0 } };
+ unit = {
+ kind: 'noise',
+ controls: { amount: mkInput(RANGE.signal.max / 2) },
+ pos: { x: 0, y: 0 }
+ };
break;
}
case 'smooth': {
unit = {
kind: 'smooth',
- controls: { frames: wrangle(3, unitRange.smooth.frames, RANGE.signal), signal: 0 },
+ controls: {
+ frames: mkInput(wrangle(3, unitRange.smooth.frames, RANGE.signal)),
+ signal: mkInput()
+ },
+ pos: { x: 0, y: 0 }
+ };
+ break;
+ }
+ case 'math': {
+ unit = {
+ kind: 'math',
+ controls: {
+ a: mkInput(0),
+ op: mkInput(RANGE.signal.min),
+ b: mkInput(0)
+ },
+ pos: { x: 0, y: 0 }
+ };
+ break;
+ }
+ case 'lag': {
+ unit = {
+ kind: 'lag',
+ controls: {
+ signal: mkInput(),
+ amount: mkInput(0)
+ },
pos: { x: 0, y: 0 }
};
break;
@@ -92,3 +132,8 @@ export type UnitDragging = null | { initPos: Pos; pos: Pos; id: UnitId };
export const unitDragging = writable<UnitDragging>(null);
export const unitStore = mkUnitStore();
+export const sinksStore = writable<Sinks>({
+ l: { value: 0, sources: [] },
+ c: { value: 0, sources: [] },
+ h: { value: 0, sources: [] }
+});
diff --git a/src/lib/types.ts b/src/lib/types.ts
@@ -1,8 +1,9 @@
-export const LOOP_CYCLES = 100_000_000;
+export const RES = 10_000;
export const INPUT_RANGE = 100;
export const ROWS = 100;
export const COLS = 100;
export const CELLS = ROWS * COLS;
+export const LOOP_CYCLES = RES * CELLS;
export type WithTarget<E, T> = E & { currentTarget: T };
export type Prettify<T> = { [k in keyof T]: T[k] } & {};
@@ -39,58 +40,51 @@ export type Pos = {
y: number;
};
-export const unitKinds = ['const', 'osc', 'noise', 'smooth'] as const;
+export const unitKinds = ['const', 'osc', 'noise', 'smooth', 'math', 'lag'] as const;
export type UnitKind = (typeof unitKinds)[number];
-export type Unit<K extends UnitKind = UnitKind> = { [P in K]: UnitMap[P] }[K];
+type Corr<X, K extends keyof X> = { [P in K]: X[P] }[K];
+export type Unit<K extends UnitKind = UnitKind> = Corr<UnitMap, K>;
type UnitMap = {
const: ConstUnit;
osc: OscUnit;
noise: NoiseUnit;
smooth: SmoothUnit;
+ math: MathUnit;
+ lag: LagUnit;
};
// TODO: can we error when keys don't exhaust UnitKind? Record can't infer the tuples correctly.
const controlNames = {
- osc: ['coarse', 'fine', 'superfine', 'amount'] as const,
+ osc: ['coarse', 'fine', 'superfine', 'amount', 'waveshape'] as const,
noise: ['amount'] as const,
const: ['value'] as const,
- smooth: ['signal', 'frames'] as const
+ smooth: ['signal', 'frames'] as const,
+ math: ['a', 'op', 'b'] as const,
+ lag: ['signal', 'amount'] as const
};
type ControlNames = typeof controlNames;
export type ControlName<K extends UnitKind> = ControlNames[K][number];
-export type Controls<K extends UnitKind = UnitKind> = Prettify<{
+export type Controls<K extends UnitKind = UnitKind> = {
[name in ControlNames[K][number]]: Input;
-}>;
-
-export type ConstUnit = {
- kind: 'const';
- pos: Pos;
- controls: Controls<'const'>;
-};
-
-export type OscUnit = {
- kind: 'osc';
- pos: Pos;
- controls: Controls<'osc'>;
};
-export type NoiseUnit = {
- kind: 'noise';
+export type GenericUnit<K extends UnitKind> = {
+ kind: K;
pos: Pos;
- controls: Controls<'noise'>;
-};
-
-export type SmoothUnit = {
- kind: 'smooth';
- pos: Pos;
- controls: Controls<'smooth'>;
+ controls: Controls<K>;
};
+export type ConstUnit = GenericUnit<'const'>;
+export type OscUnit = GenericUnit<'osc'>;
+export type NoiseUnit = GenericUnit<'noise'>;
+export type SmoothUnit = GenericUnit<'smooth'>;
+export type MathUnit = GenericUnit<'math'>;
+export type LagUnit = GenericUnit<'lag'>;
export type UnitId = string;
export type Output = { id: UnitId };
-export type Input = number | Output[];
+export type Input = { value?: number; sources: Output[] };
export type Units = { [u: UnitId]: Unit };
export type UnitStateMap = Map<UnitId, UnitState>;
@@ -103,6 +97,11 @@ export type NumberRange = {
};
export const RANGE = {
+ lch: {
+ l: { min: 0, max: 1 },
+ c: { min: 0, max: 0.5 },
+ h: { min: 0, max: 360 }
+ },
color: {
min: 0,
max: 255
@@ -119,8 +118,28 @@ export const RANGE = {
min: -5_000_000,
max: 5_000_000
}
-};
+} as const;
+type OscShapes = Array<(p: number, a: number) => number>;
+
+export const oscShapes: OscShapes = [
+ (position: number, amount: number) => {
+ return Math.sin(2 * Math.PI * position) * amount;
+ },
+ (p, a) => {
+ return (p < 0.5 ? -1 : 1) * a;
+ },
+ (p, a) => p * a
+];
+
+type MathOp = (a: number, b: number) => number;
+export const mathOps: MathOp[] = [
+ (a, b) => a + b,
+ (a, b) => a - b,
+ (a, b) => a * b,
+ (a, b) => (b === 0 ? 0 : a / b),
+ (a, b) => (b === 0 ? 0 : a % b)
+];
export const unitRange: {
[k in UnitKind]: Record<ControlName<k>, NumberRange>;
} = {
@@ -140,18 +159,22 @@ export const unitRange: {
amount: {
min: -50,
max: 50
+ },
+ waveshape: {
+ min: 0,
+ max: Object.keys(oscShapes).length - 1
}
},
const: {
value: {
- min: -50_000_000,
- max: 50_000_000
+ min: -5_000_000,
+ max: 5_000_000
}
},
noise: {
amount: {
- min: -50,
- max: 50
+ min: -5000,
+ max: 5000
}
},
smooth: {
@@ -160,8 +183,20 @@ export const unitRange: {
max: 25
},
signal: RANGE.signal
+ },
+ math: {
+ a: RANGE.signal,
+ op: { min: 0, max: mathOps.length - 1 },
+ b: RANGE.signal
+ },
+ lag: {
+ signal: RANGE.signal,
+ amount: {
+ min: 0,
+ max: 10_000
+ }
}
-};
+} as const;
export function clamp(n: number, range: NumberRange) {
return Math.max(range.min, Math.min(range.max, n));
@@ -169,6 +204,8 @@ export function clamp(n: number, range: NumberRange) {
export function rescale(n: number, origin: NumberRange, dest: NumberRange) {
return ((n - origin.min) / (origin.max - origin.min)) * (dest.max - dest.min) + dest.min;
}
+
+/** fit n within its original range, then scale it to the destination range */
export function wrangle(n: number, origin: NumberRange, dest: NumberRange) {
return rescale(clamp(n, origin), origin, dest);
}
@@ -196,7 +233,9 @@ export const is = {
const: isUnit('const'),
osc: isUnit('osc'),
noise: isUnit('noise'),
- smooth: isUnit('smooth')
+ smooth: isUnit('smooth'),
+ math: isUnit('math'),
+ lag: isUnit('lag')
}
};
@@ -214,44 +253,24 @@ export const ensure = {
const: ensureUnit('const'),
osc: ensureUnit('osc'),
noise: ensureUnit('noise'),
- smooth: ensureUnit('smooth')
+ smooth: ensureUnit('smooth'),
+ math: ensureUnit('math'),
+ lag: ensureUnit('lag')
}
};
export const inp = {
toggle: (input: Input, output: Output): Input => {
- if (typeof input === 'number') {
- return [output];
- } else {
- const deduped = input.filter((o) => o.id !== output.id);
- if (input.length === deduped.length) {
- return [...input, output];
- } else {
- // it's there and we removed it
- // if the last input signal was removed, we're back to a value.
- return deduped.length === 0 ? 0 : deduped;
- }
- }
+ const deduped = input.sources.filter((o) => o.id !== output.id);
+ const inSources = deduped.length !== input.sources.length;
+ return { ...input, sources: [...deduped, ...(inSources ? [] : [output])] };
},
connected: (input: Input, output: Output): boolean => {
- if (Array.isArray(input)) {
- return Boolean(input.find((o) => o.id === output.id));
- }
- return false;
+ return Boolean(input.sources.find((o) => o.id === output.id));
+ },
+ setValue: (input: Input, value: number): Input => {
+ return { ...input, value };
}
};
export type UnitToConnect = Output | false;
-
-type OscShapes = {
- [k: string]: (p: number, a: number) => number;
-};
-export const oscShapes: OscShapes = {
- sine: (position: number, amount: number) => {
- return Math.sin(2 * Math.PI * position) * amount;
- },
- square: (p, a) => {
- return (p < 0.5 ? -1 : 1) * a;
- },
- triangle: (p, a) => p * a
-};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
- import { unitDragging, unitStore, unitToConnect } from '$lib/stores';
+ import { unitDragging, unitStore, unitToConnect, sinksStore } from '$lib/stores';
import { onMount } from 'svelte';
import { debounce } from 'lodash';
import Sink from '$lib/Sink.svelte';
@@ -19,7 +19,10 @@
import NoiseUnitComponent from '$lib/NoiseUnit.svelte';
import ConstUnitComponent from '$lib/ConstUnit.svelte';
import SmoothUnitComponent from '$lib/SmoothUnit.svelte';
+ import MathUnitComponent from '$lib/MathUnit.svelte';
+ import LagUnitComponent from '$lib/LagUnit.svelte';
import UnitDrag from '$lib/UnitDrag.svelte';
+ import Wires from '$lib/Wires.svelte';
let cvs: HTMLCanvasElement | undefined;
@@ -28,7 +31,7 @@
const loadWorker = async () => {
const EngineWorker = await import('$lib/engine.worker?worker');
engineWorker = new EngineWorker.default();
- engineWorker.postMessage({ kind: 'config', content: { units, sinks } });
+ engineWorker.postMessage({ kind: 'config', content: { units, sinks: $sinksStore } });
};
onMount(loadWorker);
@@ -79,12 +82,14 @@
const getUnit = (id: UnitId): Unit => _getUnit(null, $unitStore, id);
let uid = 0;
$: units = $unitStore;
- let sinks: Sinks = { l: 0, c: 0, h: 0 };
// let's update the engine whenever the config changes for now, even though we could ignore pos at least.
$: {
if (engineWorker) {
- engineWorker.postMessage({ kind: 'config', content: { units: $unitStore, sinks } });
+ engineWorker.postMessage({
+ kind: 'config',
+ content: { units: $unitStore, sinks: $sinksStore }
+ });
}
}
onMount(() => {
@@ -92,7 +97,7 @@
if (doc) {
$unitStore = doc.units;
uid = Object.keys(doc.units).length;
- sinks = doc.sinks;
+ $sinksStore = doc.sinks;
}
});
@@ -103,7 +108,7 @@
};
function toUrl() {
- return btoa(JSON.stringify({ version: 1, sinks, units }));
+ return btoa(JSON.stringify({ version: 1, sinks: $sinksStore, units }));
}
function fromUrl() {
@@ -141,17 +146,24 @@
if (!$unitToConnect) {
throw new Error('cant connect sink to nonexistant signal');
}
- sinks[ch] = inp.toggle(sinks[ch], $unitToConnect);
- engineWorker && engineWorker.postMessage({ kind: 'config', content: { sinks, units } });
+ $sinksStore[ch] = inp.toggle($sinksStore[ch], $unitToConnect);
+ engineWorker &&
+ engineWorker.postMessage({ kind: 'config', content: { sinks: $sinksStore, units } });
$unitToConnect = false;
};
+ const setSinkInput = (ch: 'l' | 'c' | 'h', input: Input) => {
+ $sinksStore[ch] = input;
+ engineWorker &&
+ engineWorker.postMessage({ kind: 'config', content: { sinks: $sinksStore, units } });
+ };
+
$: onSinkConnect = $unitToConnect ? _onSinkConnect : null;
- type SinkEntries = [keyof Sinks, Input | null][];
+ type SinkEntries = [keyof Sinks, Input][];
// Object.entries COULD have extra stuff, so it doesn't assume keys are keyof Sinks exactly.
// see https://stackoverflow.com/questions/60141960/typescript-key-value-relation-preserving-object-entries-type
- $: sinkEntries = [...Object.entries(sinks)] as SinkEntries;
+ $: sinkEntries = [...Object.entries($sinksStore)] as SinkEntries;
const getConnectHandler = (sdrag: false | Output, input: UnitId) => {
if (!sdrag) return null;
@@ -163,12 +175,14 @@
const unit = getUnit(input);
// @ts-ignore unit[k] is a channel but i don't wanna prove it.
unitStore.setUnit(input, { ...unit, [k]: inp.toggle(unit[k], { id: output }) });
- engineWorker && engineWorker.postMessage({ kind: 'config', content: { sinks, units } });
+ engineWorker &&
+ engineWorker.postMessage({ kind: 'config', content: { sinks: $sinksStore, units } });
$unitToConnect = false;
};
</script>
<canvas bind:this={cvs} />
+<Wires />
<div id="buttons">
{#each unitKinds as kind}
@@ -180,6 +194,7 @@
<div id="sinks">
{#each sinkEntries as [channel, input]}
<Sink
+ setInput={setSinkInput.bind(null, channel)}
{input}
{channel}
{onSinkConnect}
@@ -191,7 +206,7 @@
{#each [...Object.entries(units)] as [id, unit]}
{@const pos = $unitDragging?.id === id ? $unitDragging.pos : unit.pos}
<div class="unit" style={`top: ${pos.y}px; left: ${pos.x}px`}>
- <div>
+ <div data-unit-id={id}>
<UnitDrag {id}>
<h2>{id.substring(0, 5)}-{unit.kind}</h2>
</UnitDrag>
@@ -210,6 +225,10 @@
<NoiseUnitComponent {id} />
{:else if unit.kind === 'smooth'}
<SmoothUnitComponent {id} />
+ {:else if unit.kind === 'math'}
+ <MathUnitComponent {id} />
+ {:else if unit.kind === 'lag'}
+ <LagUnitComponent {id} />
{/if}
</div>
</div>
diff --git a/static/turtle-by-olga-tsai.jpg b/static/turtle-by-olga-tsai.jpg
Binary files differ.
diff --git a/tsconfig.json b/tsconfig.json
@@ -12,7 +12,7 @@
"noErrorTruncation": true,
"lib": [
"webworker",
- "es2019"
+ "es2023"
]
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias