@msb235/blits 0.7.1 → 0.8.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 (153) hide show
  1. package/CHANGELOG.md +180 -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 +3 -0
  11. package/dist/clone.js +16 -1
  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 +213 -28
  36. package/dist/gather.d.ts +15 -9
  37. package/dist/gather.js +60 -25
  38. package/dist/gone.js +10 -0
  39. package/dist/handle.d.ts +16 -5
  40. package/dist/handle.js +56 -6
  41. package/dist/held.d.ts +8 -5
  42. package/dist/held.js +56 -58
  43. package/dist/history.d.ts +17 -13
  44. package/dist/history.js +47 -28
  45. package/dist/hosts.js +55 -8
  46. package/dist/index.d.ts +9 -4
  47. package/dist/index.js +5 -2
  48. package/dist/keyfill.d.ts +7 -0
  49. package/dist/keyfill.js +124 -0
  50. package/dist/keyrows.d.ts +1 -0
  51. package/dist/keyrows.js +1 -1
  52. package/dist/lane.d.ts +32 -2
  53. package/dist/lane.js +5 -0
  54. package/dist/lanes.d.ts +32 -8
  55. package/dist/lanes.js +69 -37
  56. package/dist/leavings.d.ts +26 -0
  57. package/dist/leavings.js +70 -0
  58. package/dist/meet.js +14 -8
  59. package/dist/mixer.d.ts +39 -7
  60. package/dist/mixer.js +50 -10
  61. package/dist/motion.d.ts +2 -0
  62. package/dist/motion.js +4 -1
  63. package/dist/motions.d.ts +22 -15
  64. package/dist/motions.js +40 -34
  65. package/dist/move.js +23 -16
  66. package/dist/moved.d.ts +11 -0
  67. package/dist/moved.js +29 -0
  68. package/dist/origin.d.ts +19 -0
  69. package/dist/origin.js +68 -0
  70. package/dist/owed.d.ts +4 -4
  71. package/dist/owed.js +8 -8
  72. package/dist/owner.d.ts +1 -1
  73. package/dist/owner.js +2 -2
  74. package/dist/pace.d.ts +13 -5
  75. package/dist/pace.js +21 -8
  76. package/dist/pack.d.ts +3 -1
  77. package/dist/pack.js +15 -24
  78. package/dist/paging.d.ts +8 -2
  79. package/dist/paging.js +44 -4
  80. package/dist/patch.d.ts +14 -9
  81. package/dist/patch.js +83 -21
  82. package/dist/place.js +18 -3
  83. package/dist/project.d.ts +12 -1
  84. package/dist/project.js +101 -83
  85. package/dist/pull.d.ts +2 -0
  86. package/dist/pull.js +8 -0
  87. package/dist/qualify.js +10 -2
  88. package/dist/reading.d.ts +5 -0
  89. package/dist/reading.js +5 -0
  90. package/dist/record.d.ts +9 -0
  91. package/dist/record.js +42 -0
  92. package/dist/relink.d.ts +11 -5
  93. package/dist/relink.js +7 -5
  94. package/dist/restored.d.ts +41 -0
  95. package/dist/restored.js +91 -0
  96. package/dist/revive.d.ts +10 -3
  97. package/dist/revive.js +31 -23
  98. package/dist/roster.d.ts +11 -0
  99. package/dist/roster.js +46 -0
  100. package/dist/rotation.d.ts +29 -0
  101. package/dist/rotation.js +91 -0
  102. package/dist/rows.d.ts +5 -5
  103. package/dist/rows.js +5 -5
  104. package/dist/sample.d.ts +3 -2
  105. package/dist/sample.js +32 -15
  106. package/dist/seek.js +97 -114
  107. package/dist/shared.d.ts +6 -0
  108. package/dist/shared.js +15 -0
  109. package/dist/signals.d.ts +24 -3
  110. package/dist/signals.js +50 -11
  111. package/dist/spans.js +7 -0
  112. package/dist/stack.d.ts +11 -0
  113. package/dist/stack.js +47 -0
  114. package/dist/standing.d.ts +10 -0
  115. package/dist/standing.js +79 -0
  116. package/dist/store.d.ts +3 -3
  117. package/dist/store.js +3 -3
  118. package/dist/strays.js +3 -0
  119. package/dist/tape.d.ts +8 -5
  120. package/dist/tape.js +21 -13
  121. package/dist/testing.d.ts +35 -0
  122. package/dist/testing.js +116 -0
  123. package/dist/touch.d.ts +6 -0
  124. package/dist/touch.js +28 -0
  125. package/dist/transport.d.ts +49 -6
  126. package/dist/transport.js +89 -19
  127. package/dist/turns.d.ts +18 -0
  128. package/dist/turns.js +72 -0
  129. package/dist/tweened.js +1 -1
  130. package/dist/types/channel.d.ts +46 -0
  131. package/dist/types/channel.js +1 -0
  132. package/dist/types/history.d.ts +139 -0
  133. package/dist/types/history.js +1 -0
  134. package/dist/types/mix.d.ts +336 -0
  135. package/dist/types/mix.js +1 -0
  136. package/dist/types/patch.d.ts +187 -0
  137. package/dist/types/patch.js +1 -0
  138. package/dist/types/place.d.ts +157 -0
  139. package/dist/types/place.js +1 -0
  140. package/dist/types/transport.d.ts +71 -0
  141. package/dist/types/transport.js +1 -0
  142. package/dist/types/voice.d.ts +323 -0
  143. package/dist/types/voice.js +1 -0
  144. package/dist/types.d.ts +7 -1162
  145. package/dist/unpage.d.ts +2 -2
  146. package/dist/unpage.js +53 -25
  147. package/dist/voice.d.ts +90 -22
  148. package/dist/voice.js +105 -43
  149. package/dist/wave.d.ts +42 -0
  150. package/dist/wave.js +62 -0
  151. package/dist/weigh.js +1 -1
  152. package/docs/schema.html +42 -36
  153. package/package.json +8 -3
