@msb235/blits 0.10.0 → 0.12.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 (126) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/README.md +15 -4
  3. package/dist/arcs.d.ts +40 -0
  4. package/dist/arcs.js +239 -0
  5. package/dist/blend.js +1 -5
  6. package/dist/book.d.ts +2 -0
  7. package/dist/book.js +27 -3
  8. package/dist/chain.d.ts +17 -1
  9. package/dist/chain.js +57 -15
  10. package/dist/channels.d.ts +2 -0
  11. package/dist/channels.js +12 -1
  12. package/dist/clock.d.ts +14 -0
  13. package/dist/clock.js +9 -1
  14. package/dist/clone.js +16 -4
  15. package/dist/columns.d.ts +4 -5
  16. package/dist/columns.js +11 -5
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.js +63 -7
  20. package/dist/departed.d.ts +8 -3
  21. package/dist/departed.js +76 -29
  22. package/dist/due.d.ts +4 -0
  23. package/dist/due.js +16 -3
  24. package/dist/easing.d.ts +12 -1
  25. package/dist/easing.js +7 -1
  26. package/dist/everyone.d.ts +14 -1
  27. package/dist/everyone.js +27 -2
  28. package/dist/fade.d.ts +12 -5
  29. package/dist/fade.js +57 -19
  30. package/dist/fill.js +16 -9
  31. package/dist/fit.d.ts +8 -1
  32. package/dist/fit.js +7 -3
  33. package/dist/fold.d.ts +1 -1
  34. package/dist/fold.js +112 -9
  35. package/dist/frames.d.ts +2 -0
  36. package/dist/frames.js +17 -0
  37. package/dist/gather.js +38 -7
  38. package/dist/handle.d.ts +7 -2
  39. package/dist/handle.js +28 -12
  40. package/dist/held.d.ts +13 -2
  41. package/dist/held.js +43 -9
  42. package/dist/history.d.ts +25 -2
  43. package/dist/history.js +163 -35
  44. package/dist/hosts.js +45 -17
  45. package/dist/index.d.ts +5 -2
  46. package/dist/index.js +3 -1
  47. package/dist/keyfill.js +3 -1
  48. package/dist/lane.d.ts +1 -1
  49. package/dist/lane.js +3 -1
  50. package/dist/lanes.d.ts +31 -7
  51. package/dist/lanes.js +57 -6
  52. package/dist/lean.d.ts +15 -0
  53. package/dist/lean.js +23 -0
  54. package/dist/leavings.d.ts +6 -1
  55. package/dist/leavings.js +16 -2
  56. package/dist/marks.d.ts +7 -3
  57. package/dist/marks.js +105 -27
  58. package/dist/meet.js +1 -0
  59. package/dist/mixer.d.ts +35 -6
  60. package/dist/mixer.js +113 -45
  61. package/dist/motion.d.ts +4 -1
  62. package/dist/motion.js +16 -2
  63. package/dist/motions.d.ts +4 -0
  64. package/dist/motions.js +45 -11
  65. package/dist/move.js +35 -12
  66. package/dist/numbers.d.ts +5 -1
  67. package/dist/numbers.js +4 -0
  68. package/dist/owner.d.ts +18 -2
  69. package/dist/owner.js +44 -3
  70. package/dist/pack.d.ts +1 -0
  71. package/dist/pack.js +3 -0
  72. package/dist/place.d.ts +8 -0
  73. package/dist/place.js +16 -8
  74. package/dist/placed.d.ts +19 -0
  75. package/dist/placed.js +54 -0
  76. package/dist/project.d.ts +5 -2
  77. package/dist/project.js +238 -34
  78. package/dist/pull.js +37 -8
  79. package/dist/qualify.d.ts +10 -0
  80. package/dist/qualify.js +60 -3
  81. package/dist/reading.d.ts +8 -1
  82. package/dist/reading.js +8 -1
  83. package/dist/record.d.ts +6 -0
  84. package/dist/record.js +2 -0
  85. package/dist/restored.d.ts +69 -17
  86. package/dist/restored.js +163 -50
  87. package/dist/revive.d.ts +6 -3
  88. package/dist/revive.js +11 -3
  89. package/dist/roster.js +2 -1
  90. package/dist/rows.js +15 -5
  91. package/dist/seek.d.ts +7 -2
  92. package/dist/seek.js +152 -43
  93. package/dist/shared.d.ts +5 -0
  94. package/dist/shared.js +5 -0
  95. package/dist/signals.d.ts +10 -0
  96. package/dist/signals.js +17 -0
  97. package/dist/spans.d.ts +5 -0
  98. package/dist/spans.js +75 -5
  99. package/dist/store.d.ts +13 -0
  100. package/dist/store.js +23 -0
  101. package/dist/strays.js +50 -2
  102. package/dist/tape.d.ts +29 -4
  103. package/dist/tape.js +49 -11
  104. package/dist/touch.js +3 -2
  105. package/dist/transport.d.ts +11 -2
  106. package/dist/transport.js +26 -10
  107. package/dist/types/arc.d.ts +55 -0
  108. package/dist/types/arc.js +1 -0
  109. package/dist/types/mix.d.ts +57 -14
  110. package/dist/types/patch.d.ts +5 -1
  111. package/dist/types/place.d.ts +11 -4
  112. package/dist/types/transport.d.ts +12 -3
  113. package/dist/types/voice.d.ts +72 -5
  114. package/dist/types/watch.d.ts +126 -0
  115. package/dist/types/watch.js +1 -0
  116. package/dist/types.d.ts +2 -0
  117. package/dist/unpage.d.ts +6 -1
  118. package/dist/unpage.js +34 -2
  119. package/dist/voice.d.ts +93 -16
  120. package/dist/voice.js +60 -3
  121. package/dist/watch.d.ts +26 -0
  122. package/dist/watch.js +441 -0
  123. package/dist/weigh.d.ts +13 -2
  124. package/dist/weigh.js +10 -3
  125. package/docs/schema.html +106 -38
  126. package/package.json +10 -4
