@msb235/blits 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ import { type Curve } from './easing.js';
2
+ import type { Easing, Keyframe, Kit, Patch, Setting } from './types.js';
3
+ /**
4
+ * What `patch` takes besides its period and its function.
5
+ *
6
+ * @category patch
7
+ */
8
+ export interface PatchOptions<I, O, S> {
9
+ /** The channels this patch contributes to. Every key `at` sets, and no others. Default: `kit`'s. */
10
+ writes?: readonly (keyof O)[];
11
+ /** The channels this patch was written against, which `cue` checks a mix's kit against. */
12
+ kit?: Partial<Kit<O>>;
13
+ /** The fields of `setting.host` this patch reads, which `cue` checks the mix's host for. */
14
+ reads?: readonly string[];
15
+ state?(subject: I): S;
16
+ step?(state: S, dt: number, subject: I, setting: Setting<S>): void;
17
+ }
18
+ /**
19
+ * The procedural form. Flicker, roving, a hash walk, a starter strike.
20
+ *
21
+ * @category patch
22
+ */
23
+ export declare function patch<I, O, S = void>(period: number, at: (phase: number, subject: I, setting: Setting<S>) => Partial<O>, opts: PatchOptions<I, O, S>): Patch<I, O, S>;
24
+ /**
25
+ * What `keys` takes besides its period and its stops.
26
+ *
27
+ * @category patch
28
+ */
29
+ export interface KeysOptions<O> {
30
+ /** The curve every segment takes unless a stop or `easeBy` overrides it. */
31
+ ease?: Easing;
32
+ /** A curve for one channel, where it differs from the rest. */
33
+ easeBy?: (channel: keyof O) => Easing | undefined;
34
+ /** Milliseconds one channel waits before it starts moving, within the period. */
35
+ delayBy?: (channel: keyof O) => number;
36
+ /** How one channel interpolates, where its values are not numbers or number arrays. */
37
+ lerpBy?: (channel: keyof O) => ((a: never, b: never, u: number) => unknown) | undefined;
38
+ /** The channels these stops were written against, which `cue` checks a mix's kit against. */
39
+ kit?: Partial<Kit<O>>;
40
+ }
41
+ /** The options a `keys` patch was built with, for an engine reading its stops rather than calling it. */
42
+ export declare function keysOptionsOf<I, O, S>(p: Patch<I, O, S>): KeysOptions<O> | undefined;
43
+ interface Point {
44
+ at: number;
45
+ value: unknown;
46
+ /** The curve into this point from the one before: the stop's own, else the channel's. */
47
+ ease: Curve | undefined;
48
+ }
49
+ /** One channel of a stop list, sorted and resolved once, so a read is a binary search. */
50
+ interface Track {
51
+ channel: string;
52
+ /** Every stop carrying this channel, by phase. */
53
+ all: Point[];
54
+ /** The same without stops at 0, which a `from: 'current'` base replaces. */
55
+ tail: Point[];
56
+ delay: number;
57
+ lerp: ((a: never, b: never, u: number) => unknown) | undefined;
58
+ }
59
+ export interface Built {
60
+ tracks: Track[];
61
+ period: number;
62
+ }
63
+ /** Evaluates built stops into `out`; a channel with nothing to read at this phase is left undefined. */
64
+ export declare function readKeys(built: Built, phase: number, out: Record<string, unknown>, base?: Record<string, unknown>): Record<string, unknown>;
65
+ /** The built form of a patch's stops, made on first ask for a patch `keys()` did not build. */
66
+ export declare function builtOf<I, O, S>(p: Patch<I, O, S>): Built;
67
+ /** Evaluates a stop list into a fresh delta. */
68
+ export declare function evalKeys<O>(stops: readonly Keyframe<O>[], writes: readonly (keyof O)[], phase: number, period: number, opts: KeysOptions<O>, base?: Partial<O>): Partial<O>;
69
+ /**
70
+ * The declarative form. klieg's transition sugar and wod's keyframes.
71
+ *
72
+ * @category patch
73
+ */
74
+ export declare function keys<I, O>(period: number, stops: readonly Keyframe<O>[], opts?: KeysOptions<O>): Patch<I, O, void>;
75
+ export {};
package/dist/patch.js ADDED
@@ -0,0 +1,149 @@
1
+ import { curve } from './easing.js';
2
+ /**
3
+ * The procedural form. Flicker, roving, a hash walk, a starter strike.
4
+ *
5
+ * @category patch
6
+ */
7
+ export function patch(period, at, opts) {
8
+ const writes = opts.writes ?? (opts.kit ? Object.keys(opts.kit) : undefined);
9
+ if (writes === undefined)
10
+ throw new Error('blits: a patch needs writes, or a kit to take them from');
11
+ return {
12
+ form: 'fn',
13
+ period,
14
+ writes,
15
+ kit: opts.kit,
16
+ reads: opts.reads,
17
+ at,
18
+ state: opts.state,
19
+ step: opts.step,
20
+ };
21
+ }
22
+ const options = new WeakMap();
23
+ /** The options a `keys` patch was built with, for an engine reading its stops rather than calling it. */
24
+ export function keysOptionsOf(p) {
25
+ return options.get(p);
26
+ }
27
+ const linear = (a, b, u) => a + (b - a) * u;
28
+ function interpolate(a, b, u) {
29
+ if (typeof a === 'number' && typeof b === 'number')
30
+ return linear(a, b, u);
31
+ if (Array.isArray(a) && Array.isArray(b)) {
32
+ const out = new Array(a.length);
33
+ for (let i = 0; i < a.length; i++)
34
+ out[i] = interpolate(a[i], b[i], u);
35
+ return out;
36
+ }
37
+ return u < 0.5 ? a : b;
38
+ }
39
+ function build(stops, writes, period, opts) {
40
+ const fallback = opts.ease === undefined ? undefined : curve(opts.ease);
41
+ const tracks = [];
42
+ for (const channel of writes) {
43
+ const own = opts.easeBy?.(channel);
44
+ const trackEase = own === undefined ? fallback : curve(own);
45
+ const all = [];
46
+ for (const stop of stops) {
47
+ const value = stop.delta[channel];
48
+ if (value === undefined)
49
+ continue;
50
+ all.push({
51
+ at: stop.at,
52
+ value,
53
+ ease: stop.ease === undefined ? trackEase : curve(stop.ease),
54
+ });
55
+ }
56
+ all.sort((x, y) => x.at - y.at);
57
+ tracks.push({
58
+ channel: channel,
59
+ all,
60
+ tail: all.filter((pt) => pt.at !== 0),
61
+ delay: opts.delayBy?.(channel) ?? 0,
62
+ lerp: opts.lerpBy?.(channel),
63
+ });
64
+ }
65
+ return { tracks, period };
66
+ }
67
+ /**
68
+ * Reads one track at a phase. `base` stands in for the value at phase 0, which is how
69
+ * `from: 'current'` starts a voice wherever the subject already is.
70
+ */
71
+ function read(track, phase, base) {
72
+ const pts = base === undefined ? track.all : track.tail;
73
+ const o = base === undefined ? 0 : 1;
74
+ const n = pts.length + o;
75
+ if (n === 0)
76
+ return undefined;
77
+ const firstAt = o === 1 ? 0 : pts[0].at;
78
+ if (phase <= firstAt)
79
+ return o === 1 ? base : pts[0].value;
80
+ const last = pts[pts.length - 1];
81
+ if (last === undefined || phase >= last.at)
82
+ return last === undefined ? base : last.value;
83
+ // The first point at or past phase; the segment ends there.
84
+ let lo = 1;
85
+ let hi = n - 1;
86
+ while (lo < hi) {
87
+ const mid = (lo + hi) >> 1;
88
+ if (pts[mid - o].at >= phase)
89
+ hi = mid;
90
+ else
91
+ lo = mid + 1;
92
+ }
93
+ const b = pts[lo - o];
94
+ const aAt = lo - 1 < o ? 0 : pts[lo - 1 - o].at;
95
+ const aValue = lo - 1 < o ? base : pts[lo - 1 - o].value;
96
+ const u = (phase - aAt) / (b.at - aAt);
97
+ const eased = b.ease ? b.ease(u) : u;
98
+ return track.lerp
99
+ ? track.lerp(aValue, b.value, eased)
100
+ : interpolate(aValue, b.value, eased);
101
+ }
102
+ /** Evaluates built stops into `out`; a channel with nothing to read at this phase is left undefined. */
103
+ export function readKeys(built, phase, out, base) {
104
+ const period = built.period;
105
+ for (const track of built.tracks) {
106
+ const delay = track.delay;
107
+ const shifted = delay === 0 || period === 0 ? phase : Math.max(0, (phase * period - delay) / period);
108
+ const value = read(track, shifted, base?.[track.channel]);
109
+ if (value !== undefined)
110
+ out[track.channel] = value;
111
+ else if (out[track.channel] !== undefined)
112
+ out[track.channel] = undefined;
113
+ }
114
+ return out;
115
+ }
116
+ const cache = new WeakMap();
117
+ /** The built form of a patch's stops, made on first ask for a patch `keys()` did not build. */
118
+ export function builtOf(p) {
119
+ let held = cache.get(p);
120
+ if (held === undefined) {
121
+ held = build(p.keys, p.writes, p.period, keysOptionsOf(p) ?? {});
122
+ cache.set(p, held);
123
+ }
124
+ return held;
125
+ }
126
+ /** Evaluates a stop list into a fresh delta. */
127
+ export function evalKeys(stops, writes, phase, period, opts, base) {
128
+ return readKeys(build(stops, writes, period, opts), phase, {}, base);
129
+ }
130
+ /**
131
+ * The declarative form. klieg's transition sugar and wod's keyframes.
132
+ *
133
+ * @category patch
134
+ */
135
+ export function keys(period, stops, opts = {}) {
136
+ const writes = [...new Set(stops.flatMap((s) => Object.keys(s.delta)))];
137
+ const made = build(stops, writes, period, opts);
138
+ const p = {
139
+ form: 'keys',
140
+ period,
141
+ writes,
142
+ kit: opts.kit,
143
+ keys: stops,
144
+ at: (phase) => readKeys(made, phase, {}),
145
+ };
146
+ options.set(p, opts);
147
+ cache.set(p, made);
148
+ return p;
149
+ }
@@ -0,0 +1,50 @@
1
+ import type { Signal } from './types.js';
2
+ /**
3
+ * The loudest of several signals.
4
+ *
5
+ * @category signal
6
+ */
7
+ export declare function peak<I>(...signals: readonly Signal<I>[]): Signal<I>;
8
+ /**
9
+ * A signal the host writes. klieg's `level`.
10
+ *
11
+ * @category signal
12
+ */
13
+ export declare function level<I>(initial?: number): Signal<I> & {
14
+ set(v: number): void;
15
+ };
16
+ /**
17
+ * The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
18
+ * unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
19
+ * handed the same slew each follow on their own.
20
+ *
21
+ * @category signal
22
+ */
23
+ export declare function slew<I>(of: Signal<I>, { riseMs, fallMs }: {
24
+ riseMs?: number;
25
+ fallMs?: number;
26
+ }): Signal<I>;
27
+ /**
28
+ * The exponential counterpart to `slew`: closes the same fraction of the distance to its input in
29
+ * every equal stretch of time, `1 − exp(−gap / ms)`, so it reads the same however the frames that
30
+ * sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
31
+ * one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
32
+ * subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
33
+ *
34
+ * @category signal
35
+ */
36
+ export declare function lag<I>(of: Signal<I>, { riseMs, fallMs, floor }: {
37
+ riseMs?: number;
38
+ fallMs?: number;
39
+ floor?: number;
40
+ }): Signal<I>;
41
+ /**
42
+ * The other half of the pair: a band, not an edge. It drops at `off`, passes at `on`, and between
43
+ * the two it holds whatever it last did, so an input resting on a threshold cannot chatter.
44
+ *
45
+ * @category signal
46
+ */
47
+ export declare function gate<I>(of: Signal<I>, band: {
48
+ on: number;
49
+ off: number;
50
+ } | number): Signal<I>;
@@ -0,0 +1,111 @@
1
+ const inputOf = (signals) => signals.some((s) => s.input);
2
+ const marked = (read, input) => (input ? Object.assign(read, { input: true }) : read);
3
+ /**
4
+ * The loudest of several signals.
5
+ *
6
+ * @category signal
7
+ */
8
+ export function peak(...signals) {
9
+ return marked((subject, setting) => {
10
+ let out = 0;
11
+ for (const s of signals) {
12
+ const v = s(subject, setting);
13
+ if (v > out)
14
+ out = v;
15
+ }
16
+ return out;
17
+ }, inputOf(signals));
18
+ }
19
+ /**
20
+ * A signal the host writes. klieg's `level`.
21
+ *
22
+ * @category signal
23
+ */
24
+ export function level(initial = 0) {
25
+ let value = initial;
26
+ return Object.assign(() => value, {
27
+ input: true,
28
+ set(v) {
29
+ value = v;
30
+ },
31
+ });
32
+ }
33
+ /**
34
+ * The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
35
+ * unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
36
+ * handed the same slew each follow on their own.
37
+ *
38
+ * @category signal
39
+ */
40
+ export function slew(of, { riseMs = 0, fallMs = 0 }) {
41
+ const read = (subject, setting) => {
42
+ const target = of(subject, setting);
43
+ const held = setting.keep(read, () => ({ value: target, seen: Number.NaN }));
44
+ if (setting.timestamp === held.seen)
45
+ return held.value;
46
+ let next = target;
47
+ if (!Number.isNaN(held.seen) && Number.isFinite(setting.dt)) {
48
+ const gap = target - held.value;
49
+ const ms = gap > 0 ? riseMs : fallMs;
50
+ if (ms > 0) {
51
+ const step = (setting.timestamp - held.seen) / ms;
52
+ next = gap > 0 ? Math.min(target, held.value + step) : Math.max(target, held.value - step);
53
+ }
54
+ }
55
+ held.value = next;
56
+ held.seen = setting.timestamp;
57
+ return next;
58
+ };
59
+ return marked(read, inputOf([of]));
60
+ }
61
+ /**
62
+ * The exponential counterpart to `slew`: closes the same fraction of the distance to its input in
63
+ * every equal stretch of time, `1 − exp(−gap / ms)`, so it reads the same however the frames that
64
+ * sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
65
+ * one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
66
+ * subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
67
+ *
68
+ * @category signal
69
+ */
70
+ export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6 }) {
71
+ const read = (subject, setting) => {
72
+ const target = of(subject, setting);
73
+ const held = setting.keep(read, () => ({ value: target, seen: Number.NaN }));
74
+ if (setting.timestamp === held.seen)
75
+ return held.value;
76
+ let next = target;
77
+ if (!Number.isNaN(held.seen) && Number.isFinite(setting.dt)) {
78
+ const ms = target > held.value ? riseMs : fallMs;
79
+ if (ms > 0) {
80
+ const gap = setting.timestamp - held.seen;
81
+ next = held.value + (target - held.value) * (1 - Math.exp(-gap / ms));
82
+ if (Math.abs(target - next) <= floor)
83
+ next = target;
84
+ }
85
+ }
86
+ held.value = next;
87
+ held.seen = setting.timestamp;
88
+ return next;
89
+ };
90
+ return marked(read, inputOf([of]));
91
+ }
92
+ /**
93
+ * The other half of the pair: a band, not an edge. It drops at `off`, passes at `on`, and between
94
+ * the two it holds whatever it last did, so an input resting on a threshold cannot chatter.
95
+ *
96
+ * @category signal
97
+ */
98
+ export function gate(of, band) {
99
+ const on = typeof band === 'number' ? band : band.on;
100
+ const off = typeof band === 'number' ? band : band.off;
101
+ const read = (subject, setting) => {
102
+ const input = of(subject, setting);
103
+ const held = setting.keep(read, () => ({ on: input >= on, seen: Number.NaN }));
104
+ if (setting.timestamp !== held.seen) {
105
+ held.on = input >= on ? true : input <= off ? false : held.on;
106
+ held.seen = setting.timestamp;
107
+ }
108
+ return held.on ? 1 : 0;
109
+ };
110
+ return marked(read, inputOf([of]));
111
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
3
+ * drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
4
+ * goes in a Map that only `drop` and `clear` empty, which is why a host keying by value should
5
+ * call `drop`.
6
+ */
7
+ export declare class Store<K, V> {
8
+ private readonly weak;
9
+ private readonly strong;
10
+ get(key: K): V | undefined;
11
+ set(key: K, value: V): void;
12
+ has(key: K): boolean;
13
+ delete(key: K): void;
14
+ clear(): void;
15
+ }
package/dist/store.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
3
+ * drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
4
+ * goes in a Map that only `drop` and `clear` empty, which is why a host keying by value should
5
+ * call `drop`.
6
+ */
7
+ export class Store {
8
+ weak = new WeakMap();
9
+ strong = new Map();
10
+ get(key) {
11
+ return typeof key === 'object' && key !== null
12
+ ? this.weak.get(key)
13
+ : this.strong.get(key);
14
+ }
15
+ set(key, value) {
16
+ if (typeof key === 'object' && key !== null)
17
+ this.weak.set(key, value);
18
+ else
19
+ this.strong.set(key, value);
20
+ }
21
+ has(key) {
22
+ return this.get(key) !== undefined;
23
+ }
24
+ delete(key) {
25
+ if (typeof key === 'object' && key !== null)
26
+ this.weak.delete(key);
27
+ else
28
+ this.strong.delete(key);
29
+ }
30
+ clear() {
31
+ this.strong.clear();
32
+ }
33
+ }