@msb235/blits 0.2.1 → 0.4.0

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,22 @@
1
+ /** What a number's subject is once it has none: released, or let go by the host. */
2
+ export declare const absent: unique symbol;
3
+ /**
4
+ * Hands each subject a small number to index flat arrays by, and takes it back when the subject is
5
+ * released or, for an object, when the host lets it go. Objects are held weakly, as `Store` holds
6
+ * them, so numbering one never keeps it alive.
7
+ */
8
+ export declare class Numbers<I> {
9
+ private readonly gone;
10
+ private next;
11
+ private readonly free;
12
+ private readonly refs;
13
+ private live;
14
+ private readonly tokens;
15
+ private readonly registry;
16
+ constructor(gone: (slot: number) => void);
17
+ /** One past the highest number handed out, so a loop over 0..size meets every live one. */
18
+ get size(): number;
19
+ take(subject: I): number;
20
+ subject(slot: number): I | typeof absent;
21
+ release(slot: number): void;
22
+ }
@@ -0,0 +1,64 @@
1
+ /** What a number's subject is once it has none: released, or let go by the host. */
2
+ export const absent = Symbol('absent');
3
+ /**
4
+ * Hands each subject a small number to index flat arrays by, and takes it back when the subject is
5
+ * released or, for an object, when the host lets it go. Objects are held weakly, as `Store` holds
6
+ * them, so numbering one never keeps it alive.
7
+ */
8
+ export class Numbers {
9
+ gone;
10
+ next = 0;
11
+ free = [];
12
+ refs = [];
13
+ live = new Uint8Array(0);
14
+ tokens = [];
15
+ registry = new FinalizationRegistry((slot) => this.release(slot));
16
+ constructor(gone) {
17
+ this.gone = gone;
18
+ }
19
+ /** One past the highest number handed out, so a loop over 0..size meets every live one. */
20
+ get size() {
21
+ return this.next;
22
+ }
23
+ take(subject) {
24
+ const slot = this.free.pop() ?? this.next++;
25
+ if (slot >= this.live.length) {
26
+ const grown = new Uint8Array(Math.max(64, this.live.length * 2, slot + 1));
27
+ grown.set(this.live);
28
+ this.live = grown;
29
+ }
30
+ this.live[slot] = 1;
31
+ if (typeof subject === 'object' && subject !== null) {
32
+ this.refs[slot] = new WeakRef(subject);
33
+ const token = {};
34
+ this.tokens[slot] = token;
35
+ this.registry.register(subject, slot, token);
36
+ }
37
+ else
38
+ this.refs[slot] = subject;
39
+ return slot;
40
+ }
41
+ subject(slot) {
42
+ if (this.live[slot] !== 1)
43
+ return absent;
44
+ const ref = this.refs[slot];
45
+ if (ref instanceof WeakRef) {
46
+ const s = ref.deref();
47
+ return s === undefined ? absent : s;
48
+ }
49
+ return ref;
50
+ }
51
+ release(slot) {
52
+ if (this.live[slot] !== 1)
53
+ return;
54
+ this.live[slot] = 0;
55
+ const token = this.tokens[slot];
56
+ if (token !== undefined) {
57
+ this.registry.unregister(token);
58
+ this.tokens[slot] = undefined;
59
+ }
60
+ this.refs[slot] = undefined;
61
+ this.gone(slot);
62
+ this.free.push(slot);
63
+ }
64
+ }
package/dist/patch.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { type LerpInto } from './channels.js';
1
2
  import { type Curve } from './easing.js';
2
- import type { Easing, Keyframe, Kit, Patch, Setting } from './types.js';
3
+ import type { Channel, Easing, Keyframe, Kit, Patch, Setting } from './types.js';
3
4
  /**
4
5
  * What `patch` takes besides its period and its function.
5
6
  *
@@ -64,6 +65,16 @@ export interface Built {
64
65
  tracks: Track[];
65
66
  period: number;
66
67
  }
68
+ /**
69
+ * Arrays a reader lets keyed reads interpolate into, by track. Each only ever holds what a read made,
70
+ * never a stop's or a base's, and a delta read through it is good until the next read through it.
71
+ */
72
+ export type Scratch = (unknown[] | undefined)[];
73
+ /**
74
+ * Per track, the in-place form of the `lerp` a read takes, where it is the mix channel's own and the
75
+ * channel has one. `channels` holds the mix's channel per track, in `writes` order.
76
+ */
77
+ export declare function intosOf(built: Built, channels: readonly Channel<unknown>[]): (LerpInto | undefined)[];
67
78
  /**
68
79
  * Evaluates built stops into `out`; a channel with nothing to read at this phase is left undefined.
69
80
  * `lerps` holds the mix's channel `lerp` per track, in `writes` order, for a track the patch did not
@@ -71,7 +82,7 @@ export interface Built {
71
82
  */
