@msb235/blits 0.7.0 → 0.8.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.
Files changed (156) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/README.md +22 -4
  3. package/dist/book.js +4 -4
  4. package/dist/chain.d.ts +1 -1
  5. package/dist/chain.js +57 -54
  6. package/dist/channels.d.ts +26 -5
  7. package/dist/channels.js +12 -8
  8. package/dist/clock.d.ts +8 -1
  9. package/dist/clock.js +14 -3
  10. package/dist/clone.d.ts +3 -0
  11. package/dist/clone.js +16 -1
  12. package/dist/closed.d.ts +1 -1
  13. package/dist/closed.js +3 -2
  14. package/dist/color.d.ts +0 -11
  15. package/dist/color.js +4 -24
  16. package/dist/columns.js +0 -1
  17. package/dist/copyout.d.ts +6 -0
  18. package/dist/copyout.js +38 -0
  19. package/dist/crowd.d.ts +7 -5
  20. package/dist/crowd.js +32 -18
  21. package/dist/cue.d.ts +3 -1
  22. package/dist/cue.js +33 -20
  23. package/dist/departed.d.ts +34 -0
  24. package/dist/departed.js +73 -0
  25. package/dist/easing.d.ts +5 -0
  26. package/dist/easing.js +31 -2
  27. package/dist/everyone.d.ts +56 -0
  28. package/dist/everyone.js +221 -0
  29. package/dist/fade.d.ts +8 -1
  30. package/dist/fade.js +36 -6
  31. package/dist/fill.d.ts +22 -13
  32. package/dist/fill.js +104 -41
  33. package/dist/fit.js +2 -1
  34. package/dist/fold.d.ts +37 -0
  35. package/dist/fold.js +213 -28
  36. package/dist/gather.d.ts +15 -9
  37. package/dist/gather.js +60 -25
  38. package/dist/gone.d.ts +32 -0
  39. package/dist/gone.js +144 -0
  40. package/dist/handle.d.ts +16 -5
  41. package/dist/handle.js +56 -6
  42. package/dist/held.d.ts +8 -5
  43. package/dist/held.js +56 -58
  44. package/dist/history.d.ts +17 -13
  45. package/dist/history.js +49 -28
  46. package/dist/hosts.js +57 -9
  47. package/dist/index.d.ts +9 -4
  48. package/dist/index.js +5 -2
  49. package/dist/keyfill.d.ts +7 -0
  50. package/dist/keyfill.js +124 -0
  51. package/dist/keyrows.d.ts +1 -0
  52. package/dist/keyrows.js +1 -1
  53. package/dist/lane.d.ts +32 -2
  54. package/dist/lane.js +5 -0
  55. package/dist/lanes.d.ts +32 -8
  56. package/dist/lanes.js +69 -37
  57. package/dist/leavings.d.ts +26 -0
  58. package/dist/leavings.js +70 -0
  59. package/dist/meet.js +14 -8
  60. package/dist/mixer.d.ts +43 -7
  61. package/dist/mixer.js +60 -11
  62. package/dist/motion.d.ts +2 -0
  63. package/dist/motion.js +4 -1
  64. package/dist/motions.d.ts +22 -15
  65. package/dist/motions.js +40 -34
  66. package/dist/move.js +31 -20
  67. package/dist/moved.d.ts +11 -0
  68. package/dist/moved.js +29 -0
  69. package/dist/origin.d.ts +19 -0
  70. package/dist/origin.js +68 -0
  71. package/dist/owed.d.ts +4 -4
  72. package/dist/owed.js +8 -8
  73. package/dist/owner.d.ts +1 -1
  74. package/dist/owner.js +2 -2
  75. package/dist/pace.d.ts +13 -5
  76. package/dist/pace.js +21 -8
  77. package/dist/pack.d.ts +3 -1
  78. package/dist/pack.js +15 -24
  79. package/dist/paging.d.ts +8 -2
  80. package/dist/paging.js +44 -4
  81. package/dist/patch.d.ts +14 -9
  82. package/dist/patch.js +83 -21
  83. package/dist/place.js +188 -30
  84. package/dist/project.d.ts +12 -1
  85. package/dist/project.js +101 -83
  86. package/dist/pull.d.ts +2 -0
  87. package/dist/pull.js +8 -0
  88. package/dist/qualify.js +10 -2
  89. package/dist/reading.d.ts +5 -0
  90. package/dist/reading.js +5 -0
  91. package/dist/record.d.ts +9 -0
  92. package/dist/record.js +42 -0
  93. package/dist/relink.d.ts +11 -5
  94. package/dist/relink.js +7 -5
  95. package/dist/restored.d.ts +41 -0
  96. package/dist/restored.js +91 -0
  97. package/dist/revive.d.ts +10 -3
  98. package/dist/revive.js +31 -23
  99. package/dist/roster.d.ts +11 -0
  100. package/dist/roster.js +46 -0
  101. package/dist/rotation.d.ts +29 -0
  102. package/dist/rotation.js +91 -0
  103. package/dist/rows.d.ts +5 -5
  104. package/dist/rows.js +5 -5
  105. package/dist/sample.d.ts +3 -2
  106. package/dist/sample.js +32 -15
  107. package/dist/scored.d.ts +57 -0
  108. package/dist/scored.js +196 -0
  109. package/dist/seek.js +97 -114
  110. package/dist/shared.d.ts +6 -0
  111. package/dist/shared.js +15 -0
  112. package/dist/signals.d.ts +24 -3
  113. package/dist/signals.js +50 -11
  114. package/dist/spans.js +7 -0
  115. package/dist/stack.d.ts +11 -0
  116. package/dist/stack.js +47 -0
  117. package/dist/standing.d.ts +10 -0
  118. package/dist/standing.js +79 -0
  119. package/dist/store.d.ts +3 -3
  120. package/dist/store.js +3 -3
  121. package/dist/strays.js +3 -0
  122. package/dist/tape.d.ts +8 -5
  123. package/dist/tape.js +21 -13
  124. package/dist/testing.d.ts +35 -0
  125. package/dist/testing.js +116 -0
  126. package/dist/touch.d.ts +6 -0
  127. package/dist/touch.js +28 -0
  128. package/dist/transport.d.ts +53 -6
  129. package/dist/transport.js +102 -20
  130. package/dist/turns.d.ts +18 -0
  131. package/dist/turns.js +72 -0
  132. package/dist/tweened.js +1 -1
  133. package/dist/types/channel.d.ts +46 -0
  134. package/dist/types/channel.js +1 -0
  135. package/dist/types/history.d.ts +139 -0
  136. package/dist/types/history.js +1 -0
  137. package/dist/types/mix.d.ts +336 -0
  138. package/dist/types/mix.js +1 -0
  139. package/dist/types/patch.d.ts +187 -0
  140. package/dist/types/patch.js +1 -0
  141. package/dist/types/place.d.ts +157 -0
  142. package/dist/types/place.js +1 -0
  143. package/dist/types/transport.d.ts +71 -0
  144. package/dist/types/transport.js +1 -0
  145. package/dist/types/voice.d.ts +323 -0
  146. package/dist/types/voice.js +1 -0
  147. package/dist/types.d.ts +7 -1162
  148. package/dist/unpage.d.ts +2 -2
  149. package/dist/unpage.js +55 -25
  150. package/dist/voice.d.ts +92 -22
  151. package/dist/voice.js +108 -43
  152. package/dist/wave.d.ts +42 -0
  153. package/dist/wave.js +62 -0
  154. package/dist/weigh.js +1 -1
  155. package/docs/schema.html +42 -36
  156. package/package.json +8 -3