package/dist/history.js CHANGED
@@ -9,14 +9,18 @@ import { within } from './transport.js';
9
9
  import { none } from './voice.js';
10
10
  /** The last entry taken before `t`, or at it where `inclusive` says so. */
11
11
  export function last(list, t, inclusive) {
12
+ return list[lastIndex(list, t, inclusive)];
13
+ }
14
+ /** Where `last` finds its entry in the list, -1 where it finds none. */
15
+ export function lastIndex(list, t, inclusive) {
12
16
  let lo = 0;
13
17
  let hi = list.length - 1;
14
- let found;
18
+ let found = -1;
15
19
  while (lo <= hi) {
16
20
  const mid = (lo + hi) >> 1;
17
21
  const e = list[mid];
18
22
  if (e.at < t || (e.at === t && (typeof inclusive === 'function' ? inclusive(e) : inclusive))) {
19
- found = e;
23
+ found = mid;
20
24
  lo = mid + 1;
21
25
  }
22
26
  else
@@ -86,41 +90,125 @@ export function noted(mix, voice) {
86
90
  pageOut(mix, 'controls', voice.id, undefined, log.splice(0, drop), copied, reach);
87
91
  }
88
92
  const copied = (e) => ({ ...e });
93
+ const nothing = Object.freeze([]);
89
94
  /** Takes off the front of a list kept by time what a read back to `reach` no longer needs. */
90
95
  function older(list, reach) {
96
+ const n = past(list, reach);
97
+ return n === 0 ? nothing : list.splice(0, n);
98
+ }
99
+ /** How many entries at the front of a list kept by time a read back to `reach` no longer needs. */
100
+ function past(list, reach) {
91
101
  let n = 0;
92
102
  while (n + 1 < list.length && list[n + 1].at <= reach)
93
103
  n++;
94
- return n === 0 ? [] : list.splice(0, n);
95
- }
96
- /** A copy of a subject's record that shares nothing a read could change. */
97
- export function copyHeld(voice, h) {
98
- let kept = null;
99
- if (h.kept !== null) {
100
- kept = new Map();
101
- for (const [owner, value] of h.kept)
102
- kept.set(owner, clone(value));
103
- }
104
+ return n;
105
+ }
106
+ /** A copy of the state a voice's patch keeps for a subject. */
107
+ export function copyState(voice, state) {
104
108
  const patch = voice.patch;
105
- return {
106
- ...h,
109
+ return patch.clone ? patch.clone(state) : clone(state);
110
+ }
111
+ /**
112
+ * A copy of a subject's record that shares nothing a read could change; with `shared`, its state is
113
+ * the record's own, for a copy that takes one of its own before it steps.
114
+ */
115
+ export function copyHeld(voice, h, shared = false) {
116
+ const kept = keptOf(h);
117
+ // Every field in `record`'s order, written out: a spread with fields after it costs a third more.
118
+ return unread({
119
+ reaches: h.reaches,
120
+ delay: h.delay,
121
+ since: h.since,
122
+ shown: h.shown,
123
+ weight: h.weight,
124
+ rested: h.rested,
107
125
  bands: h.bands === null ? null : h.bands.slice(),
108
- state: h.state === undefined ? undefined : patch.clone ? patch.clone(h.state) : clone(h.state),
109
- kept,
110
- unkept: h.unkept?.map(clone),
111
- probed: Number.NaN,
112
- delta: null,
113
- phase: 0,
114
- seeks: 0,
126
+ state: h.state === undefined || shared ? h.state : copyState(voice, h.state),
127
+ stepped: h.stepped,
128
+ ticks: h.ticks,
129
+ probed: h.probed,
130
+ delta: h.delta,
131
+ phase: h.phase,
132
+ seeks: h.seeks,
133
+ rebuilt: h.rebuilt,
134
+ copied: h.copied,
115
135
  base: h.base === undefined ? undefined : clone(h.base),
116
136
  slope: h.slope === undefined ? undefined : clone(h.slope),
117
- snaps: undefined,
118
- inputs: undefined,
119
- next: null,
120
- version: Number.NaN,
121
- loci: null,
122
- slot: -1,
123
- };
137
+ kept,
138
+ unkept: h.unkept?.map(clone),
139
+ snaps: h.snaps,
140
+ from: h.from,
141
+ unknown: h.unknown,
142
+ inputs: h.inputs,
143
+ replay: h.replay,
144
+ voice: h.voice,
145
+ next: h.next,
146
+ version: h.version,
147
+ loci: h.loci,
148
+ slot: h.slot,
149
+ run: h.run,
150
+ freed: h.freed,
151
+ // In no store, so stamped by none: a store trusts its stamp to say a record is still its own.
152
+ restored: null,
153
+ });
154
+ }
155
+ /** A copy of what a record keeps through `setting.keep`. */
156
+ function keptOf(h) {
157
+ if (h.kept === null)
158
+ return null;
159
+ const kept = new Map();
160
+ for (const [owner, value] of h.kept)
161
+ kept.set(owner, clone(value));
162
+ return kept;
163
+ }
164
+ /**
165
+ * As `copyHeld`, written over `into`, a record nothing else holds, so a copy made in its place
166
+ * leaves nothing new to collect. test/history.test.ts holds this to `copyHeld` field by field.
167
+ */
168
+ export function copyHeldInto(voice, h, into, shared = false) {
169
+ into.reaches = h.reaches;
170
+ into.delay = h.delay;
171
+ into.since = h.since;
172
+ into.shown = h.shown;
173
+ into.weight = h.weight;
174
+ into.rested = h.rested;
175
+ into.bands = h.bands === null ? null : h.bands.slice();
176
+ into.state = h.state === undefined || shared ? h.state : copyState(voice, h.state);
177
+ into.stepped = h.stepped;
178
+ into.ticks = h.ticks;
179
+ into.rebuilt = h.rebuilt;
180
+ into.copied = h.copied;
181
+ into.base = h.base === undefined ? undefined : clone(h.base);
182
+ into.slope = h.slope === undefined ? undefined : clone(h.slope);
183
+ into.kept = keptOf(h);
184
+ into.unkept = h.unkept?.map(clone);
185
+ into.from = h.from;
186
+ into.unknown = h.unknown;
187
+ into.replay = h.replay;
188
+ into.voice = h.voice;
189
+ into.slot = h.slot;
190
+ into.run = h.run;
191
+ into.freed = h.freed;
192
+ into.restored = null;
193
+ // `probed`, `delta`, `phase`, `seeks`, `snaps`, `inputs`, `next`, `version` and `loci` as
194
+ // `unread` leaves them.
195
+ return unread(into);
196
+ }
197
+ /**
198
+ * Lets go of what a record holds from its last probe and its place in a chain, as a copy of it
199
+ * starts: a seek puts back a record nothing else holds this way, in place of a copy.
200
+ */
201
+ export function unread(h) {
202
+ h.probed = Number.NaN;
203
+ h.delta = null;
204
+ h.phase = 0;
205
+ h.seeks = 0;
206
+ h.snaps = undefined;
207
+ h.inputs = undefined;
208
+ h.next = null;
209
+ h.version = Number.NaN;
210
+ h.loci = null;
211
+ return h;
124
212
  }
125
213
  /** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
126
214
  export function recordHost(mix, now) {
@@ -160,6 +248,23 @@ export function record(mix, voice, subject, held, value) {
160
248
  if (out.length > 0)
161
249
  pageOut(mix, 'input', voice.id, mix.keys?.key(subject), out, (e) => e.value, reach);
162
250
  }
251
+ /** How far apart, in mix ms, a record's copies are kept. */
252
+ export function apart(history) {
253
+ return history.every ?? 200;
254
+ }
255
+ /**
256
+ * A copy of a record to keep now, written over a spare the latest seek back cut, or one just let go
257
+ * of, where it has one.
258
+ */
259
+ function snapOf(mix, voice, held, gone) {
260
+ const spare = voice.spares?.pop() ?? gone;
261
+ if (spare === undefined)
262
+ return { at: mix.now, seq: mix.transport.seq, held: copyHeld(voice, held) };
263
+ spare.at = mix.now;
264
+ spare.seq = mix.transport.seq;
265
+ copyHeldInto(voice, held, spare.held);
266
+ return spare;
267
+ }
163
268
  /** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
164
269
  export function remember(mix, voice, subject, held) {
165
270
  const history = mix.opts.history;
@@ -172,13 +277,34 @@ export function remember(mix, voice, subject, held) {
172
277
  const snaps = held.snaps ?? [];
173
278
  held.snaps = snaps;
174
279
  const prev = snaps[snaps.length - 1];
175
- if (prev !== undefined && mix.now - prev.at < (history.every ?? 200))
280
+ if (prev !== undefined && mix.now - prev.at < apart(history))
176
281
  return;
177
- snaps.push({ at: mix.now, seq: mix.transport.seq, held: copyHeld(voice, held) });
282
+ // What falls out is the same before the push as after it, since the copy pushed is newer.
178
283
  const reach = mix.now - history.ms;
179
- const out = older(snaps, reach);
180
- if (out.length > 0)
181
- pageOut(mix, 'snap', voice.id, mix.keys?.key(subject), out, (e) => packHeld(voice, e.held), reach);
284
+ const n = past(snaps, reach);
285
+ let gone;
286
+ if (n > 0) {
287
+ if (mix.transport.pager !== null)
288
+ pageOut(mix, 'snap', voice.id, mix.keys?.key(subject), snaps.slice(0, n), (e) => packHeld(voice, e.held), reach);
289
+ gone = reusable(snaps, n);
290
+ // `shift` moves the list's start where `splice` copies what it keeps and allocates what it cuts.
291
+ if (n === 1)
292
+ snaps.shift();
293
+ else
294
+ snaps.splice(0, n);
295
+ }
296
+ snaps.push(snapOf(mix, voice, held, gone));
297
+ }
298
+ /** The first of a list's first `n` copies no other record's list holds, made ready to be written over. */
299
+ function reusable(snaps, n) {
300
+ for (let i = 0; i < n; i++) {
301
+ const snap = snaps[i];
302
+ if (snap.shared)
303
+ continue;
304
+ snap.ahead = undefined;
305
+ return snap;
306
+ }
307
+ return undefined;
182
308
  }
183
309
  /**
184
310
  * Under history, keeps the record a subject had when it left a voice, faded out of it or dropped,
@@ -194,13 +320,15 @@ export function leave(mix, voice, subject, held) {
194
320
  const keys = mix.keys;
195
321
  left.expire(reach, keys === null
196
322
  ? null
197
- : (e) => pageOut(mix, 'left', voice.id, keys.key(e.subject), [e], (x) => [packHeld(voice, x.held), x.sync], reach));
323
+ : (e) => pageOut(mix, 'left', voice.id, keys.key(e.subject), [e], (x) => [packHeld(voice, x.held), x.sync, x.ramp, x.parted], reach));
198
324
  left.push({
199
325
  subject,
200
326
  at: Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now,
201
327
  seq: mix.transport.seq,
202
- sync: mix.syncing,
328
+ sync: mix.syncing || !mix.looked,
203
329
  held,
330
+ ramp: voice.parts?.get(subject) ?? null,
331
+ parted: voice.parted?.get(subject) ?? null,
204
332
  });
205
333
  }
206
334
  /**
package/dist/hosts.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { release } from './arcs.js';
1
2
  import { Book } from './book.js';
3
+ import { headOf } from './chain.js';
2
4
  import { numericOf } from './channels.js';
5
+ import { stepOf } from './clock.js';
3
6
  import { crowdable } from './crowd.js';
4
7
  import { schedule } from './due.js';
5
8
  import { unshare, unsight, weightAt } from './everyone.js';
@@ -14,6 +17,7 @@ import { descendants, ownWeight, signalled } from './owner.js';
14
17
  import { localNow } from './place.js';
15
18
  import { reading } from './reading.js';
16
19
  import { heard } from './signals.js';
20
+ import { cuedById } from './strays.js';
17
21
  import { record } from './tape.js';
18
22
  import { unreach } from './unreached.js';
19
23
  export function book(mix, opts) {
@@ -69,31 +73,44 @@ export function laneHost(mix) {
69
73
  },
70
74
  passes: (was, w) => mix.passes(was, w),
71
75
  naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
72
- slotOf: (subject) => mix.chains.get(subject)?.slot ?? -1,
76
+ slotOf: (subject) => headOf(mix, subject)?.slot ?? -1,
73
77
  envelope: (voice, since) => voice.owner === null
74
78
  ? mix.envelope(voice, mix.now, since)
75
79
  : mix.envelope(voice, mix.now, since) * mix.ownedBy(voice, undefined),
76
80
  parting: (voice, subject) => mix.parting(voice, subject, mix.now),
77
- ready: (voice, subject, held, elapsed, pass, weight) => {
78
- mix.prime(voice, held, mix.now, elapsed, pass);
79
- voice.setting.weight = weight;
81
+ ready: (voice, subject, held) => {
82
+ const arg = mix.lanes.arg;
83
+ const primed = mix.primed;
84
+ primed[0 /* Primed.NOW */] = mix.now;
85
+ primed[1 /* Primed.ELAPSED */] = arg[1 /* Arg.ELAPSED */];
86
+ primed[2 /* Primed.PASS */] = arg[3 /* Arg.PASS */];
87
+ mix.prime(voice, held);
88
+ voice.setting.weight = arg[0 /* Arg.WEIGHT */];
80
89
  mix.sending.voice = voice;
81
90
  mix.sending.subject = subject;
82
91
  },
