color-synth

a synth that generates colors instead of sounds
Log | Files | Refs | README

commit b939a5926255ccddcfe4157fac3cfc76f54f4354
parent bfb66e6cafb9d9e568154f6d43515fc04e61e061
Author: massi <mdsiboldi@gmail.com>
Date:   Sun,  6 Aug 2023 04:35:22 -0700

make control more dumb and toil over types some more

Diffstat:
Msrc/lib/ConstUnit.svelte | 5+++--
Msrc/lib/Control.svelte | 62++++++++------------------------------------------------------
Msrc/lib/InputDragger.svelte | 13+++++++------
Msrc/lib/NoiseUnit.svelte | 5+++--
Msrc/lib/OscUnit.svelte | 11++++++-----
Asrc/lib/controlUtils.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/engine.worker.ts | 8++++----
Msrc/lib/stores.ts | 24++++++++++++++++++------
Msrc/lib/types.ts | 87++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/routes/+page.svelte | 12++++++------
10 files changed, 158 insertions(+), 117 deletions(-)

diff --git a/src/lib/ConstUnit.svelte b/src/lib/ConstUnit.svelte @@ -1,7 +1,8 @@ <script lang="ts"> import Control from '$lib/Control.svelte'; - import { unitStore } from '$lib/stores'; + import { unitStore, unitToConnect } from '$lib/stores'; import { ensure, getUnit, type UnitId } from '$lib/types'; + import { getControlProps } from './controlUtils'; export let id: UnitId; @@ -10,7 +11,7 @@ <div> <h1>just a const...</h1> - <Control {id} controlName="value" /> + <Control {...getControlProps(unit.kind, id, unit.controls.value, 'value', $unitToConnect)} /> </div> <style> diff --git a/src/lib/Control.svelte b/src/lib/Control.svelte @@ -1,72 +1,26 @@ <script lang="ts"> import NumberSelector from '$lib/NumberSelector.svelte'; import { unitStore, unitToConnect } from '$lib/stores'; - import { - getUnit, - range, - type Range, - type OscUnit, - type UnitId, - rescale, - type Unit, - type Input, - wrangle, - inp - } from './types'; + import type { NumberRange } from '$lib/types'; import InputDragger from './InputDragger.svelte'; - export let id: UnitId; export let controlName: string; - - $: unit = getUnit($unitStore, id); - // @ts-ignore caller responsible for ensuring unit has the appropriate control name. - $: controlRange = range[unit.kind][controlName] as range; - // @ts-ignore caller responsible for ensuring unit has the appropriate control name. - $: input = unit.controls[controlName] as Input; - $: values = - typeof input === 'number' - ? { - raw: input, - control: wrangle(input, range.signal, controlRange) - } - : null; - - $: updateValue = (controlValue: number) => { - const n = Math.round(rescale(controlValue, controlRange, range.signal)); - unitStore.setUnit(id, { ...unit, controls: { ...unit.controls, [controlName]: n } } as Unit); - }; - $: onConnect = $unitToConnect - ? () => { - const _utc = $unitToConnect; - if (!_utc) return; - console.log('connecting', $unitToConnect, controlName); - unitStore.setUnit(id, { - ...unit, - controls: { ...unit.controls, [controlName]: inp.toggle(input, _utc) } - } as Unit); - } - : null; - let connected: boolean; - $: { - const _unitToConnect = $unitToConnect; - if (_unitToConnect !== false && Array.isArray(input)) { - connected = Boolean(input.find(({ id }) => id === _unitToConnect.id)); - } else { - connected = false; - } - } + export let controlRange: NumberRange; + export let value: number | null; + export let connection: { connected: boolean; onConnect: () => void } | null; + export let updateValue: (n: number) => void; </script> <div class="control-wrapper"> <h3>{controlName}</h3> - <InputDragger {connected} {onConnect} /> - {#if values !== null} + <InputDragger {connection} /> + {#if value !== null} <input type="range" min={controlRange.min} max={controlRange.max} step={1} - value={values.control} + {value} on:input={(e) => updateValue(Number(e.currentTarget?.value))} /> {/if} diff --git a/src/lib/InputDragger.svelte b/src/lib/InputDragger.svelte @@ -1,14 +1,15 @@ <script lang="ts"> - export let onConnect: (() => void) | null | undefined; - export let connected: boolean; + export let connection: { connected: Boolean; onConnect: () => void } | null; </script> <div class="input-dragger"> - {#if onConnect} + {#if connection} <div - class={['connect', connected && 'connected'].filter(Boolean).join(' ')} - on:mouseup={onConnect} - /> + class={['connect', connection.connected && 'connected'].filter(Boolean).join(' ')} + on:mouseup={connection.onConnect} + > + hi + </div> {/if} </div> diff --git a/src/lib/NoiseUnit.svelte b/src/lib/NoiseUnit.svelte @@ -1,7 +1,8 @@ <script lang="ts"> import Control from '$lib/Control.svelte'; - import { unitStore } from '$lib/stores'; + import { unitStore, unitToConnect } from '$lib/stores'; import { ensure, getUnit, type UnitId } from '$lib/types'; + import { getControlProps } from '$lib/controlUtils'; export let id: UnitId; @@ -10,7 +11,7 @@ <div> <h1>noiseyboi</h1> - <Control {id} controlName="amount" /> + <Control {...getControlProps(unit.kind, id, unit.controls.amount, 'amount', $unitToConnect)} /> </div> <style> diff --git a/src/lib/OscUnit.svelte b/src/lib/OscUnit.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import Control from '$lib/Control.svelte'; - import { unitStore } from '$lib/stores'; + import { getControlProps } from '$lib/controlUtils'; + import { unitStore, unitToConnect } from '$lib/stores'; import { ensure, getUnit, type UnitId } from '$lib/types'; export let id: UnitId; @@ -10,10 +11,10 @@ <div> <h1>Os-KILL-8r</h1> - <Control {id} controlName="coarse" /> - <Control {id} controlName="fine" /> - <Control {id} controlName="superfine" /> - <Control {id} controlName="amount" /> + <Control {...getControlProps('osc', id, unit.controls.coarse, 'coarse', $unitToConnect)} /> + <Control {...getControlProps('osc', id, unit.controls.fine, 'fine', $unitToConnect)} /> + <Control {...getControlProps('osc', id, unit.controls.superfine, 'superfine', $unitToConnect)} /> + <Control {...getControlProps('osc', id, unit.controls.amount, 'amount', $unitToConnect)} /> </div> <style> diff --git a/src/lib/controlUtils.ts b/src/lib/controlUtils.ts @@ -0,0 +1,48 @@ +import { + RANGE, + type ControlName, + type Controls, + type UnitKind, + type UnitToConnect, + rescale, + range, + type UnitId, + inp, + wrangle, + type Input +} from '$lib/types'; +import { unitStore } from './stores'; + +export const getControlProps = <K extends UnitKind>( + kind: K, + id: UnitId, + input: Input, + controlName: ControlName<K>, + utc: UnitToConnect +) => { + const controlRange = RANGE[kind][controlName]; + const updateValue = (controlValue: number) => { + const n = Math.round(rescale(controlValue, controlRange, range.signal)); + unitStore._setControl(id, controlName, n); + }; + + let connection: { connected: boolean; onConnect: () => void } | null = null; + if (utc) { + connection = { + connected: Array.isArray(input) && inp.connected(input, utc), + onConnect: () => { + if (!utc) return; + console.log('connecting', utc, controlName); + unitStore._setControl(id, controlName, inp.toggle(input, utc)); + } + }; + } + + return { + controlName, + controlRange, + value: typeof input === 'number' ? wrangle(input, range.signal, controlRange) : null, + connection, + updateValue + }; +}; diff --git a/src/lib/engine.worker.ts b/src/lib/engine.worker.ts @@ -8,7 +8,7 @@ import type { UnitStateMap } from '$lib/types'; import { oklch } from '$lib/color'; -import { ROWS, COLS, getUnit, LOOP_CYCLES, range, rescale, wrangle } from '$lib/types'; +import { ROWS, COLS, getUnit, LOOP_CYCLES, range, rescale, wrangle, RANGE } from '$lib/types'; let config: SynthConfig | undefined = undefined; @@ -128,9 +128,9 @@ function update() { switch (unit.kind) { case 'osc': { const position = getUnitState(id); - const coarse = rescale(v(unit.controls.coarse), range.signal, range.osc.coarse); - const fine = rescale(v(unit.controls.fine), range.signal, range.osc.fine); - const superfine = rescale(v(unit.controls.superfine), range.signal, range.osc.superfine); + const coarse = rescale(v(unit.controls.coarse), range.signal, RANGE.osc.coarse); + const fine = rescale(v(unit.controls.fine), range.signal, RANGE.osc.fine); + const superfine = rescale(v(unit.controls.superfine), range.signal, RANGE.osc.superfine); setUnitState( id, (position + (coarse * LOOP_CYCLES) / 100 + fine * 20000 + superfine * 10) % LOOP_CYCLES diff --git a/src/lib/stores.ts b/src/lib/stores.ts @@ -1,17 +1,29 @@ +import type { Controls, Input, Unit, UnitId, UnitKind, UnitToConnect, Units } from '$lib/types'; import { writable } from 'svelte/store'; -import type { Output, Unit, UnitId, UnitKind, Units } from '$lib/types'; -const mkUnitStore = <T = Units>() => { - const { subscribe, set, update } = writable<T>({}); +const mkUnitStore = () => { + const { subscribe, set, update } = writable<Units>({}); + const _setControl = (id: UnitId, controlName: string, input: Input) => { + update((units) => { + // @ts-ignore + if (units[id].controls[controlName] == null) { + throw new Error(`invalid control ${controlName} for unit kind ${units[id].kind}`); + } + // @ts-ignore + units[id].controls[controlName] = input; + return units; + }); + }; return { subscribe, set, - setUnit(id: UnitId, unit: Unit) { + setUnit<K extends UnitKind>(id: UnitId, unit: Unit<K>) { update((units) => ({ ...units, [id]: unit })); - } + }, + _setControl }; }; -export const unitToConnect = writable<Output | false>(false); +export const unitToConnect = writable<UnitToConnect>(false); export const unitStore = mkUnitStore(); diff --git a/src/lib/types.ts b/src/lib/types.ts @@ -5,6 +5,7 @@ export const COLS = 100; export const CELLS = ROWS * COLS; export type WithTarget<E, T> = E & { currentTarget: T }; +export type Prettify<T> = { [k in keyof T]: T[k] } & {}; export type Color = { r: number; @@ -38,34 +39,42 @@ type Pos = { y: number; }; +const controlNames = { + osc: ['coarse', 'fine', 'superfine', 'amount'] as const, + noise: ['amount'] as const, + const: ['value'] as const +}; +type ControlNames = typeof controlNames; +export type ControlName<K extends UnitKind> = ControlNames[K][number]; +export type Controls<K extends UnitKind = UnitKind> = Prettify<{ + [name in ControlNames[K][number]]: Input; +}>; + export type ConstUnit = { kind: 'const'; pos: Pos; - controls: { - value: Input; - }; + controls: Controls<'const'>; }; export type OscUnit = { kind: 'osc'; pos: Pos; - controls: { - coarse: Input; - fine: Input; - superfine: Input; - amount: Input; - }; + controls: Controls<'osc'>; }; export type NoiseUnit = { kind: 'noise'; pos: Pos; - controls: { - amount: Input; - }; + controls: Controls<'noise'>; }; -export type Unit = OscUnit | ConstUnit | NoiseUnit; +export type Unit<K extends UnitKind = UnitKind> = K extends 'osc' + ? OscUnit + : K extends 'noise' + ? NoiseUnit + : K extends 'const' + ? ConstUnit + : never; export type UnitKind = keyof UnitMap; @@ -89,12 +98,16 @@ export type Sinks = { h: Input; }; -export type Range = { +export type NumberRange = { min: number; max: number; }; -export const range = { +export const RANGE: { + [k in UnitKind]: { + [j in ControlName<k>]: NumberRange; + }; +} = { osc: { coarse: { min: -50, @@ -111,10 +124,6 @@ export const range = { amount: { min: -50, max: 50 - }, - output: { - min: -1, - max: 1 } }, const: { @@ -128,7 +137,10 @@ export const range = { min: -50, max: 50 } - }, + } +}; + +export const range = { smooth: { frames: { min: 0, @@ -153,13 +165,13 @@ export const range = { } }; -export function clamp(n: number, range: Range) { +export function clamp(n: number, range: NumberRange) { return Math.max(range.min, Math.min(range.max, n)); } -export function rescale(n: number, origin: Range, dest: Range) { +export function rescale(n: number, origin: NumberRange, dest: NumberRange) { return ((n - origin.min) / (origin.max - origin.min)) * (dest.max - dest.min) + dest.min; } -export function wrangle(n: number, origin: Range, dest: Range) { +export function wrangle(n: number, origin: NumberRange, dest: NumberRange) { return rescale(clamp(n, origin), origin, dest); } @@ -169,20 +181,29 @@ export function getUnit(units: Units, id: UnitId): Unit { return result; } +const isUnit = + <K extends UnitKind>(k: K) => + (u: Unit): u is Unit<K> => + u.kind === k; + +const isControlName = + <K extends UnitKind>(k: K) => + (controlName: string): controlName is ControlName<K> => + !!controlNames.const.find((n) => n === controlName); + export const is = { input: (i: any): i is Input => { return i === Object(i) && i.id; }, unit: { - const: (u: Unit): u is ConstUnit => { - return u.kind === 'const'; - }, - osc: (u: Unit): u is OscUnit => { - return u.kind === 'osc'; - }, - noise: (u: Unit): u is NoiseUnit => { - return u.kind === 'noise'; - } + const: isUnit('const'), + osc: isUnit('osc'), + noise: isUnit('noise') + }, + controlName: { + const: isControlName('const'), + osc: isControlName('osc'), + noise: isControlName('noise') } }; @@ -234,3 +255,5 @@ export const inp = { return false; } }; + +export type UnitToConnect = Output | false; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte @@ -5,9 +5,9 @@ import Sink from '$lib/Sink.svelte'; import { COLS, ROWS, inp, rescale, range, is, getUnit as _getUnit } from '$lib/types'; import type { Output, Input, Unit, UnitId, ConstUnit, Units, Sinks } from '$lib/types'; - import OscUnit from '$lib/OscUnit.svelte'; - import NoiseUnit from '$lib/NoiseUnit.svelte'; - import ConstUnit from '$lib/ConstUnit.svelte'; + import OscUnitComponent from '$lib/OscUnit.svelte'; + import NoiseUnitComponent from '$lib/NoiseUnit.svelte'; + import ConstUnitComponent from '$lib/ConstUnit.svelte'; let cvs: HTMLCanvasElement | undefined; @@ -244,11 +244,11 @@ <div class="unit" style={`top: ${unit.pos.y}px; left: ${unit.pos.x}px`}> <h2 on:mousedown={handleMouseDown.bind(null, id)}>{id} {unit.kind}</h2> {#if unit.kind === 'const'} - <ConstUnit {id} /> + <ConstUnitComponent {id} /> {:else if unit.kind === 'osc'} - <OscUnit {id} /> + <OscUnitComponent {id} /> {:else if unit.kind === 'noise'} - <NoiseUnit {id} /> + <NoiseUnitComponent {id} /> {/if} <div class="output-dragger"