@msb235/blits 0.15.0 → 0.16.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 (80) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +5 -4
  4. package/dist/bare.js +22 -7
  5. package/dist/chain.js +2 -3
  6. package/dist/color.d.ts +36 -14
  7. package/dist/color.js +186 -58
  8. package/dist/columns.d.ts +3 -12
  9. package/dist/columns.js +56 -41
  10. package/dist/crowd.d.ts +8 -10
  11. package/dist/crowd.js +17 -18
  12. package/dist/cue.js +7 -4
  13. package/dist/delay.d.ts +3 -0
  14. package/dist/delay.js +7 -0
  15. package/dist/due.js +1 -1
  16. package/dist/easing.d.ts +2 -0
  17. package/dist/easing.js +4 -0
  18. package/dist/everyone.d.ts +22 -6
  19. package/dist/everyone.js +47 -8
  20. package/dist/fade.d.ts +11 -0
  21. package/dist/fade.js +44 -8
  22. package/dist/fill.js +1 -1
  23. package/dist/fold.js +4 -7
  24. package/dist/held.js +5 -5
  25. package/dist/hosts.d.ts +7 -0
  26. package/dist/hosts.js +18 -5
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +1 -1
  29. package/dist/lane.d.ts +2 -5
  30. package/dist/lane.js +14 -10
  31. package/dist/lanes.d.ts +33 -5
  32. package/dist/lanes.js +34 -28
  33. package/dist/lch.d.ts +23 -0
  34. package/dist/lch.js +126 -0
  35. package/dist/meet.js +5 -3
  36. package/dist/mixer.d.ts +6 -2
  37. package/dist/mixer.js +7 -0
  38. package/dist/motions.d.ts +4 -1
  39. package/dist/motions.js +16 -2
  40. package/dist/move.js +15 -10
  41. package/dist/named.d.ts +6 -1
  42. package/dist/named.js +29 -1
  43. package/dist/numbers.d.ts +5 -0
  44. package/dist/numbers.js +29 -0
  45. package/dist/owner.d.ts +7 -2
  46. package/dist/owner.js +13 -3
  47. package/dist/project.js +3 -8
  48. package/dist/pull.d.ts +11 -0
  49. package/dist/pull.js +18 -4
  50. package/dist/qualify.js +10 -11
  51. package/dist/renumber.d.ts +10 -0
  52. package/dist/renumber.js +75 -0
  53. package/dist/roles.js +1 -3
  54. package/dist/roster.d.ts +6 -0
  55. package/dist/roster.js +8 -1
  56. package/dist/rows.js +27 -19
  57. package/dist/sample.js +2 -1
  58. package/dist/seat.d.ts +17 -0
  59. package/dist/seat.js +25 -0
  60. package/dist/seek.js +1 -13
  61. package/dist/shared.d.ts +1 -1
  62. package/dist/shared.js +1 -1
  63. package/dist/spans.d.ts +4 -1
  64. package/dist/spans.js +6 -3
  65. package/dist/strays.js +3 -2
  66. package/dist/touch.js +1 -2
  67. package/dist/trace.d.ts +4 -0
  68. package/dist/trace.js +255 -0
  69. package/dist/turns.js +2 -1
  70. package/dist/types/mix.d.ts +15 -0
  71. package/dist/types/trace.d.ts +51 -0
  72. package/dist/types/trace.js +1 -0
  73. package/dist/types/voice.d.ts +13 -7
  74. package/dist/types.d.ts +1 -0
  75. package/dist/unreached.d.ts +2 -0
  76. package/dist/unreached.js +7 -0
  77. package/dist/voice.d.ts +22 -0
  78. package/dist/voice.js +44 -4
  79. package/docs/schema.html +48 -11
  80. package/package.json +1 -1
package/dist/rows.js CHANGED
@@ -4,6 +4,7 @@ import { reach } from './meet.js';
4
4
  import { motionOf } from './motion.js';
5
5
  import { open } from './qualify.js';
6
6
  import { settle } from './sample.js';
7
+ import { Seat, seatOf } from './seat.js';
7
8
  /** Whether two crowds' channels are the same list. */
