@msb235/blits 0.7.1 → 0.8.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 (156) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +22 -4
  3. package/dist/book.js +4 -4
  4. package/dist/chain.d.ts +1 -1
  5. package/dist/chain.js +57 -54
  6. package/dist/channels.d.ts +26 -5
  7. package/dist/channels.js +12 -8
  8. package/dist/clock.d.ts +8 -1
  9. package/dist/clock.js +14 -3
  10. package/dist/clone.d.ts +8 -0
  11. package/dist/clone.js +22 -0
  12. package/dist/closed.d.ts +1 -1
  13. package/dist/closed.js +3 -2
  14. package/dist/color.d.ts +0 -11
  15. package/dist/color.js +4 -24
  16. package/dist/columns.js +0 -1
  17. package/dist/copyout.d.ts +6 -0
  18. package/dist/copyout.js +38 -0
  19. package/dist/crowd.d.ts +7 -5
  20. package/dist/crowd.js +32 -18
  21. package/dist/cue.d.ts +3 -1
  22. package/dist/cue.js +31 -20
  23. package/dist/departed.d.ts +34 -0
  24. package/dist/departed.js +73 -0
  25. package/dist/easing.d.ts +5 -0
  26. package/dist/easing.js +31 -2
  27. package/dist/everyone.d.ts +56 -0
  28. package/dist/everyone.js +221 -0
  29. package/dist/fade.d.ts +8 -1
  30. package/dist/fade.js +34 -6
  31. package/dist/fill.d.ts +22 -13
  32. package/dist/fill.js +104 -41
  33. package/dist/fit.js +2 -1
  34. package/dist/fold.d.ts +37 -0
  35. package/dist/fold.js +216 -31
  36. package/dist/frames.d.ts +21 -0
  37. package/dist/frames.js +59 -0
  38. package/dist/gather.d.ts +15 -9
  39. package/dist/gather.js +60 -25
  40. package/dist/gone.js +10 -0
  41. package/dist/handle.d.ts +16 -5
  42. package/dist/handle.js +56 -6
  43. package/dist/held.d.ts +8 -5
  44. package/dist/held.js +56 -58
  45. package/dist/history.d.ts +17 -13
  46. package/dist/history.js +47 -28
  47. package/dist/hosts.js +55 -8
  48. package/dist/index.d.ts +9 -4
  49. package/dist/index.js +5 -2
  50. package/dist/keyfill.d.ts +7 -0
  51. package/dist/keyfill.js +124 -0
  52. package/dist/keyrows.d.ts +1 -0
  53. package/dist/keyrows.js +1 -1
  54. package/dist/lane.d.ts +32 -2
  55. package/dist/lane.js +5 -0
  56. package/dist/lanes.d.ts +32 -8
  57. package/dist/lanes.js +69 -37
  58. package/dist/leavings.d.ts +26 -0
  59. package/dist/leavings.js +70 -0
  60. package/dist/locus.js +2 -2
  61. package/dist/meet.js +14 -8
  62. package/dist/mixer.d.ts +39 -7
  63. package/dist/mixer.js +50 -10
  64. package/dist/motion.d.ts +2 -0
  65. package/dist/motion.js +4 -1
  66. package/dist/motions.d.ts +22 -15
  67. package/dist/motions.js +40 -34
  68. package/dist/move.js +23 -16
  69. package/dist/moved.d.ts +11 -0
  70. package/dist/moved.js +29 -0
  71. package/dist/origin.d.ts +19 -0
  72. package/dist/origin.js +68 -0
  73. package/dist/owed.d.ts +4 -4
  74. package/dist/owed.js +8 -8
  75. package/dist/owner.d.ts +1 -1
  76. package/dist/owner.js +2 -2
  77. package/dist/pace.d.ts +13 -5
  78. package/dist/pace.js +21 -8
  79. package/dist/pack.d.ts +3 -1
  80. package/dist/pack.js +15 -24
  81. package/dist/paging.d.ts +8 -2
  82. package/dist/paging.js +39 -4
  83. package/dist/patch.d.ts +14 -9
  84. package/dist/patch.js +83 -21
  85. package/dist/place.js +18 -3
  86. package/dist/project.d.ts +12 -1
  87. package/dist/project.js +101 -83
  88. package/dist/pull.d.ts +2 -0
  89. package/dist/pull.js +8 -0
  90. package/dist/qualify.js +10 -2
  91. package/dist/reading.d.ts +5 -0
  92. package/dist/reading.js +5 -0
  93. package/dist/record.d.ts +9 -0
  94. package/dist/record.js +42 -0
  95. package/dist/relink.d.ts +11 -5
  96. package/dist/relink.js +7 -5
  97. package/dist/restored.d.ts +41 -0
  98. package/dist/restored.js +91 -0
  99. package/dist/revive.d.ts +10 -3
  100. package/dist/revive.js +31 -23
  101. package/dist/roster.d.ts +11 -0
  102. package/dist/roster.js +46 -0
  103. package/dist/rotation.d.ts +29 -0
  104. package/dist/rotation.js +91 -0
  105. package/dist/rows.d.ts +5 -5
  106. package/dist/rows.js +5 -5
  107. package/dist/sample.d.ts +3 -2
  108. package/dist/sample.js +32 -15
  109. package/dist/seek.js +97 -114
  110. package/dist/shared.d.ts +6 -0
  111. package/dist/shared.js +15 -0
  112. package/dist/signals.d.ts +24 -3
  113. package/dist/signals.js +50 -11
  114. package/dist/spans.js +7 -0
  115. package/dist/stack.d.ts +11 -0
  116. package/dist/stack.js +47 -0
  117. package/dist/standing.d.ts +10 -0
  118. package/dist/standing.js +79 -0
  119. package/dist/store.d.ts +3 -3
  120. package/dist/store.js +3 -3
  121. package/dist/strays.js +3 -0
  122. package/dist/tape.d.ts +8 -5
  123. package/dist/tape.js +21 -13
  124. package/dist/testing.d.ts +35 -0
  125. package/dist/testing.js +116 -0
  126. package/dist/touch.d.ts +6 -0
  127. package/dist/touch.js +28 -0
  128. package/dist/transport.d.ts +45 -6
  129. package/dist/transport.js +64 -19
  130. package/dist/turns.d.ts +18 -0
  131. package/dist/turns.js +72 -0
  132. package/dist/tweened.js +1 -1
  133. package/dist/types/channel.d.ts +46 -0
  134. package/dist/types/channel.js +1 -0
  135. package/dist/types/history.d.ts +139 -0
  136. package/dist/types/history.js +1 -0
  137. package/dist/types/mix.d.ts +336 -0
  138. package/dist/types/mix.js +1 -0
  139. package/dist/types/patch.d.ts +187 -0
  140. package/dist/types/patch.js +1 -0
  141. package/dist/types/place.d.ts +157 -0
  142. package/dist/types/place.js +1 -0
  143. package/dist/types/transport.d.ts +71 -0
  144. package/dist/types/transport.js +1 -0
  145. package/dist/types/voice.d.ts +323 -0
  146. package/dist/types/voice.js +1 -0
  147. package/dist/types.d.ts +7 -1162
  148. package/dist/unpage.d.ts +2 -2
  149. package/dist/unpage.js +53 -25
  150. package/dist/voice.d.ts +90 -22
  151. package/dist/voice.js +105 -43
  152. package/dist/wave.d.ts +42 -0
  153. package/dist/wave.js +62 -0
  154. package/dist/weigh.js +1 -1
  155. package/docs/schema.html +42 -36
  156. package/package.json +8 -3
