@msb235/blits 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +31 -7
  3. package/dist/bare.js +7 -1
  4. package/dist/book.d.ts +7 -0
  5. package/dist/book.js +35 -4
  6. package/dist/chain.d.ts +2 -1
  7. package/dist/chain.js +7 -3
  8. package/dist/channels.d.ts +12 -19
  9. package/dist/channels.js +18 -118
  10. package/dist/clock.d.ts +9 -2
  11. package/dist/clock.js +10 -2
  12. package/dist/closed.d.ts +15 -0
  13. package/dist/closed.js +144 -0
  14. package/dist/color.d.ts +61 -0
  15. package/dist/color.js +229 -0
  16. package/dist/columns.js +2 -1
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.d.ts +2 -0
  20. package/dist/cue.js +80 -27
  21. package/dist/due.js +3 -0
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +57 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +15 -8
  26. package/dist/fit.d.ts +144 -0
  27. package/dist/fit.js +181 -0
  28. package/dist/fold.d.ts +3 -3
  29. package/dist/fold.js +18 -11
  30. package/dist/gather.d.ts +6 -0
  31. package/dist/gather.js +18 -0
  32. package/dist/gone.d.ts +32 -0
  33. package/dist/gone.js +134 -0
  34. package/dist/handle.d.ts +27 -2
  35. package/dist/handle.js +60 -11
  36. package/dist/held.js +4 -0
  37. package/dist/history.d.ts +15 -2
  38. package/dist/history.js +56 -9
  39. package/dist/hosts.d.ts +2 -2
  40. package/dist/hosts.js +20 -6
  41. package/dist/index.d.ts +8 -2
  42. package/dist/index.js +5 -1
  43. package/dist/lane.d.ts +1 -1
  44. package/dist/lanes.d.ts +32 -9
  45. package/dist/lanes.js +53 -24
  46. package/dist/locus.d.ts +3 -3
  47. package/dist/locus.js +1 -1
  48. package/dist/marks.d.ts +3 -0
  49. package/dist/marks.js +80 -12
  50. package/dist/meet.d.ts +3 -2
  51. package/dist/meet.js +15 -3
  52. package/dist/mixer.d.ts +56 -36
  53. package/dist/mixer.js +158 -87
  54. package/dist/motion.d.ts +2 -199
  55. package/dist/motion.js +2 -652
  56. package/dist/motions.d.ts +266 -0
  57. package/dist/motions.js +724 -0
  58. package/dist/move.d.ts +1 -1
  59. package/dist/move.js +28 -8
  60. package/dist/owner.d.ts +1 -1
  61. package/dist/owner.js +3 -3
  62. package/dist/pace.d.ts +2 -0
  63. package/dist/pace.js +8 -0
  64. package/dist/pack.d.ts +31 -0
  65. package/dist/pack.js +65 -0
  66. package/dist/paging.d.ts +63 -0
  67. package/dist/paging.js +110 -0
  68. package/dist/patch.d.ts +3 -0
  69. package/dist/patch.js +2 -0
  70. package/dist/place.js +235 -43
  71. package/dist/project.d.ts +7 -4
  72. package/dist/project.js +117 -78
  73. package/dist/pull.js +4 -3
  74. package/dist/qualify.js +4 -2
  75. package/dist/revive.d.ts +58 -0
  76. package/dist/revive.js +158 -0
  77. package/dist/sample.d.ts +1 -1
  78. package/dist/sample.js +6 -1
  79. package/dist/scored.d.ts +57 -0
  80. package/dist/scored.js +196 -0
  81. package/dist/seek.d.ts +3 -0
  82. package/dist/seek.js +311 -0
  83. package/dist/spans.d.ts +29 -0
  84. package/dist/spans.js +165 -0
  85. package/dist/strays.d.ts +1 -1
  86. package/dist/strays.js +21 -0
  87. package/dist/tape.d.ts +21 -0
  88. package/dist/tape.js +65 -0
  89. package/dist/ticker.d.ts +28 -3
  90. package/dist/ticker.js +3 -0
  91. package/dist/transport.d.ts +125 -0
  92. package/dist/transport.js +283 -0
  93. package/dist/types.d.ts +404 -38
  94. package/dist/unpage.d.ts +8 -0
  95. package/dist/unpage.js +165 -0
  96. package/dist/voice.d.ts +75 -7
  97. package/dist/voice.js +119 -4
  98. package/dist/weigh.js +2 -2
  99. package/docs/schema.html +120 -31
  100. package/package.json +2 -1
