color-synth

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

commit 4d097c501e82996ed24e00fc51b96d49afd6b9e5
parent bb77179c2d64ecda140cc065647ea5c610f55b68
Author: massi <mdsiboldi@gmail.com>
Date:   Mon, 24 Jul 2023 01:37:48 -0700

add noise unit (and more)

Diffstat:
Asrc/lib/Noise.svelte | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/Osc.svelte | 2+-
Msrc/lib/Sink.svelte | 2+-
Asrc/lib/color.ts | 29+++++++++++++++++++++++++++++
Msrc/lib/engine.worker.ts | 60++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/lib/types.ts | 30++++++++++++++++++++++++++----
Msrc/routes/+page.svelte | 11++++++++++-
7 files changed, 183 insertions(+), 27 deletions(-)

diff --git a/src/lib/Noise.svelte b/src/lib/Noise.svelte @@ -0,0 +1,76 @@ +<script lang="ts"> + import type { NoiseUnit, NoiseUnitInputs, Output } from '$lib/types'; + import { unitInputs, wrangle, rescale, range } from '$lib/types'; + import NumberSelector from '$lib/NumberSelector.svelte'; + import InputDragger from '$lib/InputDragger.svelte'; + + export let unit: NoiseUnit; + export let signalDragging: false | Output; + export let update: (n: NoiseUnit) => void; + export let onConnect: ((k: string) => void) | null; + + $: vals = { + amount: + typeof unit.amount === 'number' + ? wrangle(unit.amount, range.signal, range.noise.amount) + : null + }; + + const updateValue = (k: keyof NoiseUnitInputs, n: number) => { + update({ ...unit, [k]: Math.round(rescale(n, range.noise[k], range.signal)) }); + }; + + const isConnected = (k: keyof NoiseUnitInputs, signalDragging: false | Output) => { + const inputs = unit[k]; + if (signalDragging && Array.isArray(inputs)) { + const { id: sigId } = signalDragging; + return Boolean(inputs.find(({ id }) => id === sigId)); + } + return false; + }; +</script> + +<div class="unit-container"> + <h3>{unit.kind}</h3> + {#each unitInputs.noise as k} + {@const v = vals[k]} + {@const inputs = unit[k]} + <div class="sect"> + <InputDragger + connected={isConnected(k, signalDragging)} + onConnect={onConnect ? onConnect.bind(null, k) : null} + /> + {#if v !== null} + <h3>{k}</h3> + <NumberSelector value={v} updateValue={updateValue.bind(undefined, k)} /> + <input + type="range" + min={range.osc[k].min} + max={range.osc[k].max} + step={1} + value={v} + on:input={(e) => updateValue(k, Number(e.currentTarget?.value))} + /> + {:else} + <div>{Array.isArray(inputs) ? inputs.map((o) => o.id).join(' + ') : ''}</div> + {/if} + </div> + {/each} +</div> + +<style> + .sect { + position: relative; + width: 100%; + height: 80px; + } + .unit-container { + background: rgba(255, 255, 255, 0.3); + padding: 10px; + display: flexbox; + } + h3 { + padding: 0; + margin: 0; + } +</style> diff --git a/src/lib/Osc.svelte b/src/lib/Osc.svelte @@ -57,7 +57,7 @@ on:input={(e) => updateValue(k, Number(e.currentTarget?.value))} /> {:else} - <div>{Array.isArray(inputs) ? inputs.map((o) => o.id).join(' ') : ''}</div> + <div>{Array.isArray(inputs) ? inputs.map((o) => o.id).join(' + ') : ''}</div> {/if} </div> {/each} diff --git a/src/lib/Sink.svelte b/src/lib/Sink.svelte @@ -17,7 +17,7 @@ <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).join(' + ') : 'none'} </button> </div> diff --git a/src/lib/color.ts b/src/lib/color.ts @@ -0,0 +1,29 @@ +import type { Color } from '$lib/types'; + +// from ttps://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) + +export const oklab = (L: number, a: number, b: number): Color => { + const l_ = L + 0.3963377774 * a + 0.2158037573 * b; + const m_ = L - 0.1055613458 * a - 0.0638541728 * b; + const s_ = L - 0.0894841775 * a - 1.2914855480 * b; + const l = l_ * l_ * l_; + const m = m_ * m_ * m_; + const s = s_ * s_ * s_; + + return { + r: 255 * gamma(+4.0767245293 * l - 3.3072168827 * m + 0.2307590544 * s), + g: 255 * gamma(-1.2681437731 * l + 2.6093323231 * m - 0.3411344290 * s), + b: 255 * gamma(-0.0041119885 * l - 0.7034763098 * m + 1.7068625689 * s) + }; +} + +export const oklch = (lightness: number, chroma: number, hue: number): Color => { + const h = 2 * Math.PI * (hue / 360); + const C = chroma; + const a = C * Math.cos(h); + const b = C * Math.sin(h); + + return oklab(lightness, a, b) +} diff --git a/src/lib/engine.worker.ts b/src/lib/engine.worker.ts @@ -7,6 +7,7 @@ import type { UnitState, UnitStateMap, } from "$lib/types"; +import { oklch } from '$lib/color'; import { ROWS, COLS, @@ -64,14 +65,26 @@ function setUnitState(id: UnitId, state: UnitState) { unitState.set(id, state); } +type OscShapes = { + [k: string]: (p: number, a: number) => number +} +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 +} + function vUnit(x: { id: UnitId }): number { const { id } = x; 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)) * - v(unit.amount); + return oscShapes.sine(position / LOOP_CYCLES, v(unit.amount)) } case "const": { // range is whatever the control for it is??? @@ -157,24 +170,31 @@ function drawSquares() { const data = new Uint8ClampedArray(ROWS * COLS * 4); let di = 0; - for (let i = 0; i < ROWS * COLS; i++) { - const l = config.sinks.l == null ? 0 : v(config.sinks.l); - const c = config.sinks.c == null ? 0 : v(config.sinks.c); - const h = config.sinks.h == null ? 0 : v(config.sinks.h); - data[di++] = wrangle(l, range.signal, { - min: 0, - max: 255, - }); - data[di++] = wrangle(c, range.signal, { - min: 0, - max: 255, - }); - data[di++] = wrangle(h, range.signal, { - min: 0, - max: 255, - }); - data[di++] = 255; - update(); + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const l = config.sinks.l == null ? 0 : v(config.sinks.l); + 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, + }) + ); + data[di++] = rgb.r; + data[di++] = rgb.g; + data[di++] = rgb.b; + data[di++] = 255; + update(); + } } postMessage({ kind: "buf", content: data.buffer }, [data.buffer]); p.end("drawSquares"); diff --git a/src/lib/types.ts b/src/lib/types.ts @@ -6,6 +6,12 @@ export const CELLS = ROWS * COLS; export type WithTarget<E, T> = E & { currentTarget: T }; +export type Color = { + r: number, + g: number, + b: number +} + export type SynthConfig = { sinks: Sinks; units: UnitMap; @@ -47,21 +53,30 @@ export type OscUnitInputs = { export type NoiseUnitInputs = { amount: Input; } +export type SmoothUnitInputs = { + frames: Input; +} + type UnitInputs = { osc: (keyof OscUnitInputs)[], - noise: (keyof NoiseUnitInputs)[] + noise: (keyof NoiseUnitInputs)[], + smooth: (keyof SmoothUnitInputs)[] } export const unitInputs: UnitInputs = { osc: ['coarse', 'fine', 'superfine', 'amount'], - noise: ['amount'] + noise: ['amount'], + smooth: ['frames'] +} + +export type SmoothUnit = WithPos & SmoothUnitInputs & { + kind: "smooth"; } export type OscUnit = WithPos & OscUnitInputs & { kind: "osc"; }; -export type NoiseUnit = WithPos & { +export type NoiseUnit = WithPos & NoiseUnitInputs & { kind: "noise"; - amount: Input; }; // outputs number as it is @@ -113,6 +128,12 @@ export const range = { max: 50, }, }, + smooth: { + frames: { + min: 0, + max: 50 + } + }, color: { min: 0, max: 255, @@ -129,6 +150,7 @@ export const range = { min: -5_000_000, max: 5_000_000, }, + }; //export function rangeForInput<U extends Unit>( diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte @@ -3,6 +3,7 @@ import { debounce } from 'lodash'; import Sink from '$lib/Sink.svelte'; import Osc from '$lib/Osc.svelte'; + import Noise from '$lib/Noise.svelte'; import Slider from '$lib/Slider.svelte'; import { COLS, ROWS, inp, rescale, range, is, getUnit as _getUnit } from '$lib/types'; import type { Output, Input, Unit, UnitId, ConstUnit, UnitMap, Sinks } from '$lib/types'; @@ -131,7 +132,7 @@ pos: { x: 0, y: 0 } }); }, - n: function noise(): UnitId { + noise: function noise(): UnitId { return addUnit({ kind: 'noise', amount: randConst(), pos: { x: 0, y: 0 } }); } }; @@ -235,6 +236,7 @@ <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> </div> <div id="sinks"> @@ -260,6 +262,13 @@ onConnect={getConnectHandler(signalDragging, id)} updateOsc={updateEntireUnit.bind(null, id)} /> + {:else if unit.kind === 'noise'} + <Noise + {unit} + {signalDragging} + onConnect={getConnectHandler(signalDragging, id)} + update={updateEntireUnit.bind(null, id)} + /> {/if} <div class="output-dragger"