83
- signal: (voice, subject, held, elapsed, pass, slot) => {
92
+ signal: (voice, subject, held, slot) => {
93
+ const arg = mix.lanes.arg;
84
94
  // A blend member after the first takes the read already made, with no setting to fill.
85
95
  const read = voice.blend === null ? Number.NaN : mix.laneRead(voice, slot);
86
- if (!Number.isNaN(read))
87
- return read;
88
- mix.prime(voice, held, mix.now, elapsed, pass);
96
+ if (!Number.isNaN(read)) {
97
+ arg[0 /* Arg.WEIGHT */] = read;
98
+ return;
99
+ }
100
+ const primed = mix.primed;
101
+ primed[0 /* Primed.NOW */] = mix.now;
102
+ primed[1 /* Primed.ELAPSED */] = arg[1 /* Arg.ELAPSED */];
103
+ primed[2 /* Primed.PASS */] = arg[3 /* Arg.PASS */];
104
+ mix.prime(voice, held);
89
105
  const kept = reading.kept;
90
106
  const base = mix.base(voice, subject, mix.now, held, slot);
91
107
  if (reading.kept !== kept && !voice.keeping)
92
108
  stateful(mix, voice);
93
- return base;
109
+ arg[0 /* Arg.WEIGHT */] = base;
94
110
  },
95
111
  horizon: (voice, delay) => {
96
- reading.horizon = mix.horizonFor(voice, delay, mix.now);
112
+ if (voice.motion !== undefined)
113
+ reading.horizon = mix.horizonFor(voice, delay, mix.now);
97
114
  },
98
115
  keeps: mix.opts.history !== undefined,
99
116
  after: (voice, subject, held) => remember(mix, voice, subject, held),
@@ -161,8 +178,7 @@ function seekDoubt(mix, voice, rebuild) {
161
178
  return 'stepped';
162
179
  if (patch.step === undefined)
163
180
  return 'exact';
164
- const tick = mix.opts.stepMs;
165
- const fixed = tick !== undefined && tick > 0 && !mix.reducedNow && mix.opts.maxDt === undefined;
181
+ const fixed = stepOf(mix, voice) !== undefined && mix.opts.maxDt === undefined;
166
182
  return fixed ? 'exact' : 'stepped';
167
183
  }
168
184
  /** A voice's patch kept state on a record: it is stateful from now on. */
@@ -223,21 +239,33 @@ function handleHost(mix) {
223
239
  beginFade(mix, voice, (opts ?? {}));
224
240
  },
225
241
  rise: (voice, opts) => beginRise(mix, voice, opts ?? {}),
242
+ release: (voice) => release(mix, voice),
226
243
  weightOf: (voice, subject) => {
227
244
  if (voice.state === 'done')
228
245
  return 0;
229
246
  if (voice.holding !== null)
230
247
  return ownWeight(voice, mix.now, mix.reducedNow, (o) => mix.ownerBase(o, subject));
231
- const slot = mix.lanes === null ? -1 : (mix.chains.get(subject)?.slot ?? -1);
248
+ const slot = mix.lanes === null ? -1 : (headOf(mix, subject)?.slot ?? -1);
232
249
  if (voice.laned && mix.lanes !== null) {
233
250
  const w = mix.lanes.weightOf(voice.id, slot);
234
251
  if (w !== undefined)
235
252
  return w;
236
253
  }
237
- if (voice.sharing)
238
- return weightAt(voice, slot);
239
- return voice.subjects.get(subject)?.weight ?? 0;
254
+ const held = voice.sharing
255
+ ? undefined
256
+ : voice.subjects.get(subject);
257
+ // A voice that stopped sharing makes a subject's record at its next read, from what it kept.
258
+ return held !== undefined ? held.weight : weightAt(voice, slot);
240
259
  },
241
- record: (label, again) => record(mix, label, again),
260
+ record: (h, label, again) => record(mix, label, (stage) => {
261
+ const to = stage.mix(mix);
262
+ if (to === mix)
263
+ again(h);
264
+ else if (to !== undefined) {
265
+ const voice = cuedById(to, h.id);
266
+ if (voice !== undefined)
267
+ again(handle(to, voice));
268
+ }
269
+ }),
242
270
  };