package/dist/seek.js CHANGED
@@ -1,10 +1,14 @@
1
- import { index } from './chain.js';
2
1
  import { schedule } from './due.js';
3
- import { copyHeld, last, leftAt } from './history.js';
2
+ import { copyHeld, last, lastWithin } from './history.js';
4
3
  import { laneHost } from './hosts.js';
5
4
  import { Lanes } from './lanes.js';
6
- import { move } from './move.js';
5
+ import { Leavings } from './leavings.js';
6
+ import { move, nextFrame } from './move.js';
7
7
  import { ownerReading } from './owner.js';
8
+ import { place } from './place.js';
9
+ import { unpull } from './pull.js';
10
+ import { Restored } from './restored.js';
11
+ import { recount } from './roster.js';
8
12
  import { refitAll } from './spans.js';
9
13
  import { Store } from './store.js';
10
14
  import { hostAt, replay } from './tape.js';
@@ -35,44 +39,6 @@ function restore(voice, live, t) {
35
39
  h.inputs = inputs;
36
40
  return h;
37
41
  }
38
- /**
39
- * A voice's records after a seek back: each subject's is put back from the records it had, the first
40
- * time it is asked for, since the subjects a store holds cannot be listed.
41
- */
42
- class Restored extends Store {
43
- was;
44
- make;
45
- left;
46
- constructor(was, make, left) {
47
- super();
48
- this.was = was;
49
- this.make = make;
50
- this.left = left;
51
- }
52
- /** Subjects whose record has been put back or deleted, which `left` no longer answers for. */
53
- settled = new Store();
54
- get(key) {
55
- const v = super.get(key);
56
- if (v !== undefined)
57
- return v;
58
- // A subject that left after the moment sought had the record it left with, whatever it has now.
59
- const kept = this.settled.get(key) === undefined ? this.left(key) : undefined;
60
- this.settled.set(key, true);
61
- const live = kept ?? this.was.get(key);
62
- if (live === undefined)
63
- return undefined;
64
- this.was.delete(key);
65
- const made = this.make(live);
66
- if (made !== undefined)
67
- super.set(key, made);
68
- return made;
69
- }
70
- delete(key) {
71
- super.delete(key);
72
- this.was.delete(key);
73
- this.settled.set(key, true);
74
- }
75
- }
76
42
  /** Takes a voice cued after the moment sought out of the mix, until the tape cues it again. */
