@msb235/blits 0.10.0 → 0.12.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 (126) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/README.md +15 -4
  3. package/dist/arcs.d.ts +40 -0
  4. package/dist/arcs.js +239 -0
  5. package/dist/blend.js +1 -5
  6. package/dist/book.d.ts +2 -0
  7. package/dist/book.js +27 -3
  8. package/dist/chain.d.ts +17 -1
  9. package/dist/chain.js +57 -15
  10. package/dist/channels.d.ts +2 -0
  11. package/dist/channels.js +12 -1
  12. package/dist/clock.d.ts +14 -0
  13. package/dist/clock.js +9 -1
  14. package/dist/clone.js +16 -4
  15. package/dist/columns.d.ts +4 -5
  16. package/dist/columns.js +11 -5
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.js +63 -7
  20. package/dist/departed.d.ts +8 -3
  21. package/dist/departed.js +76 -29
  22. package/dist/due.d.ts +4 -0
  23. package/dist/due.js +16 -3
  24. package/dist/easing.d.ts +12 -1
  25. package/dist/easing.js +7 -1
  26. package/dist/everyone.d.ts +14 -1
  27. package/dist/everyone.js +27 -2
  28. package/dist/fade.d.ts +12 -5
  29. package/dist/fade.js +57 -19
  30. package/dist/fill.js +16 -9
  31. package/dist/fit.d.ts +8 -1
  32. package/dist/fit.js +7 -3
  33. package/dist/fold.d.ts +1 -1
  34. package/dist/fold.js +112 -9
  35. package/dist/frames.d.ts +2 -0
  36. package/dist/frames.js +17 -0
  37. package/dist/gather.js +38 -7
  38. package/dist/handle.d.ts +7 -2
  39. package/dist/handle.js +28 -12
  40. package/dist/held.d.ts +13 -2
  41. package/dist/held.js +43 -9
  42. package/dist/history.d.ts +25 -2
  43. package/dist/history.js +163 -35
  44. package/dist/hosts.js +45 -17
  45. package/dist/index.d.ts +5 -2
  46. package/dist/index.js +3 -1
  47. package/dist/keyfill.js +3 -1
  48. package/dist/lane.d.ts +1 -1
  49. package/dist/lane.js +3 -1
  50. package/dist/lanes.d.ts +31 -7
  51. package/dist/lanes.js +57 -6
  52. package/dist/lean.d.ts +15 -0
  53. package/dist/lean.js +23 -0
  54. package/dist/leavings.d.ts +6 -1
  55. package/dist/leavings.js +16 -2
  56. package/dist/marks.d.ts +7 -3
  57. package/dist/marks.js +105 -27
  58. package/dist/meet.js +1 -0
  59. package/dist/mixer.d.ts +35 -6
  60. package/dist/mixer.js +113 -45
  61. package/dist/motion.d.ts +4 -1
  62. package/dist/motion.js +16 -2
  63. package/dist/motions.d.ts +4 -0
  64. package/dist/motions.js +45 -11
  65. package/dist/move.js +35 -12
  66. package/dist/numbers.d.ts +5 -1
  67. package/dist/numbers.js +4 -0
  68. package/dist/owner.d.ts +18 -2
  69. package/dist/owner.js +44 -3
  70. package/dist/pack.d.ts +1 -0
  71. package/dist/pack.js +3 -0
  72. package/dist/place.d.ts +8 -0
  73. package/dist/place.js +16 -8
  74. package/dist/placed.d.ts +19 -0
  75. package/dist/placed.js +54 -0
  76. package/dist/project.d.ts +5 -2
  77. package/dist/project.js +238 -34
  78. package/dist/pull.js +37 -8
  79. package/dist/qualify.d.ts +10 -0
  80. package/dist/qualify.js +60 -3
  81. package/dist/reading.d.ts +8 -1
  82. package/dist/reading.js +8 -1
  83. package/dist/record.d.ts +6 -0
  84. package/dist/record.js +2 -0
  85. package/dist/restored.d.ts +69 -17
  86. package/dist/restored.js +163 -50
  87. package/dist/revive.d.ts +6 -3
  88. package/dist/revive.js +11 -3
  89. package/dist/roster.js +2 -1
  90. package/dist/rows.js +15 -5
  91. package/dist/seek.d.ts +7 -2
  92. package/dist/seek.js +152 -43
  93. package/dist/shared.d.ts +5 -0
  94. package/dist/shared.js +5 -0
  95. package/dist/signals.d.ts +10 -0
  96. package/dist/signals.js +17 -0
  97. package/dist/spans.d.ts +5 -0
  98. package/dist/spans.js +75 -5
  99. package/dist/store.d.ts +13 -0
  100. package/dist/store.js +23 -0
  101. package/dist/strays.js +50 -2
  102. package/dist/tape.d.ts +29 -4
  103. package/dist/tape.js +49 -11
  104. package/dist/touch.js +3 -2
  105. package/dist/transport.d.ts +11 -2
  106. package/dist/transport.js +26 -10
  107. package/dist/types/arc.d.ts +55 -0
  108. package/dist/types/arc.js +1 -0
  109. package/dist/types/mix.d.ts +57 -14
  110. package/dist/types/patch.d.ts +5 -1
  111. package/dist/types/place.d.ts +11 -4
  112. package/dist/types/transport.d.ts +12 -3
  113. package/dist/types/voice.d.ts +72 -5
  114. package/dist/types/watch.d.ts +126 -0
  115. package/dist/types/watch.js +1 -0
  116. package/dist/types.d.ts +2 -0
  117. package/dist/unpage.d.ts +6 -1
  118. package/dist/unpage.js +34 -2
  119. package/dist/voice.d.ts +93 -16
  120. package/dist/voice.js +60 -3
  121. package/dist/watch.d.ts +26 -0
  122. package/dist/watch.js +441 -0
  123. package/dist/weigh.d.ts +13 -2
  124. package/dist/weigh.js +10 -3
  125. package/docs/schema.html +106 -38
  126. package/package.json +10 -4
