color-synth

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

commit bd4581677a1717e16d44d76e5bd297265aeb1477
parent 28d2f8ed5222d05552d6e0bb2cb111325163cfdf
Author: massi <mdsiboldi@gmail.com>
Date:   Tue,  8 Aug 2023 17:35:55 -0700

smooth added and types reworked to facilitate more

Diffstat:
Msrc/lib/Control.svelte | 10+++++-----
Asrc/lib/SmoothUnit.svelte | 23+++++++++++++++++++++++
Msrc/lib/engine.worker.ts | 38+++++++++++++++++++++++++++++---------
Msrc/lib/stores.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/lib/types.ts | 136++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/routes/+page.svelte | 38++++++++------------------------------
6 files changed, 177 insertions(+), 131 deletions(-)

diff --git a/src/lib/Control.svelte b/src/lib/Control.svelte @@ -1,10 +1,10 @@ <script lang="ts" generics="TKind extends UnitKind"> import { unitStore, unitToConnect } from '$lib/stores'; import { - RANGE, + unitRange, getUnit, inp, - range, + RANGE, rescale, type ControlName, type Controls, @@ -21,10 +21,10 @@ $: unit = getUnit(kind, $unitStore, id); - const controlRange = RANGE[kind][controlName]; + 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)); + const n = Math.round(rescale(controlValue, controlRange, RANGE.signal)); unitStore.setControl(kind, id, controlName, n); }; let connection: { connected: boolean; onConnect: () => void } | null; @@ -42,7 +42,7 @@ } } let value: number | null; - $: value = typeof input === 'number' ? wrangle(input, range.signal, controlRange) : null; + $: value = typeof input === 'number' ? wrangle(input, RANGE.signal, controlRange) : null; $: slotProps = { range: controlRange, value: value || 0, update: updateValue }; </script> diff --git a/src/lib/SmoothUnit.svelte b/src/lib/SmoothUnit.svelte @@ -0,0 +1,23 @@ +<script lang="ts"> + import Control from '$lib/Control.svelte'; + import type { UnitId } from '$lib/types'; + + export let id: UnitId; + + const kind = 'smooth'; + const KControl = Control<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" /> +</div> + +<style> + h1 { + color: salmon; + } +</style> diff --git a/src/lib/engine.worker.ts b/src/lib/engine.worker.ts @@ -8,12 +8,10 @@ import type { UnitStateMap } from '$lib/types'; import { oklch } from '$lib/color'; -import { ROWS, COLS, getUnit, LOOP_CYCLES, range, rescale, wrangle, RANGE } from '$lib/types'; +import { ROWS, COLS, getUnit, LOOP_CYCLES, RANGE, rescale, wrangle, unitRange } from '$lib/types'; let config: SynthConfig | undefined = undefined; - let unitState: UnitStateMap = new Map(); - let canvas: OffscreenCanvas = new OffscreenCanvas(1, 1); onmessage = (message: { data: EngineMessage }) => { @@ -45,6 +43,10 @@ function getUnitState(id: UnitId): UnitState { } break; } + case 'smooth': { + return theState === undefined ? [] : theState; + break; + } default: { throw new Error('state for this invalid or NYI'); } @@ -84,6 +86,10 @@ function vUnit(x: { id: UnitId }): number { case 'noise': { return Math.random() * v(unit.controls.amount); } + case 'smooth': { + const frames: number[] = getUnitState(id); + return frames.reduce((acc, item) => item + acc, 0) / frames.length; + } } } @@ -129,15 +135,29 @@ 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, unitRange.osc.coarse); + const fine = rescale(v(unit.controls.fine), RANGE.signal, unitRange.osc.fine); + const superfine = rescale( + v(unit.controls.superfine), + RANGE.signal, + unitRange.osc.superfine + ); setUnitState( id, (position + (coarse * LOOP_CYCLES) / 100 + fine * 20000 + superfine * 10) % LOOP_CYCLES ); break; } + case 'smooth': { + // wrangle frames since <0 means nothing + const n = wrangle(v(unit.controls.frames), RANGE.signal, unitRange.smooth.frames); + // keep n frames + let frames = [v(unit.controls.signal), ...getUnitState(id)]; + if (frames.length > n) { + frames = frames.slice(0, n); + } + setUnitState(id, frames); + } } } } @@ -163,15 +183,15 @@ 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, { + wrangle(l, RANGE.signal, { min: 0, max: 1 }), - wrangle(c, range.signal, { + wrangle(c, RANGE.signal, { min: 0, max: 0.5 }), - wrangle(h, range.signal, { + wrangle(h, RANGE.signal, { min: 0, max: 360 }) diff --git a/src/lib/stores.ts b/src/lib/stores.ts @@ -1,15 +1,26 @@ -import type { - ControlName, - Controls, - Input, - Unit, - UnitId, - UnitKind, - UnitToConnect, - Units +import { + rescale, + type ControlName, + type Controls, + type Input, + type Unit, + type UnitId, + type UnitKind, + type UnitToConnect, + type Units, + RANGE, + wrangle, + unitRange } from '$lib/types'; import { writable } from 'svelte/store'; +let uuid = 0; + +const randConst = () => { + const sliderVal = Math.round(rescale(Math.random(), { min: 0, max: 1 }, RANGE.slider)); + return rescale(sliderVal, RANGE.slider, RANGE.signal); +}; + const mkUnitStore = () => { const { subscribe, set, update } = writable<Units>({}); const setControl = <K extends UnitKind>( @@ -30,7 +41,39 @@ const mkUnitStore = () => { setUnit<K extends UnitKind>(id: UnitId, unit: Unit<K>) { update((units) => ({ ...units, [id]: unit })); }, - setControl + setControl, + addUnit<K extends UnitKind>(kind: K): UnitId { + const id = uuid++; + let unit: Unit; + switch (kind) { + case 'const': { + unit = { kind: 'const', controls: { value: 0 }, pos: { x: 0, y: 0 } }; + break; + } + case 'osc': { + unit = { + kind: 'osc', + controls: { coarse: 0, fine: 0, superfine: 0, amount: RANGE.signal.max }, + pos: { x: 0, y: 0 } + }; + break; + } + case 'noise': { + unit = { kind: 'noise', controls: { amount: 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 }, + pos: { x: 0, y: 0 } + }; + break; + } + } + update((units) => ({ ...units, [id]: unit })); + return String(id); + } }; }; diff --git a/src/lib/types.ts b/src/lib/types.ts @@ -39,10 +39,23 @@ type Pos = { y: number; }; +export const unitKinds = ['const', 'osc', 'noise', 'smooth'] as const; +export type UnitKind = (typeof unitKinds)[number]; +export type Unit<K extends UnitKind = UnitKind> = { [P in K]: UnitMap[P] }[K]; + +type UnitMap = { + const: ConstUnit; + osc: OscUnit; + noise: NoiseUnit; + smooth: SmoothUnit; +}; + +// 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, noise: ['amount'] as const, - const: ['value'] as const + const: ['value'] as const, + smooth: ['signal', 'frames'] as const }; type ControlNames = typeof controlNames; export type ControlName<K extends UnitKind> = ControlNames[K][number]; @@ -68,20 +81,10 @@ export type NoiseUnit = { controls: Controls<'noise'>; }; -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; - -type UnitMap = { - const: ConstUnit; - osc: OscUnit; - noise: NoiseUnit; +export type SmoothUnit = { + kind: 'smooth'; + pos: Pos; + controls: Controls<'smooth'>; }; export type UnitId = string; @@ -99,10 +102,27 @@ export type NumberRange = { max: number; }; -export const RANGE: { - [k in UnitKind]: { - [j in ControlName<k>]: NumberRange; - }; +export const RANGE = { + 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 const unitRange: { + [k in UnitKind]: Record<ControlName<k>, NumberRange>; } = { osc: { coarse: { @@ -133,31 +153,13 @@ export const RANGE: { min: -50, max: 50 } - } -}; - -export const range = { + }, smooth: { frames: { - min: 0, - 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 + min: 1, + max: 25 + }, + signal: RANGE.signal } }; @@ -189,50 +191,30 @@ const isUnit = (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: isUnit('const'), osc: isUnit('osc'), - noise: isUnit('noise') - }, - controlName: { - const: isControlName('const'), - osc: isControlName('osc'), - noise: isControlName('noise') + noise: isUnit('noise'), + smooth: isUnit('smooth') } }; +const ensureUnit = <K extends UnitKind>(k: K) => { + return (u: Unit) => { + if (is.unit[k](u)) { + return u as Unit<K>; + } else { + throw new Error('ensure check failed'); + } + }; +}; export const ensure = { unit: { - const: (u: Unit): ConstUnit => { - if (is.unit.const(u)) { - return u; - } else { - throw new Error('this is not a const'); - } - }, - osc: (u: Unit): OscUnit => { - if (is.unit.osc(u)) { - return u; - } else { - throw new Error('this is not an osc'); - } - }, - noise: (u: Unit): NoiseUnit => { - if (is.unit.noise(u)) { - return u; - } else { - throw new Error('this is not a noise unit'); - } - } + const: ensureUnit('const'), + osc: ensureUnit('osc'), + noise: ensureUnit('noise'), + smooth: ensureUnit('smooth') } }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte @@ -3,11 +3,12 @@ import { onMount } from 'svelte'; import { debounce } from 'lodash'; 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 { COLS, ROWS, inp, rescale, RANGE, is, getUnit as _getUnit, unitKinds } from '$lib/types'; + import type { Output, Input, Unit, UnitId, ConstUnit, Units, Sinks, UnitKind } from '$lib/types'; import OscUnitComponent from '$lib/OscUnit.svelte'; import NoiseUnitComponent from '$lib/NoiseUnit.svelte'; import ConstUnitComponent from '$lib/ConstUnit.svelte'; + import SmoothUnitComponent from '$lib/SmoothUnit.svelte'; let cvs: HTMLCanvasElement | undefined; @@ -114,31 +115,6 @@ return id; } - 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(): UnitId { - return addUnit({ - kind: 'const', - controls: { value: randConst() }, - pos: { x: 0, y: 0 } - }); - }, - osc: function mkOsc(): UnitId { - return addUnit({ - kind: 'osc', - controls: { coarse: randConst(), fine: randConst(), superfine: 0, amount: randConst() }, - pos: { x: 0, y: 0 } - }); - }, - noise: function noise(): UnitId { - return addUnit({ kind: 'noise', controls: { amount: randConst() }, pos: { x: 0, y: 0 } }); - } - }; - let dragging: false | UnitId = false; const handleMouseMove = (e: MouseEvent) => { @@ -209,9 +185,9 @@ <canvas bind:this={cvs} /> <div id="buttons"> - <button on:click={mk.c}>add const</button> - <button on:click={mk.osc}>add osc</button> - <button on:click={mk.noise}>add noise</button> + {#each unitKinds as kind} + <button on:click={() => unitStore.addUnit(kind)}>add {kind}</button> + {/each} </div> <div id="sinks"> @@ -234,6 +210,8 @@ <OscUnitComponent {id} /> {:else if unit.kind === 'noise'} <NoiseUnitComponent {id} /> + {:else if unit.kind === 'smooth'} + <SmoothUnitComponent {id} /> {/if} <div class="output-dragger"