72
83
  export declare function readKeys(built: Built, phase: number, out: Record<string, unknown>, base?: Record<string, unknown>, lerps?: readonly (Lerp | undefined)[],
73
84
  /** Per channel, how fast a retargeted subject was moving, units per ms. */
74
- slopes?: Record<string, unknown>): Record<string, unknown>;
85
+ slopes?: Record<string, unknown>, intos?: readonly (LerpInto | undefined)[], scratch?: Scratch): Record<string, unknown>;
75
86
  /** The built form of a patch's stops, made on first ask for a patch `keys()` did not build. */
76
87
  export declare function builtOf<I, O, S>(p: Patch<I, O, S>): Built;
77
88
  /** Evaluates a stop list into a fresh delta. */
package/dist/patch.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { lerpInto } from './channels.js';
1
2
  import { curve } from './easing.js';
2
3
  /**
3
4
  * The procedural form. Flicker, roving, a hash walk, a starter strike.
@@ -68,7 +69,9 @@ function build(stops, writes, period, opts) {
68
69
  * Reads one track at a phase. `base` stands in for the value at phase 0, which is how
69
70
  * `from: 'current'` starts a voice wherever the subject already is.
70
71
  */
71
- function read(track, phase, base, lerp, slope) {
72
+ function read(track, phase, base, lerp,
73
+ /** Units per ms, for a retarget's first segment. */
74
+ slope, period, into, reuse) {
72
75
  const pts = base === undefined ? track.all : track.tail;
73
76
  const o = base === undefined ? 0 : 1;
74
77
  const n = pts.length + o;
@@ -95,26 +98,50 @@ function read(track, phase, base, lerp, slope) {
95
98
  const aValue = lo - 1 < o ? base : pts[lo - 1 - o].value;
96
99
  const u = (phase - aAt) / (b.at - aAt);
97
100
  const eased = b.ease ? b.ease(u) : u;
98
- const by = track.lerp ?? lerp;
99
- const value = by
100
- ? by(aValue, b.value, eased)
101
- : interpolate(aValue, b.value, eased);
101
+ let value;
102
+ if (into !== undefined) {
103
+ value = into(reuse, aValue, b.value, eased);
104
+ lastRead.wrote = value;
105
+ }
106
+ else {
107
+ const by = track.lerp ?? lerp;
108
+ value = by ? by(aValue, b.value, eased) : interpolate(aValue, b.value, eased);
109
+ }
102
110
  // Leaving a retarget's base, bend the first segment so it starts at the slope the subject had:
103
111
  // add c·u(1−u)², which is 0 at both ends, flat at the end, and fixes the slope at the start.
104
112
  if (slope === undefined || lo - 1 >= o)
105
113
  return value;
106
114
  const len = b.at - aAt;
107
115
  const e0 = b.ease ? b.ease(1e-6) / 1e-6 : 1;
108
- const bend = (s, a, z) => (s * len - (z - a) * e0) * u * (1 - u) * (1 - u);
116
+ const bend = (s, a, z) => (s * period * len - (z - a) * e0) * u * (1 - u) * (1 - u);
109
117
  if (typeof value === 'number' && typeof slope === 'number')
110
118
  return value + bend(slope, aValue, b.value);
111
- if (Array.isArray(value) && Array.isArray(slope))
112
- return value.map((v, i) => typeof v === 'number' && typeof slope[i] === 'number'
113
- ? v +
114
- bend(slope[i], aValue[i], b.value[i])
115
- : v);
119
+ if (Array.isArray(value) && Array.isArray(slope)) {
120
+ const out = value === lastRead.wrote ? value : new Array(value.length);
121
+ for (let i = 0; i < value.length; i++) {
122
+ const v = value[i];
123
+ const s = slope[i];
124
+ out[i] =
125
+ typeof v === 'number' && typeof s === 'number'
126
+ ? v + bend(s, aValue[i], b.value[i])
127
+ : v;
128
+ }
129
+ return out;
130
+ }
116
131
  return value;
117
132
  }
133
+ /** The array the last `read` interpolated into, which only its caller's record holds. */
134
+ const lastRead = { wrote: undefined };
135
+ /**
136
+ * Per track, the in-place form of the `lerp` a read takes, where it is the mix channel's own and the
137
+ * channel has one. `channels` holds the mix's channel per track, in `writes` order.
138
+ */
139
+ export function intosOf(built, channels) {
140
+ return built.tracks.map((track, i) => {
141
+ const channel = channels[i];
142
+ return track.lerp === undefined || track.lerp === channel.lerp ? lerpInto(channel) : undefined;
143
+ });
144
+ }
118
145
  /**
119
146
  * Evaluates built stops into `out`; a channel with nothing to read at this phase is left undefined.
120
147
  * `lerps` holds the mix's channel `lerp` per track, in `writes` order, for a track the patch did not
@@ -122,19 +149,18 @@ function read(track, phase, base, lerp, slope) {
122
149
  */
123
150
  export function readKeys(built, phase, out, base, lerps,
124
151
  /** Per channel, how fast a retargeted subject was moving, units per ms. */
125
- slopes) {
152
+ slopes, intos, scratch) {
126
153
  const period = built.period;
127
154
  for (let i = 0; i < built.tracks.length; i++) {
128
155
  const track = built.tracks[i];
129
156
  const delay = track.delay;
130
157
  const shifted = delay === 0 || period === 0 ? phase : Math.max(0, (phase * period - delay) / period);
131
158
  const perMs = slopes?.[track.channel];
132
- const slope = perMs === undefined || period === 0
133
- ? undefined
134
- : typeof perMs === 'number'
135
- ? perMs * period
136
- : perMs.map((s) => s * period);
137
- const value = read(track, shifted, base?.[track.channel], lerps?.[i], slope);
159
+ const into = scratch === undefined ? undefined : intos?.[i];
160
+ lastRead.wrote = undefined;
161
+ const value = read(track, shifted, base?.[track.channel], lerps?.[i], period === 0 ? undefined : perMs, period, into, into === undefined ? undefined : scratch[i]);
162
+ if (into !== undefined && value === lastRead.wrote)
163
+ scratch[i] = value;
138
164
  if (value !== undefined)
139
165
  out[track.channel] = value;
140
166
  else if (out[track.channel] !== undefined)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Internal: what kind of read the mix is making right now, for patches that keep state of their own
3
+ * outside the mix. Not exported from the package.
4
+ */
5
+ export declare const reading: {
6
+ /** False while a projection reads: a patch must not commit anything then. */
7
+ live: boolean;
8
+ /**
9
+ * The earliest voice time a read may still ask for; a patch keeping a history may drop what is
10
+ * older. Infinity where the mix keeps none, so only what plays now is kept.
11
+ */
12
+ horizon: number;
13
+ /**
14
+ * How many entries `setting.keep` has made in any mix, so a patch call that made one shows
15
+ * without looking into the record it was made on.
16
+ */
17
+ kept: number;
18
+ /**
19
+ * How many retargets and pushes any motion patch has taken, so a mix can tell one came mid-frame.
20
+ * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
+ */
22
+ moved: number;
23
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Internal: what kind of read the mix is making right now, for patches that keep state of their own
3
+ * outside the mix. Not exported from the package.
4
+ */
5
+ export const reading = {
6
+ /** False while a projection reads: a patch must not commit anything then. */
7
+ live: true,
8
+ /**
9
+ * The earliest voice time a read may still ask for; a patch keeping a history may drop what is
10
+ * older. Infinity where the mix keeps none, so only what plays now is kept.
11
+ */
12
+ horizon: Number.POSITIVE_INFINITY,
13
+ /**
14
+ * How many entries `setting.keep` has made in any mix, so a patch call that made one shows
15
+ * without looking into the record it was made on.
16
+ */
17
+ kept: 0,
18
+ /**
19
+ * How many retargets and pushes any motion patch has taken, so a mix can tell one came mid-frame.
20
+ * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
+ */
22
+ moved: 0,
23
+ };