package/CHANGELOG.md CHANGED
@@ -4,6 +4,254 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
4
4
  version and everything else the patch. Each release lists its changes as **Breaking**, **Added** and
5
5
  **Fixed**, and the release workflow refuses a tag with no section here.
6
6
 
7
+ ## 0.12.0
8
+
9
+ ### Added
10
+
11
+ - `curve(easing)` and `Curve`: the function an easing stands for, from a CSS name, a `bezier`, a
12
+ `steps` or a function, for a host shaping its own ramps with the curves patches use.
13
+ - `mix.arc(spec)`: an owner holding three parts that hand over in order on its clock, `enter`,
14
+ `active` for the `hold` and `exit`, each a voice's spec or several layered, with a `blend`
15
+ crossfading across each boundary. Its marks are its own: `in` as the enter ends, `coast` and
16
+ `out` as the hold ends and the exit begins, `end` as the exit ends, where it leaves and `done`
17
+ resolves. `hold: 'open'` holds until `release()` on its handle, which a seek replays; `fade`
18
+ takes it out with no exit. An arc with a finite hold sits in a span. It is klieg's `fire` as a
19
+ blits primitive, under the same part names. The schema page's "Arcs" says what each part does.
20
+ - A span child's `earliest` and `rank`. `earliest` is the soonest it may start, a time on the
21
+ span's clock or an anchor; the fit never places it sooner, and the children after it are laid
22
+ out from where it starts. One already past when it is cued places it there, partway through, so a
23
+ span made afresh after a seek plays as it did. One whose anchor has no answer yet waits, with every child
24
+ after it under `queue` or `stagger`, and the span fits again once the anchor answers. `rank` orders the
25
+ children still to start, lower first, ties as cued. Together they replace astv's own change
26
+ order, which sorts its changes by line and starts each no sooner than its orb lands.
27
+ - `watched()`: every mix on the page, for a debugger, in one registry that all copies of blits
28
+ share. Each entry can:
29
+ - `watch` what the host reads each frame
30
+ - `read` a subject the host never probes, now or back in history, through `project`
31
+ - `label` a subject
32
+ - list its kit's `channels`, and the `name` and `tags` a voice was cued with
33
+ - `hold` the mix's clock to pause, step and seek it, without writing to the tape, then `free`
34
+ it
35
+
36
+ A mix nobody watches runs the same code as before, and one a debugger has let go of runs as fast
37
+ as one it never watched. `watched().history` gives history to mixes
38
+ made later that name none. `MixOptions.labelOf` names subjects for it.
39
+
40
+ ### Fixed
41
+
42
+ - Replacing a voice makes less garbage again: a one-subject tween voice retargeted as it is cued,
43
+ while another leaves and its subject is dropped, allocates 11.4–11.7 KB, from 13.0–13.4
44
+ (`bench/churnwho.mjs`, 5,000 voices, teitou, three passes a side). Frame time is unchanged.
45
+ - A voice over every subject that plays more than 60 frames costs less a frame: it now keeps a
46
+ record for each subject, where it kept one for them all that every read had to point at its
47
+ subject and back. Three `fn` voices over 1,000 subjects (`fn`) take 0.212 ms a frame against
48
+ 0.226, eight 0.468 against 0.526 (teitou, medians of nine alternated runs of 1,500 frames against
49
+ `a77c5c9`). A voice cued and gone within a few frames still shares one record, which is what it
50
+ was for.
51
+ - A crowd of `fn` voices writing arrays, one voice per subject, costs about a tenth less a frame
52
+ where nothing else writes its channels: it runs off lanes, where its subjects are walked once
53
+ instead of twice. A `fn` voice writing a vec for each of 10,000 subjects (`fns`) takes 1.348 ms
54
+ a frame against 1.519 (msb-uai, medians of nine alternated runs of one build with lanes kept
55
+ for it and not). So does a single `fn` voice over many subjects writing a vec, where no other
56
+ voice writes it: weasel's animator written as one (`weaselfn`) takes 1.734 ms against 1.823,
57
+ and one writing a position from each subject's own numbers (`tweenfn`) 1.163 against 1.410.
58
+ - A mix with lanes on whose voices all run off lanes costs a probe less bookkeeping: `tweenfn`
59
+ takes 1.097 ms a frame against 1.172 and `weaselfn` 1.644 against 1.723 (msb-uai, medians of
60
+ thirteen alternated runs of one build with the change switched on and off).
61
+ - A voice on a lane costs a call less a subject each frame: since 0.11.0 the check for a voice
62
+ hiding past its end left its clock read out of line. A glow weighted by a signal over 10,000
63
+ subjects (`glow`) takes 1.636 ms a frame against 1.696 (teitou, medians of fifteen alternated runs
64
+ against `11a1471`).
65
+ - A mix keeping history costs about a fifth less a frame and allocates a third less: the copy of a
66
+ record that falls out of the window is written over as the next one kept, only a motion voice
67
+ works out how far back a read may still reach, and a stock `sum`, `mul` or `max` channel folds a
68
+ voice's number without calling the channel. Three voices over 10,000 subjects with 5 s of history
69
+ (`kept`) take 5.33 ms a frame against 6.56 and allocate 5.7 MB against 8.5; sought back and
70
+ returned before every sync (`seek`), 5.90 ms against 6.76; at 1,000 subjects, 0.412 against
71
+ 0.500 and 0.540 against 0.598 (teitou, medians of nine alternated runs of 1,500 frames against
72
+ `00ad148`, each in its own process).
73
+ - A `seek` back and a return to where the mix was cost about a third of what they did: a record put
74
+ back takes up the state of the record the seek replaced instead of stepping again from its copy,
75
+ is written over that record instead of copied anew, and the copy its next probe keeps is written
76
+ over one the seek cut; subjects keep their numbers across the seek. Three voices over 10,000
77
+ subjects with 5 s of history, sought 300 ms back and returned before every sync (`seek`), take
78
+ 9.83 ms a frame against 28.60, and allocate 8.6 MB a frame against 35.2; 1,000 subjects take 0.86
79
+ ms against 1.88 (msb-uai, medians of nine alternated runs against `4374f34`, each in its own
80
+ process). The same mix never sought (`kept`) reads 7.41 ms against 7.33 at 10,000 subjects and
81
+ 0.645 against 0.676 at 1,000. A `step` that reads `setting.weight`, `elapsed` or `pass` now shows,
82
+ after a seek back and return, what a mix never sought shows. A seek no longer boxes its moment
83
+ once per record it puts back: the `seek` row at 10,000 subjects allocates 7.9 MB a frame against
84
+ 8.8, and takes 10.44 ms against 9.88, inside its runs' spread (msb-uai, medians of nine
85
+ alternated runs against `1077ab4`). A seek's relink takes each voice's record from the subject's
86
+ old chain where its store can tell the record is still its own, instead of looking it up by
87
+ subject: the `seek` row at 10,000 subjects takes 9.42 ms a frame against 9.95, and 1,000 subjects
88
+ 0.864 against 0.884 (msb-uai, medians of nine alternated runs against `c3fe7a8`).
89
+ - A mix sought back often no longer grows without bound: each voice kept a little for every seek
90
+ back to a later moment than the last, and the records subjects left with after each seek, and
91
+ let go of neither. Three voices sought back and returned every frame held 2.26 MB more after
92
+ 12,000 frames; they now hold what their live records need.
93
+ - A lane fill no longer boxes numbers it hands between its own calls, which it did for 3 to 5
94
+ numbers a subject in some processes and none in others, by the order V8 happened to compile in.
95
+ 10k subjects under a `fn` voice and a signal-weighted glow over 200 of them, with 200 subjects
96
+ leaving the glow and 200 entering each frame (bench/reach.mjs `part:200`), allocate 1.27 MB a
97
+ frame against 1.89 and take 1.924 ms against 2.037 read by `probe`, and 1.255 against 1.405 by
98
+ `pull`; with nothing moving by `pull`, 1.108 against 1.221 (keiei). `probed` allocates 1.02 MB a
99
+ frame against 1.65. No steady row moves past its spread: `fn` 2.942 against 2.903, `fn^` 2.323
100
+ against 2.390, `keys` 1.355 against 1.342 (msb-uai); `signal` reads 3.820 against 3.578, inside
101
+ its spread but slower in each of three such comparisons. Medians of six alternated runs, each in
102
+ its own process, `bench/ab.sh` against `main` at `4374f34`.
103
+
104
+ ## 0.11.0
105
+
106
+ ### Breaking
107
+
108
+ - A voice plays one pass unless told otherwise: `loop` defaults to `false`, where it defaulted to
109
+ `true`. A `cue` that left `loop` out and relied on looping for good now needs `loop: true`. A
110
+ patch with no duration, and a motion patch, have no passes and play as before.
111
+ - `Marked.mark` and an anchor's `mark` are a `MarkName`, one of a voice's five or a name it placed
112
+ inside itself, where they were a `Mark`. Code that reads `mark` as one of the five, such as an
113
+ exhaustive `switch`, needs a case for the rest.
114
+
115
+ ### Added
116
+
117
+ - `marks` on a voice's, an owner's and a span's spec: named marks inside it beside its five, each
118
+ at voice ms into a pass, `marks: { drop: 340 }`. An anchor names one as it names the five,
119
+ `{ of: 'lead', mark: 'drop' }`; `marks` lists it, `book` books it under its name, and a read
120
+ ahead stops at it. Each is reached once, on the first pass that reaches it from where the voice
121
+ starts, while its clock runs forward and before it ends. A name of one of the five, and a time
122
+ outside a pass, are refused at `cue`.
123
+ - `@msb235/blits/lean`: the package with a `mix` that leaves lanes out, for a host whose scenes
124
+ are too small to need them. It gives the same poses, and a bundle of `mix`, `kit`, `sum` and
125
+ `patch` is 44.5 kB gzipped against 59.9 from the main entry (esbuild, minified). `lanes: true`
126
+ is refused there. Its engine is exported as `lean`.
127
+ - A read ahead after a seek back plays the calls the tape holds: `project(t)` past a recorded
128
+ call threw, and now makes the call on its copies as a seek there makes it on the mix. It still
129
+ throws for a `drop` of a subject a motion patch holds, and for a retarget or push made before
130
+ its voice had met the subject. The tape moves forward and back while the read is made.
131
+ - `stepMs` on a voice's spec: the interval its `step` runs at, in place of the mix's. 0 steps it
132
+ once a frame by the frame's gap.
133
+ - `share(by, i, n)`: member `i` of `n`'s share of a signal, as `mix.blend` weighs it, to weight a
134
+ voice outside the blend as that member, such as one member played alone.
135
+ - `mix.blend` takes a `BlendMember`, `{ patch, name?, tags?, hits? }`, in place of a bare patch, so
136
+ a member answers to its own name in an anchor and carries its own tags and hits. Bare patches
137
+ take the spec's as before.
138
+ - A mix warns, once per patch and outside production, when the first delta a voice's `at` returns
139
+ holds a key outside the patch's `writes`, naming the voice and the key. The mix still drops the
140
+ key, as it always has; `checkPatch` in `@msb235/blits/testing` still finds one returned only on
141
+ some subjects or phases.
142
+ - `hits` and `seek` on an owner's and a span's spec. A hit is an event at a time on the owner's
143
+ clock, booked once while the owner plays, with its `score` and `tags`; a seek is where that clock
144
+ starts, so an owner cued at a rate below 0 with a seek plays its children back from there.
145
+ - `seek({ frame })` and `project({ frame })`, on a mix and a transport, reach a frame by its
146
+ number, which `mix.seq` and `transport.seq` read. Rate 0 can hold mix time still across several
147
+ frames, and a seek or read by time lands on the last of them; by number it lands on any one
148
+ history still holds.
149
+ - `catchUp: 'fresh'` on a voice's spec: a stateful subject first probed after its delay ran out
150
+ starts stepping from that probe with fresh state, where the default, `exact`, steps it from its
151
+ origin. The `stepMs` grid still counts from the origin, the fade-in is unchanged, and a handle
152
+ `seek` still rebuilds from the voice's start. Its state depends on when the host first looked,
153
+ so a read that meets such a subject late assesses it `held`.
154
+ - A read ahead stops at each known event between now and the time read: each call the tape holds,
155
+ and each mark `marks` would list. A subject asked about is read at every stop, so a patch that
156
+ steps takes one step per stretch and a signal that keeps state sees each boundary: a voice
157
+ starting on the way meets its subject at its start, where it met it at the time read. `assess`
158
+ says `exact` for a stateful signal a read ahead fed, where it said `stepped`. The stops are made
159
+ only while a voice has a `step` or a signal weight; otherwise a read ahead crosses the gap in
160
+ one go as before.
161
+
162
+ ### Fixed
163
+
164
+ - A seek ahead, or a read ahead, to the mix time rate 0 is holding stays at the host's own time.
165
+ It took the earliest host time the mix read that time at, behind calls already on the tape, so
166
+ a later seek back cued a voice still playing a second time and the next probe never returned.
167
+ - `weightOf` on lanes reports the weight a voice over every subject gave a subject before a
168
+ subject fade made it keep a record per subject, until the next read. It said 0, where the
169
+ general path said the weight.
170
+ - A seek or read back to before a `drop` finds a subject's fade out of a voice as it stood: mid
171
+ ramp, or faded out. It found the subject at full weight, and one faded out of a voice that had
172
+ not met it stayed out even before the fade. A subject a motion's retarget brought back after a
173
+ fade out is found faded out between the two.
174
+ - `rise` on a voice faded before its start leaves it `pending`, to start on time and follow its
175
+ anchor again, as a seek back finds it. It went `live` at once and played from where its clock
176
+ would have read 0, ahead of its start, so a seek back played on differently from playing
177
+ straight through.
178
+ - A read back to the frame a `drop` was made in, before anything read the mix that frame, shows
179
+ the subject met afresh, as the frame did. It showed the record the subject had before the drop,
180
+ a slewed weight's state included, in `project(t)` and `project({ frame })`.
181
+ - `assess` after a seek back says `stepped` for a stateful signal the seek put back from a copy
182
+ history kept and stepped on, where a read at the mix's own time said `exact`. It goes on saying
183
+ so for that record as the mix plays on, since the state carries on from the step.
184
+ - A voice running back from past its end shows nothing until it reaches its passes, unless it
185
+ freezes after, as a voice playing forward shows nothing before its start unless it freezes
186
+ before. It showed its last frame: `rate: -1, seek: 600` on a 300 ms pass showed it for its
187
+ first 300 ms, and so did a child its owner, turned back, had put past its end.
188
+ - An owner's `out` and `end` marks are known while it plays, at its last child's end, once every
189
+ child's end is. They were fixed only when it left, so `marks` listed them only then and only
190
+ under `history`, a booker took them a frame late, an anchor to an owner's end waited until it
191
+ left, and without `history` they were never booked. Its `coast` stays listed once it has left,
192
+ where it went with its last child.
193
+ - `pull` reads `undefined` at a position its list has gained. A list read as `[undefined]` and
194
+ then as `[undefined, undefined]` threw, reading the new position as the subject it read there
195
+ last time.
196
+ - A spring, glide or tween made by another copy of blits of the same release, loaded beside this
197
+ one, runs as a motion on this copy's mix: on lanes, and retargeted, pushed and read through its
198
+ handle, which refused it. It played through `at` off the lanes. Copies of different releases
199
+ still keep apart.
200
+ - `mix.span` refuses a rate below 0. A span cued backward placed none of its children, which
201
+ waited for good, and never left. Its handle's `rate` still turns one that plays.
202
+ - A voice whose patch has no duration books no hits while its clock runs back, as every other
203
+ voice does. One cued backward booked each hit ahead of it.
204
+ - A fade set for a time after a voice's passes run out no longer begins in place of the voice
205
+ ending, where one frame crossed both: `fade({ at })` on a voice with no `freeze` that ended
206
+ before `at` played on, held at its last frame, until the fade finished.
207
+ - A seek forward to the time of a recorded call lands that frame once. It landed it again after
208
+ the call, so a `rise` played there on a voice past its end began the voice's fade over.
209
+ - A voice cued with `seek`, or sought while pending, keeps its count of rebuilds with its
210
+ controls when it starts. A seek back to after its start, then a handle `seek` played again
211
+ before the subject was read, left the subject's state as it was rather than rebuilding it.
212
+ - A mix sought back time after time no longer steps a subject from the same copy of its state,
213
+ further each time. A seek lets go of the copies made after the moment it sought, so a mix
214
+ sought 300 ms back and to where it was before every frame stepped each subject from the copy
215
+ before its first seek: 70 steps at the first frame and 1069 at the 300th, at `stepMs` 5. The
216
+ copy a record is put back from now keeps its state stepped ahead to near the moment sought,
217
+ and the next record put back from it steps on from there: 67 to 74 steps at every frame. It
218
+ shows the same values. Under `maxDt`, without `stepMs`, or for a patch that `reads` a host
219
+ field, a record steps from the copy as before.
220
+ - Replacing a voice makes less garbage. A one-subject tween voice retargeted as it is cued, while
221
+ another leaves and its subject is dropped, allocates about 13 KB, from 21.5
222
+ (`bench/churnwho.mjs`, 5,000 voices), and a `pull` keeps what it knows of a channel between
223
+ calls, about 1.2 KB a call for four channels. Frame time is unchanged.
224
+ - A seek back, or a read back, places a subject whose start had moved while it waited where the
225
+ move put it. A copy of a record kept while its `stagger` delay was running, on a voice with
226
+ `freeze: 'before'`, holds the start as it was then; a rate, a ramp, a `seek` with
227
+ `state: 'keep'`, an owner's rate or the mix's rate then moved that start, and a record put back
228
+ from the copy to a moment past the old start kept the old one, so its `step` ran on from the
229
+ wrong time. A seek lets go of the later copies, so a mix sought back before every frame showed
230
+ it at every frame from then on; `project(t)` showed it for `t` up to one copy's interval past
231
+ the move.
232
+ - A seek back to before a subject faded out of a voice finds the voice reaching it again, where
233
+ the voice had not met the subject before the fade. The subject stayed out of the voice with
234
+ `lanes: false`, and with lanes on for a voice naming its `subjects`; otherwise lanes brought it
235
+ back, so the two paths read differently. A read back to before the fade, `project(t)`, left
236
+ it out on both, and reaches it now.
237
+ - A seek or a read back into a subject's ramp out of a voice, `fade({ subject, over })`, finds
238
+ the ramp once it has ended. The subject read at full weight there, and after a seek it stayed
239
+ in the voice at full weight for good.
240
+ - `weightOf` reads 0 after a seek back until the subject is probed again. What a subject's last
241
+ probe by then read is not kept, and it reported a weight from after the moment sought, or one
242
+ older than its last probe for a voice on a lane, so the two paths read differently.
243
+ - A write to the handle of a voice waiting for its cue after a seek back changes nothing, and the
244
+ tape does not record it. It put the voice on the mix's schedule, so the voice started at its
245
+ start time outside the mix, folded in but missing from `voices()`, and it branched the tape,
246
+ which left the voice's cue in the old future.
247
+ - Dropping a subject no longer costs time in step with how many subjects the mix holds. With a
248
+ tween voice per subject, stopping one voice and dropping its subject cost 2.2 µs among 1,000
249
+ subjects and 15.6 among 10,000; it costs 0.9 and 2.6 (`bench/restart.mjs`).
250
+ - The first `pull` after a voice naming a subject comes or goes, or a subject is dropped, no
251
+ longer reads every subject's chain to find the ones that changed, and a retargeted tween copies
252
+ its stretch without an iterator. The frame after 100 voices are replaced among 10,000 took
253
+ about 2.1 ms and takes about 1.35.
254
+
7
255
  ## 0.10.0