package/dist/fade.d.ts CHANGED
@@ -16,4 +16,17 @@ export declare function beginFade<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, op
16
16
  at?: 'rest';
17
17
  deadline?: number;
18
18
  }, at?: number): void;
19
+ /**
20
+ * Turns a fading voice around: it climbs back from where its fade out had got to, up the same
21
+ * curve, at the whole curve per `over` ms. A fade waiting for rest is taken back at once. A voice
22
+ * not fading is left as it is.
23
+ */
24
+ export declare function beginRise<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, opts: {
25
+ over?: number;
26
+ }): void;
27
+ /**
28
+ * Fades a voice out from mix time `at`: ahead, it plays untouched until then and begins its fade
29
+ * exactly there; at or behind now, the fade began then and is partway.
30
+ */
31
+ export declare function fadeAt<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, at: number, over: number | undefined): void;
19
32
  export declare function retire<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, at?: number): void;
package/dist/fade.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { changed } from './chain.js';
2
2
  import { envelope } from './clock.js';
3
3
  import { schedule } from './due.js';
4
- import { noted } from './history.js';
4
+ import { leave, noted, releasing } from './history.js';
5
5
  import { orphan } from './owner.js';
6
6
  import { startOf } from './place.js';
7
+ import { scoreTouched } from './scored.js';
8
+ import { lingers, refit } from './spans.js';
7
9
  import { unreach } from './unreached.js';
8
10
  /** Starts one subject's ramp out of a voice, or takes it out at once for a ramp of 0. */
