@msb235/blits 0.15.1 → 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 (52) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +5 -4
  4. package/dist/chain.js +2 -3
  5. package/dist/color.d.ts +36 -14
  6. package/dist/color.js +186 -58
  7. package/dist/columns.d.ts +1 -11
  8. package/dist/columns.js +1 -6
  9. package/dist/crowd.d.ts +0 -6
  10. package/dist/crowd.js +0 -11
  11. package/dist/cue.js +7 -4
  12. package/dist/everyone.d.ts +17 -6
  13. package/dist/everyone.js +17 -8
  14. package/dist/fade.d.ts +11 -0
  15. package/dist/fade.js +44 -8
  16. package/dist/fold.js +4 -7
  17. package/dist/held.js +3 -4
  18. package/dist/hosts.d.ts +7 -0
  19. package/dist/hosts.js +18 -5
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/lanes.d.ts +26 -1
  23. package/dist/lanes.js +7 -0
  24. package/dist/lch.d.ts +23 -0
  25. package/dist/lch.js +126 -0
  26. package/dist/mixer.d.ts +4 -1
  27. package/dist/mixer.js +3 -0
  28. package/dist/motions.d.ts +4 -1
  29. package/dist/motions.js +16 -2
  30. package/dist/move.js +15 -10
  31. package/dist/owner.d.ts +7 -2
  32. package/dist/owner.js +13 -3
  33. package/dist/project.js +3 -8
  34. package/dist/pull.d.ts +11 -0
  35. package/dist/pull.js +10 -6
  36. package/dist/qualify.js +1 -1
  37. package/dist/roles.js +1 -3
  38. package/dist/roster.d.ts +6 -0
  39. package/dist/roster.js +8 -1
  40. package/dist/sample.js +2 -1
  41. package/dist/seek.js +1 -13
  42. package/dist/shared.d.ts +1 -1
  43. package/dist/shared.js +1 -1
  44. package/dist/spans.d.ts +4 -1
  45. package/dist/spans.js +6 -3
  46. package/dist/touch.js +1 -2
  47. package/dist/turns.js +2 -1
  48. package/dist/types/voice.d.ts +3 -2
  49. package/dist/voice.d.ts +19 -0
  50. package/dist/voice.js +35 -1
  51. package/docs/schema.html +37 -11
  52. package/package.json +1 -1
package/dist/motions.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { applied, change, commit, cut, dueTo, insert, peek, queue, rewind } from './changes.js';
2
- import { closed } from './closed.js';
2
+ import { BOUND, closed } from './closed.js';
3
+ import { closedLch } from './lch.js';
3
4
  import { made, pack, prune, reclaim, release, restretch, runAt, runThen, unpack, unrelease, } from './lives.js';
4
5
  import { absent, Numbers } from './numbers.js';
5
6
  import { reading } from './reading.js';
@@ -16,6 +17,7 @@ const PENDING = 2 /* RunFlag.PENDING */;
16
17
  const OLDER = 4 /* RunFlag.OLDER */;
17
18
  const LANDED = 8 /* RunFlag.LANDED */;
18
19
  const wrongKind = (channel, holdsNumber) => new Error(`blits: channel ${channel} holds ${holdsNumber ? 'a number' : 'an array'}, and this motion patch gives a subject ${holdsNumber ? 'an array' : 'a number'}`);
20
+ const notOklab = (channel) => new Error(`blits: channel ${channel} goes round the hue, and a motion on it moves oklab() values, four numbers each`);
19
21
  /**
20
22
  * A motion patch's subjects: the stretch each is playing, in one flat array by a number the patch
21
23
  * gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
@@ -68,11 +70,18 @@ export class Motions {
68
70
  /** Whether the channel it was cued on holds a number; undefined until it is cued. */
69
71
  holdsNumber = undefined;
70
72
  channel = '';
73
+ /** Whether the channel it was cued on travels round the hue, as `ink` does, and its stretches too. */
74
+ round = false;
71
75
  /**
72
76
  * Records what the channel the patch is cued on holds, refusing a value of the other kind: at once
73
77
  * where every subject's value is one kind, else on the first sample of a subject whose value is not.
78
+ * `round` for a channel that travels round the hue, as `ink` does.
74
79
  */