package/dist/record.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A subject's record for `voice` holding every field one can come to hold, in one order, so every
3
+ * record in a mix shares one hidden class: a record built elsewhere, or given `base`, `snaps` or
4
+ * `from` later, took a class of its own, and a fold walking a chain of several kinds reads each
5
+ * field the slow way. The caller sets what it knows; nothing here takes a number, which a call V8
6
+ * does not inline would box.
7
+ */
8
+ export function record(voice, reaches, state) {
9
+ return {
10
+ reaches,
11
+ delay: 0,
12
+ since: 0,
13
+ shown: 0,
14
+ weight: 0,
15
+ rested: false,
16
+ bands: null,
17
+ state,
18
+ stepped: 0,
19
+ ticks: 0,
20
+ probed: Number.NaN,
21
+ delta: null,
22
+ phase: 0,
23
+ seeks: 0,
24
+ rebuilt: 0,
25
+ base: undefined,
26
+ slope: undefined,
27
+ kept: null,
28
+ unkept: undefined,
29
+ snaps: undefined,
30
+ from: undefined,
31
+ unknown: undefined,
32
+ inputs: undefined,
33
+ replay: undefined,
34
+ voice,
35
+ next: null,
36
+ version: Number.NaN,
37
+ loci: null,
38
+ slot: -1,
39
+ run: -1,
40
+ freed: -1,
41
+ };
42
+ }
package/dist/relink.d.ts CHANGED
@@ -3,12 +3,16 @@ import type { Subject } from './voice.js';
3
3
  interface Ordered {
4
4
  readonly id: number;
5
5
  }
6
+ /** What `patch` asks a mix: a voice's record for a subject where a full relink would link it. */
7
+ interface Linker<V, I> {
8
+ linkable(voice: V, subject: I, slot: number): Subject<unknown> | null;
9
+ }
6
10
  /**
7
11
  * The voices over every subject behind each step of a mix's `version`, so a chain linked a few
8
12
  * steps back can be brought up to date by those voices alone; null for a step that changed
9
13
  * anything else, after which every chain links afresh.
10
14
  */