8
9
  function same(a, b) {
9
10
  if (a.length !== b.length)
@@ -38,14 +39,13 @@ export function unplace(lanes, c, p) {
38
39
  export function sameCrowds(lanes, members) {
39
40
  let live = 0;
40
41
  for (const v of members) {
41
- if (!lanes.crowdOf.has(v.id))
42
+ if (!lanes.seats.has(v.id))
42
43
  return false;
43
44
  live++;
44
45
  }
45
46
  let rows = 0;
46
- for (const [id, c] of lanes.crowdOf) {
47
- const p = c.rowOf.get(id);
48
- if (c.voices[p].state !== 'done')
47
+ for (const s of lanes.seats.values()) {
48
+ if (s.crowd.voices[s.row].state !== 'done')
49
49
  rows++;
50
50
  }
51
51
  return rows === live;
@@ -64,13 +64,15 @@ export function regroup(lanes, members) {
64
64
  leaveCrowd(lanes, c, p);
65
65
  }
66
66
  const old = lanes.crowds;
67
- const was = new Map(lanes.crowdOf);
67
+ const was = new Map(lanes.seats);
68
+ for (const s of was.values())
69
+ s.lanes = null;
68
70
  lanes.crowds = [];
69
- lanes.crowdOf.clear();
71
+ lanes.seats.clear();
70
72
  lanes.overlap = false;
71
73
  for (const v of members) {
72
74
  const from = was.get(v.id);
73
- join(lanes, v, from, from?.rowOf.get(v.id), old);
75
+ join(lanes, v, from?.crowd, from?.row, old);
74
76
  }
75
77
  if (lanes.crowds.length === 0) {
76
78
  lanes.lawsByKey.clear();
@@ -104,8 +106,9 @@ export function join(lanes, v, from, at, old) {
104
106
  newRow(lanes, c, p, v);
105
107
  if (c.hot[p * c.stride + 6 /* Hot.EPOCH */] === 0 && v.state !== 'pending')
106
108
  c.hot[p * c.stride + 6 /* Hot.EPOCH */] = open(lanes, v);
107
- c.rowOf.set(v.id, p);
108
- lanes.crowdOf.set(v.id, c);
109
+ const seat = new Seat(lanes, c, p);
110
+ lanes.seats.set(v.id, seat);
111
+ v.seat = seat;
109
112
  v.laned = true;
110
113
  }
111
114
  /** A crowd row takes up its voice as it now stands: opened once it starts, its fields copied again. */
@@ -122,27 +125,28 @@ export function retouchRow(lanes, c, p, v) {
122
125
  export function compactSparse(lanes) {
123
126
  for (const c of lanes.crowds)
124
127
  if (c.dead > 64 && c.dead * 4 >= c.size)
125
- compact(c);
128
+ compact(lanes, c);
126
129
  }
127
130
  /**
128
131
  * Slides a crowd's rows down over the empty ones its departed voices left, in order, so its rows
129
132
  * stay in voice order; a qualify did this by rebuilding every crowd, a dropped frame at 10k rows.
130
133
  */
131
- function compact(c) {
134
+ function compact(lanes, c) {
132
135
  // The rows' stops are copied into a fresh pool, leaving the departed rows' behind.
133
136
  const keys = c.keys;
134
137
  c.keys = new KeyRows();
135
138
  let q = 0;
136
139
  for (let p = 0; p < c.size; p++) {
137
140
  const v = c.voices[p];
138
- if (c.rowOf.get(v.id) !== p) {
141
+ const s = seatOf(lanes, v);
142
+ if (s === null || s.crowd !== c || s.row !== p) {
139
143
  c.odd?.delete(p);
140
144
  continue;
141
145
  }
142
146
  if (q !== p) {
143
147
  copyRow(c, p, c, q, keys);
144
148
  c.odd?.delete(p);
145
- c.rowOf.set(v.id, q);
149
+ s.row = q;
146
150
  }
147
151
  else
148
152
  copyKeys(keys, c, q);
@@ -166,8 +170,12 @@ export function bury(lanes, c, p) {
166
170
  c.deltas[p] = null;
167
171
  c.motions[p] = undefined;
168
172
  c.hot[p * c.stride + 0 /* Hot.FLAGS */] = 0;
169
- c.rowOf.delete(v.id);
170
- lanes.crowdOf.delete(v.id);
173
+ lanes.seats.delete(v.id);
174
+ const s = v.seat;
175
+ if (s !== null && s.lanes === lanes) {
176
+ s.lanes = null;
177
+ v.seat = null;
178
+ }
171
179
  c.dead++;
172
180
  }
173
181
  /** Copies `n` numbers of a row; a view of them to hand `set` was three objects a row moved. */
@@ -202,7 +210,7 @@ function copyRow(from, p, to, q, keys = from.keys) {
202
210
  function copyKeys(keys, c, q) {
203
211
  const h = q * c.stride;
204
212
  if ((c.hot[h + 0 /* Hot.FLAGS */] & 512 /* Flag.FLAT */) !== 0)
205
- c.hot[h + 8 /* Hot.AT */] = c.keys.copy(keys, c.hot[h + 8 /* Hot.AT */]);
213
+ c.hot[h + 9 /* Hot.AT */] = c.keys.copy(keys, c.hot[h + 9 /* Hot.AT */]);
206
214
  }
207
215
  function newRow(lanes, c, p, v) {
208
216
  const run = motionOf(v.patch);
@@ -210,9 +218,9 @@ function newRow(lanes, c, p, v) {
210
218
  c.hot.fill(0, h, h + c.stride);
211
219
  const at = v.built === null ? -1 : c.keys.add(v.built, c.chans);
212
220
  if (at >= 0) {
213
- c.hot[h + 8 /* Hot.AT */] = at;
214
- c.hot[h + 10 /* Hot.X0 */] = v.duration;
215
- c.hot[h + 9 /* Hot.MS */] = v.passes;
221
+ c.hot[h + 9 /* Hot.AT */] = at;
222
+ c.hot[h + 11 /* Hot.X0 */] = v.duration;
223
+ c.hot[h + 10 /* Hot.MS */] = v.passes;
216
224
  }
217
225
  c.restale(p, 64 /* Flag.VOICE */ |
218
226
  (run === undefined ? 0 : 128 /* Flag.MOTION */) |
package/dist/sample.js CHANGED
@@ -43,7 +43,8 @@ export function runMotion(lanes, lane, run) {
43
43
  }
44
44
  const samples = lane.samples;
45
45
  const xs = run.xs;
46
- const ease = run.ease;
46
+ // A tween round the hue is no straight line between its ends: `sampleBare` reads it round the hue.
47
+ const ease = run.round ? undefined : run.ease;
47
48
  // A tween whose stretches tell the lane before they change keeps no sample: see `unbareLane`.
48
49
  if (run.watcher === null) {
49
50
  run.watcher = lanes;
package/dist/seat.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Crowd } from './crowd.js';
2
+ import type { Voice } from './voice.js';
3
+ /**
4
+ * A crowd voice's row, held on the voice and by its id in its lanes' `seats`: one place to read
5
+ * both its crowd and its row, where two maps by id were two cache misses a stop at 40,000 voices.
6
+ * `lanes` is the lanes it was given in, null once they let it go.
7
+ */
8
+ export declare class Seat<I, O> {
9
+ lanes: object | null;
10
+ readonly crowd: Crowd<I, O>;
11
+ row: number;
12
+ constructor(lanes: object | null, crowd: Crowd<I, O>, row: number);
13
+ }
14
+ /** `voice`'s seat in `lanes`, null where it has none there. */
15
+ export declare function seatOf<I, O>(lanes: object, voice: Voice<I, O>): Seat<I, O> | null;
16
+ /** Whether row `p` of `c` is `voice`'s seat in `lanes`, not a row it has left. */
17
+ export declare function seated<I, O>(lanes: object, voice: Voice<I, O>, c: Crowd<I, O>, p: number): boolean;
package/dist/seat.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A crowd voice's row, held on the voice and by its id in its lanes' `seats`: one place to read
3
+ * both its crowd and its row, where two maps by id were two cache misses a stop at 40,000 voices.
4
+ * `lanes` is the lanes it was given in, null once they let it go.
5
+ */
6
+ export class Seat {
7
+ lanes;
8
+ crowd;
9
+ row;
10
+ constructor(lanes, crowd, row) {
11
+ this.lanes = lanes;
12
+ this.crowd = crowd;
13
+ this.row = row;
14
+ }
15
+ }
16
+ /** `voice`'s seat in `lanes`, null where it has none there. */
17
+ export function seatOf(lanes, voice) {
18
+ const s = voice.seat;
19
+ return s !== null && s.lanes === lanes ? s : null;
20
+ }
21
+ /** Whether row `p` of `c` is `voice`'s seat in `lanes`, not a row it has left. */
22
+ export function seated(lanes, voice, c, p) {
23
+ const s = seatOf(lanes, voice);
24
+ return s !== null && s.crowd === c && s.row === p;
25
+ }
package/dist/seek.js CHANGED
@@ -6,7 +6,6 @@ import { apart, copyHeld, copyHeldInto, lastIndex, lastWithin, unread } from './
6
6
  import { laneHost } from './hosts.js';
7
7
  import { Leavings } from './leavings.js';
8
8
  import { move, nextFrame } from './move.js';
9
- import { ownerReading } from './owner.js';
10
9
  import { place } from './place.js';
11
10
  import { unpull } from './pull.js';
12
11
  import { isRested } from './record.js';
@@ -209,17 +208,6 @@ function restoreVoice(mix, voice, t, seq) {
209
208
  }
210
209
  records.back(t, cut, future);
211
210
  }
212
- /** Each voice's state at mix time `t`, once every clock above it is back where it was. */
213
- function stateAt(voice, t) {
214
- // A fade begins at once, even one whose ramp starts later, at the voice's start.
215
- if (voice.out !== null)
216
- return 'fading';
217
- if ((voice.owner === null ? t : ownerReading(voice.owner, t)) < voice.start)
218
- return 'pending';
219
- if (voice.freezesAfter && voice.elapsedAt(t) >= voice.span + voice.latest)
220
- return 'frozen';
221
- return 'live';
222
- }
223
211
  /**
224
212
  * Moves every mix on the transport to mix time `to`, as it stood at the end of that frame, or to
225
213
  * the end of the frame numbered `to.frame`.
@@ -334,7 +322,7 @@ function back(mix, t, seq, on) {
334
322
  for (const voice of kept)
335
323
  restoreVoice(mix, voice, t, seq);
336
324
  for (const voice of kept)
337
- voice.state = stateAt(voice, t);
325
+ voice.state = voice.stateAt(t);
338
326
  mix.cued = kept;
339
327
  mix.gone = gone;
340
328
  mix.retired = [];
package/dist/shared.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export declare const release = "0.15.0";
5
+ export declare const release = "0.16.0";
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/shared.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export const release = '0.15.0';
5
+ export const release = '0.16.0';
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/spans.d.ts CHANGED
@@ -29,7 +29,10 @@ export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>, from?:
29
29
  export declare function refloor<I, O>(mix: Mixer<I, O>): void;
30
30
  /** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
31
31
  export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
32
- /** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
32
+ /**
33
+ * Lets the spans out of budget that hold no child still playing leave, at the moment their budget
34
+ * ran out.
35
+ */
33
36
  export declare function lapse<I, O>(mix: Mixer<I, O>, now: number): void;
34
37
  /** Fits every span again, as after a seek put its children back. */
35
38
  export declare function refitAll<I, O>(mix: Mixer<I, O>): void;
package/dist/spans.js CHANGED
@@ -6,7 +6,7 @@ import { reorigin } from './held.js';
6
6
  import { noted } from './history.js';
7
7
  import { nextFrame } from './move.js';
8
8
  import { leave } from './origin.js';
9
- import { ownerReading } from './owner.js';
9
+ import { ownerReading, plays } from './owner.js';
10
10
  import { resolve } from './place.js';
11
11
  /** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
12
12
  export class Fitting {
@@ -222,13 +222,16 @@ export function lingers(span, at) {
222
222
  const f = span.fitting;
223
223
  return f !== null && Number.isFinite(f.budget) && ownerReading(span, at) < f.budget;
224
224
  }
225
- /** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
225
+ /**
226
+ * Lets the spans out of budget that hold no child still playing leave, at the moment their budget
227
+ * ran out.
228
+ */
226
229
  export function lapse(mix, now) {
227
230
  for (const span of mix.owners ?? []) {
228
231
  const f = span.fitting;
229
232
  if (f === null || span.state === 'done' || !Number.isFinite(f.budget))
230
233
  continue;
231
- if ((span.holding?.children.length ?? 0) > 0)
234
+ if (plays(span, now))
232
235
  continue;
233
236
  if (ownerReading(span, now) >= f.budget)
234
237
  retire(mix, span, span.timeAt(f.budget));
package/dist/strays.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { delayOf } from './delay.js';
1
2
  import { unpart } from './fade.js';
2
3
  import { pageOut } from './paging.js';
3
4
  import { record, taping, unplayable } from './tape.js';
@@ -75,7 +76,7 @@ export function motionOwner(mix) {
75
76
  return Number.NaN;
76
77
  unpart(m, v, subject);
77
78
  // Its record is gone until it is next read, so its stagger is asked here as it will be there.
78
- return timeIn(m, v, v.spec.stagger ? v.spec.stagger(subject) : 0);
79
+ return timeIn(m, v, delayOf(v, subject));
79
80
  },
80
81
  };
81
82
  }
@@ -107,7 +108,7 @@ function changeAhead(to, id, motion, given, change) {
107
108
  unpart(to, v, subject);
108
109
  if (at === undefined) {
109
110
  const frame = frameOf(to, v, subject);
110
- const back = parted ? timeIn(to, v, v.spec.stagger ? v.spec.stagger(subject) : 0) : frame;
111
+ const back = parted ? timeIn(to, v, delayOf(v, subject)) : frame;
111
112
  if (!Number.isNaN(frame))
112
113
  at = frame;
113
114
  else if (!Number.isNaN(back))
package/dist/touch.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { headOf } from './chain.js';
2
- import { restamp } from './everyone.js';
3
2
  import { nextFrame } from './move.js';
4
3
  /**
5
4
  * What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
@@ -22,7 +21,7 @@ export function touch(mix, subject) {
22
21
  held.seeks = -1;
23
22
  const slot = headOf(mix, subject)?.slot ?? -1;
24
23
  for (const voice of mix.sharers)
25
- restamp(voice, slot);
24
+ mix.share?.restamp(voice, slot);
26
25
  }
27
26
  mix.lanes?.refill();
28
27
  mix.stir();
@@ -0,0 +1,4 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Trace, TraceOptions } from './types.js';
3
+ /** `mix.trace`: see `Mix.trace`. */
4
+ export declare function traceMix<I, O>(mix: Mixer<I, O>, from: number, to: number, opts?: TraceOptions): Trace<I, O>;
package/dist/trace.js ADDED
@@ -0,0 +1,255 @@
1
+ import { straight } from './easing.js';
2
+ /**
3
+ * How far inside a sampled stretch its ends are read, ms, but for the window's own: a stretch ends
4
+ * where a voice may jump, and its end is read on its own side of the jump.
5
+ */
6
+ const NUDGE = 1e-6;
7
+ /** Two values at one time this close are one point, not a jump. */
8
+ const close = (a, b) => Math.abs(a - b) <= 1e-6 * Math.max(1, Math.abs(a), Math.abs(b));
9
+ /** `mix.trace`: see `Mix.trace`. */
10
+ export function traceMix(mix, from, to, opts = {}) {
11
+ if (!(from <= to) || !Number.isFinite(from) || !Number.isFinite(to))
12
+ throw new RangeError(`blits: a trace runs from a time to one no earlier, not ${from} to ${to}`);
13
+ const grid = opts.grid ?? 5;
14
+ if (!(grid > 0 && grid < Number.POSITIVE_INFINITY))
15
+ throw new RangeError(`blits: a trace's grid is a finite number of ms above 0, not ${grid}`);
16
+ const known = knowable(mix, from);
17
+ const projections = new Map();
18
+ const poses = new Map();
19
+ const plans = new Map();
20
+ const poseAt = (subject, t) => {
21
+ let mine = poses.get(subject);
22
+ if (mine === undefined) {
23
+ mine = new Map();
24
+ poses.set(subject, mine);
25
+ }
26
+ let pose = mine.get(t);
27
+ if (pose === undefined) {
28
+ let p = projections.get(t);
29
+ if (p === undefined) {
30
+ p = mix.project(t);
31
+ projections.set(t, p);
32
+ }
33
+ pose = p.probe(subject, mix.pose());
34
+ mine.set(t, pose);
35
+ }
36
+ return pose;
37
+ };
38
+ return {
39
+ from,
40
+ to,
41
+ of(subject, channel) {
42
+ const name = channel;
43
+ const slot = mix.slotOf.get(name);
44
+ if (slot === undefined)
45
+ throw new Error(`blits: the mix has no channel ${name}`);
46
+ const value = (t) => {
47
+ const v = poseAt(subject, t)[name];
48
+ return typeof v === 'number' ? v : Number.NaN;
49
+ };
50
+ let plan = plans.get(subject);
51
+ if (plan === undefined) {
52
+ plan = planOf(mix, subject, from, to);
53
+ plans.set(subject, plan);
54
+ }
55
+ const op = mix.scalars[slot];
56
+ const shaped = known &&
57
+ !plan.unknown.has(slot) &&
58
+ !mix.bounded.includes(slot) &&
59
+ (op === 'sum' || (op === 'mul' && (plan.writers.get(slot) ?? 0) <= 1));
60
+ const bent = plan.bent.get(slot);
61
+ const straightOver = (a, b) => shaped && (bent === undefined || bent.every(([lo, hi]) => !(lo < b && hi > a)));
62
+ if (from === to)
63
+ return { at: [from], value: [value(from)], exact: shaped };
64
+ const at = [];
65
+ const values = [];
66
+ let exact = true;
67
+ const add = (t, v) => {
68
+ const n = at.length;
69
+ if (n > 0 && at[n - 1] === t && close(values[n - 1], v))
70
+ values[n - 1] = v;
71
+ else {
72
+ at.push(t);
73
+ values.push(v);
74
+ }
75
+ };
76
+ const knots = [from, ...(plan.knots.get(slot) ?? []), to];
77
+ for (let i = 0; i + 1 < knots.length; i++) {
78
+ const a = knots[i];
79
+ const b = knots[i + 1];
80
+ if (straightOver(a, b)) {
81
+ // Read inside, so a jump at either end is not read; the line reaches each end exactly.
82
+ const t1 = a + (b - a) / 3;
83
+ const t2 = b - (b - a) / 3;
84
+ const v1 = value(t1);
85
+ const slope = (value(t2) - v1) / (t2 - t1);
86
+ add(a, v1 - slope * (t1 - a));
87
+ add(b, v1 + slope * (b - t1));
88
+ continue;
89
+ }
90
+ exact = false;
91
+ const e = Math.min(NUDGE, (b - a) / 4);
92
+ add(a, value(a === from ? a : a + e));
93
+ for (let k = Math.max(0, Math.ceil((a - from) / grid));; k++) {
94
+ const g = from + k * grid;
95
+ if (g >= b - e)
96
+ break;
97
+ if (g > a + e)
98
+ add(g, value(g));
99
+ }
100
+ add(b, value(b === to ? b : b - e));
101
+ }
102
+ return { at, value: values, exact };
103
+ },
104
+ };
105
+ }
106
+ /**
107
+ * Whether the mix's own state lets a trace know any shape: a window at or after the mix's time,
108
+ * with no recorded call ahead.
109
+ */
110
+ function knowable(mix, from) {
111
+ const transport = mix.transport;
112
+ if (!(from >= mix.now))
113
+ return false;
114
+ const tape = transport.tape;
115
+ return tape === undefined || tape.timestampAt(tape.undoDepth()) === undefined;
116
+ }
117
+ function planOf(mix, subject, from, to) {
118
+ const plan = { knots: new Map(), bent: new Map(), unknown: new Set(), writers: new Map() };
119
+ for (const voice of mix.cued) {
120
+ if (voice.state === 'done' || !mix.aims(voice, subject))
121
+ continue;
122
+ if (shapeOf(mix, voice, subject, from, to, plan))
123
+ continue;
124
+ for (const slot of voice.slots)
125
+ plan.unknown.add(slot);
126
+ }
127
+ for (const [slot, knots] of plan.knots)
128
+ plan.knots.set(slot, knots.sort((a, b) => a - b).filter((t, i, all) => i === 0 || t !== all[i - 1]));
129
+ return plan;
130
+ }
131
+ /** The most breakpoints one voice may add to a window before it is read on the grid instead. */
132
+ const MOST = 4096;
133
+ /**
134
+ * Adds a voice's breakpoints for one subject to `plan`, and returns true, where its shape is known
135
+ * through the window: a `keys` voice on its own clock at a steady rate and a number weight. Returns
136
+ * false, adding nothing, for any other.
137
+ */
138
+ function shapeOf(mix, voice, subject, from, to, plan) {
139
+ const spec = voice.spec;
140
+ const built = voice.built;
141
+ const patch = voice.patch;
142
+ if (patch.form !== 'keys' || built === null || patch.step !== undefined)
143
+ return false;
144
+ if (typeof spec.weight === 'function' || voice.owner !== null || voice.holding !== null)
145
+ return false;
146
+ if (voice.fitting !== null || voice.arc !== null || voice.blend !== null)
147
+ return false;
148
+ if (spec.anchor !== undefined || spec.role !== undefined || spec.within !== undefined)
149
+ return false;
150
+ if (spec.stepMs !== undefined || spec.from !== undefined)
151
+ return false;
152
+ if (voice.ramp !== null || !(voice.rate > 0) || voice.out !== null || voice.back !== null)
153
+ return false;
154
+ if (voice.parts !== null || voice.parted !== null || voice.left !== null)
155
+ return false;
156
+ if (Number.isFinite(voice.outAt) || voice.state === 'fading')
157
+ return false;
158
+ // Under a mix rate, a waiting voice's start is pinned to the host's clock, where a ramp moves it.
159
+ if (voice.state === 'pending' && mix.transport.pace !== null)
160
+ return false;
161
+ const knots = [];
162
+ const bent = [];
163
+ const bentBy = new Map();
164
+ const at = (t) => {
165
+ if (t > from && t < to)
166
+ knots.push(t);
167
+ };
168
+ const reduced = mix.reduced;
169
+ const held = voice.sharing ? undefined : voice.subjects.get(subject);
170
+ const delay = held?.delay ?? (spec.stagger ? spec.stagger(subject) : 0);
171
+ const time = (raw) => voice.timeAt(raw + delay);
172
+ const fadeIn = reduced ? 0 : (voice.fade.in ?? 0);
173
+ if (fadeIn > 0) {
174
+ // Where its fade in counts from is known only once the subject has met the voice.
175
+ if (held === undefined)
176
+ return false;
177
+ if (held.shown + fadeIn > from) {
178
+ bent.push([held.shown, held.shown + fadeIn]);
179
+ at(held.shown);
180
+ at(held.shown + fadeIn);
181
+ }
182
+ }
183
+ const duration = voice.duration;
184
+ const span = voice.span;
185
+ at(voice.start);
186
+ at(time(0));
187
+ if (Number.isFinite(span))
188
+ at(time(span));
189
+ const ends = voice.endsAt();
190
+ at(ends);
191
+ if (!voice.freezesAfter && Number.isFinite(ends)) {
192
+ const over = reduced ? 0 : Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
193
+ if (over > 0) {
194
+ bent.push([ends, ends + over]);
195
+ at(ends + over);
196
+ }
197
+ // It leaves when its latest subject ends, which a subject first met later can move.
198
+ if (spec.stagger !== undefined)
199
+ bent.push([time(span), Number.POSITIVE_INFINITY]);
200
+ }
201
+ if (duration > 0) {
202
+ const first = Math.max(0, Math.floor((voice.elapsedAt(from) - delay) / duration));
203
+ const last = Math.min(Math.ceil(voice.passes) - 1, Math.floor((voice.elapsedAt(to) - delay) / duration));
204
+ let stops = 0;
205
+ for (const track of built.tracks)
206
+ stops += track.all.length + 1;
207
+ if ((last - first + 1) * stops > MOST)
208
+ return false;
209
+ for (let pass = first; pass <= last; pass++) {
210
+ const base = pass * duration;
211
+ at(time(base));
212
+ at(time(base + duration));
213
+ for (const track of built.tracks) {
214
+ const slot = mix.slotOf.get(track.channel);
215
+ if (track.numbers === null)
216
+ return false;
217
+ const wait = track.delay;
218
+ if (wait >= duration)
219
+ continue;
220
+ if (wait > 0)
221
+ at(time(base + wait));
222
+ let before = Number.NaN;
223
+ for (const pt of track.all) {
224
+ const t = time(base + wait + Math.min(1, Math.max(0, pt.at)) * (duration - wait));
225
+ at(t);
226
+ if (!Number.isNaN(before) && !straight(pt.ease)) {
227
+ const list = bentBy.get(slot);
228
+ if (list === undefined)
229
+ bentBy.set(slot, [[before, t]]);
230
+ else
231
+ list.push([before, t]);
232
+ }
233
+ before = t;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ for (const slot of voice.slots) {
239
+ const times = plan.knots.get(slot);
240
+ if (times === undefined)
241
+ plan.knots.set(slot, [...knots]);
242
+ else
243
+ times.push(...knots);
244
+ plan.writers.set(slot, (plan.writers.get(slot) ?? 0) + 1);
245
+ const mine = [...bent, ...(bentBy.get(slot) ?? [])];
246
+ if (mine.length === 0)
247
+ continue;
248
+ const list = plan.bent.get(slot);
249
+ if (list === undefined)
250
+ plan.bent.set(slot, mine);
251
+ else
252
+ list.push(...mine);
253
+ }
254
+ return true;
255
+ }
package/dist/turns.js CHANGED
@@ -51,7 +51,8 @@ export function turnToward(ch, values, base, value, taken, u) {
51
51
  }
52
52
  const axes = ch.axes;
53
53
  let out = value;
54
- if (taken !== 0) {
54
+ // A first value of other axes than the lane's, as an ink contribution is, widens through the lerp.
55
+ if (taken !== 0 || out.length !== axes) {
55
56
  held = lift(values, base, axes, held);
56
57
  out = ch.channel.lerp(held, value, u);
57
58
  }
@@ -3,6 +3,7 @@ import type { Kit } from './channel.js';
3
3
  import type { HistoryOptions, Tape } from './history.js';
4
4
  import type { Band, Easing, Patch, Sent, Signal } from './patch.js';
5
5
  import type { Booker, BookOptions, Marked } from './place.js';
6
+ import type { Trace, TraceOptions } from './trace.js';
6
7
  import type { Transport } from './transport.js';
7
8
  import type { BlendMember, Circle, Handle, OwnerSpec, SpanHandle, SpanSpec, VoiceSpec } from './voice.js';
8
9
  /**
@@ -250,6 +251,20 @@ export interface Mix<I, O, H = unknown> {
250
251
  project(at: number | {
251
252
  readonly frame: number;
252
253
  }): Projection<I, O>;
254
+ /**
255
+ * Reads the mix over a window of mix time, `from` to `to`, without moving it, as `project` reads
256
+ * one moment: `trace.of(subject, channel)` gives that channel as points joined by straight lines.
257
+ * Where every voice feeding the channel is a `keys` voice whose shape is known through the
258
+ * window, the points are its own breakpoints and the lines are exact; elsewhere the run is read
259
+ * every `grid` ms, as many `project` calls would read it.
260
+ *
261
+ * Exact needs, of each voice reaching the subject and writing the channel: a `keys` patch whose
262
+ * stops are plain numbers, eased `linear`; a number weight; no owner, anchor, role, blend,
263
+ * `within`, `stepMs`, `from: 'current'`, or rate ramp; a fade in finished by `from` and no fade
264
+ * out under way or due. The channel folds by `sum`, or by `mul` from one such voice, and has no
265
+ * `bounds`. The window lies at or after the mix's own time, and the tape holds no calls ahead.
266
+ */
267
+ trace(from: number, to: number, opts?: TraceOptions): Trace<I, O>;
253
268
  /**
254
269
  * Moves the mix to mix time `time` and plays on from there. It lands as the mix stood at the end
255
270
  * of that frame; where rate 0 held the clock at `time` for several frames, the last of them.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What `mix.trace` takes besides its window.
3
+ *
4
+ * @category mix
5
+ */
6
+ export interface TraceOptions {
7
+ /**
8
+ * Where a run cannot be known exactly, how far apart it is read, in ms of mix time. Default 5.
9
+ */
10
+ grid?: number;
11
+ }
12
+ /**
13
+ * One channel of one subject over a trace's window, as points joined by straight lines: the value
14
+ * between two points is the straight line between them. Two points at one time are a jump, from
15
+ * the first value to the second. The first point is at the window's start and the last at its end.
16
+ *
17
+ * @category mix
18
+ */
19
+ export interface Run {
20
+ /** Mix times, ascending; a time given twice is a jump. */
21
+ readonly at: readonly number[];
22
+ /** The channel's value at each of `at`; NaN where it holds no number. */
23
+ readonly value: readonly number[];
24
+ /**
25
+ * Whether the lines are the mix's own values everywhere in the window, not only at the points.
26
+ * Where they are not, the points are each `grid` apart and the lines between them approximate.
27
+ */
28
+ readonly exact: boolean;
29
+ }
30
+ /**
31
+ * The channels of `O` that hold one number.
32
+ *
33
+ * @category mix
34
+ */
35
+ export type NumberChannel<O> = {
36
+ [K in keyof O]-?: NonNullable<O[K]> extends number ? K : never;
37
+ }[keyof O];
38
+ /**
39
+ * The mix read over a window of mix time, with nothing in the live mix moved. Valid until the mix
40
+ * is next synced or cued.
41
+ *
42
+ * @category mix
43
+ */
44
+ export interface Trace<I, O> {
45
+ /** The window's start, in mix time. */
46
+ readonly from: number;
47
+ /** The window's end, in mix time. */
48
+ readonly to: number;
49
+ /** One subject's channel over the window. */
50
+ of(subject: I, channel: NumberChannel<O>): Run;
51
+ }