package/dist/signals.js CHANGED
@@ -1,5 +1,16 @@
1
+ import { reading } from './reading.js';
2
+ /** Input signals whose every change bumps `reading.inputs`: `input`'s, and those built only on them. */
3
+ const told = new WeakSet();
4
+ /** Whether a change to an input signal reaches `reading.inputs`, so a lane fill can see it. */
5
+ export const heard = (signal) => told.has(signal);
1
6
  const inputOf = (signals) => signals.some((s) => s.input);
2
- const marked = (read, input) => (input ? Object.assign(read, { input: true }) : read);
7
+ const marked = (read, of) => {
8
+ if (!inputOf(of))
9
+ return read;
10
+ if (of.every((s) => !s.input || told.has(s)))
11
+ told.add(read);
12
+ return Object.assign(read, { input: true });
13
+ };
3
14
  /**
4
15
  * The loudest of several signals.
5
16
  *
@@ -14,19 +25,46 @@ export function peak(...signals) {
14
25
  out = v;
15
26
  }
16
27
  return out;
17
- }, inputOf(signals));
28
+ }, signals);
18
29
  }
30
+ const touch = () => {
31
+ reading.inputs++;
32
+ };
19
33
  /**
20
- * A signal the host writes. klieg's `level`.
34
+ * Marks `read` as a signal driven from outside the clock, which a host's own signal has to be for
35
+ * `assess` and a history's `inputs` to treat it as one. Two uses:
36
+ *
37
+ * `input(read)` is an input of the host's own: a pointer, a volume, a value in a store. The host
38
+ * calls `touch()` after each change to what `read` reads, and a voice weighted by the signal, or by
39
+ * `peak`, `slew`, `lag` or `gate` over it, keeps its lane. A change nobody reports shows once the
40
+ * mix's clock next moves.
41
+ *
42
+ * `input(read, of)` is a signal built on others, as a wrapper that clamps one is: `of` lists every
43
+ * signal `read` calls, and the result is an input where any of them is, and reports its changes
44
+ * where they all do.
45
+ *
46
+ * @category signal
47
+ */
48
+ export function input(read, of) {
49
+ if (of !== undefined)
50
+ return Object.assign(marked(read, of), { touch });
51
+ told.add(read);
52
+ return Object.assign(read, { input: true, touch });
53
+ }
54
+ /**
55
+ * A signal the host writes: `input` over one number, with `set` reporting each change. klieg's
56
+ * `level`.
21
57
  *
22
58
  * @category signal
23
59
  */