75
- cuedOn(channel, holdsNumber) {
80
+ cuedOn(channel, holdsNumber, round = false) {
81
+ if (round && this.shape.law[0] === BOUND)
82
+ throw new Error(`blits: channel ${channel} goes round the hue, and a glide's bounds are a box on its OKLab axes; glide it without bounds`);
83
+ if (round && this.n >= 0 && this.n !== 4)
84
+ throw notOklab(channel);
76
85
  const every = this.shape.scalarEvery();
77
86
  if (every !== undefined && every !== holdsNumber)
78
87
  throw wrongKind(channel, holdsNumber);
@@ -82,6 +91,7 @@ export class Motions {
82
91
  throw wrongKind(channel, holdsNumber);
83
92
  this.holdsNumber = holdsNumber;
84
93
  this.channel = channel;
94
+ this.round = round;
85
95
  }
86
96
  frame(subject) {
87
97
  return this.owner === null ? Number.NaN : this.owner.frame(this.ownerId, subject);
@@ -127,6 +137,8 @@ export class Motions {
127
137
  }
128
138
  /** Sets the axes every subject moves on, at the first. */
129
139
  size(n) {
140
+ if (this.round && n !== 4)
141
+ throw notOklab(this.channel);
130
142
  this.n = n;
131
143
  this.stride = 3 + 3 * n;
132
144
  if (shared.xs.length < n)
@@ -426,6 +438,8 @@ export class Motions {
426
438
  this.evaluate(seg.at, seg.ms, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
427
439
  }
428
440
  evaluate(at, ms, x0, x, v0, v, to, g, t, xo, vo) {
441
+ if (this.round)
442
+ return closedLch(this.runs, this.shape.ease, at, ms, x0, x, v0, v, to, g, t, xo, vo);
429
443
  return closed(this.runs, this.shape.ease, this.n, at, ms, x0, x, v0, v, to, g, t, xo, vo);
430
444
  }
431
445
  }
package/dist/move.js CHANGED
@@ -2,16 +2,15 @@ import { settleArcs } from './arcs.js';
2
2
  import { changed } from './chain.js';
3
3
  import { elapsedWith } from './clock.js';
4
4
  import { byId, popDue, schedule } from './due.js';
5
- import { detour, outgrow } from './everyone.js';
6
- import { beginFade, part, retire } from './fade.js';
5
+ import { beginFade, part, retire, rouse, stay } from './fade.js';
7
6
  import { expireGone, keepGone } from './gone.js';
8
7
  import { begin } from './held.js';
9
8
  import { noted } from './history.js';
10
9
  import { leave as leaveClock } from './origin.js';
11
- import { ownerReading } from './owner.js';
10
+ import { ownerReading, plays } from './owner.js';
12
11
  import { mixAt, place, repin, startAt, startOf } from './place.js';
13
12
  import { pageVoice } from './revive.js';
14
- import { delist } from './roster.js';
13
+ import { delist, detour } from './roster.js';
15
14
  import { scoredLeft, scoredRemove } from './scored.js';
16
15
  import { lapse } from './spans.js';
17
16
  import { unplay } from './strays.js';
@@ -80,6 +79,8 @@ function moveTo(mix, now) {
80
79
  if (voice.state !== 'pending') {
81
80
  if (voice.turns)
82
81
  turn(mix, voice, now);
82
+ if (voice.state === 'live' && voice.out !== null && !voice.quietAt(now))
83
+ rouse(mix, voice, now);
83
84
  // Its passes run out at its end running forward, and at its start running back. A clock
84
85
  // standing still at or before its start is where it was: waiting, or frozen there.
85
86
  const way = heading(voice, now);
@@ -87,7 +88,7 @@ function moveTo(mix, now) {
87
88
  const back = way < 0;
88
89
  const atStart = !voice.beginless && at <= voice.soonest;
89
90
  // An owner run back to its start stays for what it holds, and leaves with its last child.
90
- const holds = voice.holding !== null && voice.holding.children.length > 0;
91
+ const holds = voice.holding !== null && plays(voice, now);
91
92
  const over = back
92
93
  ? atStart && !holds
93
94
  : Number.isFinite(voice.span) && at >= voice.span + voice.latest;
@@ -95,11 +96,11 @@ function moveTo(mix, now) {
95
96
  voice.play(true, now, mix.transport.seq);
96
97
  // The fade starts when its passes ran out, not at the frame that noticed, so it plays
97
98
  // the same at any frame rate and a read at another time can find it.
98
- if (voice.state === 'live') {
99
+ if (voice.state === 'live' && voice.out === null) {
99
100
  if (back ? voice.freezesBefore : voice.freezesAfter)
100
101
  voice.state = 'frozen';
101
102
  else
102
- beginFade(mix, voice, { back }, Math.max(startOf(voice), Math.min(now, voice.endsAt())));
103
+ beginFade(mix, voice, { back, stays: voice.owner !== null && mix.opts.history !== undefined }, Math.max(startOf(voice), Math.min(now, voice.endsAt())));
103
104
  }
104
105
  }
105
106
  else if (voice.state === 'frozen' && !(atStart && way === 0))
@@ -126,8 +127,12 @@ function moveTo(mix, now) {
126
127
  else if (settled)
127
128
  retire(mix, voice);
128
129
  }
129
- else if (spent >= over)
130
- retire(mix, voice, at + over);
130
+ else if (spent >= over) {
131
+ if (voice.out.stays)
132
+ stay(mix, voice, at + over);
133
+ else
134
+ retire(mix, voice, at + over);
135
+ }
131
136
  }
132
137
  if (!mix.projecting)
133
138
  schedule(mix, voice);
@@ -137,7 +142,7 @@ function moveTo(mix, now) {
137
142
  settleArcs(mix, now);
138
143
  }
139
144
  if (mix.sharers.length > 0 && !mix.projecting)
140
- outgrow(mix);
145
+ mix.share?.outgrow(mix);
141
146
  mix.stirred = false;
142
147
  mix.asleep = false;
143
148
  if (mix.retired.length > 0)
package/dist/owner.d.ts CHANGED
@@ -36,8 +36,13 @@ export declare function mixTime<I, O>(owner: Voice<I, O>, t: number): number;
36
36
  export declare function turnedBack<I, O>(voice: Voice<I, O>): boolean;
37
37
  /** Takes a child into its owner. */
38
38
  export declare function adopt<I, O>(owner: Voice<I, O>, child: Voice<I, O>): void;
39
- /** A child left: its owner, where that was its last child and it should leave too. */
40
- export declare function orphan<I, O>(child: Voice<I, O>): Voice<I, O> | null;
39
+ /**
40
+ * A child left at mix time `now`: its owner, where that was its last child still playing and it
41
+ * should leave too.
42
+ */
43
+ export declare function orphan<I, O>(child: Voice<I, O>, now: number): Voice<I, O> | null;
44
+ /** Whether an owner holds a child at mix time `now` that is not kept quiet past its passes. */
45
+ export declare function plays<I, O>(owner: Voice<I, O>, now: number): boolean;
41
46
  /** A child finished its passes: its owner has played once every child it held has. */
42
47
  export declare function childPlayed<I, O>(owner: Voice<I, O>, at: number, seq: number): void;
43
48
  /** Every voice an owner holds, its children's children included. */
package/dist/owner.js CHANGED
@@ -76,14 +76,24 @@ export function adopt(owner, child) {
76
76
  h.children.push(child);
77
77
  h.kids++;
78
78
  }
79
- /** A child left: its owner, where that was its last child and it should leave too. */
80
- export function orphan(child) {
79
+ /**
80
+ * A child left at mix time `now`: its owner, where that was its last child still playing and it
81
+ * should leave too.
82
+ */
83
+ export function orphan(child, now) {
81
84
  const owner = child.owner;
82
85
  const h = owner.holding;
83
86
  const i = h.children.indexOf(child);
84
87
  if (i >= 0)
85
88
  h.children.splice(i, 1);
86
- return h.children.length === 0 && owner.state !== 'done' ? owner : null;
89
+ return !plays(owner, now) && owner.state !== 'done' ? owner : null;
90
+ }
91
+ /** Whether an owner holds a child at mix time `now` that is not kept quiet past its passes. */
92
+ export function plays(owner, now) {
93
+ for (const child of owner.holding.children)
94
+ if (!child.quietAt(now))
95
+ return true;
96
+ return false;
87
97
  }
88
98
  /** A child finished its passes: its owner has played once every child it held has. */
89
99
  export function childPlayed(owner, at, seq) {
package/dist/project.js CHANGED
@@ -7,7 +7,7 @@ import { fadesAt } from './fade.js';
7
7
  import { meet, origin } from './held.js';
8
8
  import { copyHeld, last, lastWithin } from './history.js';
9
9
  import { Mixer } from './mixer.js';
10
- import { heldByInput, ownerReading, relink } from './owner.js';
10
+ import { heldByInput, relink } from './owner.js';
11
11
  import { reading } from './reading.js';
12
12
  import { record } from './record.js';
13
13
  import { recount } from './roster.js';
@@ -72,6 +72,7 @@ export function projectAll(transport, at) {
72
72
  }, null);
73
73
  copy.slot = mix.slot;
74
74
  copy.projecting = true;
75
+ copy.share = mix.share;
75
76
  copy.places = mix.places;
76
77
  copies.set(mix, copy);
77
78
  if (ahead)
@@ -183,13 +184,7 @@ function copyBack(mix, c, t, cut) {
183
184
  /** Sets each copied voice's state to what its clock and fade make it at `t`. */
184
185
  export function stateAt(c, t) {
185
186
  for (const copy of c.cued)
186
- copy.state = copy.out
187
- ? 'fading'
188
- : (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
189
- ? 'pending'
190
- : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
191
- ? 'frozen'
192
- : 'live';
187
+ copy.state = copy.stateAt(t);
193
188
  }
194
189
  /**
195
190
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
package/dist/pull.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import type { Mixer } from './mixer.js';
2
2
  import type { Columns } from './types.js';
3
+ /** One channel `pull` writes: where in the kit, how many numbers a subject, and its rest. */
4
+ export interface Column {
5
+ key: string;
6
+ slot: number;
7
+ axes: number;
8
+ out: Float64Array;
9
+ /** The rest, clamped, or NaN for a channel with none. */
10
+ rest: Float64Array;
11
+ bounds: readonly [number, number] | undefined;
12
+ }
13
+ export declare function clampRun(out: Float64Array, at: number, n: number, [lo, hi]: readonly [number, number]): void;
3
14
  /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
4
15
  export declare function keep<I, O>(this: Mixer<I, O>, subject: I, pose: O, out: O | undefined): void;
5
16
  export declare function pull<I, O>(mix: Mixer<I, O>, subjects: Iterable<I>, into: Columns<O>): void;
package/dist/pull.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { axesOf } from './channels.js';
2
2
  import { copy } from './clone.js';
3
- import { clampRun, flush, pullRun, writeLater } from './columns.js';
4
- import { renumber } from './renumber.js';
5
3
  /** What `pull` remembers at a position it has read no subject at. */
6
4
  const UNREAD = Symbol('unread');
5
+ export function clampRun(out, at, n, [lo, hi]) {
6
+ for (let i = at; i < at + n; i++) {
7
+ const x = out[i];
8
+ out[i] = x < lo ? lo : x > hi ? hi : x;
9
+ }
10
+ }
7
11
  /** Writes a folded pose's value into subject `n`'s places in a column. */
8
12
  function writeValue(c, v, n) {
9
13
  const at = n * c.axes;
@@ -112,7 +116,7 @@ export function pull(mix, subjects, into) {
112
116
  }
113
117
  finally {
114
118
  if (lanes !== null)
115
- flush(lanes);
119
+ lanes.pulls.flush(lanes);
116
120
  }
117
121
  // Every head is current only if nothing relinked or changed version while they were read.
118
122
  const unchanged = live && mix.version === version && mix.relinks === relinks;
@@ -136,7 +140,7 @@ function readAll(mix, list, columns, live, current, since, whole) {
136
140
  const runs = live && whole;
137
141
  for (let n = 0; n < list.length; n++) {
138
142
  if (runs) {
139
- n = pullRun(lanes, slots, list, was, since, n, columns, now, mix.version);
143
+ n = lanes.pulls.pullRun(lanes, slots, list, was, since, n, columns, now, mix.version);
140
144
  if (n === list.length)
141
145
  break;
142
146
  }
@@ -156,7 +160,7 @@ function readAll(mix, list, columns, live, current, since, whole) {
156
160
  // Numbered out of the list's order, as swap-remove leaves a subject: renumbered into it.
157
161
  const to = n === 0 ? 0 : slots[n - 1] + 1;
158
162
  if (runs && slot >= 0 && slot !== to && to >= 0) {
159
- if (renumber(mix, subject, head, to)) {
163
+ if (lanes.pulls.renumber(mix, subject, head, to)) {
160
164
  for (let j = was.indexOf(subject); j >= 0; j = was.indexOf(subject, j + 1))
161
165
  slots[j] = to;
162
166
  slot = to;
@@ -167,7 +171,7 @@ function readAll(mix, list, columns, live, current, since, whole) {
167
171
  }
168
172
  const laned = live && lanes?.prepare(slot, subject, mix.version, heads[n] ?? null) === true;
169
173
  if (laned && whole && lanes.whole && !lanes.owes(slot)) {
170
- writeLater(lanes, slot, columns, n, slots);
174
+ lanes.pulls.writeLater(lanes, slot, columns, n, slots);
171
175
  continue;
172
176
  }
173
177
  const head = live ? heads[n] : null;
package/dist/qualify.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { numericOf } from './channels.js';
2
2
  import { flush } from './columns.js';
3
- import { crowdable } from './crowd.js';
4
3
  import { byId } from './due.js';
5
4
  import { weighAt } from './everyone.js';
6
5
  import { wake } from './fill.js';
6
+ import { crowdable } from './hosts.js';
7
7
  import { Lane } from './lane.js';
8
8
  import { reach } from './meet.js';
9
9
  import { bury, compactSparse, join, regroup, retouchRow, sameCrowds } from './rows.js';
package/dist/roles.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import { first } from './clone.js';
2
- import { open as openRecord } from './everyone.js';
3
2
  import { merged, unband } from './fold.js';
4
3
  import { roleScratch } from './role.js';
5
4
  // V8 reads a local const as a constant, where it reads an import from its module on every call.
6
- const open = openRecord;
7
5
  /**
8
6
  * Whether a fold's next voice is its chain's next record, `held`, and not the next voice sharing
9
7
  * one record, `sharers[q]`: the two lists are each in voice order, and a fold takes them as one.
@@ -54,7 +52,7 @@ export function foldRoles(subject, pose, head, laned, dry, except, owed) {
54
52
  continue;
55
53
  const held = rec;
56
54
  if (!mine)
57
- open(voice, held, head.slot);
55
+ this.share.open(voice, held, head.slot);
58
56
  const delta = this.read(voice, subject, now, dry, held);
59
57
  if (owed >= 0 && voice.laned)
60
58
  this.lanes.paid(voice.id, owed, held.weight);
package/dist/roster.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { Mixer } from './mixer.js';
2
2
  import type { Voice } from './voice.js';
3
+ /**
4
+ * Counts what makes a mix's fold more than a walk of each chain: a role, a voice sharing one
5
+ * record, a voice that stopped sharing this frame. Called wherever one of the three changes, so
6
+ * `foldWith` asks one number.
7
+ */
8
+ export declare function detour<I, O>(mix: Mixer<I, O>): void;
3
9
  /** Counts a voice just put in the list into everything a sync and a fold skip work by. */
4
10
  export declare function enlist<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
5
11
  /**
package/dist/roster.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { index, unindex } from './chain.js';
2
- import { detour } from './everyone.js';
3
2
  import { Store } from './store.js';
3
+ /**
4
+ * Counts what makes a mix's fold more than a walk of each chain: a role, a voice sharing one
5
+ * record, a voice that stopped sharing this frame. Called wherever one of the three changes, so
6
+ * `foldWith` asks one number.
7
+ */
8
+ export function detour(mix) {
9
+ mix.detours = mix.roleVoices + mix.sharers.length + (mix.unshared ? 1 : 0);
10
+ }
4
11
  function tally(mix, voice, by) {
5
12
  if (voice.spec.role !== undefined)
6
13
  mix.roleVoices += by;
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/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.1";
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.1';
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/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();
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
  }
@@ -249,8 +249,9 @@ export interface OwnerSpec<I, H = unknown> extends SpanHints {
249
249
  /**
250
250
  * Its clock's rate, multiplied into every child's own. Default 1. Below 0 its clock runs back,
251
251
  * and so does every child's: a child plays backward to its start and leaves there, as a voice cued
252
- * backward does, and the owner leaves with its last child. A span is not cued backward, since it
253
- * lays out its children forward from its start; its handle's `rate` turns it once it plays.
252
+ * backward does, and the owner leaves with its last child. Under `history` a child that already
253
+ * left runs back too: a child leaves only when its owner does. A span is not cued backward, since
254
+ * it lays out its children forward from its start; its handle's `rate` turns it once it plays.
254
255
  */
255
256
  rate?: number;
256
257
  /**
package/dist/voice.d.ts CHANGED
@@ -46,6 +46,11 @@ export interface Ramp {
46
46
  deadline?: number;
47
47
  /** Begun where the voice ran back to its start, whose first frame it holds through the fade. */
48
48
  back?: boolean;
49
+ /**
50
+ * Begun where the passes of a voice an owner holds under history ran out: faded, it stays live
51
+ * and quiet with its owner, which brings it back by taking its clock back into its passes.
52
+ */
53
+ stays?: boolean;
49
54
  }
50
55
  /**
51
56
  * A climb back from a fade out that was turned around: from `from`, the place along the fade's
@@ -326,6 +331,20 @@ export declare class Voice<I, O> {
326
331
  * turn it around.
327
332
  */
328
333
  endsAt(): number;
334
+ /** The mix time its clock reads `elapsed`, by its controls now, through every owner above it. */
335
+ reaches(elapsed: number): number;
336
+ /**
337
+ * Its state at mix time `t`, from its clock and fade, once every clock above it is back where it
338
+ * was: a fade begins at once, even one whose ramp starts later, at the voice's start, and one
339
+ * from where an owner under history keeps it ends with it quiet and live.
340
+ */
341
+ stateAt(t: number): Voice<I, O>['state'];
342
+ /**
343
+ * Whether it is kept quiet past its passes at mix time `now`: its passes ran out while an owner
344
+ * held it under history, and its clock has not come back into them. At their very edge it is
345
+ * quiet unless its clock runs into them.
346
+ */
347
+ quietAt(now: number): boolean;
329
348
  /**
330
349
  * The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
331
350
  * `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.
package/dist/voice.js CHANGED
@@ -472,9 +472,43 @@ export class Voice {
472
472
  return Number.POSITIVE_INFINITY;
473
473
  to = this.span + this.latest;
474
474
  }
475
- const t = backs ? backWith(this, to) : timeWith(this, to);
475
+ return this.reaches(to);
476
+ }
477
+ /** The mix time its clock reads `elapsed`, by its controls now, through every owner above it. */
478
+ reaches(elapsed) {
479
+ const t = this.backs ? backWith(this, elapsed) : timeWith(this, elapsed);
476
480
  return this.owner === null ? t : mixTime(this.owner, t);
477
481
  }
482
+ /**
483
+ * Its state at mix time `t`, from its clock and fade, once every clock above it is back where it
484
+ * was: a fade begins at once, even one whose ramp starts later, at the voice's start, and one
485
+ * from where an owner under history keeps it ends with it quiet and live.
486
+ */
487
+ stateAt(t) {
488
+ const out = this.out;
489
+ if (out !== null)
490
+ return out.stays && t >= out.at + out.over ? 'live' : 'fading';
491
+ if ((this.owner === null ? t : ownerReading(this.owner, t)) < this.start)
492
+ return 'pending';
493
+ if (this.freezesAfter && this.elapsedAt(t) >= this.span + this.latest)
494
+ return 'frozen';
495
+ return 'live';
496
+ }
497
+ /**
498
+ * Whether it is kept quiet past its passes at mix time `now`: its passes ran out while an owner
499
+ * held it under history, and its clock has not come back into them. At their very edge it is
500
+ * quiet unless its clock runs into them.
501
+ */
502
+ quietAt(now) {
503
+ if (this.state !== 'live' || this.out?.stays !== true)
504
+ return false;
505
+ const at = this.elapsedAt(now);
506
+ const end = this.span + this.latest;
507
+ const back = this.backs !== (this.owner !== null && turnedBack(this));
508
+ if (at > end || (at === end && !back))
509
+ return true;
510
+ return !this.beginless && (at < this.soonest || (at === this.soonest && back));
511
+ }
478
512
  /**
479
513
  * The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
480
514
  * `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.