@msb235/blits 0.16.0 → 0.16.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 (56) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +6 -9
  4. package/dist/clock.d.ts +11 -1
  5. package/dist/clock.js +28 -9
  6. package/dist/contribution.js +11 -4
  7. package/dist/cue.js +12 -3
  8. package/dist/due.d.ts +18 -0
  9. package/dist/due.js +101 -2
  10. package/dist/fade.d.ts +12 -2
  11. package/dist/fade.js +41 -8
  12. package/dist/fill.js +2 -1
  13. package/dist/fit.js +8 -0
  14. package/dist/gone.js +1 -0
  15. package/dist/handle.d.ts +1 -1
  16. package/dist/handle.js +3 -1
  17. package/dist/held.d.ts +2 -1
  18. package/dist/held.js +14 -6
  19. package/dist/hosts.js +5 -1
  20. package/dist/lane.d.ts +2 -0
  21. package/dist/lane.js +2 -0
  22. package/dist/lanes.js +2 -1
  23. package/dist/meet.js +9 -0
  24. package/dist/mixer.d.ts +2 -0
  25. package/dist/mixer.js +2 -0
  26. package/dist/motion.js +6 -6
  27. package/dist/move.js +309 -82
  28. package/dist/origin.d.ts +2 -0
  29. package/dist/origin.js +4 -0
  30. package/dist/owner.js +4 -2
  31. package/dist/paging.d.ts +5 -1
  32. package/dist/paging.js +13 -3
  33. package/dist/patch.js +24 -4
  34. package/dist/place.js +67 -31
  35. package/dist/project.d.ts +5 -2
  36. package/dist/project.js +7 -4
  37. package/dist/qualify.js +6 -1
  38. package/dist/rests.js +9 -1
  39. package/dist/revive.d.ts +11 -1
  40. package/dist/revive.js +7 -0
  41. package/dist/roles.js +5 -2
  42. package/dist/rows.js +4 -1
  43. package/dist/seat.d.ts +5 -1
  44. package/dist/seat.js +5 -1
  45. package/dist/seek.js +6 -4
  46. package/dist/shared.d.ts +1 -1
  47. package/dist/shared.js +1 -1
  48. package/dist/spans.d.ts +10 -4
  49. package/dist/spans.js +121 -31
  50. package/dist/trace.js +7 -2
  51. package/dist/types/transport.d.ts +2 -1
  52. package/dist/voice.d.ts +42 -4
  53. package/dist/voice.js +45 -5
  54. package/dist/weigh.js +1 -1
  55. package/docs/schema.html +1 -1
  56. package/package.json +3 -1
package/dist/fade.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { changed, headOf, unlink } from './chain.js';
2
- import { envelope } from './clock.js';
2
+ import { envelope, fadeInAt } from './clock.js';
3
3
  import { schedule } from './due.js';
4
4
  import { leave, noted, releasing } from './history.js';
5
5
  import { markOf } from './marks.js';
6
+ import { startedAt } from './origin.js';
6
7
  import { orphan, ownerReading, plays } from './owner.js';
7
8
  import { startOf } from './place.js';
8
9
  import { isRested } from './record.js';
@@ -27,12 +28,26 @@ export function fadeSubject(mix, voice, subject, over) {
27
28
  schedule(mix, voice);
28
29
  mix.lanes?.refill();
29
30
  }
30
- /** What a subject's own ramp out of a voice leaves of its weight this frame, 0..1. */
31
+ /**
32
+ * What a subject's own ramp out of a voice leaves of its weight this frame. It also holds the
33
+ * voice's fade in where the ramp began, as a fade out of the whole voice does, so it is a ratio to
34
+ * the fade in the voice's envelope gives.
35
+ */
31
36
  export function parting(voice, subject, now) {
32
37
  const r = voice.parts?.get(subject);
33
38
  if (r === undefined)
34
39
  return 1;
35
- return envelope(0, { at: r.at, over: r.over, rest: false }, voice.ease, this.reducedNow, now, 0);
40
+ const reduced = this.reducedNow;
41
+ const w = envelope(0, { at: r.at, over: r.over, rest: false }, voice.ease, reduced, now, 0);
42
+ const fadeIn = voice.fade.in ?? 0;
43
+ const since = voice.subjects.get(subject)?.shown;
44
+ if (!(fadeIn > 0) || reduced || since === undefined || !(r.at < now))
45
+ return w;
46
+ const out = voice.out;
47
+ const climbing = fadeInAt(fadeIn, out, voice.ease, now, since, voice.stalls);
48
+ if (!(climbing > 0))
49
+ return w;
50
+ return w * (fadeInAt(fadeIn, out, voice.ease, now, since, voice.stalls, r.at) / climbing);
36
51
  }