24
60
  export function level(initial = 0) {
25
61
  let value = initial;
26
- return Object.assign(() => value, {
27
- input: true,
62
+ return Object.assign(input(() => value), {
28
63
  set(v) {
64
+ if (Object.is(v, value))
65
+ return;
29
66
  value = v;
67
+ touch();
30
68
  },
31
69
  });
32
70
  }
@@ -38,7 +76,7 @@ const firstSight = (target, from, setting) => from === undefined || !Number.isFi
38
76
  ? { value: target, seen: Number.NaN }
39
77
  : { value: from, seen: setting.timestamp };
40
78
  /**
41
- * The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
79
+ * The package's linear decay primitive: follows its input at a rate limit, no faster than `riseMs` per
42
80
  * unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
43
81
  * handed the same slew each follow on their own. A subject starts on its input, or at `from`
44
82
  * unless `dt` is infinite.
@@ -56,7 +94,8 @@ export function slew(of, { riseMs = 0, fallMs = 0, from }) {
56
94
  const gap = target - held.value;
57
95
  const ms = gap > 0 ? riseMs : fallMs;
58
96
  if (ms > 0) {
59
- const step = (setting.timestamp - held.seen) / ms;
97
+ // No longer than `dt`, which `maxDt` caps, as every step is.
98
+ const step = Math.min(setting.timestamp - held.seen, setting.dt) / ms;
60
99
  next = gap > 0 ? Math.min(target, held.value + step) : Math.max(target, held.value - step);
61
100
  }
62
101
  }
@@ -64,7 +103,7 @@ export function slew(of, { riseMs = 0, fallMs = 0, from }) {
64
103
  held.seen = setting.timestamp;
65
104
  return next;
66
105
  };
67
- return marked(read, inputOf([of]));
106
+ return marked(read, [of]);
68
107
  }
69
108
  /**
70
109
  * The exponential counterpart to `slew`: closes the same fraction of the distance to its input in
@@ -86,7 +125,7 @@ export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6, from, }) {
86
125
  if (!Number.isNaN(held.seen) && Number.isFinite(setting.dt)) {
87
126
  const ms = target > held.value ? riseMs : fallMs;
88
127
  if (ms > 0) {
89
- const gap = setting.timestamp - held.seen;
128
+ const gap = Math.min(setting.timestamp - held.seen, setting.dt);
90
129
  next = held.value + (target - held.value) * (1 - Math.exp(-gap / ms));
91
130
  if (Math.abs(target - next) <= floor)
92
131
  next = target;
@@ -96,7 +135,7 @@ export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6, from, }) {
96
135
  held.seen = setting.timestamp;
97
136
  return next;
98
137
  };
99
- return marked(read, inputOf([of]));
138
+ return marked(read, [of]);
100
139
  }
101
140
  /**
102
141
  * The other half of the pair: a band, not an edge. It drops at `off`, passes at `on`, and between
@@ -116,5 +155,5 @@ export function gate(of, band) {
116
155
  }
117
156
  return held.on ? 1 : 0;
118
157
  };
119
- return marked(read, inputOf([of]));
158
+ return marked(read, [of]);
120
159
  }
package/dist/spans.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { elapsedWith, retime } from './clock.js';
2
2
  import { retire } from './fade.js';
3
3
  import { defaultFit, layout, settle } from './fit.js';
4
+ import { reorigin } from './held.js';
4
5
  import { noted } from './history.js';
5
6
  import { nextFrame } from './move.js';
7
+ import { leave } from './origin.js';
6
8
  import { ownerReading } from './owner.js';
7
9
  /** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
8
10
  export class Fitting {
@@ -89,6 +91,7 @@ export function refit(mix, span) {
89
91
  kids.forEach((c, i) => {
90
92
  const rate = (c.spec.rate ?? 1) * plan.rate[i];
91
93
  const skipped = plan.skip[i] && c.holding === null;
94
+ c.skipped = skipped;
92
95
  let moved = false;
93
96
  if (c.state === 'pending') {
94
97
  const start = local + at[i];
@@ -99,23 +102,27 @@ export function refit(mix, span) {
99
102
  c.anchorElapsed = elapsed;
100
103
  c.rate = rate;
101
104
  c.ramp = null;
105
+ c.clocks = null;
102
106
  moved = true;
103
107
  }
104
108
  }
105
109
  else {
106
110
  if (skipped) {
111
+ leave(c, local);
107
112
  c.rebase(local);
108
113
  c.anchorElapsed = c.span;
109
114
  c.seeks++;
110
115
  moved = true;
111
116
  }
112
117
  if (c.rate !== rate || c.ramp !== null) {
118
+ leave(c, local);
113
119
  retime(c, local, rate, 0);
114
120
  moved = true;
115
121
  }
116
122
  }
117
123
  if (moved) {
118
124
  mix.frame = nextFrame();
125
+ reorigin(mix, c, mix.now);
119
126
  noted(mix, c);
120
127
  }
121
128
  });
@@ -0,0 +1,11 @@
1
+ import type { Kit } from './types.js';
2
+ /**
3
+ * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
4
+ * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
5
+ * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
6
+ * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
7
+ * a mix's default band first switches one on, and stays undefined where none is.
8
+ *
9
+ * @category channel
10
+ */
11
+ export declare function fold<O>(kit: Kit<O>, deltas: readonly Partial<O>[], weights?: readonly number[]): O;
package/dist/stack.js ADDED
@@ -0,0 +1,47 @@
1
+ import { copy } from './clone.js';
2
+ import { bound, merged } from './fold.js';
3
+ /** The weight a mix's default band first switches a rest-less channel on at. */
4
+ const ON = 0.6;
5
+ /**
6
+ * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
7
+ * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
8
+ * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
9
+ * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
10
+ * a mix's default band first switches one on, and stays undefined where none is.
11
+ *
12
+ * @category channel
13
+ */
14
+ export function fold(kit, deltas, weights) {
15
+ const pose = {};
16
+ const names = Object.keys(kit);
17
+ for (const key of names) {
18
+ const channel = kit[key];
19
+ const rest = channel.rest;
20
+ pose[key] = rest === undefined ? undefined : channel.copy ? channel.copy(rest) : copy(rest);
21
+ }
22
+ for (let i = 0; i < deltas.length; i++) {
23
+ const given = weights?.[i] ?? 1;
24
+ const w = given > 1 ? 1 : given;
25
+ if (!(w > 0))
26
+ continue;
27
+ const delta = deltas[i];
28
+ for (const key of names) {
29
+ const value = delta[key];
30
+ if (value === undefined)
31
+ continue;
32
+ const channel = kit[key];
33
+ if (channel.rest !== undefined && channel.scale)
34
+ pose[key] = channel.fold
35
+ ? channel.fold(pose[key], value, w)
36
+ : merged(channel, pose[key], channel.scale(value, w));
37
+ else if (w >= ON)
38
+ pose[key] = pose[key] === undefined ? copy(value) : merged(channel, pose[key], value);
39
+ }
40
+ }
41
+ for (const key of names) {
42
+ const channel = kit[key];
43
+ if (channel.bounds)
44
+ pose[key] = bound(channel, pose[key]);
45
+ }
46
+ return pose;
47
+ }
@@ -0,0 +1,10 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Cut, Transport } from './transport.js';
3
+ /**
4
+ * Throws unless a read back to mix time `t` is exact with no history: every voice plays as it was
5
+ * cued and reads the same whenever it is asked, and no mix on the transport has changed since `t`.
6
+ * A mix keeps only how it stands now, which is how it has stood since its last change.
7
+ */
8
+ export declare function stands(transport: Transport, t: number): void;
9
+ /** A mix's copy for a read back to `t` with no history: its voices as they stand, each at `t`. */
10
+ export declare function copyStanding<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>, t: number, cut: Cut): void;
@@ -0,0 +1,79 @@
1
+ import { ownBlends } from './blend.js';
2
+ import { relink } from './owner.js';
3
+ import { pin } from './place.js';
4
+ import { mirror, recall, stateAt } from './project.js';
5
+ import { recount } from './roster.js';
6
+ /**
7
+ * Why a voice cannot be read at an earlier time from how it stands now, or undefined where it can:
8
+ * what it shows depends on more than its clock as cued, so the frames it has played decided it.
9
+ */
10
+ function unfit(mix, voice) {
11
+ const patch = voice.patch;
12
+ const spec = voice.spec;
13
+ if (patch.state !== undefined || patch.step !== undefined)
14
+ return 'its patch keeps state';
15
+ if (voice.motion !== undefined)
16
+ return 'it plays a motion';
17
+ if (voice.keeping)
18
+ return 'its patch or its weight keeps state';
19
+ if (spec.from === 'current')
20
+ return 'it starts from the pose its subjects had';
21
+ if (voice.written)
22
+ return 'its handle has been written to';
23
+ if (spec.anchor !== undefined)
24
+ return 'an anchor places it';
25
+ if (voice.holding !== null)
26
+ return 'it is an owner';
27
+ if (voice.owner !== null)
28
+ return 'it plays under an owner';
29
+ // A rest-less channel switches on across a band, by the weights its voice has passed through.
30
+ const steady = typeof spec.weight !== 'function' &&
31
+ spec.locus === undefined &&
32
+ !((voice.fade.in ?? 0) > 0) &&
33
+ !((voice.fade.out ?? 0) > 0);
34
+ if (!steady && voice.slots.some((s) => mix.channels[s]?.rest === undefined))
35
+ return 'it writes a channel with no rest at a weight that moves';
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Throws unless a read back to mix time `t` is exact with no history: every voice plays as it was
40
+ * cued and reads the same whenever it is asked, and no mix on the transport has changed since `t`.
41
+ * A mix keeps only how it stands now, which is how it has stood since its last change.
42
+ */
43
+ export function stands(transport, t) {
44
+ const need = `blits: reading back to ${t} needs a mix made with history`;
45
+ if (transport.pace !== null)
46
+ throw new Error(`${need}: the mix's rate has been set`);
47
+ if (t < transport.settled)
48
+ throw new Error(`${need}: a mix changed or a voice left at ${transport.settled}, and without history a mix keeps only how it stands now`);
49
+ for (const mix of transport.members)
50
+ for (const voice of mix.cued) {
51
+ if (voice.state === 'done')
52
+ continue;
53
+ const why = unfit(mix, voice);
54
+ if (why !== undefined)
55
+ throw new Error(`${need}: voice ${voice.id} is cued, and ${why}`);
56
+ }
57
+ }
58
+ /** A mix's copy for a read back to `t` with no history: its voices as they stand, each at `t`. */
59
+ export function copyStanding(mix, c, t, cut) {
60
+ mirror(mix, c);
61
+ c.backward = true;
62
+ c.cued = mix.cued
63
+ .filter((v) => v.state !== 'done')
64
+ .map((v) => {
65
+ const copy = v.copy((subject) => recall(mix, v, subject, t, cut));
66
+ // A fade its own end began after `t` had not begun then.
67
+ if (copy.out !== null && copy.out.at > t)
68
+ copy.out = null;
69
+ const at = mix.pins?.get(v);
70
+ if (at !== undefined)
71
+ pin(c, copy, at);
72
+ return copy;
73
+ });
74
+ ownBlends(c.cued);
75
+ if (mix.owners !== null)
76
+ relink(c.cued);
77
+ stateAt(c, t);
78
+ recount(c);
79
+ }
package/dist/store.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
3
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`. Each map is made when first written, since a mix holds a store per voice and patch;
6
- * until then it is a shared empty one, so the field always holds a map.
4
+ * goes in a Map that only `delete` and `clear` empty, which is why a host keying by value should
5
+ * call `Mix.drop`. Each map is made when first written, since a mix holds a store per voice and
6
+ * patch; until then it is a shared empty one, so the field always holds a map.
7
7
  */
8
8
  export declare class Store<K, V> {
9
9
  private weak;
package/dist/store.js CHANGED
@@ -4,9 +4,9 @@ const noStrong = new Map();
4
4
  /**
5
5
  * Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
6
6
  * drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
7
- * goes in a Map that only `drop` and `clear` empty, which is why a host keying by value should
8
- * call `drop`. Each map is made when first written, since a mix holds a store per voice and patch;
9
- * until then it is a shared empty one, so the field always holds a map.
7
+ * goes in a Map that only `delete` and `clear` empty, which is why a host keying by value should
8
+ * call `Mix.drop`. Each map is made when first written, since a mix holds a store per voice and
9
+ * patch; until then it is a shared empty one, so the field always holds a map.
10
10
  */
11
11
  export class Store {
12
12
  weak = noWeak;
package/dist/strays.js CHANGED
@@ -42,6 +42,9 @@ export function motionOwner(mix) {
42
42
  now() {
43
43
  return mix.deref()?.now ?? Number.NaN;
44
44
  },
45
+ seq() {
46
+ return mix.deref()?.transport.seq ?? Number.NaN;
47
+ },
45
48
  changed(id, motion, subject, change) {
46
49
  const m = mix.deref();
47
50
  if (m === undefined)
package/dist/tape.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { Pace } from './pace.js';
2
2
  import type { Transport } from './transport.js';
3
3
  import type { Tape } from './types.js';
4
- /** The tape `history.tape` makes for a transport, stamping each call with the mix time it was made at. */
4
+ /**
5
+ * The tape `history.tape` makes for a transport, stamping each call with the host time it was made
6
+ * at: rate 0 holds mix time still across frames, and host time tells them apart.
7
+ */
5
8
  export declare function tapeOf(transport: Transport): Tape | undefined;
6
9
  /** Records a host call, which `again` makes once more when the mix plays past it after a seek. */
7
10
  export declare function record(on: {
@@ -14,8 +17,8 @@ export declare function hostAt(clock: {
14
17
  readonly pace: Pace | null;
15
18
  }, t: number): number;
16
19
  /**
17
- * Makes the recorded calls due by mix time `to()` again, each with every mix on the transport
18
- * moved to the time it was made at, as the host made it then. `to` is asked again after every
19
- * call, since a call may change the rate and so where a host time lands.
20
+ * Makes the recorded calls `due` by the host time each was made at again, each host time's in a
21
+ * frame of its own with every mix on the transport moved to where it stood then, as the host made
22
+ * them. Answers the host time of the last frame it played, NaN for none.
20
23
  */
21
- export declare function replay(transport: Transport, to: () => number): void;
24
+ export declare function replay(transport: Transport, due: (u: number) => boolean): number;
package/dist/tape.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { move } from './move.js';
2
2
  /** What a recorded call's `invert` gives: a mix goes back by restoring, never by undoing a call. */
3
3
  const undone = { apply() { }, invert: () => undone };
4
- /** The tape `history.tape` makes for a transport, stamping each call with the mix time it was made at. */
4
+ /**
5
+ * The tape `history.tape` makes for a transport, stamping each call with the host time it was made
6
+ * at: rate 0 holds mix time still across frames, and host time tells them apart.
7
+ */
5
8
  export function tapeOf(transport) {
6
9
  const make = transport.history?.tape;
7
10
  if (make === undefined)
8
11
  return undefined;
9
12
  return make(transport, {
10
- now: () => (Number.isNaN(transport.now) ? Number.NEGATIVE_INFINITY : transport.now),
13
+ now: () => (Number.isNaN(transport.u) ? Number.NEGATIVE_INFINITY : transport.u),
11
14
  branching: true,
12
15
  coalesceWindowMs: 0,
13
16
  });
@@ -31,24 +34,29 @@ export function hostAt(clock, t) {
31
34
  return clock.pace === null ? t : clock.pace.timeOf(t);
32
35
  }
33
36
  /**
34
- * Makes the recorded calls due by mix time `to()` again, each with every mix on the transport
35
- * moved to the time it was made at, as the host made it then. `to` is asked again after every
36
- * call, since a call may change the rate and so where a host time lands.
37
+ * Makes the recorded calls `due` by the host time each was made at again, each host time's in a
38
+ * frame of its own with every mix on the transport moved to where it stood then, as the host made
39
+ * them. Answers the host time of the last frame it played, NaN for none.
37
40
  */
38
- export function replay(transport, to) {
41
+ export function replay(transport, due) {
39
42
  const tape = transport.tape;
40
43
  if (tape === undefined)
41
- return;
44
+ return Number.NaN;
42
45
  const u = transport.u;
46
+ let last = Number.NaN;
43
47
  try {
44
48
  for (;;) {
45
49
  const at = tape.timestampAt(tape.undoDepth());
46
- if (at === undefined || !(at <= to()))
47
- return;
48
- if (at > transport.now) {
49
- transport.u = hostAt(transport, at);
50
- for (const m of transport.members)
51
- move(m, at);
50
+ if (at === undefined || !due(at))
51
+ return last;
52
+ if (at !== last) {
53
+ last = at;
54
+ transport.u = at;
55
+ const now = transport.pace === null ? at : transport.pace.reading(at);
56
+ if (now > transport.now)
57
+ for (const m of transport.members)
58
+ move(m, now);
59
+ transport.tick(transport.now);
52
60
  }
53
61
  transport.replaying = true;
54
62
  try {
@@ -0,0 +1,35 @@
1
+ import type { Kit, Patch, Setting } from './types.js';
2
+ /**
3
+ * A `Setting` for calling a patch's `at` or `step` outside a mix: `keep` holds per owner as a mix's
4
+ * does, and `send` keeps what is sent in `sent`. Any field may be given; the rest read a voice at
5
+ * full weight at the start of its first pass, 1/60 s after the last frame.
6
+ *
7
+ * @category patch
8
+ */
9
+ export declare function setting<S = void, H = unknown>(fields?: Partial<Omit<Setting<S, H>, 'keep' | 'send'>>): Setting<S, H> & {
10
+ sent: unknown[];
11
+ };
12
+ /** @category patch */
13
+ export interface CheckOptions<I, O, H = unknown> {
14
+ /** The subject to read the patch for. */
15
+ subject: I;
16
+ /** A kit the patch should play in: each channel it writes, of the kind it was written for. */
17
+ kit?: Kit<O>;
18
+ /** What the patch's `reads` find on `setting.host`. */
19
+ host?: H;
20
+ /** The interval `step` is given, as under `stepMs`. Default 1000 / 60. */
21
+ stepMs?: number;
22
+ /** How many steps to run before reading state. Default 30. */
23
+ steps?: number;
24
+ }
25
+ /**
26
+ * What a patch does that a mix cannot rely on, one sentence per problem; empty when none. It reads
27
+ * `at` across its phase, twice from fresh state, and checks that it writes only its `writes`, gives
28
+ * the same delta each time, and leaves the subject alone; that `state` makes a new value per call,
29
+ * that `step` lands the same from the same start, and that `clone` and `pack`/`unpack` give back an
30
+ * equal state; that every field it `reads` is on `host`; and, given a kit, that `cue` would
31
+ * take it. Meant for a library's own tests: `expect(checkPatch(glow, { subject })).toEqual([])`.
32
+ *
33
+ * @category patch
34
+ */
35
+ export declare function checkPatch<I, O, S = void, H = unknown>(patch: Patch<I, O, S, H>, opts: CheckOptions<I, O, H>): string[];
@@ -0,0 +1,116 @@
1
+ import { clone } from './clone.js';
2
+ import { fitsKit } from './cue.js';
3
+ import { same } from './history.js';
4
+ /**
5
+ * A `Setting` for calling a patch's `at` or `step` outside a mix: `keep` holds per owner as a mix's
6
+ * does, and `send` keeps what is sent in `sent`. Any field may be given; the rest read a voice at
7
+ * full weight at the start of its first pass, 1/60 s after the last frame.
8
+ *
9
+ * @category patch
10
+ */
11
+ export function setting(fields = {}) {
12
+ const kept = new Map();
13
+ const sent = [];
14
+ return {
15
+ timestamp: 0,
16
+ dt: 1000 / 60,
17
+ elapsed: 0,
18
+ pass: 0,
19
+ weight: 1,
20
+ state: undefined,
21
+ host: undefined,
22
+ ...fields,
23
+ keep(owner, init) {
24
+ if (!kept.has(owner))
25
+ kept.set(owner, init());
26
+ return kept.get(owner);
27
+ },
28
+ send(event) {
29
+ sent.push(event);
30
+ },
31
+ sent,
32
+ };
33
+ }
34
+ /**
35
+ * What a patch does that a mix cannot rely on, one sentence per problem; empty when none. It reads
36
+ * `at` across its phase, twice from fresh state, and checks that it writes only its `writes`, gives
37
+ * the same delta each time, and leaves the subject alone; that `state` makes a new value per call,
38
+ * that `step` lands the same from the same start, and that `clone` and `pack`/`unpack` give back an
39
+ * equal state; that every field it `reads` is on `host`; and, given a kit, that `cue` would
40
+ * take it. Meant for a library's own tests: `expect(checkPatch(glow, { subject })).toEqual([])`.
41
+ *
42
+ * @category patch
43
+ */
44
+ export function checkPatch(patch, opts) {
45
+ const problems = [];
46
+ const { subject, host } = opts;
47
+ const tick = opts.stepMs ?? 1000 / 60;
48
+ const steps = opts.steps ?? 30;
49
+ const writes = new Set(patch.writes);
50
+ if (opts.kit !== undefined)
51
+ try {
52
+ fitsKit(patch, opts.kit);
53
+ }
54
+ catch (e) {
55
+ problems.push(e.message);
56
+ }
57
+ for (const field of patch.reads ?? [])
58
+ if (typeof host !== 'object' || host === null || !(field in host))
59
+ problems.push(`reads host.${field}, which the host given lacks`);
60
+ const fresh = () => (patch.state ? patch.state(subject) : undefined);
61
+ if (patch.state) {
62
+ const a = fresh();
63
+ if (typeof a === 'object' && a !== null && a === fresh())
64
+ problems.push('state() hands every subject the same object, so they share state');
65
+ }
66
+ /** The state after `steps` steps from fresh. */
67
+ const stepped = () => {
68
+ const state = fresh();
69
+ if (patch.step)
70
+ for (let n = 1; n <= steps; n++)
71
+ patch.step(state, tick, subject, setting({ state, host, dt: tick, timestamp: n * tick }));
72
+ return state;
73
+ };
74
+ const before = clone(subject);
75
+ const phases = [0, 0.25, 0.5, 0.75, 1];
76
+ const read = (state) => phases.map((phase) => clone(patch.at(phase, subject, setting({ state, host, elapsed: phase * patch.duration }))));
77
+ let first = [];
78
+ try {
79
+ first = read(stepped());
80
+ const second = read(stepped());
81
+ const stray = new Set();
82
+ for (const delta of first)
83
+ for (const key of Object.keys(delta))
84
+ if (!writes.has(key))
85
+ stray.add(key);
86
+ for (const key of stray)
87
+ problems.push(`at() writes ${key}, which is not in writes`);
88
+ if (!same(first, second))
89
+ problems.push('at() gives a different delta for the same phase, subject and state');
90
+ }
91
+ catch (e) {
92
+ problems.push(`at() or step() threw: ${e.message}`);
93
+ }
94
+ if (!same(subject, before))
95
+ problems.push('at() or step() changed the subject');
96
+ if (patch.step || patch.state) {
97
+ const state = stepped();
98
+ if (!same(state, stepped()))
99
+ problems.push('step() lands somewhere else from the same start and steps');
100
+ if (patch.clone) {
101
+ const c = patch.clone(state);
102
+ if (!same(c, state))
103
+ problems.push('clone() gives a state unequal to the one it was given');
104
+ else if (typeof state === 'object' && state !== null && c === state)
105
+ problems.push('clone() hands back the state itself, not a copy');
106
+ }
107
+ if ((patch.pack === undefined) !== (patch.unpack === undefined))
108
+ problems.push('pack and unpack come as a pair');
109
+ else if (patch.pack && patch.unpack) {
110
+ const back = patch.unpack(JSON.parse(JSON.stringify(patch.pack(state))));
111
+ if (!same(back, state))
112
+ problems.push('unpack(pack(state)) through JSON gives another state');
113
+ }
114
+ }
115
+ return problems;
116
+ }
@@ -0,0 +1,6 @@
1
+ import type { Mixer } from './mixer.js';
2
+ /**
3
+ * What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
4
+ * the next probe calls each patch again, on either path.
5
+ */
6
+ export declare function touch<I, O>(mix: Mixer<I, O>, subject?: I): void;
package/dist/touch.js ADDED
@@ -0,0 +1,28 @@
1
+ import { restamp } from './everyone.js';
2
+ import { nextFrame } from './move.js';
3
+ /**
4
+ * What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
5
+ * the next probe calls each patch again, on either path.
6
+ */
7
+ export function touch(mix, subject) {
8
+ mix.frame = nextFrame();
9
+ if (subject === undefined) {
10
+ // A motion patch reads nothing from outside the mix, and an owner has no delta.
11
+ for (const voice of mix.cued)
12
+ if (voice.motion === undefined && voice.holding === null) {
13
+ voice.seeks++;
14
+ voice.touches++;
15
+ }
16
+ }
17
+ else {
18
+ // No voice's `seeks` is below 0, so no record stamped with it reads as current.
19
+ let held = mix.chains.get(subject) ?? null;
20
+ for (; held !== null; held = held.next)
21
+ held.seeks = -1;
22
+ const slot = mix.chains.get(subject)?.slot ?? -1;
23
+ for (const voice of mix.sharers)
24
+ restamp(voice, slot);
25
+ }
26
+ mix.lanes?.refill();
27
+ mix.stir();
28
+ }