8
256
 
9
257
  ### Breaking
package/README.md CHANGED
@@ -28,7 +28,7 @@ const flicker = patch<Part, PartPose>(
28
28
  );
29
29
 
30
30
  const m = mix<Part, PartPose>(PART);
31
- const handle = m.cue({ patch: flicker, fade: { in: 300, out: 300 } });
31
+ const handle = m.cue({ patch: flicker, loop: true, fade: { in: 300, out: 300 } });
32
32
 
33
33
  // Once a frame: tell the mix what time it is, then ask for the parts you are about to draw.
34
34
  function frame(timestamp: number) {
@@ -48,12 +48,12 @@ In order, on the same mix:
48
48
 
49
49
  ```ts
50
50
  // Place a voice at a time on the host's clock, or against another voice's marks.
51
- m.cue({ patch: typeIn, name: 'title', start: 1000, loop: false });
52
- m.cue({ patch: tint, loop: false, anchor: { start: { after: 'title', by: 200 } } });
51
+ m.cue({ patch: typeIn, name: 'title', start: 1000 });
52
+ m.cue({ patch: tint, anchor: { start: { after: 'title', by: 200 } } });
53
53
 
54
54
  // A span fits the voices it holds into a budget, one after another by default.
55
55
  const intro = m.span({ duration: 2000 });
56
- for (const line of lines) m.cue({ patch: typeIn, loop: false, owner: intro });
56
+ for (const line of lines) m.cue({ patch: typeIn, owner: intro });
57
57
 
58
58
  // Read the mix half a second ahead without moving it, or move it: a scrubber.
59
59
  const next = m.project(m.now + 500).probe(part);
@@ -91,6 +91,17 @@ npm run site:smoke # build, then load every page headless and fail on any erro
91
91
 
92
92
  `.github/workflows/site.yml` deploys it to GitHub Pages on every push to `main`, and by hand.
93
93
 
94
+ ## A smaller bundle
95
+
96
+ Lanes, which work a channel out for every subject at once, are about a quarter of the package. A
97
+ host with tens of subjects does not need them:
98
+
99
+ ```ts
100
+ import { kit, mix, patch, sum } from '@msb235/blits/lean';
101
+ ```
102
+
103
+ Everything else is the same and so are the poses. `lanes: true` is refused there.
104
+
94
105
  ## Writing patches for blits in another library
95
106
 
96
107
  A library that ships patches exports them with the channels they write, as a `Kit` of its own
package/dist/arcs.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { ArcHandle, ArcSpec } from './types.js';
3
+ import type { Voice } from './voice.js';
4
+ /** What an arc keeps beside its owner: how long its enter and exit last, which parts it has, its blend. */
5
+ export declare class Arc {
6
+ readonly enter: number;
7
+ readonly exit: number;
8
+ readonly blend: number;
9
+ /** By part, whether it has one. */
10
+ readonly has: readonly boolean[];
11
+ /** How long its hold lasts as cued; Infinity for one held open. */
12
+ readonly hold: number;
13
+ constructor(enter: number, exit: number, blend: number,
14
+ /** By part, whether it has one. */
15
+ has: readonly boolean[],
16
+ /** How long its hold lasts as cued; Infinity for one held open. */
17
+ hold: number);
18
+ }
19
+ /** Where an arc ends on its own clock: Infinity while its hold is open. */
20
+ export declare function arcEnd<I, O>(voice: Voice<I, O>): number;
21
+ /**
22
+ * What an arc multiplies into the weight of a voice it holds at mix time `now`: its part's ramp,
23
+ * scaled down where a hold shorter than the blend leaves all three ramps up at once and their sum
24
+ * would pass 1.
25
+ */
26
+ export declare function arcShare<I, O>(arcVoice: Voice<I, O>, child: Voice<I, O>, now: number, reduced: boolean): number;
27
+ /** Cues an arc: its owner, then each part's voices on its clock. */
28
+ export declare function cueArc<I, O>(mix: Mixer<I, O>, spec: ArcSpec<I, O>): ArcHandle<I>;
29
+ /**
30
+ * Ends an arc's hold at mix time now, so its exit begins half a blend later and its blend starts
31
+ * now; where the enter is still playing, the exit follows it. Nothing where the exit has begun.
32
+ */
33
+ export declare function release<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
34
+ /**
35
+ * Lets each arc's parts leave once the blend after them is over, and the arc itself at its end:
36
+ * played, as a voice that ran its passes is.
37
+ */
38
+ export declare function settleArcs<I, O>(mix: Mixer<I, O>, now: number): void;
39
+ /** An arc's marks: when its enter ends, its hold ends and its exit begins and ends. */
40
+ export declare function arcMark<I, O>(voice: Voice<I, O>, start: number, mark: 'in' | 'coast' | 'out' | 'end'): number | undefined;
package/dist/arcs.js ADDED
@@ -0,0 +1,239 @@
1
+ import { retire } from './fade.js';
2
+ import { noted } from './history.js';
3
+ import { ownerReading } from './owner.js';
4
+ import { startAt } from './place.js';
5
+ import { refit } from './spans.js';
6
+ /** An arc's parts in the order they play: its enter, its active and its exit. */
7
+ const PARTS = ['enter', 'active', 'exit'];
8
+ /** What an arc keeps beside its owner: how long its enter and exit last, which parts it has, its blend. */
9
+ export class Arc {
10
+ enter;
11
+ exit;
12
+ blend;
13
+ has;
14
+ hold;
15
+ constructor(enter, exit, blend,
16
+ /** By part, whether it has one. */
17
+ has,
18
+ /** How long its hold lasts as cued; Infinity for one held open. */
19
+ hold) {
20
+ this.enter = enter;
21
+ this.exit = exit;
22
+ this.blend = blend;
23
+ this.has = has;
24
+ this.hold = hold;
25
+ }
26
+ }
27
+ /** Where each part an arc has starts and ends on its clock, for a hold of `hold`; null for a part it lacks. */
28
+ function segments(arc, hold) {
29
+ const bounds = [0, arc.enter, arc.enter + hold, arc.enter + hold + arc.exit];
30
+ const end = bounds[3];
31
+ const out = [null, null, null];
32
+ for (let p = 0; p < 3; p++) {
33
+ if (!arc.has[p])
34
+ continue;
35
+ let next = p + 1;
36
+ while (next < 3 && !arc.has[next])
37
+ next++;
38
+ out[p] = [bounds[p], next < 3 ? bounds[next] : end];
39
+ }
40
+ return out;
41
+ }
42
+ /** Where an arc ends on its own clock: Infinity while its hold is open. */
43
+ export function arcEnd(voice) {
44
+ const arc = voice.arc;
45
+ return arc.enter + voice.hold + arc.exit;
46
+ }
47
+ /**
48
+ * A part's weight at `t` on its arc's clock, klieg's: up over the blend centered on where it
49
+ * starts and down over the one where it ends, except at the arc's own start and end, where it
50
+ * holds full weight.
51
+ */
52
+ function rampOf(s, e, end, blend, t) {
53
+ if (e <= s)
54
+ return 0;
55
+ const half = blend / 2;
56
+ const first = s === 0;
57
+ const last = e === end;
58
+ if ((!first && t < s - half) || (!last && t >= e + half))
59
+ return 0;
60
+ const up = first ? 1 : blend > 0 ? Math.min(1, (t - s + half) / blend) : 1;
61
+ const down = last ? 1 : blend > 0 ? Math.min(1, (e + half - t) / blend) : 1;
62
+ return Math.min(up, down);
63
+ }
64
+ /**
65
+ * What an arc multiplies into the weight of a voice it holds at mix time `now`: its part's ramp,
66
+ * scaled down where a hold shorter than the blend leaves all three ramps up at once and their sum
67
+ * would pass 1.
68
+ */
69
+ export function arcShare(arcVoice, child, now, reduced) {
70
+ const arc = arcVoice.arc;
71
+ const read = ownerReading(arcVoice, now);
72
+ const t = read > 0 ? read : 0;
73
+ const segs = segments(arc, arcVoice.hold);
74
+ const end = arc.enter + arcVoice.hold + arc.exit;
75
+ const blend = reduced ? 0 : arc.blend;
76
+ const mine = segs[child.part];
77
+ if (mine == null)
78
+ return 0;
79
+ const own = rampOf(mine[0], mine[1], end, blend, t);
80
+ if (own <= 0)
81
+ return 0;
82
+ let total = 0;
83
+ for (const s of segs)
84
+ if (s !== null)
85
+ total += rampOf(s[0], s[1], end, blend, t);
86
+ return total > 1 ? own / total : own;
87
+ }
88
+ /** How long a part lasts: its longest voice's `duration`, or patch duration times passes. */
89
+ function lengthOf(part) {
90
+ let most = 0;
91
+ for (const v of part) {
92
+ const loop = v.loop;
93
+ const passes = loop === undefined || loop === false ? 1 : loop === true ? Infinity : loop;
94
+ const own = v.duration ?? (v.patch.duration > 0 ? v.patch.duration * passes : 0);
95
+ if (!(own >= 0))
96
+ throw new RangeError(`blits: an arc's part lasts 0 ms or more, not ${own}`);
97
+ if (own > most)
98
+ most = own;
99
+ }
100
+ return most;
101
+ }
102
+ const listOf = (part) => part === undefined ? undefined : Array.isArray(part) ? part : [part];
103
+ /** Cues an arc: its owner, then each part's voices on its clock. */
104
+ export function cueArc(mix, spec) {
105
+ if ((spec.rate ?? 1) < 0)
106
+ throw new Error('blits: an arc plays its parts forward from its start, so it is not cued backward');
107
+ if (spec.freeze !== undefined)
108
+ throw new Error('blits: an arc holds each part’s first and last frames itself, so it takes no freeze');
109
+ const { enter, active, exit, hold, blend, ...own } = spec;
110
+ const parts = [listOf(enter), listOf(active), listOf(exit)];
111
+ const lengths = parts.map((p) => (p === undefined ? 0 : lengthOf(p)));
112
+ for (const p of [0, 2])
113
+ if (!Number.isFinite(lengths[p]))
114
+ throw new Error(`blits: an arc's ${PARTS[p]} has to end, so it cannot loop for good`);
115
+ const held = hold === 'open' ? Number.POSITIVE_INFINITY : (hold ?? lengths[1] ?? 0);
116
+ if (!(held >= 0))
117
+ throw new RangeError(`blits: an arc holds for 0 ms or more, not ${hold}`);
118
+ const fade = blend ?? 0;
119
+ if (!(fade >= 0 && Number.isFinite(fade)))
120
+ throw new RangeError(`blits: an arc blends over 0 ms or more, not ${blend}`);
121
+ const arc = new Arc(lengths[0], lengths[2], fade, parts.map((p) => p !== undefined), held);
122
+ mix.arcing = arc;
123
+ let handle;
124
+ try {
125
+ handle = mix.owns(own);
126
+ }
127
+ finally {
128
+ mix.arcing = null;
129
+ }
130
+ const starts = [0, arc.enter, arc.enter + held];
131
+ parts.forEach((part, p) => {
132
+ for (const v of part ?? []) {
133
+ const { duration: _, ...rest } = v;
134
+ const child = {
135
+ ...rest,
136
+ owner: handle,
137
+ start: starts[p],
138
+ freeze: 'both',
139
+ };
140
+ // The hold, not the patch, says how long the active plays, so it loops unless told otherwise.
141
+ if (p === 1 && hold !== undefined && v.loop === undefined)
142
+ child.loop = true;
143
+ mix.cue(child);
144
+ mix.cued[mix.cued.length - 1].part = p;
145
+ }
146
+ });
147
+ return handle;
148
+ }
149
+ /**
150
+ * Ends an arc's hold at mix time now, so its exit begins half a blend later and its blend starts
151
+ * now; where the enter is still playing, the exit follows it. Nothing where the exit has begun.
152
+ */
153
+ export function release(mix, voice) {
154
+ const arc = voice.arc;
155
+ if (arc === null)
156
+ throw new Error('blits: release ends an arc’s hold, and this is no arc');
157
+ if (voice.state === 'done' || voice.state === 'fading')
158
+ return;
159
+ const local = ownerReading(voice, mix.now);
160
+ const lead = arc.exit > 0 && !mix.reduced ? arc.blend / 2 : 0;
161
+ const at = Math.max(arc.enter, local + lead);
162
+ if (!(at < arc.enter + voice.hold))
163
+ return;
164
+ voice.hold = at - arc.enter;
165
+ for (const child of voice.holding?.children ?? [])
166
+ if (child.part === 2 && child.state === 'pending')
167
+ startAt(mix, child, at);
168
+ noteArc(mix, voice);
169
+ }
170
+ /** Records an arc's new hold, and has the span holding it fit again. */
171
+ function noteArc(mix, voice) {
172
+ noted(mix, voice);
173
+ if (voice.owner?.fitting)
174
+ refit(mix, voice.owner);
175
+ mix.lanes?.refill();
176
+ }
177
+ /**
178
+ * Lets each arc's parts leave once the blend after them is over, and the arc itself at its end:
179
+ * played, as a voice that ran its passes is.
180
+ */
181
+ export function settleArcs(mix, now) {
182
+ for (const voice of mix.owners ?? []) {
183
+ const arc = voice.arc;
184
+ if (arc === null || voice.state === 'done' || voice.state === 'pending')
185
+ continue;
186
+ const local = ownerReading(voice, now);
187
+ if (!Number.isFinite(local))
188
+ continue;
189
+ const end = arcEnd(voice);
190
+ if (local >= end) {
191
+ const at = voice.timeAt(end);
192
+ voice.play(true, at, mix.transport.seq);
193
+ retire(mix, voice, at);
194
+ continue;
195
+ }
196
+ const segs = segments(arc, voice.hold);
197
+ const half = mix.reduced ? 0 : arc.blend / 2;
198
+ for (const child of [...(voice.holding?.children ?? [])]) {
199
+ const s = segs[child.part];
200
+ if (s == null || s[1] === end || child.state === 'done')
201
+ continue;
202
+ if (local >= s[1] + half)
203
+ retire(mix, child, voice.timeAt(s[1] + half));
204
+ }
205
+ }
206
+ }
207
+ /** An arc's marks: when its enter ends, its hold ends and its exit begins and ends. */
208
+ export function arcMark(voice, start, mark) {
209
+ const arc = voice.arc;
210
+ const at = (t) => {
211
+ const u = voice.timeAt(t);
212
+ return Number.isFinite(u) ? Math.max(start, u) : undefined;
213
+ };
214
+ if (mark === 'in') {
215
+ const t = at(arc.enter);
216
+ return t === undefined ? t : Math.max(t, start + (voice.fade.in ?? 0));
217
+ }
218
+ const coast = at(arc.enter + voice.hold);
219
+ // Faded, or set to fade, before its hold ended: it left with no exit.
220
+ const out = voice.out;
221
+ const cut = out !== null ? out.at : Number.isFinite(voice.outAt) ? voice.outAt : undefined;
222
+ const cutShort = cut !== undefined && (coast === undefined || cut < coast);
223
+ if (mark === 'coast')
224
+ return cutShort ? undefined : coast;
225
+ if (mark === 'out') {
226
+ if (cutShort)
227
+ return cut;
228
+ return voice.state === 'done' && coast === undefined ? voice.doneAt : coast;
229
+ }
230
+ if (voice.state === 'done')
231
+ return voice.doneAt;
232
+ if (cutShort) {
233
+ if (out !== null)
234
+ return out.at + out.over;
235
+ const over = Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
236
+ return cut + over;
237
+ }
238
+ return at(arcEnd(voice));
239
+ }
package/dist/blend.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { stateful } from './hosts.js';
2
2
  import { reading } from './reading.js';