37
52
  /**
38
53
  * Takes a subject out of one voice for good, as of mix time `at`: the voice forgets its record,
@@ -164,7 +179,8 @@ export function beginFade(mix, voice, opts, at) {
164
179
  }
165
180
  /**
166
181
  * Keeps a voice faded at the end of its passes with its owner, live and quiet, rather than letting
167
- * it leave; the owner leaves at `at` if it holds no child still playing.
182
+ * it leave; the owner, where it holds no child still playing, leaves at `at`, or stays quiet in
183
+ * turn where an owner holds it.
168
184
  */
169
185
  export function stay(mix, voice, at) {
170
186
  voice.state = 'live';
@@ -173,10 +189,22 @@ export function stay(mix, voice, at) {
173
189
  if (owner.state === 'done')
174
190
  return;
175
191
  const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now;
176
- if (!plays(owner, now) && !lingers(owner, at))
192
+ if (plays(owner, now) || lingers(owner, at)) {
193
+ if (owner.fitting !== null && !voice.skipped)
194
+ refit(mix, owner, at);
195
+ }
196
+ else
197
+ release(mix, owner, at);
198
+ }
199
+ /**
200
+ * Lets an owner holding no child still playing leave at `at`; under history one an owner holds
201
+ * stays with it instead, quiet while its children are.
202
+ */
203
+ export function release(mix, owner, at) {
204
+ if (owner.owner !== null && owner.state === 'live' && mix.opts.history !== undefined)
205
+ beginFade(mix, owner, { over: 0, stays: true }, at);
206
+ else
177
207
  retire(mix, owner, at);
178
- else if (owner.fitting !== null && !voice.skipped)
179
- refit(mix, owner, at);
180
208
  }
181
209
  /**
182
210
  * Turns a fading voice around: it climbs back from where its fade out had got to, up the same
@@ -205,7 +233,12 @@ export function beginRise(mix, voice, opts) {
205
233
  voice.out = null;
206
234
  voice.outAt = Number.POSITIVE_INFINITY;
207
235
  voice.state = waiting ? 'pending' : 'live';
236
+ if (waiting)
237
+ voice.begunSeq = Number.POSITIVE_INFINITY;
208
238
  voice.back = over > 0 && from < 1 ? { from, at, over } : null;
239
+ // Its fade in stood still from where the fade out began, and climbs on from where it stood.
240
+ if (!out.rest && out.at < at)
241
+ voice.stalls = [...(voice.stalls ?? []), out.at, at];
209
242
  if (waiting)
210
243
  changed(mix, voice);
211
244
  noted(mix, voice);
@@ -223,7 +256,7 @@ export function fadeAt(mix, voice, at, over) {
223
256
  return;
224
257
  const now = Number.isNaN(mix.now) ? startOf(voice) : mix.now;
225
258
  if (at <= now) {
226
- beginFade(mix, voice, { over }, Math.max(startOf(voice), at));
259
+ beginFade(mix, voice, { over }, Math.max(startedAt(voice), at));
227
260
  mix.stir();
228
261
  return;
229
262
  }
package/dist/fill.js CHANGED
@@ -472,7 +472,8 @@ export function call(voice, chans, rec, slot) {
472
472
  rec.delta = null;
473
473
  rec.probed = this.now;
474
474
  rec.seeks = voice.seeks;
475
- return false;
475
+ // A role member is one at weight 0 too, unasked, and holds its role's place as on the general path.
476
+ return voice.spec.role !== undefined;
476
477
  }
477
478
  const kept = reading.kept;
478
479
  host.ready(voice, subject, rec);
package/dist/fit.js CHANGED
@@ -50,6 +50,14 @@ function least(lo, hi, ok) {
50
50
  return lo;
51
51
  if (!ok(hi))
52
52
  return hi;
53
+ // An unbounded `hi`, as an `Infinity` hint gives, is brought to a finite one before halving.
54
+ if (hi === Number.POSITIVE_INFINITY) {
55
+ hi = Math.max(lo, 1) * 2;
56
+ while (!ok(hi)) {
57
+ lo = hi;
58
+ hi *= 2;
59
+ }
60
+ }
53
61
  for (let i = 0; i < 60; i++) {
54
62
  const mid = (lo + hi) / 2;
55
63
  if (ok(mid))
package/dist/gone.js CHANGED
@@ -77,6 +77,7 @@ export function goneIndex(mix) {
77
77
  /** Hands voices let go of from `gone` to the departed index, noting the latest that left. */
78
78
  function forgot(mix, out) {
79
79
  for (const v of out) {
80
+ mix.parters.delete(v);
80
81
  mix.departed.add(mix, v);
81
82
  if (v.doneAt > mix.transport.forgotTo)
82
83
  mix.transport.forgotTo = v.doneAt;
package/dist/handle.d.ts CHANGED
@@ -11,7 +11,7 @@ export interface HandleHost<I, O> {
11
11
  * The voice's clock jumped: for an owner, so did every clock it holds. With `rebuild`, their
12
12
  * state is made again for where they now are. How sure that leaves it.
13
13
  */
14
- sought(voice: Voice<I, O>, rebuild: boolean): Doubt;
14
+ sought(voice: Voice<I, O>, rebuild: boolean, ahead: boolean): Doubt;
15
15
  fade(voice: Voice<I, O>, opts: FadeOptions<I> | undefined): void;
16
16
  rise(voice: Voice<I, O>, opts: {
17
17
  over?: number;
package/dist/handle.js CHANGED
@@ -115,14 +115,16 @@ export class VoiceHandle {
115
115
  return 'exact';
116
116
  voice.written = true;
117
117
  // A pending voice's clock is anchored at its start, so it starts there on time.
118
+ let ahead = false;
118
119
  if (voice.state !== 'pending') {
119
120
  const u = this.#host.nowFor(voice);
120
121
  leave(voice, u, true);
121
122
  voice.rebase(u);
123
+ ahead = elapsed > voice.anchorElapsed;
122
124
  }
123
125
  voice.anchorElapsed = elapsed;
124
126
  voice.seeks++;
125
- const doubt = this.#host.sought(voice, opts?.state !== 'keep');
127
+ const doubt = this.#host.sought(voice, opts?.state !== 'keep', ahead);
126
128
  this.#host.changed(voice);
127
129
  if (this.#host.taping())
128
130
  this.#host.record(this, 'seek', (h) => h.seek(elapsed, opts));
package/dist/held.d.ts CHANGED
@@ -22,7 +22,8 @@ export declare function sinceOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, del
22
22
  /**
23
23
  * The mix time a subject's fade in counts from: `since`, but no earlier than its voice starts,
24
24
  * where a voice sought ahead of its start has a subject's delay run out before it; and from when
25
- * the voice first showed, for one freezing before.
25
+ * the voice first showed, for one freezing before. Its start is where its owners' clocks first
26
+ * read it, as `since` is, not where they would by their controls now.
26
27
  */
27
28
  export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, since: number): number;
28
29
  /** Lists a record whose origin is still ahead of `now`, for a later change to the clock to move. */
package/dist/held.js CHANGED
@@ -2,8 +2,7 @@ import { changed } from './chain.js';
2
2
  import { stepOf } from './clock.js';
3
3
  import { delayOf } from './delay.js';
4
4
  import { schedule } from './due.js';
5
- import { originOf } from './origin.js';
6
- import { startOf } from './place.js';
5
+ import { originOf, startedAt } from './origin.js';
7
6
  import { record } from './record.js';
8
7
  import { unreach, unreached } from './unreached.js';
9
8
  /** The record a voice gives every subject it does not reach. */
@@ -118,13 +117,21 @@ export function sinceOf(voice, delay) {
118
117
  /**
119
118
  * The mix time a subject's fade in counts from: `since`, but no earlier than its voice starts,
120
119
  * where a voice sought ahead of its start has a subject's delay run out before it; and from when
121
- * the voice first showed, for one freezing before.
120
+ * the voice first showed, for one freezing before. Its start is where its owners' clocks first
121
+ * read it, as `since` is, not where they would by their controls now.
122
122
  */
123
123
  export function shownOf(voice, since) {
124
- const start = startOf(voice);
124
+ const start = startedAt(voice);
125
125
  const from = since < start ? start : since;
126
126
  return voice.freezesBefore && voice.opened < from ? voice.opened : from;
127
127
  }
128
+ /**
129
+ * Whether a listed record stays listed at `now`: its origin is ahead, or it is a motion voice's,
130
+ * unread since its origin, which `still` asks after since the motion has no run for it yet.
131
+ */
132
+ function stillEarly(voice, held, now) {
133
+ return !(held.since <= now) || (voice.motion !== undefined && !(held.probed >= held.since));
134
+ }
128
135
  /** Lists a record whose origin is still ahead of `now`, for a later change to the clock to move. */
129
136
  export function waits(voice, held, now) {
130
137
  if (held.since <= now)
@@ -135,7 +142,7 @@ export function waits(voice, held, now) {
135
142
  if (early.length >= 64 && (early.length & (early.length - 1)) === 0) {
136
143
  let kept = 0;
137
144
  for (const h of early)
138
- if (!(h.since <= now))
145
+ if (stillEarly(voice, h, now))
139
146
  early[kept++] = h;
140
147
  early.length = kept;
141
148
  }
@@ -147,6 +154,7 @@ export function waits(voice, held, now) {
147
154
  */
148
155
  export function begin(mix, voice, now) {
149
156
  voice.state = 'live';
157
+ voice.begunSeq = mix.transport.seq;
150
158
  if (voice.anchorElapsed !== 0) {
151
159
  voice.seeks++;
152
160
  voice.rebuilds++;
@@ -178,7 +186,7 @@ export function reorigin(mix, voice, now) {
178
186
  let kept = 0;
179
187
  for (const held of early) {
180
188
  origin(mix, voice, held);
181
- if (!(held.since <= now))
189
+ if (stillEarly(voice, held, now))
182
190
  early[kept++] = held;
183
191
  }
184
192
  early.length = kept;
package/dist/hosts.js CHANGED
@@ -216,7 +216,11 @@ function handleHost(mix) {
216
216
  mix.lanes?.refill();
217
217
  },
218
218
  // A delta read before an owner's seek, or a hit booked, is not taken as standing after it.
219
- sought: (voice, rebuild) => {
219
+ sought: (voice, rebuild, ahead) => {
220
+ if (ahead && voice.holding !== null && !mix.projecting) {
221
+ mix.leapt ??= new Set();
222
+ mix.leapt.add(voice);
223
+ }
220
224
  if (rebuild)
221
225
  voice.rebuilds++;
222
226
  let doubt = seekDoubt(mix, voice, rebuild);
package/dist/lane.d.ts CHANGED
@@ -185,6 +185,8 @@ export declare class Lane<I, O> implements Positions<I, O> {
185
185
  * checked against every lane up to some epoch has met this one if this one's is no later.
186
186
  */
187
187
  epoch: number;
188
+ /** The lanes' probe count when it was made: a probe no later read its subject elsewhere. */
189
+ made: number;
188
190
  /**
189
191
  * Per position, `STRIDE` numbers side by side, so one subject's are one read from memory: its
190
192
  * delay, its `since`, its weight at the last fill, its weight at the last fill a probe read, a
package/dist/lane.js CHANGED
@@ -37,6 +37,8 @@ export class Lane {
37
37
  * checked against every lane up to some epoch has met this one if this one's is no later.
38
38
  */
39
39
  epoch = 0;
40
+ /** The lanes' probe count when it was made: a probe no later read its subject elsewhere. */
41
+ made = 0;
40
42
  /**
41
43
  * Per position, `STRIDE` numbers side by side, so one subject's are one read from memory: its
42
44
  * delay, its `since`, its weight at the last fill, its weight at the last fill a probe read, a
package/dist/lanes.js CHANGED
@@ -268,7 +268,8 @@ export class Lanes {
268
268
  }
269
269
  reported(lane, slot) {
270
270
  const o = slot * 6 /* Per.SLOT */;
271
- if (!(this.per[o + 3 /* Per.LANE_PROBE */] > this.per[o + 4 /* Per.GENERAL_PROBE */]))
271
+ const probe = this.per[o + 3 /* Per.LANE_PROBE */];
272
+ if (!(probe > this.per[o + 4 /* Per.GENERAL_PROBE */]) || !(probe > lane.made))
272
273
  return undefined;
273
274
  const p = lane.positionOf(slot);
274
275
  if (p < 0)
package/dist/meet.js CHANGED
@@ -86,6 +86,15 @@ export function meet(lanes, slot, subject, head, lane) {
86
86
  }
87
87
  if (!met)
88
88
  return lane;
89
+ // Met newest first; folded, as the general path folds them, in voice order.
90
+ const ids = lanes.met;
91
+ for (let i = 1; i < lanes.metN; i++) {
92
+ const id = ids[i];
93
+ let j = i - 1;
94
+ for (; j >= 0 && ids[j] > id; j--)
95
+ ids[j + 1] = ids[j];
96
+ ids[j + 1] = id;
97
+ }
89
98
  // The fill ran before the subject had these positions. Where every voice it just met folds
90
99
  // after every other laned voice, the general path folds just those onto the lanes' values;
91
100
  // otherwise it reads the general path all frame.
package/dist/mixer.d.ts CHANGED
@@ -106,6 +106,8 @@ export declare class Mixer<I, O> implements Mix<I, O> {
106
106
  retired: Voice<I, O>[];
107
107
  /** Visit every voice each sync, as before the due queue; for tests that compare the two. */
108
108
  walkAll: boolean;
109
+ /** Owners sought forward since the last frame, which passes what each seek went past in order. */
110
+ leapt: Set<Voice<I, O>> | null;
109
111
  /** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
110
112
  pulled: I[];
111
113
  pulledHeads: (Subject<unknown> | undefined)[];
package/dist/mixer.js CHANGED
@@ -95,6 +95,8 @@ export class Mixer {
95
95
  retired = [];
96
96
  /** Visit every voice each sync, as before the due queue; for tests that compare the two. */
97
97
  walkAll = false;
98
+ /** Owners sought forward since the last frame, which passes what each seek went past in order. */
99
+ leapt = null;
98
100
  /** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
99
101
  pulled = [];
100
102
  pulledHeads = [];
package/dist/motion.js CHANGED
@@ -241,8 +241,8 @@ export function spring(writes, opts) {
241
241
  const k = opts.stiffness ?? 170;
242
242
  const c = opts.damping ?? 26;
243
243
  const m = opts.mass ?? 1;
244
- if (!(k > 0) || !(m > 0) || !(c >= 0))
245
- throw new RangeError('blits: a spring takes a positive stiffness and mass, and damping of 0 or more');
244
+ if (!(k > 0 && k < Infinity) || !(m > 0 && m < Infinity) || !(c >= 0 && c < Infinity))
245
+ throw new RangeError('blits: a spring takes a finite positive stiffness and mass, and finite damping of 0 or more');
246
246
  const settle = opts.settle ?? 1e-4;
247
247
  const w0 = Math.sqrt(k / m);
248
248
  const zeta = c / (2 * Math.sqrt(k * m));
@@ -273,11 +273,11 @@ export function spring(writes, opts) {
273
273
  */
274
274
  export function glide(writes, opts) {
275
275
  const ms = opts.ms ?? 325;
276
- if (!(ms > 0))
277
- throw new RangeError('blits: a glide takes a positive ms');
276
+ if (!(ms > 0 && ms < Infinity))
277
+ throw new RangeError('blits: a glide takes a finite positive ms');
278
278
  const k = opts.stiffness ?? 170;
279
- if (!(k > 0))
280
- throw new RangeError('blits: a glide takes a positive stiffness');
279
+ if (!(k > 0 && k < Infinity))
280
+ throw new RangeError('blits: a glide takes a finite positive stiffness');
281
281
  const tau = ms / 1000;
282
282
  const settle = opts.settle ?? 1e-4;
283
283
  const bounded = opts.bounds !== undefined;