9
11
  export function fadeSubject(mix, voice, subject, over) {
@@ -43,7 +45,7 @@ export function part(mix, voice, subject, at) {
43
45
  forgetIn(mix, voice, subject);
44
46
  const motion = voice.motion;
45
47
  if (motion !== undefined && asks(mix, motion, voice))
46
- motion.release(subject);
48
+ motion.release(subject, ...releasing(mix));
47
49
  }
48
50
  /** Brings a subject faded out of a voice back, to be met afresh on its next probe. */
49
51
  export function unpart(mix, voice, subject) {
@@ -63,6 +65,7 @@ function forgetIn(mix, voice, subject) {
63
65
  unreach(voice, head.slot, false);
64
66
  const held = voice.subjects.get(subject);
65
67
  if (held !== undefined) {
68
+ leave(mix, voice, subject, held);
66
69
  voice.subjects.delete(subject);
67
70
  if (held.reaches)
68
71
  voice.seen--;
@@ -102,6 +105,52 @@ export function beginFade(mix, voice, opts, at) {
102
105
  if (over === 0 && opts.at !== 'rest')
103
106
  retire(mix, voice, voice.out.at);
104
107
  }
108
+ /**
109
+ * Turns a fading voice around: it climbs back from where its fade out had got to, up the same
110
+ * curve, at the whole curve per `over` ms. A fade waiting for rest is taken back at once. A voice
111
+ * not fading is left as it is.
112
+ */
113
+ export function beginRise(mix, voice, opts) {
114
+ const out = voice.out;
115
+ if (voice.state !== 'fading' && voice.outSet) {
116
+ // A fade set ahead and not begun is taken back, and the weight never moved.
117
+ voice.outAt = Number.POSITIVE_INFINITY;
118
+ voice.outOver = Number.NaN;
119
+ voice.outSet = false;
120
+ noted(mix, voice);
121
+ return;
122
+ }
123
+ if (voice.state !== 'fading' || out === null)
124
+ return;
125
+ const at = Number.isNaN(mix.now) ? startOf(voice) : mix.now;
126
+ const over = mix.reduced ? 0 : (opts.over ?? voice.fade.in ?? voice.fade.out ?? 0);
127
+ const left = out.rest || out.over === 0 ? 1 : 1 - (at - out.at) / out.over;
128
+ const from = left < 0 ? 0 : left > 1 ? 1 : left;
129
+ voice.out = null;
130
+ voice.outAt = Number.POSITIVE_INFINITY;
131
+ voice.state = 'live';
132
+ voice.back = over > 0 && from < 1 ? { from, at, over } : null;
133
+ noted(mix, voice);
134
+ mix.lanes?.refill();
135
+ }
136
+ /**
137
+ * Fades a voice out from mix time `at`: ahead, it plays untouched until then and begins its fade
138
+ * exactly there; at or behind now, the fade began then and is partway.
139
+ */
140
+ export function fadeAt(mix, voice, at, over) {
141
+ if (voice.state === 'done' || voice.state === 'fading')
142
+ return;
143
+ const now = Number.isNaN(mix.now) ? startOf(voice) : mix.now;
144
+ if (at <= now) {
145
+ beginFade(mix, voice, { over }, Math.max(startOf(voice), at));
146
+ mix.stir();
147
+ return;
148
+ }
149
+ voice.outAt = at;
150
+ voice.outOver = over ?? Number.NaN;
151
+ voice.outSet = true;
152
+ noted(mix, voice);
153
+ }
105
154
  /** Removes a voice, recording that it left at `at`, default now. */
106
155
  /** Whether a motion patch still asks this voice of this mix for its time. */
107
156
  function asks(mix, motion, voice) {
@@ -114,11 +163,12 @@ export function retire(mix, voice, at) {
114
163
  if (motion !== undefined && asks(mix, motion, voice))
115
164
  motion.owner = null;
116
165
  voice.state = 'done';
166
+ scoreTouched(mix, voice);
117
167
  // The record its setting last wrote to, which a retired voice no longer calls for.
118
168
  voice.keepOn = null;
119
169
  mix.retired.push(voice);
120
170
  voice.doneAt = at ?? (Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now);
121
- voice.play(false);
171
+ voice.play(false, voice.doneAt);
122
172
  voice.resolve();
123
173
  // An owner takes what it holds with it, and leaves with its last child.
124
174
  if (voice.holding !== null)
@@ -126,8 +176,11 @@ export function retire(mix, voice, at) {
126
176
  if (child.state !== 'done')
127
177
  retire(mix, child, voice.doneAt);
128
178
  if (voice.owner !== null) {
179
+ const span = voice.owner;
129
180
  const owner = orphan(voice);
130
- if (owner !== null)
181
+ if (owner !== null && !lingers(owner, voice.doneAt))
131
182
  retire(mix, owner, voice.doneAt);
183
+ else if (span.fitting !== null && span.state !== 'done')
184
+ refit(mix, span);
132
185
  }
133
186
  }
package/dist/fill.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare function reset<I, O>(this: Lanes<I, O>, lane: Lane<I, O>): void;
23
23
  export declare function run<I, O>(this: Lanes<I, O>, lane: Lane<I, O>): void;
24
24
  /**
25
25
  * One subject's contribution from a lane's voice. True where the voice gave the subject a delta,
26
- * as the general path's `influence` does where it returns one, which is what makes a voice in a
26
+ * as the general path's `contribution` does where it returns one, which is what makes a voice in a
27
27
  * locus one of its members for the subject.
28
28
  */
29
29
  export declare function one<I, O>(this: Lanes<I, O>, lane: Lane<I, O>, p: number, slot: number, elapsedNow: number, duration: number, passes: number): boolean;
package/dist/fill.js CHANGED
@@ -39,6 +39,7 @@ export function fillAll(lanes, now, version) {
39
39
  const decide = lanes.fresh;
40
40
  lanes.fresh = false;
41
41
  const share = lanes.live > 0 ? lanes.lastDistinct / lanes.live : 1;
42
+ lanes.lately = share < 1 ? lanes.lastFrom : -1;
42
43
  // Deciding which lanes and crowds go idle first, then running them, in voice order: a crowd's
43
44
  // rows are folded between the lanes on either side of their voices, as the general path
44
45
  // folds every voice in order.
@@ -95,11 +96,9 @@ export function fillAll(lanes, now, version) {
95
96
  }
96
97
  }
97
98
  // With every lane idle there was nothing to fill, and no subject reads a lane this frame.
98
- if (busy)
99
- for (let s = 0; s < size; s++)
100
- lanes.per[s * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = lanes.fills;
99
+ lanes.busyFill = busy ? lanes.fills : 0;
101
100
  for (const slot of lanes.late)
102
- lanes.per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = 0;
101
+ lanes.per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = lanes.fills;
103
102
  if (lanes.holding) {
104
103
  lanes.subjects.fill(undefined, 0, size);
105
104
  lanes.holding = false;
@@ -208,7 +207,11 @@ export function reset(lane) {
208
207
  lane.read = false;
209
208
  lane.weighed = false;
210
209
  // With no fade in or out the envelope is 1 for every subject, which is what it would return.
211
- lane.flat = !((voice.fade.in ?? 0) > 0) && voice.out === null && voice.owner === null;
210
+ lane.flat =
211
+ !((voice.fade.in ?? 0) > 0) &&
212
+ voice.out === null &&
213
+ voice.back === null &&
214
+ voice.owner === null;
212
215
  lane.fade = 1;
213
216
  }
214
217
  /** One voice's contribution to every subject it plays on. */
@@ -238,7 +241,7 @@ export function run(lane) {
238
241
  }
239
242
  /**
240
243
  * One subject's contribution from a lane's voice. True where the voice gave the subject a delta,
241
- * as the general path's `influence` does where it returns one, which is what makes a voice in a
244
+ * as the general path's `contribution` does where it returns one, which is what makes a voice in a
242
245
  * locus one of its members for the subject.
243
246
  */
244
247
  export function one(lane, p, slot, elapsedNow, duration, passes) {
@@ -249,6 +252,8 @@ export function one(lane, p, slot, elapsedNow, duration, passes) {
249
252
  // A probe read this subject from the last fill: keep the weight it read for `weightOf`.
250
253
  if (this.per[slot * 6 /* Per.SLOT */ + 5 /* Per.LANE_FILL */] === this.fills - 1)
251
254
  data[o + 3 /* Row.PROBED */] = data[o + 2 /* Row.WEIGHT */];
255
+ if (this.lately >= 0 && this.unread(slot))
256
+ return false;
252
257
  const delay = data[o + 0 /* Row.DELAY */];
253
258
  const elapsed = frozenAt(voice, elapsedNow - delay);
254
259
  if (!(elapsed >= 0)) {
@@ -278,6 +283,8 @@ export function one(lane, p, slot, elapsedNow, duration, passes) {
278
283
  else
279
284
  w = weighed(voice.weight, lane.fade, this.parting(voice, slot));
280
285
  data[o + 2 /* Row.WEIGHT */] = w;
286
+ this.folding = lane;
287
+ this.rec = lane.records[p] ?? null;
281
288
  if (voice.built !== null) {
282
289
  if (!lane.read || !Object.is(elapsed, lane.readAt)) {
283
290
  this.readKeyed(voice, lane.phase, lane.delta, lane.scratch);
@@ -367,12 +374,12 @@ export function call(voice, chans, rec, slot, elapsed, phase, pass, delay, w) {
367
374
  else
368
375
  reading.horizon = Number.POSITIVE_INFINITY;
369
376
  const delta = voice.patch.at(phase, subject, voice.setting);
370
- // What `influence` leaves on the record, so a probe on the general path this frame reuses it.
377
+ // What `contribution` leaves on the record, so a probe on the general path this frame reuses it.
371
378
  rec.delta = delta;
372
379
  rec.probed = this.now;
373
380
  rec.seeks = voice.seeks;
374
381
  if (this.keeps)
375
- host.after(voice, rec);
382
+ host.after(voice, subject, rec);
376
383
  if (reading.kept !== kept && !voice.keeping)
377
384
  host.kept(voice);
378
385
  if (w > 0)
package/dist/fit.d.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * How hard a claim on time holds when it meets another: the weaker gives way.
3
+ *
4
+ * @category score
5
+ */
6
+ export type Strength = 'weak' | 'strong' | 'required';
7
+ /**
8
+ * How a span lays its children out: each after the one before has ended, partway through it, or
9
+ * all at once.
10
+ *
11
+ * @category score
12
+ */
13
+ export type Order = 'queue' | 'stagger' | 'together';
14
+ /**
15
+ * One child of a span as a fit sees it: what is left of it, and the ways it may give way.
16
+ *
17
+ * @category score
18
+ */
19
+ export interface Claim {
20
+ /** Span ms left of it at its own rate. */
21
+ natural: number;
22
+ /** A playing child has started, and stays at the front. */
23
+ state: 'pending' | 'playing';
24
+ /** How many times faster it may play; 1 for no faster. */
25
+ faster: number;
26
+ /** How many times slower it may play; 1 for no slower. */
27
+ slower: number;
28
+ /** Whether it may run alongside the one before it where the span's order would not have it. */
29
+ overlap: boolean;
30
+ /** Whether it may jump to its end instead of playing. */
31
+ ballast: boolean;
32
+ priority: Strength;
33
+ }
34
+ /**
35
+ * The span as a fit sees it.
36
+ *
37
+ * @category score
38
+ */
39
+ export interface SpanClaim {
40
+ /** Span ms left of its budget; Infinity for a span with none. */
41
+ left: number;
42
+ /** Its whole budget. */
43
+ budget: number;
44
+ priority: Strength;
45
+ order: Order;
46
+ /** How far through the one before a staggered child starts, 0..1. */
47
+ share: number;
48
+ }
49
+ /**
50
+ * What a fit decides, per child in the order given: a rate over its own, and whether it is
51
+ * skipped. `share` is how far through the one before a child that may overlap starts, and `allow`
52
+ * the span ms the span may run past its budget.
53
+ *
54
+ * @category score
55
+ */
56
+ export interface FitPlan {
57
+ rate: number[];
58
+ skip: boolean[];
59
+ share: number;
60
+ allow: number;
61
+ }
62
+ /**
63
+ * A strategy for fitting a span's children into its budget: from the plan so far to a new one.
64
+ *
65
+ * @category score
66
+ */
67
+ export type Fit = (span: SpanClaim, kids: readonly Claim[], plan: FitPlan) => FitPlan;
68
+ /** How far through the one before a child starts under each order, before any fit. */
69
+ export declare function shareOf(order: Order, stagger: number): number;
70
+ /**
71
+ * The plan before any fit: every child at its own rate, played, in the span's order.
72
+ *
73
+ * @category score
74
+ */
75
+ export declare function plain(span: SpanClaim, kids: readonly Claim[]): FitPlan;
76
+ /**
77
+ * Where a plan puts each child, span ms from now, and how long they take together. A playing child
78
+ * is where it is, at 0; each pending one starts its share through the one before it.
79
+ *
80
+ * @category score
81
+ */
82
+ export declare function layout(span: SpanClaim, kids: readonly Claim[], plan: FitPlan): {
83
+ at: number[];
84
+ length: number;
85
+ };
86
+ /**
87
+ * Moves every child's rate by one factor toward the budget, before the span's fit: faster where
88
+ * they run past it, each no faster than its `faster` allows, and slower where they leave room in
89
+ * it, each no slower than its `slower` allows, so they fill it.
90
+ */
91
+ export declare function rescale(span: SpanClaim, kids: readonly Claim[], plan: FitPlan): FitPlan;
92
+ /**
93
+ * Starts children that may overlap sooner after the one before, toward all at once, until they
94
+ * fit.
95
+ *
96
+ * @category score
97
+ */
98
+ export declare function condense(): Fit;
99
+ /**
100
+ * Jumps children that may be skipped to their end, latest first, until they fit.
101
+ *
102
+ * @category score
103
+ */
104
+ export declare function shed(): Fit;
105
+ /**
106
+ * Jumps every child left to its end at once, whether or not it fits otherwise.
107
+ *
108
+ * @category score
109
+ */
110
+ export declare function conclude(): Fit;
111
+ /**
112
+ * Lets the span run past its budget: up to `cap` times it, or without limit.
113
+ *
114
+ * @category score
115
+ */
116
+ export declare function overrun(opts?: {
117
+ cap?: number;
118
+ }): Fit;
119
+ /**
120
+ * Tries each fit in turn, handing the plan on, and stops once the children fit.
121
+ *
122
+ * @category score
123
+ */
124
+ export declare function pipe(...fits_: readonly Fit[]): Fit;
125
+ /**
126
+ * Every way of giving way after the span's retiming, overrunning only once the rest are spent.
127
+ *
128
+ * @category score
129
+ */
130
+ export declare function lax(opts?: {
131
+ cap?: number;
132
+ }): Fit;
133
+ /** The fit a span takes without one of its own. */
134
+ export declare const defaultFit: Fit;
135
+ /**
136
+ * A span's fit settled: the plan, and what it had to do past the strategy, which starts from the
137
+ * children retimed toward the budget. Where the children still
138
+ * do not fit, a span weaker than `strong` lets them run long; otherwise `spill` either skips
139
+ * every child weaker than the span (`instant`), or lets them run long (`overrun`).
140
+ */
141
+ export declare function settle(span: SpanClaim, kids: readonly Claim[], fit: Fit, spill: 'instant' | 'overrun'): {
142
+ plan: FitPlan;
143
+ fell: boolean;
144
+ };
package/dist/fit.js ADDED
@@ -0,0 +1,181 @@
1
+ /** How far through the one before a child starts under each order, before any fit. */
2
+ export function shareOf(order, stagger) {
3
+ return order === 'queue' ? 1 : order === 'together' ? 0 : stagger;
4
+ }
5
+ /**
6
+ * The plan before any fit: every child at its own rate, played, in the span's order.
7
+ *
8
+ * @category score
9
+ */
10
+ export function plain(span, kids) {
11
+ return {
12
+ rate: kids.map(() => 1),
13
+ skip: kids.map(() => false),
14
+ share: shareOf(span.order, span.share),
15
+ allow: 0,
16
+ };
17
+ }
18
+ /**
19
+ * Where a plan puts each child, span ms from now, and how long they take together. A playing child
20
+ * is where it is, at 0; each pending one starts its share through the one before it.
21
+ *
22
+ * @category score
23
+ */
24
+ export function layout(span, kids, plan) {
25
+ const base = shareOf(span.order, span.share);
26
+ const at = [];
27
+ let length = 0;
28
+ let prevAt = 0;
29
+ let prevLen = 0;
30
+ kids.forEach((k, i) => {
31
+ const len = plan.skip[i] ? 0 : k.natural / plan.rate[i];
32
+ const t = k.state === 'playing' || i === 0
33
+ ? 0
34
+ : prevAt + (k.overlap ? Math.min(plan.share, base) : base) * prevLen;
35
+ at.push(t);
36
+ length = Math.max(length, t + len);
37
+ prevAt = t;
38
+ prevLen = len;
39
+ });
40
+ return { at, length };
41
+ }
42
+ const fits = (span, kids, plan) => layout(span, kids, plan).length <= span.left + plan.allow + 1e-9;
43
+ /** The smallest value in `lo..hi` at which `ok` holds, where it holds from some point up. */
44
+ function least(lo, hi, ok) {
45
+ if (ok(lo))
46
+ return lo;
47
+ if (!ok(hi))
48
+ return hi;
49
+ for (let i = 0; i < 60; i++) {
50
+ const mid = (lo + hi) / 2;
51
+ if (ok(mid))
52
+ hi = mid;
53
+ else
54
+ lo = mid;
55
+ }
56
+ return hi;
57
+ }
58
+ /**
59
+ * Moves every child's rate by one factor toward the budget, before the span's fit: faster where
60
+ * they run past it, each no faster than its `faster` allows, and slower where they leave room in
61
+ * it, each no slower than its `slower` allows, so they fill it.
62
+ */
63
+ export function rescale(span, kids, plan) {
64
+ if (!fits(span, kids, plan)) {
65
+ const top = Math.max(1, ...kids.map((k) => k.faster));
66
+ const at = (f) => ({
67
+ ...plan,
68
+ rate: kids.map((k, i) => Math.max(plan.rate[i], Math.min(f, k.faster))),
69
+ });
70
+ return at(least(1, top, (f) => fits(span, kids, at(f))));
71
+ }
72
+ if (!Number.isFinite(span.left))
73
+ return plan;
74
+ const top = Math.max(1, ...kids.map((k) => k.slower));
75
+ if (top === 1)
76
+ return plan;
77
+ const at = (f) => ({
78
+ ...plan,
79
+ rate: kids.map((k, i) => plan.rate[i] / Math.min(f, k.slower)),
80
+ });
81
+ // The largest factor that still fits: `least` over the factors that do not.
82
+ const f = least(1, top, (x) => !fits(span, kids, at(x)));
83
+ return fits(span, kids, at(f)) ? at(f) : at(Math.max(1, f - 1e-9));
84
+ }
85
+ /**
86
+ * Starts children that may overlap sooner after the one before, toward all at once, until they
87
+ * fit.
88
+ *
89
+ * @category score
90
+ */
91
+ export function condense() {
92
+ return (span, kids, plan) => {
93
+ if (fits(span, kids, plan) || !kids.some((k) => k.overlap))
94
+ return plan;
95
+ const at = (s) => ({ ...plan, share: s });
96
+ // `least` finds the smallest x that fits; the share wanted is the largest, so search 1 - share.
97
+ const x = least(0, plan.share, (d) => fits(span, kids, at(plan.share - d)));
98
+ return at(plan.share - x);
99
+ };
100
+ }
101
+ /**
102
+ * Jumps children that may be skipped to their end, latest first, until they fit.
103
+ *
104
+ * @category score
105
+ */
106
+ export function shed() {
107
+ return (span, kids, plan) => {
108
+ let p = plan;
109
+ for (let i = kids.length - 1; i >= 0 && !fits(span, kids, p); i--) {
110
+ if (!kids[i].ballast || p.skip[i])
111
+ continue;
112
+ const s = p.skip.slice();
113
+ s[i] = true;
114
+ p = { ...p, skip: s };
115
+ }
116
+ return p;
117
+ };
118
+ }
119
+ /**
120
+ * Jumps every child left to its end at once, whether or not it fits otherwise.
121
+ *
122
+ * @category score
123
+ */
124
+ export function conclude() {
125
+ return (_span, kids, plan) => ({ ...plan, skip: kids.map(() => true) });
126
+ }
127
+ /**
128
+ * Lets the span run past its budget: up to `cap` times it, or without limit.
129
+ *
130
+ * @category score
131
+ */
132
+ export function overrun(opts = {}) {
133
+ return (span, _kids, plan) => {
134
+ const cap = opts.cap;
135
+ const allow = cap === undefined ? Number.POSITIVE_INFINITY : Math.max(0, (cap - 1) * span.budget);
136
+ return { ...plan, allow: Math.max(plan.allow, allow) };
137
+ };
138
+ }
139
+ /**
140
+ * Tries each fit in turn, handing the plan on, and stops once the children fit.
141
+ *
142
+ * @category score
143
+ */
144
+ export function pipe(...fits_) {
145
+ return (span, kids, plan) => {
146
+ let p = plan;
147
+ for (const f of fits_) {
148
+ if (fits(span, kids, p))
149
+ break;
150
+ p = f(span, kids, p);
151
+ }
152
+ return p;
153
+ };
154
+ }
155
+ /**
156
+ * Every way of giving way after the span's retiming, overrunning only once the rest are spent.
157
+ *
158
+ * @category score
159
+ */
160
+ export function lax(opts = {}) {
161
+ return pipe(condense(), shed(), overrun(opts));
162
+ }
163
+ /** The fit a span takes without one of its own. */
164
+ export const defaultFit = pipe(condense(), shed());
165
+ const rank = { weak: 0, strong: 1, required: 2 };
166
+ /**
167
+ * A span's fit settled: the plan, and what it had to do past the strategy, which starts from the
168
+ * children retimed toward the budget. Where the children still
169
+ * do not fit, a span weaker than `strong` lets them run long; otherwise `spill` either skips
170
+ * every child weaker than the span (`instant`), or lets them run long (`overrun`).
171
+ */
172
+ export function settle(span, kids, fit, spill) {
173
+ let plan = fit(span, kids, rescale(span, kids, plain(span, kids)));
174
+ if (fits(span, kids, plan) || rank[span.priority] === 0 || spill === 'overrun')
175
+ return { plan, fell: !fits(span, kids, plan) && rank[span.priority] > 0 };
176
+ plan = {
177
+ ...plan,
178
+ skip: kids.map((k, i) => plan.skip[i] || rank[k.priority] < rank[span.priority]),
179
+ };
180
+ return { plan, fell: true };
181
+ }
package/dist/fold.d.ts CHANGED
@@ -7,7 +7,7 @@ export type Values = Record<string, unknown>;
7
7
  * One live voice's delta for one subject this frame, through its record for the subject, or null
8
8
  * when it does not reach. Its weight is left in `w`.
9
9
  */
10
- export declare function influence<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, held: Subject<unknown>): Record<string, unknown> | null;
10
+ export declare function contribution<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, held: Subject<unknown>): Record<string, unknown> | null;
11
11
  /** Reads a keys voice's stops at `held.phase` into the subject's delta, through the voice's scratch. */
12
12
  export declare function keyed<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>): Record<string, unknown>;
13
13
  /**
@@ -28,7 +28,7 @@ export declare function baseFor<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, sub
28
28
  */
29
29
  export declare function slopeFor<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I): Record<string, unknown> | undefined;
30
30
  export declare function isRest<I, O>(this: Mixer<I, O>, delta: Record<string, unknown>): boolean;
31
- /** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
31
+ /** Whether a rest-less channel's contribution is switched on, across a band rather than an edge. */
32
32
  export declare function passes<I, O>(this: Mixer<I, O>, was: boolean | undefined, w: number): boolean;
33
33
  /** Folds one voice's delta into the pose, its band state kept on the subject's record. */
34
34
  export declare function apply<I, O>(this: Mixer<I, O>, pose: Record<string, unknown>, voice: Voice<I, O>, held: Subject<unknown>, delta: Record<string, unknown>, weight: number): void;
@@ -47,5 +47,5 @@ export declare function foldWith<I, O>(this: Mixer<I, O>, subject: I, out: O, he
47
47
  export declare function owedBy<I, O>(this: Mixer<I, O>, owed: number, voice: Voice<I, O>): boolean;
48
48
  /** `foldWith`'s voices with a locus in play: each locus folds at its first member's place. */
49
49
  export declare function foldLoci<I, O>(this: Mixer<I, O>, subject: I, pose: Record<string, unknown>, head: Subject<unknown>, laned: boolean, dry: boolean, except: number | undefined, owed: number): O;
50
- /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
50
+ /** `contribution`, or for a dry fold a subject already probed this frame, without advancing it. */
51
51
  export declare function read<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, dry: boolean, held: Subject<unknown>): Record<string, unknown> | null;
package/dist/fold.js CHANGED
@@ -39,7 +39,7 @@ function merged(channel, acc, b) {
39
39
  * One live voice's delta for one subject this frame, through its record for the subject, or null
40
40
  * when it does not reach. Its weight is left in `w`.
41
41
  */
42
- export function influence(voice, subject, now, held) {
42
+ export function contribution(voice, subject, now, held) {
43
43
  held.weight = 0;
44
44
  if (!held.reaches)
45
45
  return null;
@@ -89,19 +89,27 @@ export function influence(voice, subject, now, held) {
89
89
  }
90
90
  }
91
91
  let delta = null;
92
- // Keys read even when silent: a `from: 'current'` voice takes its base on its first read.
93
- if (voice.built) {
92
+ // A keys voice in a locus reads at weight 0 too: having a delta makes it a member, and the
93
+ // locus folds at its first member's place.
94
+ if (silent(voice, weight) && (voice.built === null || voice.spec.locus === undefined)) {
95
+ // A `from: 'current'` voice takes its base on its first read, silent or not.
96
+ if (voice.built && voice.spec.from === 'current' && held.base === undefined) {
97
+ held.base = this.baseFor(voice, subject);
98
+ held.slope = this.slopeFor(voice, subject);
99
+ }
100
+ }
101
+ else if (voice.built) {
94
102
  held.phase = phase;
95
103
  delta = this.keyed(voice, subject, held);
96
104
  }
97
- else if (!silent(voice, weight)) {
105
+ else {
98
106
  delta = voice.patch.at(phase, subject, setting);
99
107
  }
100
108
  held.delta = delta;
101
109
  held.probed = now;
102
110
  held.seeks = voice.seeks;
103
111
  if (history !== undefined)
104
- remember(this, voice, held);
112
+ remember(this, voice, subject, held);
105
113
  if (reading.kept !== keptBefore && !voice.keeping)
106
114
  stateful(this, voice);
107
115
  if (delta === null)
@@ -222,7 +230,7 @@ export function isRest(delta) {
222
230
  }
223
231
  return true;
224
232
  }
225
- /** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
233
+ /** Whether a rest-less channel's contribution is switched on, across a band rather than an edge. */
226
234
  export function passes(was, w) {
227
235
  const { on, off } = this.band;
228
236
  return w >= on ? true : w <= off ? false : (was ?? w >= on);
@@ -281,10 +289,9 @@ export function folded(subject, out, dry = false, except) {
281
289
  * voices on them; a subject numbered since the frame's fill folds every voice, laned ones included.
282
290
  */
283
291
  export function linked(subject) {
284
- const now = this.now;
285
- const head = Number.isNaN(now) ? null : this.chain(subject, now);
292
+ const head = Number.isNaN(this.now) ? null : this.chain(subject);
286
293
  this.linkedLaned =
287
- head !== null && this.lanes?.prepare(head.slot, subject, now, this.version, head) === true;
294
+ head !== null && this.lanes?.prepare(head.slot, subject, this.version, head) === true;
288
295
  return head;
289
296
  }
290
297
  /** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
@@ -450,7 +457,7 @@ export function foldLoci(subject, pose, head, laned, dry, except, owed) {
450
457
  this.locusDepth--;
451
458
  }
452
459
  }
453
- /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
460
+ /** `contribution`, or for a dry fold a subject already probed this frame, without advancing it. */
454
461
  export function read(voice, subject, now, dry, held) {
455
462
  if (dry) {
456
463
  if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
@@ -471,5 +478,5 @@ export function read(voice, subject, now, dry, held) {
471
478
  return held.delta;
472
479
  }
473
480
  }
474
- return this.influence(voice, subject, now, held);
481
+ return this.contribution(voice, subject, now, held);
475
482
  }
package/dist/gather.d.ts CHANGED
@@ -22,3 +22,9 @@ export declare function foldDelta<I, O>(this: Lanes<I, O>, chans: readonly Laned
22
22
  /** Folds a motion sample's axes, kept apart from `foldInto` so neither reads two array kinds. */
23
23
  export declare function foldRun<I, O>(this: Lanes<I, O>, ch: Laned, slot: number, xs: Float64Array, w: number): void;
24
24
  export declare function foldInto<I, O>(this: Lanes<I, O>, ch: Laned, slot: number, value: unknown, w: number): void;
25
+ /**
26
+ * Whether a rest-less channel's contribution passes the band, for the record `one` is folding: the
27
+ * general path's `apply` decides it from the same band state on the same record, so a subject
28
+ * moving between the paths carries it.
29
+ */
30
+ export declare function gate<I, O>(this: Lanes<I, O>, ch: Laned, w: number): boolean;