3
+ import { shareOf } from './signals.js';
3
4
  import { Store } from './store.js';
4
5
  export function blendOf(by, stops) {
5
6
  return {
@@ -29,11 +30,6 @@ export function ownBlends(voices) {
29
30
  of.members.push(v);
30
31
  }
31
32
  }
32
- /** A blend member's weight for its signal's read `k`: the members' stops sit evenly along 0..1. */
33
- function shareOf(k, i, stops) {
34
- const d = stops === 0 ? 0 : Math.abs(k * stops - i);
35
- return d >= 1 ? 0 : 1 - d;
36
- }
37
33
  /** A blend member's share of its signal's read for this subject this frame, made by the first to ask. */
38
34
  export function blended(voice, subject, held, slot) {
39
35
  const { of, i } = voice.blend;
package/dist/book.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface BookHost<I, O> {
22
22
  /** The booker canceled: forget it. */
23
23
  unhook(booker: Book<I, O>): void;
24
24
  }
25
+ /** Whether voice ms `at` falls in a pass of a patch `duration` long: 0 up to it, or 0 on for none. */
26
+ export declare function inPass(at: number, duration: number): boolean;
25
27
  /** Refuses hits a voice could never play. */
26
28
  export declare function checkHits(hits: readonly Hit[], duration: number): void;
27
29
  /** One `book` call: maps host time onto the outside clock and takes each item once. */
package/dist/book.js CHANGED
@@ -5,10 +5,14 @@ const SLEW = 0.05;
5
5
  /** A booking whose time moves by more than this is canceled and taken again. */
6
6
  const RETIME_MS = 1;
7
7
  const none = [];
8
+ /** Whether voice ms `at` falls in a pass of a patch `duration` long: 0 up to it, or 0 on for none. */
9
+ export function inPass(at, duration) {
10
+ return at >= 0 && (duration > 0 ? at < duration : Number.isFinite(at));
11
+ }
8
12
  /** Refuses hits a voice could never play. */
9
13
  export function checkHits(hits, duration) {
10
14
  for (const h of hits)
11
- if (!(h.at >= 0) || (duration > 0 ? !(h.at < duration) : !Number.isFinite(h.at)))
15
+ if (!inPass(h.at, duration))
12
16
  throw new RangeError(duration > 0
13
17
  ? `blits: a hit's at is voice ms into a pass, 0 up to ${duration}, not ${h.at}`
14
18
  : `blits: a hit's at is voice ms, 0 or more, not ${h.at}`);
@@ -198,6 +202,11 @@ export class Book {
198
202
  if (!(hi > t.seenTo))
199
203
  continue;
200
204
  const lo = Math.max(t.seenTo, voice.elapsedAt(loAt));
205
+ // Its clock runs back: hits come only as it runs forward.
206
+ if (!(lo <= hi)) {
207
+ t.seenTo = hi;
208
+ continue;
209
+ }
201
210
  const d = voice.duration;
202
211
  const last = d > 0 ? Math.min(voice.passes - 1, Math.floor(hi / d)) : 0;
203
212
  for (let pass = d > 0 ? Math.max(0, Math.floor(lo / d)) : 0; pass <= last; pass++) {
@@ -246,8 +255,23 @@ export class Book {
246
255
  return this.host.hostOf(t);
247
256
  }
248
257
  }
249
- const markIndex = { start: 1, in: 2, coast: 3, out: 4, end: 5 };
250
258
  /** A mark's identity: its voice and which of its marks, or the announcement. */
251
259
  function keyOf(m) {
252
- return m.order * 6 + (m.mark === undefined ? 0 : markIndex[m.mark]);
260
+ const k = m.order * 6;
261
+ switch (m.mark) {
262
+ case undefined:
263
+ return k;
264
+ case 'start':
265
+ return k + 1;
266
+ case 'in':
267
+ return k + 2;
268
+ case 'coast':
269
+ return k + 3;
270
+ case 'out':
271
+ return k + 4;
272
+ case 'end':
273
+ return k + 5;
274
+ default:
275
+ return `${m.order}\u0000${m.mark}`;
276
+ }
253
277
  }