243
271
  }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export type { NumberOptions, Vec } from './channels.js';
2
2
  export { kit, last, max, mul, sum, vec } from './channels.js';
3
3
  export type { ColorOptions } from './color.js';
4
4
  export { color, css, mixHex, oklab, toHex } from './color.js';
5
+ export type { Curve } from './easing.js';
6
+ export { curve } from './easing.js';
5
7
  export type { Claim, Fit, FitPlan, Order, SpanClaim, Strength } from './fit.js';
6
8
  export { conclude, condense, lax, layout, overrun, pipe, plain, shed, } from './fit.js';
7
9
  export { mix, mixer } from './mixer.js';
@@ -12,11 +14,12 @@ export type { KeysOptions, PatchOptions } from './patch.js';
12
14
  export { keys, patch } from './patch.js';
13
15
  export type { AngleOptions } from './rotation.js';
14
16
  export { angle, quat } from './rotation.js';
15
- export { gate, input, lag, level, peak, slew } from './signals.js';
17
+ export { gate, input, lag, level, peak, share, slew } from './signals.js';
16
18
  export { fold } from './stack.js';
17
19
  export type { Ticked, Ticker, TickerOptions } from './ticker.js';
18
20
  export { ticker } from './ticker.js';
19
21
  export { transport } from './transport.js';