77
43
  function park(mix, voice) {
78
44
  const owner = voice.owner;
@@ -86,34 +52,27 @@ function park(mix, voice) {
86
52
  motion.owner = null;
87
53
  voice.park();
88
54
  }
89
- /** Puts a voice's controls and records back as they stood at mix time `t`. */
90
- function restoreVoice(voice, t) {
55
+ /** Puts a voice's controls and records back as they stood at mix time `t`, the end of frame `seq`. */
56
+ function restoreVoice(mix, voice, t, seq) {
57
+ const cut = { seq, strict: false };
91
58
  const log = voice.log;
92
- const c = last(log, t, true) ?? log[0];
59
+ const c = lastWithin(log, cut) ?? log[0];
93
60
  log.length = log.indexOf(c) + 1;
94
- voice.anchorNow = c.anchorNow;
95
- voice.anchorElapsed = c.anchorElapsed;
96
- voice.rate = c.rate;
97
- voice.ramp = c.ramp;
98
- voice.weight = c.weight;
99
- voice.out = c.out;
100
- voice.start = c.start;
101
- voice.outAt = c.outAt;
102
- voice.outOver = c.outOver;
103
- voice.outSet = c.outSet;
104
- if (voice.reopen(t) && voice.owner?.holding)
61
+ voice.take(c);
62
+ if (voice.reopen(seq) && voice.owner?.holding)
105
63
  voice.owner.holding.played--;
106
64
  voice.doneAt = Number.POSITIVE_INFINITY;
65
+ voice.doneSeq = Number.POSITIVE_INFINITY;
107
66
  if (voice.parts !== null) {
108
67
  for (const [subject, r] of voice.parts)
109
- if (r.at > t)
68
+ if (r.seq > seq)
110
69
  voice.parts.delete(subject);
111
70
  if (voice.parts.size === 0)
112
71
  voice.parts = null;
113
72
  }
114
73
  if (voice.parted !== null) {
115
- for (const [subject, at] of voice.parted)
116
- if (at > t)
74
+ for (const [subject, p] of voice.parted)
75
+ if (p.seq > seq)
117
76
  voice.parted.delete(subject);
118
77
  if (voice.parted.size === 0)
119
78
  voice.parted = null;
@@ -127,30 +86,43 @@ function restoreVoice(voice, t) {
127
86
  voice.unreached = null;
128
87
  voice.unreachedBits = null;
129
88
  voice.seeks++;
130
- const left = voice.left;
131
- voice.left = left === null ? null : left.filter((e) => e.at <= t);
132
- const future = left === null ? [] : left.filter((e) => e.at > t);
133
- voice.subjects = new Restored(voice.subjects, (live) => {
134
- const h = restore(voice, live, t);
135
- if (h === undefined)
89
+ const left = voice.left?.all() ?? [];
90
+ voice.left = Leavings.from(left.filter((e) => e.seq <= seq));
91
+ const future = Leavings.from(left.filter((e) => e.seq > seq));
92
+ let records = voice.subjects;
93
+ if (!(records instanceof Restored)) {
94
+ records = new Restored(records, (live, least, top) => {
95
+ const h = restore(voice, live, least);
96
+ if (h === undefined)
97
+ return h;
98
+ if (h.reaches)
99
+ voice.seen++;
100
+ if (h.rested)
101
+ voice.restedCount++;
102
+ if (!(h.since <= least)) {
103
+ // Kept while its origin was still ahead: where the controls put back place it now.
104
+ h.since = mix.sinceOf(voice, h.delay);
105
+ h.shown = mix.shownOf(voice, h.since);
106
+ if (h.ticks === 0 && h.stepped > h.since)
107
+ h.stepped = h.since;
108
+ }
109
+ if (!(h.since <= top)) {
110
+ voice.early ??= [];
111
+ voice.early.push(h);
112
+ }
136
113
  return h;
137
- if (h.reaches)
138
- voice.seen++;
139
- if (h.rested)
140
- voice.restedCount++;
141
- if (voice.state === 'pending') {
142
- voice.early ??= [];
143
- voice.early.push(h);
144
- }
145
- return h;
146
- }, (key) => leftAt(future, key, t));
114
+ });
115
+ voice.subjects = records;
116
+ }
117
+ records.back(t, cut, future);
147
118
  }
148
119
  /** Each voice's state at mix time `t`, once every clock above it is back where it was. */
149
120
  function stateAt(voice, t) {
121
+ // A fade begins at once, even one whose ramp starts later, at the voice's start.
122
+ if (voice.out !== null)
123
+ return 'fading';
150
124
  if ((voice.owner === null ? t : ownerReading(voice.owner, t)) < voice.start)
151
125
  return 'pending';
152
- if (voice.out !== null && voice.out.at <= t)
153
- return 'fading';
154
126
  if (voice.freezesAfter && voice.elapsedAt(t) >= voice.span + voice.latest)
155
127
  return 'frozen';
156
128
  return 'live';
@@ -172,34 +144,43 @@ export function seek(transport, t) {
172
144
  if (!(t >= transport.born))
173
145
  throw new Error(`blits: ${t} is older than this mix's history reaches`);
174
146
  const unpaged = cover(transport, t);
175
- if (t >= transport.now)
176
- replay(transport, () => t);
177
- // Under the rate the recorded calls up to `t` set, ahead; behind, under the rate it had then.
178
- const u = hostAt(transport, t);
179
- if (!Number.isFinite(u))
180
- throw new RangeError(`blits: the mix's rate never reaches ${t}`);
181
147
  if (t >= transport.now) {
148
+ // Ahead, under the rate the recorded calls up to `t` set, past any frames rate 0 held at `t`.
149
+ const last = replay(transport, (at) => (transport.pace === null ? at : transport.pace.reading(at)) <= t);
150
+ const u = Number.isNaN(last) ? hostAt(transport, t) : Math.max(last, hostAt(transport, t));
151
+ if (!Number.isFinite(u))
152
+ throw new RangeError(`blits: the mix's rate never reaches ${t}`);
182
153
  transport.u = u;
183
154
  for (const m of transport.members)
184
155
  move(m, t);
185
156
  }
186
157
  else {
187
- const n = tape.depthAt(t);
188
- if (n < tape.undoDepth())
189
- tape.goto(n);
190
- transport.undrop(t);
191
- // What history kept after `t` is let go; the tape holds the calls that made it.
192
- transport.announced = transport.announced.filter((a) => a.made <= t);
193
- transport.pace?.cut(u);
158
+ const frame = transport.frameAt(t);
159
+ if (frame === undefined)
160
+ throw new Error(`blits: ${t} is older than this mix's history reaches`);
161
+ // On the frame itself, its own host time: rate 0 may have held the clock there for several.
162
+ const u = frame.at === t ? frame.u : hostAt(transport, t);
163
+ const seq = frame.seq;
164
+ tape.goto(tape.depthAt(u));
165
+ transport.undrop(seq);
166
+ // What history kept after the frame is let go; the tape holds the calls that made it.
167
+ transport.announced = transport.announced.filter((a) => a.seq <= seq);
168
+ transport.frames = transport.frames.filter((f) => f.seq <= seq);
169
+ if (transport.pacedSeq > seq) {
170
+ transport.pace = null;
171
+ transport.pacedSeq = Number.POSITIVE_INFINITY;
172
+ }
173
+ else
174
+ transport.pace?.cut(u);
194
175
  transport.u = u;
195
176
  transport.now = t;
196
177
  for (const m of transport.members)
197
- back(m, t);
198
- cut(transport, t, unpaged);
178
+ back(m, t, seq, frame.at < t);
179
+ cut(transport, seq, unpaged);
199
180
  }
181
+ transport.tick(t);
200
182
  // The host's clock reads on from here: its next sync reads `t` plus its time since its last.
201
- transport.offset = transport.last - u;
202
- transport.u = u;
183
+ transport.offset = transport.last - transport.u;
203
184
  transport.kept();
204
185
  for (const m of transport.members) {
205
186
  m.stir();
@@ -209,8 +190,11 @@ export function seek(transport, t) {
209
190
  b.sought();
210
191
  }
211
192
  }
212
- /** Puts one mix back as it stood at the end of frame `t`, its transport already there. */
213
- function back(mix, t) {
193
+ /**
194
+ * Puts one mix back as it stood at mix time `t`, the end of frame `seq`, its transport already
195
+ * there; `on` past that frame's own time, where it moves on to `t`.
196
+ */
197
+ function back(mix, t, seq, on) {
214
198
  const all = [...mix.cued, ...mix.gone].sort((a, b) => a.id - b.id);
215
199
  const kept = [];
216
200
  const gone = [];
@@ -220,14 +204,14 @@ function back(mix, t) {
220
204
  if (voice.motion !== undefined)
221
205
  motions.add(voice.motion);
222
206
  for (const voice of all)
223
- if (voice.cuedAt > t)
207
+ if (voice.cuedSeq > seq)
224
208
  park(mix, voice);
225
- else if (voice.doneAt <= t)
209
+ else if (voice.doneSeq <= seq)
226
210
  gone.push(voice);
227
211
  else
228
212
  kept.push(voice);
229
213
  for (const voice of kept)
230
- restoreVoice(voice, t);
214
+ restoreVoice(mix, voice, t, seq);
231
215
  for (const voice of kept)
232
216
  voice.state = stateAt(voice, t);
233
217
  mix.cued = kept;
@@ -242,28 +226,17 @@ function back(mix, t) {
242
226
  if (oldLanes !== null)
243
227
  mix.lanes = new Lanes(laneHost(mix));
244
228
  mix.chains = new Store();
245
- mix.named = new Store();
246
- mix.naming = 0;
247
- mix.general = [];
248
- for (const voice of kept)
249
- index(mix, voice);
250
229
  mix.owners = kept.some((v) => v.holding !== null) ? kept.filter((v) => v.holding !== null) : null;
251
- mix.loci = kept.filter((v) => v.spec.locus !== undefined).length;
252
- mix.anchored = kept.filter((v) => v.spec.anchor !== undefined).length;
230
+ recount(mix);
253
231
  mix.parters.clear();
254
232
  for (const voice of [...kept, ...gone])
255
233
  if (voice.parts !== null || voice.parted !== null)
256
234
  mix.parters.add(voice);
257
- mix.steps.push(null, ++mix.version);
258
235
  mix.relinks++;
259
236
  mix.pose = new Store();
260
237
  if (mix.restStamps !== null)
261
238
  mix.restStamps = new Store();
262
- mix.pulled = [];
263
- mix.pulledHeads = [];
264
- mix.pulledSlots = new Int32Array(0);
265
- mix.pulledVersion = Number.NaN;
266
- mix.pulledRelinks = -1;
239
+ unpull(mix);
267
240
  for (const voice of [...kept, ...gone]) {
268
241
  voice.unreached = null;
269
242
  voice.unreachedBits = null;
@@ -290,10 +263,10 @@ function back(mix, t) {
290
263
  for (const motion of motions) {
291
264
  if (oldLanes !== null && motion.watcher === oldLanes)
292
265
  motion.watcher = null;
293
- motion.rewind(t);
266
+ motion.rewind(seq);
294
267
  }
295
268
  // What history kept after `t` is let go; the tape holds the calls that made it.
296
- mix.hostLog = mix.hostLog.filter((e) => e.at <= t);
269
+ mix.hostLog = mix.hostLog.filter((e) => e.seq <= seq);
297
270
  mix.sent = mix.sent.filter((e) => e.timestamp <= t);
298
271
  mix.sentTo = t;
299
272
  mix.pins = null;
@@ -307,5 +280,15 @@ function back(mix, t) {
307
280
  for (const voice of kept)
308
281
  schedule(mix, voice);
309
282
  mix.stir();
310
- move(mix, t);
283
+ // On the frame's own time its move is behind it: a second would land what the next frame does.
284
+ if (on)
285
+ move(mix, t);
286
+ else {
287
+ mix.frame = nextFrame();
288
+ mix.reducedNow = mix.reduced;
289
+ // A voice history let go of may have placed an anchor then: placed again from what it holds.
290
+ const ms = mix.opts.history.ms;
291
+ if (mix.anchored > 0 && mix.transport.forgotTo >= t - ms)
292
+ place(mix);
293
+ }
311
294
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
3
+ * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
4
+ * what is stored changes shape, so copies that disagree on the shape keep apart.
5
+ */
6
+ export declare function shared<K extends object, V>(name: string): WeakMap<K, V>;
package/dist/shared.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
3
+ * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
4
+ * what is stored changes shape, so copies that disagree on the shape keep apart.
5
+ */
6
+ export function shared(name) {
7
+ const key = Symbol.for(`@msb235/blits/${name}`);
8
+ const g = globalThis;
9
+ let map = g[key];
10
+ if (map === undefined) {
11
+ map = new WeakMap();
12
+ g[key] = map;
13
+ }
14
+ return map;
15
+ }
package/dist/signals.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Signal } from './types.js';
1
+ import type { Setting, Signal } from './types.js';
2
+ /** Whether a change to an input signal reaches `reading.inputs`, so a lane fill can see it. */
3
+ export declare const heard: (signal: object) => boolean;
2
4
  /**
3
5
  * The loudest of several signals.
4
6
  *
@@ -6,7 +8,26 @@ import type { Signal } from './types.js';
6
8
  */
7
9
  export declare function peak<I, H = unknown>(...signals: readonly Signal<I, H>[]): Signal<I, H>;
8
10
  /**
9
- * A signal the host writes. klieg's `level`.
11
+ * Marks `read` as a signal driven from outside the clock, which a host's own signal has to be for
12
+ * `assess` and a history's `inputs` to treat it as one. Two uses:
13
+ *
14
+ * `input(read)` is an input of the host's own: a pointer, a volume, a value in a store. The host
15
+ * calls `touch()` after each change to what `read` reads, and a voice weighted by the signal, or by
16
+ * `peak`, `slew`, `lag` or `gate` over it, keeps its lane. A change nobody reports shows once the
17
+ * mix's clock next moves.
18
+ *
19
+ * `input(read, of)` is a signal built on others, as a wrapper that clamps one is: `of` lists every
20
+ * signal `read` calls, and the result is an input where any of them is, and reports its changes
21
+ * where they all do.
22
+ *
23
+ * @category signal
24
+ */
25
+ export declare function input<I, H = unknown>(read: (subject: I, setting: Setting<void, H>) => number, of?: readonly Signal<I, H>[]): Signal<I, H> & {
26
+ touch(): void;
27
+ };
28
+ /**
29
+ * A signal the host writes: `input` over one number, with `set` reporting each change. klieg's
30
+ * `level`.
10
31
  *
11
32
  * @category signal
12
33
  */
@@ -14,7 +35,7 @@ export declare function level<I, H = unknown>(initial?: number): Signal<I, H> &
14
35
  set(v: number): void;
15
36
  };
16
37
  /**
17
- * The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
38
+ * The package's linear decay primitive: follows its input at a rate limit, no faster than `riseMs` per
18
39
  * unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
19
40
  * handed the same slew each follow on their own. A subject starts on its input, or at `from`
20
41
  * unless `dt` is infinite.
package/dist/signals.js CHANGED
@@ -1,5 +1,16 @@
1
+ import { reading } from './reading.js';
2
+ /** Input signals whose every change bumps `reading.inputs`: `input`'s, and those built only on them. */
3
+ const told = new WeakSet();
4
+ /** Whether a change to an input signal reaches `reading.inputs`, so a lane fill can see it. */
5
+ export const heard = (signal) => told.has(signal);
1
6
  const inputOf = (signals) => signals.some((s) => s.input);
2
- const marked = (read, input) => (input ? Object.assign(read, { input: true }) : read);
7
+ const marked = (read, of) => {
8
+ if (!inputOf(of))
9
+ return read;
10
+ if (of.every((s) => !s.input || told.has(s)))
11
+ told.add(read);
12
+ return Object.assign(read, { input: true });
13
+ };
3
14
  /**
4
15
  * The loudest of several signals.
5
16
  *
@@ -14,19 +25,46 @@ export function peak(...signals) {
14
25
  out = v;
15
26
  }
16
27
  return out;
17
- }, inputOf(signals));
28
+ }, signals);
18
29
  }
30
+ const touch = () => {
31
+ reading.inputs++;
32
+ };
19
33
  /**
20
- * A signal the host writes. klieg's `level`.
34
+ * Marks `read` as a signal driven from outside the clock, which a host's own signal has to be for
35
+ * `assess` and a history's `inputs` to treat it as one. Two uses:
36
+ *
37
+ * `input(read)` is an input of the host's own: a pointer, a volume, a value in a store. The host
38
+ * calls `touch()` after each change to what `read` reads, and a voice weighted by the signal, or by
39
+ * `peak`, `slew`, `lag` or `gate` over it, keeps its lane. A change nobody reports shows once the
40
+ * mix's clock next moves.
41
+ *
42
+ * `input(read, of)` is a signal built on others, as a wrapper that clamps one is: `of` lists every
43
+ * signal `read` calls, and the result is an input where any of them is, and reports its changes
44
+ * where they all do.
45
+ *
46
+ * @category signal
47
+ */
48
+ export function input(read, of) {
49
+ if (of !== undefined)
50
+ return Object.assign(marked(read, of), { touch });
51
+ told.add(read);
52
+ return Object.assign(read, { input: true, touch });
53
+ }
54
+ /**
55
+ * A signal the host writes: `input` over one number, with `set` reporting each change. klieg's
56
+ * `level`.
21
57
  *
22
58
  * @category signal
23
59
  */
24
60
  export function level(initial = 0) {
25
61
  let value = initial;
26
- return Object.assign(() => value, {
27
- input: true,
62
+ return Object.assign(input(() => value), {
28
63
  set(v) {
64
+ if (Object.is(v, value))
65
+ return;
29
66
  value = v;
67
+ touch();
30
68
  },
31
69
  });
32
70
  }
@@ -38,7 +76,7 @@ const firstSight = (target, from, setting) => from === undefined || !Number.isFi
38
76
  ? { value: target, seen: Number.NaN }
39
77
  : { value: from, seen: setting.timestamp };
40
78
  /**
41
- * The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
79
+ * The package's linear decay primitive: follows its input at a rate limit, no faster than `riseMs` per
42
80
  * unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
43
81
  * handed the same slew each follow on their own. A subject starts on its input, or at `from`
44
82
  * unless `dt` is infinite.
@@ -56,7 +94,8 @@ export function slew(of, { riseMs = 0, fallMs = 0, from }) {
56
94
  const gap = target - held.value;
57
95
  const ms = gap > 0 ? riseMs : fallMs;
58
96
  if (ms > 0) {
59
- const step = (setting.timestamp - held.seen) / ms;
97
+ // No longer than `dt`, which `maxDt` caps, as every step is.
98
+ const step = Math.min(setting.timestamp - held.seen, setting.dt) / ms;
60
99
  next = gap > 0 ? Math.min(target, held.value + step) : Math.max(target, held.value - step);
61
100
  }
62
101
  }
@@ -64,7 +103,7 @@ export function slew(of, { riseMs = 0, fallMs = 0, from }) {
64
103
  held.seen = setting.timestamp;
65
104
  return next;
66
105
  };
67
- return marked(read, inputOf([of]));
106
+ return marked(read, [of]);
68
107
  }
69
108
  /**
70
109
  * The exponential counterpart to `slew`: closes the same fraction of the distance to its input in
@@ -86,7 +125,7 @@ export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6, from, }) {
86
125
  if (!Number.isNaN(held.seen) && Number.isFinite(setting.dt)) {
87
126
  const ms = target > held.value ? riseMs : fallMs;
88
127
  if (ms > 0) {
89
- const gap = setting.timestamp - held.seen;
128
+ const gap = Math.min(setting.timestamp - held.seen, setting.dt);
90
129
  next = held.value + (target - held.value) * (1 - Math.exp(-gap / ms));
91
130
  if (Math.abs(target - next) <= floor)
92
131
  next = target;
@@ -96,7 +135,7 @@ export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6, from, }) {
96
135
  held.seen = setting.timestamp;
97
136
  return next;
98
137
  };
99
- return marked(read, inputOf([of]));
138
+ return marked(read, [of]);
100
139
  }
101
140
  /**
102
141
  * The other half of the pair: a band, not an edge. It drops at `off`, passes at `on`, and between
@@ -116,5 +155,5 @@ export function gate(of, band) {
116
155
  }
117
156
  return held.on ? 1 : 0;
118
157
  };
119
- return marked(read, inputOf([of]));
158
+ return marked(read, [of]);
120
159
  }
package/dist/spans.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { elapsedWith, retime } from './clock.js';
2
2
  import { retire } from './fade.js';
3
3
  import { defaultFit, layout, settle } from './fit.js';
4
+ import { reorigin } from './held.js';
4
5
  import { noted } from './history.js';
5
6
  import { nextFrame } from './move.js';
7
+ import { leave } from './origin.js';
6
8
  import { ownerReading } from './owner.js';
7
9
  /** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
8
10
  export class Fitting {
@@ -89,6 +91,7 @@ export function refit(mix, span) {
89
91
  kids.forEach((c, i) => {
90
92
  const rate = (c.spec.rate ?? 1) * plan.rate[i];
91
93
  const skipped = plan.skip[i] && c.holding === null;
94
+ c.skipped = skipped;
92
95
  let moved = false;
93
96
  if (c.state === 'pending') {
94
97
  const start = local + at[i];
@@ -99,23 +102,27 @@ export function refit(mix, span) {
99
102
  c.anchorElapsed = elapsed;
100
103
  c.rate = rate;
101
104
  c.ramp = null;
105
+ c.clocks = null;
102
106
  moved = true;
103
107
  }
104
108
  }
105
109
  else {
106
110
  if (skipped) {
111
+ leave(c, local);
107
112
  c.rebase(local);
108
113
  c.anchorElapsed = c.span;
109
114
  c.seeks++;
110
115
  moved = true;
111
116
  }
112
117
  if (c.rate !== rate || c.ramp !== null) {
118
+ leave(c, local);
113
119
  retime(c, local, rate, 0);
114
120
  moved = true;
115
121
  }
116
122
  }
117
123
  if (moved) {
118
124
  mix.frame = nextFrame();
125
+ reorigin(mix, c, mix.now);
119
126
  noted(mix, c);
120
127
  }
121
128
  });
@@ -0,0 +1,11 @@
1
+ import type { Kit } from './types.js';
2
+ /**
3
+ * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
4
+ * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
5
+ * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
6
+ * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
7
+ * a mix's default band first switches one on, and stays undefined where none is.
8
+ *
9
+ * @category channel
10
+ */
11
+ export declare function fold<O>(kit: Kit<O>, deltas: readonly Partial<O>[], weights?: readonly number[]): O;
package/dist/stack.js ADDED
@@ -0,0 +1,47 @@
1
+ import { copy } from './clone.js';
2
+ import { bound, merged } from './fold.js';
3
+ /** The weight a mix's default band first switches a rest-less channel on at. */
4
+ const ON = 0.6;
5
+ /**
6
+ * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
7
+ * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
8
+ * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
9
+ * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
10
+ * a mix's default band first switches one on, and stays undefined where none is.
11
+ *
12
+ * @category channel
13
+ */
14
+ export function fold(kit, deltas, weights) {
15
+ const pose = {};
16
+ const names = Object.keys(kit);
17
+ for (const key of names) {
18
+ const channel = kit[key];
19
+ const rest = channel.rest;
20
+ pose[key] = rest === undefined ? undefined : channel.copy ? channel.copy(rest) : copy(rest);
21
+ }
22
+ for (let i = 0; i < deltas.length; i++) {
23
+ const given = weights?.[i] ?? 1;
24
+ const w = given > 1 ? 1 : given;
25
+ if (!(w > 0))
26
+ continue;
27
+ const delta = deltas[i];
28
+ for (const key of names) {
29
+ const value = delta[key];
30
+ if (value === undefined)
31
+ continue;
32
+ const channel = kit[key];
33
+ if (channel.rest !== undefined && channel.scale)
34
+ pose[key] = channel.fold
35
+ ? channel.fold(pose[key], value, w)
36
+ : merged(channel, pose[key], channel.scale(value, w));
37
+ else if (w >= ON)
38
+ pose[key] = pose[key] === undefined ? copy(value) : merged(channel, pose[key], value);
39
+ }
40
+ }
41
+ for (const key of names) {
42
+ const channel = kit[key];
43
+ if (channel.bounds)
44
+ pose[key] = bound(channel, pose[key]);
45
+ }
46
+ return pose;
47
+ }
@@ -0,0 +1,10 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Cut, Transport } from './transport.js';
3
+ /**
4
+ * Throws unless a read back to mix time `t` is exact with no history: every voice plays as it was
5
+ * cued and reads the same whenever it is asked, and no mix on the transport has changed since `t`.
6
+ * A mix keeps only how it stands now, which is how it has stood since its last change.
7
+ */
8
+ export declare function stands(transport: Transport, t: number): void;
9
+ /** A mix's copy for a read back to `t` with no history: its voices as they stand, each at `t`. */
10
+ export declare function copyStanding<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>, t: number, cut: Cut): void;