11
- export declare class Steps<V extends Ordered> {
15
+ export declare class Steps<V extends Ordered, I> {
12
16
  private list;
13
17
  /** The version before `list[0]`. */
14
18
  private from;
@@ -16,10 +20,12 @@ export declare class Steps<V extends Ordered> {
16
20
  push(voice: V | null, version: number): void;
17
21
  /**
18
22
  * A chain linked at `version`, its first record `head` (null where it holds none), with each voice
19
- * stepped since taken off it or put on at its place in voice order by `reach`, which gives the
20
- * voice's record where a full relink would link it and null where it would not. undefined where
21
- * the steps since do not say, so the chain must link afresh.
23
+ * stepped since taken off it or put on at its place in voice order by `mix.linkable`, which gives
24
+ * the voice's record where a full relink would link it and null where it would not. undefined
25
+ * where the steps since do not say, so the chain must link afresh. It takes the mix and the
26
+ * subject, not a function closed over them: a closure and its context a subject cost the `swap`
27
+ * row about 138 B each (bench/allocs.mjs, 2026-10-09).
22
28
  */
23
- patch(head: Subject<unknown> | null, version: number, reach: (voice: V) => Subject<unknown> | null): Subject<unknown> | null | undefined;
29
+ patch(head: Subject<unknown> | null, version: number, mix: Linker<V, I>, subject: I, slot: number): Subject<unknown> | null | undefined;
24
30
  }
25
31
  export {};
package/dist/relink.js CHANGED
@@ -21,11 +21,13 @@ export class Steps {
21
21
  }
22
22
  /**
23
23
  * A chain linked at `version`, its first record `head` (null where it holds none), with each voice
24
- * stepped since taken off it or put on at its place in voice order by `reach`, which gives the
25
- * voice's record where a full relink would link it and null where it would not. undefined where
26
- * the steps since do not say, so the chain must link afresh.
24
+ * stepped since taken off it or put on at its place in voice order by `mix.linkable`, which gives
25
+ * the voice's record where a full relink would link it and null where it would not. undefined
26
+ * where the steps since do not say, so the chain must link afresh. It takes the mix and the
27
+ * subject, not a function closed over them: a closure and its context a subject cost the `swap`
28
+ * row about 138 B each (bench/allocs.mjs, 2026-10-09).
27
29
  */
28
- patch(head, version, reach) {
30
+ patch(head, version, mix, subject, slot) {
29
31
  if (!(version >= this.from))
30
32
  return undefined;
31
33
  const list = this.list;
@@ -40,7 +42,7 @@ export class Steps {
40
42
  at = at.next;
41
43
  }
42
44
  const here = at !== null && at.voice === voice ? at : null;
43
- const want = reach(voice);
45
+ const want = mix.linkable(voice, subject, slot);
44
46
  if (want === here)
45
47
  continue;
46
48
  const after = here === null ? at : here.next;
@@ -0,0 +1,41 @@
1
+ import type { Leavings } from './leavings.js';
2
+ import { Store } from './store.js';
3
+ import type { Cut } from './transport.js';
4
+ import type { Subject } from './voice.js';
5
+ /**
6
+ * A voice's records once a seek has gone back: each subject's is put back from the record it had,
7
+ * the first time it is asked for after a seek, since the subjects a store holds cannot be listed.
8
+ * It is one store however many seeks there are: by subject it keeps how many seeks its record has
9
+ * been put back for, and a record behind is put back to the earliest moment sought since, as a
10
+ * seek back cuts what came after it and a later seek ahead of that moment must not read past it.
11
+ */
12
+ export declare class Restored<I> extends Store<I, Subject<unknown>> {
13
+ private readonly own;
14
+ private readonly make;
15
+ /** Seeks back so far. */
16
+ private n;
17
+ /** By subject, the `n` its record was last put back, written or deleted at; none reads as 0. */
18
+ private readonly at;
19
+ /**
20
+ * Each seek back sought to an earlier moment than every one after it, oldest first: the earliest
21
+ * moment sought since seek `n` is that of the first here at or after it.
22
+ */
23
+ private readonly least;
24
+ /** By seek, the records subjects left with after its moment, and where it cut; null until one has any. */
25
+ private futures;
26
+ /** The latest seek's moment. */
27
+ private t;
28
+ /**
29
+ * `own` holds the records as they stood before the first seek. `make` puts a record back as it
30
+ * stood at `t`, undefined where it had none then; `top` is the latest seek's moment.
31
+ */
32
+ constructor(own: Store<I, Subject<unknown>>, make: (live: Subject<unknown>, t: number, top: number) => Subject<unknown> | undefined);
33
+ /** A seek back to `t`: `left` has the records subjects left with after `cut`, null for none. */
34
+ back(t: number, cut: Cut, left: Leavings<I> | null): void;
35
+ /** The earliest moment sought by seek `from` or any after it. */
36
+ private since;
37
+ get(key: I): Subject<unknown> | undefined;
38
+ set(key: I, value: Subject<unknown>): void;
39
+ delete(key: I): void;
40
+ clear(): void;
41
+ }
@@ -0,0 +1,91 @@
1
+ import { Store } from './store.js';
2
+ /**
3
+ * A voice's records once a seek has gone back: each subject's is put back from the record it had,
4
+ * the first time it is asked for after a seek, since the subjects a store holds cannot be listed.
5
+ * It is one store however many seeks there are: by subject it keeps how many seeks its record has
6
+ * been put back for, and a record behind is put back to the earliest moment sought since, as a
7
+ * seek back cuts what came after it and a later seek ahead of that moment must not read past it.
8
+ */
9
+ export class Restored extends Store {
10
+ own;
11
+ make;
12
+ /** Seeks back so far. */
13
+ n = 0;
14
+ /** By subject, the `n` its record was last put back, written or deleted at; none reads as 0. */
15
+ at = new Store();
16
+ /**
17
+ * Each seek back sought to an earlier moment than every one after it, oldest first: the earliest
18
+ * moment sought since seek `n` is that of the first here at or after it.
19
+ */
20
+ least = [];
21
+ /** By seek, the records subjects left with after its moment, and where it cut; null until one has any. */
22
+ futures = null;
23
+ /** The latest seek's moment. */
24
+ t = Number.NaN;
25
+ /**
26
+ * `own` holds the records as they stood before the first seek. `make` puts a record back as it
27
+ * stood at `t`, undefined where it had none then; `top` is the latest seek's moment.
28
+ */
29
+ constructor(own, make) {
30
+ super();
31
+ this.own = own;
32
+ this.make = make;
33
+ }
34
+ /** A seek back to `t`: `left` has the records subjects left with after `cut`, null for none. */
35
+ back(t, cut, left) {
36
+ const n = ++this.n;
37
+ const least = this.least;
38
+ while (least.length > 0 && least[least.length - 1].t >= t)
39
+ least.pop();
40
+ least.push({ n, t });
41
+ this.t = t;
42
+ if (left !== null) {
43
+ this.futures ??= new Map();
44
+ this.futures.set(n, { left, cut });
45
+ }
46
+ }
47
+ /** The earliest moment sought by seek `from` or any after it. */
48
+ since(from) {
49
+ const least = this.least;
50
+ let lo = 0;
51
+ let hi = least.length - 1;
52
+ while (lo < hi) {
53
+ const mid = (lo + hi) >> 1;
54
+ if (least[mid].n >= from)
55
+ hi = mid;
56
+ else
57
+ lo = mid + 1;
58
+ }
59
+ return least[lo].t;
60
+ }
61
+ get(key) {
62
+ const at = this.at.get(key) ?? 0;
63
+ if (at === this.n)
64
+ return this.own.get(key);
65
+ this.at.set(key, this.n);
66
+ // A subject that left after the moment the next seek sought had the record it left with,
67
+ // whatever it has now. Leaving is a write, so no later seek's leavings can hold it.
68
+ const future = this.futures?.get(at + 1);
69
+ const live = future?.left.at(key, future.cut) ?? this.own.get(key);
70
+ if (live === undefined)
71
+ return undefined;
72
+ const made = this.make(live, this.since(at + 1), this.t);
73
+ if (made === undefined)
74
+ this.own.delete(key);
75
+ else
76
+ this.own.set(key, made);
77
+ return made;
78
+ }
79
+ set(key, value) {
80
+ this.own.set(key, value);
81
+ this.at.set(key, this.n);
82
+ }
83
+ delete(key) {
84
+ this.own.delete(key);
85
+ this.at.set(key, this.n);
86
+ }
87
+ clear() {
88
+ this.own.clear();
89
+ this.at.clear();
90
+ }
91
+ }
package/dist/revive.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Mixer } from './mixer.js';
2
2
  import type { Run } from './motions.js';
3
+ import type { Past } from './origin.js';
3
4
  import { type PackedRecord } from './pack.js';
4
5
  import { type Key } from './paging.js';
5
6
  import type { Described } from './types.js';
@@ -22,8 +23,13 @@ interface PackedVoice {
22
23
  outAt: number;
23
24
  outOver: number;
24
25
  outSet: boolean;
26
+ rebuilds: number;
27
+ past: number;
28
+ clocks: Past[] | null;
25
29
  cuedAt: number;
30
+ cuedSeq: number;
26
31
  doneAt: number;
32
+ doneSeq: number;
27
33
  opened: number;
28
34
  pinned: number;
29
35
  latest: number;
@@ -32,11 +38,12 @@ interface PackedVoice {
32
38
  log: Controls[] | null;
33
39
  played: boolean | undefined;
34
40
  playedAt: number;
41
+ playedSeq: number;
35
42
  records: [Key, PackedRecord][];
36
43
  runs: [Key, Run][];
37
- left: [Key, number, PackedRecord][];
38
- parts: [Key, number, number][];
39
- parted: [Key, number][];
44
+ left: [Key, number, number, PackedRecord, boolean][];
45
+ parts: [Key, number, number, number][];
46
+ parted: [Key, number, number][];
40
47
  }
41
48
  /**
42
49
  * Voices a history store holds whose handles something still holds, by id: a seek that revives
package/dist/revive.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { playable } from './cue.js';
2
2
  import { VoiceHandle } from './handle.js';
3
3
  import { handle } from './hosts.js';
4
+ import { Leavings } from './leavings.js';
4
5
  import { packHeld, unpackHeld } from './pack.js';
5
6
  import { pageOut } from './paging.js';
6
7
  import { Voice } from './voice.js';
@@ -47,7 +48,7 @@ export function pageVoice(mix, voice, reach) {
47
48
  return false;
48
49
  const keys = mix.keys;
49
50
  const data = packVoice(voice, keys);
50
- pageOut(mix, 'voice', voice.id, undefined, [{ at: voice.doneAt }], () => data, reach);
51
+ pageOut(mix, 'voice', voice.id, undefined, [{ at: voice.doneAt, seq: voice.doneSeq }], () => data, reach);
51
52
  const h = voice.handle;
52
53
  VoiceHandle.page(h, { weight: voice.weight, played: voice.settled.played === true });
53
54
  mix.outs.add(voice.id, voice.doneAt, h);
@@ -81,8 +82,13 @@ function packVoice(voice, keys) {
81
82
  outAt: voice.outAt,
82
83
  outOver: voice.outOver,
83
84
  outSet: voice.outSet,
85
+ rebuilds: voice.rebuilds,
86
+ past: voice.clocks?.length ?? 0,
87
+ clocks: voice.clocks,
84
88
  cuedAt: voice.cuedAt,
89
+ cuedSeq: voice.cuedSeq,
85
90
  doneAt: voice.doneAt,
91
+ doneSeq: voice.doneSeq,
86
92
  opened: voice.opened,
87
93
  pinned: voice.pinned,
88
94
  latest: voice.latest,
@@ -91,11 +97,18 @@ function packVoice(voice, keys) {
91
97
  log: voice.log,
92
98
  played: settled.played,
93
99
  playedAt: settled.at,
100
+ playedSeq: settled.seq,
94
101
  records,
95
102
  runs,
96
- left: (voice.left ?? []).map((e) => [keys.key(e.subject), e.at, packHeld(voice, e.held)]),
97
- parts: [...(voice.parts ?? [])].map(([s, r]) => [keys.key(s), r.at, r.over]),
98
- parted: [...(voice.parted ?? [])].map(([s, at]) => [keys.key(s), at]),
103
+ left: (voice.left?.all() ?? []).map((e) => [
104
+ keys.key(e.subject),
105
+ e.at,
106
+ e.seq,
107
+ packHeld(voice, e.held),
108
+ e.sync,
109
+ ]),
110
+ parts: [...(voice.parts ?? [])].map(([s, r]) => [keys.key(s), r.at, r.over, r.seq]),
111
+ parted: [...(voice.parted ?? [])].map(([s, p]) => [keys.key(s), p.at, p.seq]),
99
112
  };
100
113
  }
101
114
  /** A voice a history store paged out, rebuilt from its descriptor and put back as it left. */
@@ -106,18 +119,12 @@ export function reviveVoice(mix, id, d) {
106
119
  const spec = { ...revive(d.as), as: d.as };
107
120
  playable(mix, spec);
108
121
  const voice = new Voice(id, spec, spec.patch, spec.fade ?? {}, Number.NEGATIVE_INFINITY, d.start, mix.slotOf, mix.channels, mix.opts.host, mix.send, null);
109
- voice.anchorNow = d.anchorNow;
110
- voice.anchorElapsed = d.anchorElapsed;
111
- voice.rate = d.rate;
112
- voice.ramp = d.ramp;
113
- voice.weight = d.weight;
114
- voice.out = d.out;
115
- voice.back = d.back;
116
- voice.outAt = d.outAt;
117
- voice.outOver = d.outOver;
118
- voice.outSet = d.outSet;
122
+ voice.clocks = d.clocks;
123
+ voice.take(d);
119
124
  voice.cuedAt = d.cuedAt;
125
+ voice.cuedSeq = d.cuedSeq;
120
126
  voice.doneAt = d.doneAt;
127
+ voice.doneSeq = d.doneSeq;
121
128
  voice.opened = d.opened;
122
129
  voice.pinned = d.pinned;
123
130
  voice.latest = d.latest;
@@ -126,7 +133,7 @@ export function reviveVoice(mix, id, d) {
126
133
  voice.log = d.log;
127
134
  voice.state = 'done';
128
135
  if (d.played !== undefined)
129
- voice.play(d.played, d.playedAt);
136
+ voice.play(d.played, d.playedAt, d.playedSeq);
130
137
  voice.resolve();
131
138
  const keys = mix.keys;
132
139
  const subjectOf = (key) => keys.subject(key);
@@ -137,16 +144,17 @@ export function reviveVoice(mix, id, d) {
137
144
  }
138
145
  for (const [key, run] of d.runs)
139
146
  voice.motion?.unpack(subjectOf(key), run);
140
- if (d.left.length > 0)
141
- voice.left = d.left.map(([key, at, p]) => ({
142
- subject: subjectOf(key),
143
- at,
144
- held: unpackHeld(voice, p),
145
- }));
147
+ voice.left = Leavings.from(d.left.map(([key, at, seq, p, sync]) => ({
148
+ subject: subjectOf(key),
149
+ at,
150
+ seq,
151
+ sync,
152
+ held: unpackHeld(voice, p),
153
+ })));
146
154
  if (d.parts.length > 0)
147
- voice.parts = new Map(d.parts.map(([key, at, over]) => [subjectOf(key), { at, over }]));
155
+ voice.parts = new Map(d.parts.map(([key, at, over, seq]) => [subjectOf(key), { at, over, seq }]));
148
156
  if (d.parted.length > 0)
149
- voice.parted = new Map(d.parted.map(([key, at]) => [subjectOf(key), at]));
157
+ voice.parted = new Map(d.parted.map(([key, at, seq]) => [subjectOf(key), { at, seq }]));
150
158
  const h = mix.outs?.take(id);
151
159
  if (h === undefined)
152
160
  voice.handle = handle(mix, voice);
@@ -0,0 +1,11 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Voice } from './voice.js';
3
+ /** Counts a voice just put in the list into everything a sync and a fold skip work by. */
4
+ export declare function enlist<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
5
+ /**
6
+ * Counts a voice leaving the list back out. A voice naming no subject stays in `general` and
7
+ * `sharers`, which the caller filters once for every voice leaving together.
8
+ */
9
+ export declare function delist<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
10
+ /** Counts the list over from nothing, for a seek or a projection's copy. `owners` is the caller's. */
11
+ export declare function recount<I, O>(mix: Mixer<I, O>): void;
package/dist/roster.js ADDED
@@ -0,0 +1,46 @@
1
+ import { index, unindex } from './chain.js';
2
+ import { detour } from './everyone.js';
3
+ import { Store } from './store.js';
4
+ function tally(mix, voice, by) {
5
+ if (voice.spec.locus !== undefined)
6
+ mix.loci += by;
7
+ if (voice.spec.anchor !== undefined)
8
+ mix.anchored += by;
9
+ }
10
+ /** Counts a voice just put in the list into everything a sync and a fold skip work by. */
11
+ export function enlist(mix, voice) {
12
+ if (voice.holding !== null) {
13
+ mix.owners ??= [];
14
+ mix.owners.push(voice);
15
+ }
16
+ index(mix, voice);
17
+ tally(mix, voice, 1);
18
+ detour(mix);
19
+ }
20
+ /**
21
+ * Counts a voice leaving the list back out. A voice naming no subject stays in `general` and
22
+ * `sharers`, which the caller filters once for every voice leaving together.
23
+ */
24
+ export function delist(mix, voice) {
25
+ if (voice.named !== null)
26
+ unindex(mix, voice);
27
+ if (voice.holding !== null && mix.owners !== null)
28
+ mix.owners = mix.owners.filter((v) => v !== voice);
29
+ tally(mix, voice, -1);
30
+ detour(mix);
31
+ }
32
+ /** Counts the list over from nothing, for a seek or a projection's copy. `owners` is the caller's. */
33
+ export function recount(mix) {
34
+ mix.named = new Store();
35
+ mix.naming = 0;
36
+ mix.general = [];
37
+ mix.sharers = [];
38
+ mix.loci = 0;
39
+ mix.anchored = 0;
40
+ for (const voice of mix.cued) {
41
+ index(mix, voice);
42
+ tally(mix, voice, 1);
43
+ }
44
+ detour(mix);
45
+ mix.steps.push(null, ++mix.version);
46
+ }
@@ -0,0 +1,29 @@
1
+ import { type Vec } from './channels.js';
2
+ import type { Channel } from './types.js';
3
+ /**
4
+ * What `angle` takes.
5
+ *
6
+ * @category channel
7
+ */
8
+ export interface AngleOptions {
9
+ /** One full turn in the channel's unit. Default 360, for degrees; `2 * Math.PI` for radians. */
10
+ turn?: number;
11
+ }
12
+ /**
13
+ * A rotation about one axis, which adds as `sum` does and interpolates the short way round: from
14
+ * 350 to 10 it passes 0, not 180. A weight below 1 fades it the short way to rest too. Its values
15
+ * are angles, so two a whole number of turns apart are the same value, and a fold may answer with
16
+ * either.
17
+ *
18
+ * @category channel
19
+ */
20
+ export declare function angle(opts?: AngleOptions): Channel<number>;
21
+ /**
22
+ * A rotation in space, as a unit quaternion `[x, y, z, w]`. Contributions compose by the Hamilton
23
+ * product in the order their voices were cued, which matters: rotations do not commute. A weight
24
+ * turns a contribution part of the way from no rotation, along the short arc, and `lerp` takes the
25
+ * short arc between two.
26
+ *
27
+ * @category channel
28
+ */
29
+ export declare function quat(): Channel<Vec<4>>;
@@ -0,0 +1,91 @@
1
+ import { doubles, vouch } from './channels.js';
2
+ import { shortWay } from './turns.js';
3
+ /**
4
+ * A rotation about one axis, which adds as `sum` does and interpolates the short way round: from
5
+ * 350 to 10 it passes 0, not 180. A weight below 1 fades it the short way to rest too. Its values
6
+ * are angles, so two a whole number of turns apart are the same value, and a fold may answer with
7
+ * either.
8
+ *
9
+ * @category channel
10
+ */
11
+ export function angle(opts) {
12
+ const turn = opts?.turn ?? 360;
13
+ if (!(turn > 0))
14
+ throw new RangeError('blits: an angle takes a positive turn');
15
+ const channel = {
16
+ kind: `angle(${turn})`,
17
+ rest: 0,
18
+ merge: (a, b) => a + b,
19
+ scale: (v, w) => shortWay(v, turn) * w,
20
+ lerp: (a, b, u) => a + shortWay(b - a, turn) * u,
21
+ };
22
+ vouch(channel, { op: 'sum', axes: 1, turn });
23
+ return channel;
24
+ }
25
+ /** `a` then `b`, written into `out`, which may be either. */
26
+ function multiply(out, a, b) {
27
+ const ax = a[0];
28
+ const ay = a[1];
29
+ const az = a[2];
30
+ const aw = a[3];
31
+ const bx = b[0];
32
+ const by = b[1];
33
+ const bz = b[2];
34
+ const bw = b[3];
35
+ out[0] = aw * bx + ax * bw + ay * bz - az * by;
36
+ out[1] = aw * by - ax * bz + ay * bw + az * bx;
37
+ out[2] = aw * bz + ax * by - ay * bx + az * bw;
38
+ out[3] = aw * bw - ax * bx - ay * by - az * bz;
39
+ return out;
40
+ }
41
+ /** The short arc from `a` to `b` at `u`, a unit quaternion, written into `out`. */
42
+ function slerp(out, a, b, u) {
43
+ let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
44
+ // `q` and `-q` are one rotation; the nearer of the two is the short way.
45
+ const sign = dot < 0 ? -1 : 1;
46
+ dot *= sign;
47
+ let p = 1 - u;
48
+ let q = u;
49
+ // Nearly parallel, the arc's sine is too small to divide by, and a straight line is as good.
50
+ if (dot < 1 - 1e-9) {
51
+ const theta = Math.acos(dot);
52
+ const s = Math.sin(theta);
53
+ p = Math.sin((1 - u) * theta) / s;
54
+ q = Math.sin(u * theta) / s;
55
+ }
56
+ q *= sign;
57
+ const x = p * a[0] + q * b[0];
58
+ const y = p * a[1] + q * b[1];
59
+ const z = p * a[2] + q * b[2];
60
+ const w = p * a[3] + q * b[3];
61
+ const n = Math.sqrt(x * x + y * y + z * z + w * w) || 1;
62
+ out[0] = x / n;
63
+ out[1] = y / n;
64
+ out[2] = z / n;
65
+ out[3] = w / n;
66
+ return out;
67
+ }
68
+ const IDENTITY = [0, 0, 0, 1];
69
+ const scratch = doubles(4, 0);
70
+ /**
71
+ * A rotation in space, as a unit quaternion `[x, y, z, w]`. Contributions compose by the Hamilton
72
+ * product in the order their voices were cued, which matters: rotations do not commute. A weight
73
+ * turns a contribution part of the way from no rotation, along the short arc, and `lerp` takes the
74
+ * short arc between two.
75
+ *
76
+ * @category channel
77
+ */
78
+ export function quat() {
79
+ const rest = doubles(4, 0);
80
+ rest[3] = 1;
81
+ const channel = {
82
+ kind: 'quat',
83
+ rest: rest,
84
+ merge: (a, b) => multiply(doubles(4, 0), a, b),
85
+ scale: (v, w) => slerp(doubles(4, 0), IDENTITY, v, w),
86
+ fold: (into, v, w) => multiply(into, into, slerp(scratch, IDENTITY, v, w)),
87
+ lerp: (a, b, u) => slerp(doubles(4, 0), a, b, u),
88
+ };
89
+ vouch(channel, { op: 'own', axes: 4 });
90
+ return channel;
91
+ }
package/dist/rows.d.ts CHANGED
@@ -15,19 +15,19 @@ export declare function sameCrowds<I, O>(lanes: Lanes<I, O>, members: readonly V
15
15
  */
16
16
  export declare function regroup<I, O>(lanes: Lanes<I, O>, members: Voice<I, O>[]): void;
17
17
  /**
18
- * Gives a voice the next row of its channel's crowd, made if there is none: a copy of the row it
18
+ * Gives a voice the next row of its channels' crowd, made if there is none: a copy of the row it
19
19
  * had in `from` at `at`, or a new one. `old` holds the crowds being replaced, whose idle state a
20
- * new crowd on the same channel keeps. Rows stay in voice order only because voices join in it.
20
+ * new crowd on the same channels keeps. Rows stay in voice order only because voices join in it.
21
21
  */
22
22
  export declare function join<I, O>(lanes: Lanes<I, O>, v: Voice<I, O>, from: Crowd<I, O> | undefined, at: number | undefined, old: readonly Crowd<I, O>[]): void;
23
23
  /** A crowd row takes up its voice as it now stands: opened once it starts, its fields copied again. */
24
24
  export declare function retouchRow<I, O>(lanes: Lanes<I, O>, c: Crowd<I, O>, p: number, v: Voice<I, O>): void;
25
25
  /**
26
- * Compacts every crowd a quarter or more empty rows: each fill walks every row, and at half a
27
- * crowd replacing a voice a frame carried as many empty rows as live ones.
26
+ * Compacts every crowd a quarter or more empty rows, past 64 of them: each fill walks every row,
27
+ * and at half a crowd replacing a voice a frame carried as many empty rows as live ones.
28
28
  */
29
29
  export declare function compactSparse<I, O>(lanes: Lanes<I, O>): void;
30
- /** A crowd voice left: its row stays, empty, until the crowds are next rebuilt. */
30
+ /** A crowd voice left: its row stays, empty, until the crowd is compacted or rebuilt. */
31
31
  export declare function bury<I, O>(lanes: Lanes<I, O>, c: Crowd<I, O>, p: number): void;
32
32
  /** A crowd row's weight as `reported` reads a lane position's. */
33
33
  export declare function reportedRow<I, O>(lanes: Lanes<I, O>, c: Crowd<I, O>, p: number, slot: number): number | undefined;
package/dist/rows.js CHANGED
@@ -75,9 +75,9 @@ export function regroup(lanes, members) {
75
75
  }
76
76
  }
77
77
  /**
78
- * Gives a voice the next row of its channel's crowd, made if there is none: a copy of the row it
78
+ * Gives a voice the next row of its channels' crowd, made if there is none: a copy of the row it
79
79
  * had in `from` at `at`, or a new one. `old` holds the crowds being replaced, whose idle state a
80
- * new crowd on the same channel keeps. Rows stay in voice order only because voices join in it.
80
+ * new crowd on the same channels keeps. Rows stay in voice order only because voices join in it.
81
81
  */
82
82
  export function join(lanes, v, from, at, old) {
83
83
  const chans = v.slots.map((s) => lanes.bySlot[s]);
@@ -113,8 +113,8 @@ export function retouchRow(lanes, c, p, v) {
113
113
  c.hot[h + 0 /* Hot.FLAGS */] = c.hot[h + 0 /* Hot.FLAGS */] | 64 /* Flag.VOICE */;
114
114
  }
115
115
  /**
116
- * Compacts every crowd a quarter or more empty rows: each fill walks every row, and at half a
117
- * crowd replacing a voice a frame carried as many empty rows as live ones.
116
+ * Compacts every crowd a quarter or more empty rows, past 64 of them: each fill walks every row,
117
+ * and at half a crowd replacing a voice a frame carried as many empty rows as live ones.
118
118
  */
119
119
  export function compactSparse(lanes) {
120
120
  for (const c of lanes.crowds)
@@ -154,7 +154,7 @@ function compact(c) {
154
154
  c.laws.length = q;
155
155
  c.dead = 0;
156
156
  }
157
- /** A crowd voice left: its row stays, empty, until the crowds are next rebuilt. */
157
+ /** A crowd voice left: its row stays, empty, until the crowd is compacted or rebuilt. */
158
158
  export function bury(lanes, c, p) {
159
159
  const v = c.voices[p];
160
160
  leaveCrowd(lanes, c, p);
package/dist/sample.d.ts CHANGED
@@ -11,9 +11,10 @@ export declare function runMotion<I, O>(lanes: Lanes<I, O>, lane: Lane<I, O>, ru
11
11
  * A motion voice's value for a subject, sampled from the patch's state as its `at` would, but only
12
12
  * where that sample changes nothing: one with a change to stamp, commit or let go of goes to the
13
13
  * general path, which samples it if and when a probe asks, as it would with no lanes. A probe
14
- * this frame already read keeps the value it read, as the general path's record would.
14
+ * this frame already read keeps the value it read, as the general path's record would. The
15
+ * subject's voice time, delay and weight are in `Arg`.
15
16
  */
16
- export declare function move<I, O>(lanes: Lanes<I, O>, lane: Positions<I, O>, run: Motions<I>, p: number, slot: number, held: Subject<unknown>, elapsed: number, delay: number, w: number): void;
17
+ export declare function move<I, O>(lanes: Lanes<I, O>, lane: Positions<I, O>, run: Motions<I>, p: number, slot: number, held: Subject<unknown>): void;
17
18
  /**
18
19
  * A probe this frame read a motion voice's value for the subject from an earlier fill: write onto
19
20
  * its record what the general path's sample would have, so a later read this frame reuses it.