20
- export type { Anchor, BookedHit, Booker, BookOptions, Channel, Columns, Described, Doubt, Easing, Engine, FadeOptions, FadeSpec, FitResult, Handle, HistoryOptions, HistoryStore, Hit, Keyframe, Kit, Mark, Marked, Mix, MixOptions, MotionSpec, OwnerSpec, Paged, PagedStream, Patch, Placement, Projection, Query, SeekOptions, Sent, Setting, Signal, SpanHandle, SpanHints, SpanSpec, Tape, TapeBranch, TapeMaker, TapeOp, Transport, TransportOptions, TransportProjection, VoiceSpec, } from './types.js';
22
+ export type { Anchor, ArcHandle, ArcPart, ArcSpec, ArcVoice, BlendMember, BookedHit, Booker, BookOptions, Channel, Columns, Described, Doubt, Easing, Engine, FadeOptions, FadeSpec, FitResult, Handle, HistoryOptions, HistoryStore, Hit, Hold, Keyframe, Kit, Mark, Marked, MarkName, Mix, MixOptions, MotionSpec, OwnerSpec, Paged, PagedStream, Patch, Placement, Projection, Query, Registry, SeekOptions, Sent, Setting, Signal, SpanHandle, SpanHints, SpanSpec, Tape, TapeBranch, TapeMaker, TapeOp, Transport, TransportOptions, TransportProjection, VoiceSpec, Watched, WatchedFrame, } from './types.js';
23
+ export { watched } from './watch.js';
21
24
  export type { WaveOptions, WaveShape } from './wave.js';
22
25
  export { wave, waveAt } from './wave.js';
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  export { kit, last, max, mul, sum, vec } from './channels.js';
2
2
  export { color, css, mixHex, oklab, toHex } from './color.js';
3
+ export { curve } from './easing.js';
3
4
  export { conclude, condense, lax, layout, overrun, pipe, plain, shed, } from './fit.js';
4
5
  export { mix, mixer } from './mixer.js';
5
6
  export { glide, spring, tween } from './motion.js';
6
7
  export { HistoryMiss } from './paging.js';
7
8
  export { keys, patch } from './patch.js';
8
9
  export { angle, quat } from './rotation.js';
9
- export { gate, input, lag, level, peak, slew } from './signals.js';
10
+ export { gate, input, lag, level, peak, share, slew } from './signals.js';
10
11
  export { fold } from './stack.js';
11
12
  export { ticker } from './ticker.js';
12
13
  export { transport } from './transport.js';
14
+ export { watched } from './watch.js';
13
15
  export { wave, waveAt } from './wave.js';
package/dist/keyfill.js CHANGED
@@ -23,6 +23,7 @@ export function runKeys(lanes, lane) {
23
23
  const before = voice.freezesBefore;
24
24
  const after = voice.freezesAfter;
25
25
  const span = voice.span;
26
+ const hides = voice.hidesPast;
26
27
  const flat = lane.flat;
27
28
  const parts = voice.parts !== null;
28
29
  const whole = clampWeight(voice.weight);
@@ -42,7 +43,8 @@ export function runKeys(lanes, lane) {
42
43
  data[o + 3 /* Row.PROBED */] = data[o + 2 /* Row.WEIGHT */];
43
44
  if (lately >= 0 && lanes.unread(slot))
44
45
  continue;
45
- const elapsed = frozenTime(elapsedNow - data[o + 0 /* Row.DELAY */], before, after, span);
46
+ const raw = elapsedNow - data[o + 0 /* Row.DELAY */];
47
+ const elapsed = raw > span && hides ? Number.NaN : frozenTime(raw, before, after, span);
46
48
  if (!(elapsed >= 0)) {
47
49
  data[o + 2 /* Row.WEIGHT */] = 0;
48
50
  unband(records[p]);
package/dist/lane.d.ts CHANGED
@@ -235,6 +235,6 @@ export declare class Lane<I, O> implements Positions<I, O> {
235
235
  }
236
236
  /**
237
237
  * A subject's voice time once its voice's freezes apply: frozen at 0 before it starts and at the end
238
- * of its passes after, and NaN where it shows nothing.
238
+ * of its passes after, and NaN where it shows nothing, as past its end running back.
239
239
  */
240
240
  export declare function frozenAt<I, O>(voice: Voice<I, O>, elapsed: number): number;
package/dist/lane.js CHANGED
@@ -185,8 +185,10 @@ export class Lane {
185
185
  }
186
186
  /**
187
187
  * A subject's voice time once its voice's freezes apply: frozen at 0 before it starts and at the end
188
- * of its passes after, and NaN where it shows nothing.
188
+ * of its passes after, and NaN where it shows nothing, as past its end running back.
189
189
  */
190
190
  export function frozenAt(voice, elapsed) {
191
+ if (elapsed > voice.span && voice.hidesPast)
192
+ return Number.NaN;
191
193
  return frozenTime(elapsed, voice.freezesBefore, voice.freezesAfter, voice.span);
192
194
  }
package/dist/lanes.d.ts CHANGED
@@ -7,6 +7,7 @@ import { type Lane, type Laned, type Locus } from './lane.js';
7
7
  import type { Watcher } from './motions.js';
8
8
  import { absent, Numbers } from './numbers.js';
9
9
  import { Owed } from './owed.js';
10
+ import { Placed } from './placed.js';
10
11
  import type { Channel } from './types.js';
11
12
  import type { Subject, Voice } from './voice.js';
12
13
  /** What a lane fill asks of the mix it belongs to. */
@@ -32,14 +33,18 @@ export interface LaneHost<I, O> {
32
33
  envelope(voice: Voice<I, O>, since: number): number;
33
34
  /** What a subject's own ramp out of the voice leaves of its weight, 0..1. */
34
35
  parting(voice: Voice<I, O>, subject: I): number;
35
- /** Fills the voice's setting and the send target for a call to its patch. */
36
- ready(voice: Voice<I, O>, subject: I, held: Subject<unknown>, elapsed: number, pass: number, weight: number): void;
37
36
  /**
38
- * A voice with a signal weight: the signal's value for a subject the general path has met, before
39
- * the fade and ramp `weighed` applies, with the setting filled in for the call; makes the voice
40
- * stateful if the signal keeps state. `slot` is the subject's number.
37
+ * Fills the voice's setting and the send target for a call to its patch, at the voice time, pass
38
+ * and weight in `Arg.ELAPSED`, `Arg.PASS` and `Arg.WEIGHT`.
41
39
  */
42
- signal(voice: Voice<I, O>, subject: I, held: Subject<unknown>, elapsed: number, pass: number, slot: number): number;
40
+ ready(voice: Voice<I, O>, subject: I, held: Subject<unknown>): void;
41
+ /**
42
+ * A voice with a signal weight: the signal's value for a subject the general path has met, at the
43
+ * voice time and pass in `Arg.ELAPSED` and `Arg.PASS`, left in `Arg.WEIGHT` before the fade and
44
+ * ramp `weighed` applies, with the setting filled in for the call; makes the voice stateful if the
45
+ * signal keeps state. `slot` is the subject's number.
46
+ */
47
+ signal(voice: Voice<I, O>, subject: I, held: Subject<unknown>, slot: number): void;
43
48
  /** Whether a rest-less channel's contribution passes the band, as the general path decides it. */
44
49
  passes(was: boolean | undefined, w: number): boolean;
45
50
  /** Sets `reading.horizon` for the voice and a subject delayed `delay` voice ms. */
@@ -65,6 +70,13 @@ export declare class Lanes<I, O> implements Watcher {
65
70
  /** The crowds of single-subject voices, one per list of channels, and which holds each voice. */
66
71
  crowds: Crowd<I, O>[];
67
72
  readonly crowdOf: Map<number, Crowd<I, O>>;
73
+ /** Which crowd voices' rows are placed on each subject. */
74
+ readonly placed: Placed;
75
+ /**
76
+ * By subject number, the mix's `relinks` when the subject's chain was last made stale by itself,
77
+ * so `pull` tells a head it remembers is stale without reading the head.
78
+ */
79
+ unlinked: Float64Array<ArrayBuffer>;
68
80
  /** Whether two crowds share a channel, so their rows must interleave in voice order. */
69
81
  overlap: boolean;
70
82
  /** The loci whose voices are on lanes, every member of each on one. */
@@ -168,7 +180,12 @@ export declare class Lanes<I, O> implements Watcher {
168
180
  lately: number;
169
181
  filledVersion: number;
170
182
  qualifiedVersion: number;
171
- constructor(host: LaneHost<I, O>);
183
+ /**
184
+ * With `from`, the lanes a seek replaces: their subject numbers carry over, each met as newly
185
+ * numbered when its chain is next linked (`renew`), so a seek makes no new weak reference per
186
+ * subject.
187
+ */
188
+ constructor(host: LaneHost<I, O>, from?: Lanes<I, O>);
172
189
  /** A change that may move a voice on or off its lane: qualify again, then fill again. */
173
190
  invalidate(): void;
174
191
  /** A change to a laned voice's weight or clock between probes: fill again. */
@@ -181,6 +198,13 @@ export declare class Lanes<I, O> implements Watcher {
181
198
  /** Numbers a subject the mix sees for the first time. */
182
199
  number(subject: I): number;
183
200
  release(slot: number): void;
201
+ /** Whether `slot` is a number a seek carried over whose chain has not been linked since. */
202
+ sought(slot: number): boolean;
203
+ /**
204
+ * A chain is being linked for the subject at `slot`: one a seek carried over is from now on as
205
+ * `number` leaves a subject it numbers. Says whether it was one.
206
+ */
207
+ renew(slot: number): boolean;
184
208
  /** A voice faded a subject out of itself alone: its lane lets the subject's position go. */
185
209
  part(id: number, slot: number): void;
186
210
  /** A voice brought a subject back: its next probe meets every lane again, as it met them first. */
package/dist/lanes.js CHANGED
@@ -5,6 +5,7 @@ import { fold, foldDelta, foldInto, foldRun, gate, gather, unplain } from './gat
5
5
  import { meet, reach } from './meet.js';
6
6
  import { absent, Numbers } from './numbers.js';
7
7
  import { Owed } from './owed.js';
8
+ import { Placed } from './placed.js';
8
9
  import { requalify, retouch } from './qualify.js';
9
10
  import { reading } from './reading.js';
10
11
  import { reportedRow, unplace } from './rows.js';
@@ -23,6 +24,13 @@ export class Lanes {
23
24
  /** The crowds of single-subject voices, one per list of channels, and which holds each voice. */
24
25
  crowds = [];
25
26
  crowdOf = new Map();
27
+ /** Which crowd voices' rows are placed on each subject. */
28
+ placed = new Placed();
29
+ /**
30
+ * By subject number, the mix's `relinks` when the subject's chain was last made stale by itself,
31
+ * so `pull` tells a head it remembers is stale without reading the head.
32
+ */
33
+ unlinked = new Float64Array(0);
26
34
  /** Whether two crowds share a channel, so their rows must interleave in voice order. */
27
35
  overlap = false;
28
36
  /** The loci whose voices are on lanes, every member of each on one. */
@@ -126,9 +134,27 @@ export class Lanes {
126
134
  lately = -1;
127
135
  filledVersion = Number.NaN;
128
136
  qualifiedVersion = Number.NaN;
129
- constructor(host) {
137
+ /**
138
+ * With `from`, the lanes a seek replaces: their subject numbers carry over, each met as newly
139
+ * numbered when its chain is next linked (`renew`), so a seek makes no new weak reference per
140
+ * subject.
141
+ */
142
+ constructor(host, from) {
130
143
  this.host = host;
131
- this.numbers = new Numbers(this);
144
+ if (from === undefined) {
145
+ this.numbers = new Numbers(this);
146
+ return;
147
+ }
148
+ this.numbers = from.numbers;
149
+ this.numbers.adopt(this);
150
+ this.live = from.live;
151
+ const size = this.numbers.size;
152
+ this.grow(size);
153
+ const per = this.per;
154
+ for (let slot = 0; slot < size; slot++) {
155
+ per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = Number.NaN;
156
+ per[slot * 6 /* Per.SLOT */ + 1 /* Per.SEEN */] = -1;
157
+ }
132
158
  }
133
159
  /** A change that may move a voice on or off its lane: qualify again, then fill again. */
134
160
  invalidate() {
@@ -165,6 +191,22 @@ export class Lanes {
165
191
  release(slot) {
166
192
  this.numbers.release(slot);
167
193
  }
194
+ /** Whether `slot` is a number a seek carried over whose chain has not been linked since. */
195
+ sought(slot) {
196
+ return Number.isNaN(this.per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */]);
197
+ }
198
+ /**
199
+ * A chain is being linked for the subject at `slot`: one a seek carried over is from now on as
200
+ * `number` leaves a subject it numbers. Says whether it was one.
201
+ */
202
+ renew(slot) {
203
+ if (!this.sought(slot))
204
+ return false;
205
+ this.per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = this.fills;
206
+ if (this.filling)
207
+ this.late.push(slot);
208
+ return true;
209
+ }
168
210
  /** A voice faded a subject out of itself alone: its lane lets the subject's position go. */
169
211
  part(id, slot) {
170
212
  const lane = this.byId.get(id);
@@ -374,10 +416,15 @@ export class Lanes {
374
416
  this.live--;
375
417
  for (const lane of this.lanes)
376
418
  lane.remove(slot);
377
- for (const c of this.crowds)
378
- for (let p = 0; p < c.size; p++)
379
- if (c.list[p] === slot)
380
- unplace(this, c, p);
419
+ const placed = this.placed;
420
+ for (let id = placed.any(slot); id !== 0; id = placed.any(slot)) {
421
+ const c = this.crowdOf.get(id);
422
+ const p = c?.rowOf.get(id);
423
+ if (c !== undefined && p !== undefined && c.list[p] === slot)
424
+ unplace(this, c, p);
425
+ else
426
+ placed.remove(slot, id);
427
+ }
381
428
  if (slot < this.cap)
382
429
  this.per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = this.fills;
383
430
  this.subjects[slot] = undefined;
@@ -390,6 +437,10 @@ export class Lanes {
390
437
  per.set(this.per);
391
438
  this.per = per;
392
439
  this.owed.grow(cap);
440
+ this.placed.grow(cap);
441
+ const unlinked = new Float64Array(cap);
442
+ unlinked.set(this.unlinked);
443
+ this.unlinked = unlinked;
393
444
  for (const ch of this.laned) {
394
445
  const values = new Float64Array(cap * ch.axes);
395
446
  values.set(ch.values);
package/dist/lean.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Engine, Kit, Mix, MixOptions } from './types.js';
2
+ export * from './index.js';
3
+ /**
4
+ * The stock engine without lanes: every voice folds subject by subject, to the same values, and a
5
+ * bundle that takes `mix` from here leaves the lanes' code out. `lanes: true` is refused.
6
+ *
7
+ * @category engine
8
+ */
9
+ export declare const lean: Engine;
10
+ /**
11
+ * Makes a mix over a kit, on the engine the options name or the lean one.
12
+ *
13
+ * @category mix
14
+ */
15
+ export declare function mix<I, O, H = unknown>(kit: Kit<O>, opts?: MixOptions<H>): Mix<I, O, H>;