@msb235/blits 0.14.0 → 0.15.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 (187) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +3 -3
  3. package/dist/ahead.d.ts +26 -0
  4. package/dist/ahead.js +264 -0
  5. package/dist/around.d.ts +11 -0
  6. package/dist/around.js +32 -0
  7. package/dist/bare.js +37 -14
  8. package/dist/blank.d.ts +10 -0
  9. package/dist/blank.js +26 -0
  10. package/dist/blend.d.ts +2 -1
  11. package/dist/blend.js +1 -6
  12. package/dist/bound.d.ts +70 -0
  13. package/dist/bound.js +351 -0
  14. package/dist/chain.d.ts +2 -1
  15. package/dist/chain.js +7 -5
  16. package/dist/changes.d.ts +24 -0
  17. package/dist/changes.js +189 -0
  18. package/dist/channels.d.ts +39 -13
  19. package/dist/channels.js +14 -5
  20. package/dist/color.d.ts +12 -10
  21. package/dist/color.js +25 -25
  22. package/dist/columns.d.ts +2 -1
  23. package/dist/columns.js +62 -38
  24. package/dist/contribution.d.ts +30 -0
  25. package/dist/contribution.js +374 -0
  26. package/dist/copyout.d.ts +10 -0
  27. package/dist/copyout.js +69 -24
  28. package/dist/crowd.d.ts +16 -6
  29. package/dist/crowd.js +20 -11
  30. package/dist/cue.d.ts +8 -1
  31. package/dist/cue.js +39 -13
  32. package/dist/cut.d.ts +8 -0
  33. package/dist/cut.js +4 -0
  34. package/dist/delay.d.ts +3 -0
  35. package/dist/delay.js +7 -0
  36. package/dist/drop.d.ts +9 -0
  37. package/dist/drop.js +84 -0
  38. package/dist/due.d.ts +24 -5
  39. package/dist/due.js +114 -34
  40. package/dist/easing.d.ts +6 -4
  41. package/dist/easing.js +7 -3
  42. package/dist/everyone.d.ts +8 -2
  43. package/dist/everyone.js +33 -3
  44. package/dist/fade.d.ts +2 -1
  45. package/dist/fade.js +2 -1
  46. package/dist/fill.d.ts +3 -2
  47. package/dist/fill.js +52 -20
  48. package/dist/fit.d.ts +44 -26
  49. package/dist/fit.js +10 -9
  50. package/dist/fold.d.ts +4 -37
  51. package/dist/fold.js +5 -513
  52. package/dist/gather.d.ts +12 -7
  53. package/dist/gather.js +179 -44
  54. package/dist/gone.js +2 -2
  55. package/dist/grid.d.ts +47 -0
  56. package/dist/grid.js +247 -0
  57. package/dist/handle.d.ts +2 -0
  58. package/dist/handle.js +14 -8
  59. package/dist/held.d.ts +2 -1
  60. package/dist/held.js +2 -1
  61. package/dist/history.d.ts +6 -5
  62. package/dist/history.js +7 -9
  63. package/dist/hosts.js +9 -15
  64. package/dist/index.d.ts +5 -4
  65. package/dist/keyfill.d.ts +1 -1
  66. package/dist/keyfill.js +5 -3
  67. package/dist/keyrows.d.ts +12 -0
  68. package/dist/keyrows.js +99 -12
  69. package/dist/lane.d.ts +38 -11
  70. package/dist/lane.js +46 -11
  71. package/dist/lanes.d.ts +30 -12
  72. package/dist/lanes.js +56 -36
  73. package/dist/leavings.d.ts +2 -2
  74. package/dist/leavings.js +1 -1
  75. package/dist/lives.d.ts +47 -0
  76. package/dist/lives.js +180 -0
  77. package/dist/meet.d.ts +2 -2
  78. package/dist/meet.js +95 -7
  79. package/dist/mixer.d.ts +56 -37
  80. package/dist/mixer.js +57 -171
  81. package/dist/motion.d.ts +14 -118
  82. package/dist/motion.js +16 -8
  83. package/dist/motions.d.ts +62 -155
  84. package/dist/motions.js +56 -370
  85. package/dist/moved.d.ts +1 -1
  86. package/dist/moved.js +16 -7
  87. package/dist/named.d.ts +6 -2
  88. package/dist/named.js +49 -3
  89. package/dist/numbers.d.ts +7 -0
  90. package/dist/numbers.js +33 -0
  91. package/dist/owed.d.ts +2 -0
  92. package/dist/owed.js +4 -0
  93. package/dist/pack.d.ts +2 -1
  94. package/dist/pack.js +2 -3
  95. package/dist/patch.d.ts +7 -3
  96. package/dist/patch.js +6 -2
  97. package/dist/places.d.ts +53 -0
  98. package/dist/places.js +171 -0
  99. package/dist/plot.d.ts +12 -0
  100. package/dist/plot.js +52 -0
  101. package/dist/project.d.ts +4 -4
  102. package/dist/project.js +9 -212
  103. package/dist/pull.js +22 -9
  104. package/dist/qualify.js +93 -31
  105. package/dist/reading.d.ts +6 -0
  106. package/dist/reading.js +5 -0
  107. package/dist/record.d.ts +5 -1
  108. package/dist/record.js +3 -2
  109. package/dist/relink.d.ts +1 -1
  110. package/dist/renumber.d.ts +10 -0
  111. package/dist/renumber.js +75 -0
  112. package/dist/restored.d.ts +2 -2
  113. package/dist/restored.js +1 -1
  114. package/dist/rests.d.ts +9 -0
  115. package/dist/rests.js +76 -0
  116. package/dist/revive.d.ts +3 -2
  117. package/dist/revive.js +2 -2
  118. package/dist/{locus.d.ts → role.d.ts} +10 -9
  119. package/dist/{locus.js → role.js} +3 -3
  120. package/dist/roles.d.ts +10 -0
  121. package/dist/roles.js +158 -0
  122. package/dist/roster.js +3 -3
  123. package/dist/rotation.d.ts +1 -1
  124. package/dist/rotation.js +1 -1
  125. package/dist/rows.js +28 -20
  126. package/dist/sample.d.ts +2 -2
  127. package/dist/sample.js +33 -7
  128. package/dist/seat.d.ts +17 -0
  129. package/dist/seat.js +25 -0
  130. package/dist/seek.js +5 -5
  131. package/dist/setting.d.ts +27 -0
  132. package/dist/setting.js +55 -0
  133. package/dist/shared.d.ts +1 -1
  134. package/dist/shared.js +1 -1
  135. package/dist/signals.d.ts +64 -32
  136. package/dist/signals.js +20 -18
  137. package/dist/stack.d.ts +5 -5
  138. package/dist/stack.js +5 -5
  139. package/dist/standing.d.ts +2 -1
  140. package/dist/standing.js +3 -2
  141. package/dist/store.d.ts +9 -0
  142. package/dist/store.js +24 -0
  143. package/dist/strays.d.ts +2 -1
  144. package/dist/strays.js +23 -15
  145. package/dist/stretch.d.ts +112 -0
  146. package/dist/stretch.js +9 -0
  147. package/dist/subject.d.ts +142 -0
  148. package/dist/subject.js +1 -0
  149. package/dist/tape.d.ts +9 -5
  150. package/dist/tape.js +5 -1
  151. package/dist/then.d.ts +9 -0
  152. package/dist/then.js +57 -0
  153. package/dist/ticker.d.ts +4 -4
  154. package/dist/trace.d.ts +4 -0
  155. package/dist/trace.js +255 -0
  156. package/dist/transport.d.ts +0 -8
  157. package/dist/transport.js +0 -4
  158. package/dist/turns.d.ts +4 -3
  159. package/dist/turns.js +4 -3
  160. package/dist/types/arc.d.ts +12 -11
  161. package/dist/types/channel.d.ts +12 -8
  162. package/dist/types/history.d.ts +35 -19
  163. package/dist/types/mix.d.ts +237 -163
  164. package/dist/types/motion.d.ts +151 -0
  165. package/dist/types/motion.js +1 -0
  166. package/dist/types/patch.d.ts +67 -34
  167. package/dist/types/place.d.ts +51 -36
  168. package/dist/types/trace.d.ts +51 -0
  169. package/dist/types/trace.js +1 -0
  170. package/dist/types/transport.d.ts +20 -18
  171. package/dist/types/voice.d.ts +144 -73
  172. package/dist/types/watch.d.ts +88 -40
  173. package/dist/types.d.ts +2 -0
  174. package/dist/unreached.d.ts +2 -0
  175. package/dist/unreached.js +7 -0
  176. package/dist/voice.d.ts +10 -143
  177. package/dist/voice.js +37 -82
  178. package/dist/warn.d.ts +2 -0
  179. package/dist/warn.js +1 -1
  180. package/dist/watch.d.ts +2 -2
  181. package/dist/watch.js +5 -3
  182. package/dist/wave.d.ts +4 -3
  183. package/dist/wave.js +3 -2
  184. package/dist/weigh.d.ts +7 -1
  185. package/dist/weigh.js +27 -0
  186. package/docs/schema.html +223 -34
  187. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,204 @@ 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.15.1
8
+
9
+ ### Added
10
+
11
+ - `mix.trace(from, to, { grid })` reads the mix over a window of mix time without moving it, and
12
+ `trace.of(subject, channel)` gives a channel of one number as a `Run`: points joined by straight
13
+ lines, two points at one time for a jump, and `exact` where the lines are the mix's own values
14
+ throughout. Where every voice feeding the channel is a `keys` voice of known shape (plain number
15
+ stops eased `linear`, a number weight, a steady rate, no fade under way), the points are its own
16
+ breakpoints; elsewhere the run is read every `grid` ms, default 5. One trace shares its reads
17
+ across subjects. `Run`, `Trace`, `TraceOptions` and `NumberChannel` are exported.
18
+
19
+ ### Changed
20
+
21
+ - A voice cued with `subjects` asks `stagger` for each of them at `cue`, not on first sight, and a
22
+ finite loop ends once its latest-staggered subject is done whether or not the host has read that
23
+ subject yet. A read ahead now sees the same end as the live mix. A voice reaching its subjects by
24
+ `target` still ends by the subjects it has met.
25
+
26
+ ### Fixed
27
+
28
+ - One-subject tween voices cost about a fifth less a frame once they have landed: a fill folds a
29
+ landed tween's end as it stands, without reading its clock or easing it, until its voice, its
30
+ stretch or its subject's delay changes or the clock reads earlier than when it landed. 10,000 of
31
+ them read by `pull` (`tweens^`) take 0.235 ms a frame against 0.296, `churn^` 0.301 against
32
+ 0.356 and `turnover^` 0.419 against 0.458 (studio, medians of seven runs of 5,000 frames, each in
33
+ its own process); one tween voice over 10,000 subjects (`weasel^`) is unchanged.
34
+ - A crowd of one-subject `keys` or `fn` voices applies its voices' freezes in the fill's own loop
35
+ rather than through a call a row. 10,000 `keys` voices read by `pull` (`keyses^`) take 0.449 ms
36
+ a frame against 0.479, and allocate 6 kB a frame against 162 (studio, medians of seven runs of
37
+ 1,500 frames, each in its own process); the same voices freezing at both ends (`frozen`,
38
+ `frozen^`) are unchanged.
39
+ - `pull` over a list whose order has drifted from the order the mix first saw it in, as a list
40
+ kept dense by swap-remove does, writes rests in a few runs, not one per short stretch of
41
+ subjects. With one subject leaving and one arriving every frame (`turnover^`), a frame takes
42
+ 0.253 ms against 0.262. Teitou, medians of seven runs of 5,000 frames, each in its own process.
43
+ Other rows are unchanged.
44
+ - A list kept dense by swap-remove stays in the mix's own order for `pull`: a subject moved into
45
+ the place a dropped one left takes the number the drop freed, so `pull` reads the list in long
46
+ runs. `turnover^` takes 0.223 ms a frame against 0.263, close to the 0.210 of `churn^`, which
47
+ replaces voices without reordering anything (teitou with the node to itself, medians of seven
48
+ runs). The other rows are unchanged. A mix that keeps history, or has roles or a bound on its
49
+ lanes, keeps the old order.
50
+
51
+ ## 0.15.0
52
+
53
+ ### Breaking
54
+
55
+ - `Setting`'s fields are `readonly`, so a patch or signal that assigns to
56
+ `setting.weight`, `dt` or any other field no longer type-checks. The mix reuses one setting per
57
+ voice across subjects, so such a write already reached whatever it called next. A patch's
58
+ `state` is still its own to change in place.
59
+
60
+ ### Changed
61
+
62
+ - A locus is now a **role**, `VoiceSpec.role`: voices in one role are alternatives, so whichever
63
+ plays it, the subject shows one performance, not two stacked. `locus` is still taken as `role`
64
+ for one release, deprecated, and the mix warns once outside production; a voice given both with
65
+ different values is refused. The mix option for tags that act as roles is `roles`, and `loci` is
66
+ taken the same way. Errors and warnings say role.
67
+
68
+ ### Added
69
+
70
+ - `within` on a cue: a circle `{ x, y, r }` (the type `Circle`) the host moves, outside which the
71
+ voice's weight signal is 0, so the mix gives those subjects nothing without calling the signal for
72
+ them. The host gives the subjects' positions with `mix.plot(subjects, xy)`, two numbers a subject
73
+ read in place, and calls `mix.moved()` after writing new ones. Positions hold an object subject
74
+ weakly, and `drop` takes a subject's away. Outside production the mix warns once if the signal is
75
+ above 0 just outside the circle, which would cut it off there. A voice naming its `subjects` and
76
+ a role member, an owner's child, a motion, a patch keeping state and a `from: 'current'` voice
77
+ take it; a patch that steps still steps outside the circle, and a `from: 'current'` voice still
78
+ takes its base there, as each does at weight 0. Under history a bound reads as its signal does:
79
+ live on a read back, or with `inputs`, as the signal's recorded reads, a 0 recorded for each
80
+ subject read outside.
81
+ - `within` also takes a finder of the host's own, `{ find(out) }` (the type `Finder`), over any axes
82
+ or none: once a frame `find` writes into `out`, an `Int32Array` the mix lends it, the indices of
83
+ the subjects the signal can be above 0 for, and returns how many. A circle and a finder share one
84
+ path through the mix.
85
+ - `mix.place(subject)` gives a subject its index, which a finder names it by: the one it has, or a
86
+ new one. It keeps it until `drop`, or for an object until it is collected, and the index then goes
87
+ to the next subject placed. `plot` hands out indices too, in its list's order. With no `plot`,
88
+ `put` writes into positions the mix keeps itself, and a placed subject has none until then.
89
+ - `mix.around(circle, { ease })`, a weight signal over the plotted positions: 1 at the circle's
90
+ center, falling to 0 at `r` along an easing, linear by default. A voice it weighs is bounded to
91
+ the circle as if cued `within` it, with no check for a clipped signal, from the first `plot` if it
92
+ was cued before one.
93
+ - `mix.put(subject, x, y)` moves one subject, for a host where few move at once. It writes into the
94
+ array `plot` was given, so it mixes freely with writing that array and calling `moved`, or with no
95
+ `plot`, into the mix's own, and keeps the grid as built until a sixteenth of the subjects have left
96
+ their cells. It throws for a subject with no index, or one past the array `plot` was given.
97
+ - A watched mix's `voice(handle)` gives `within`, the circle a voice is bounded to by `within` or
98
+ an `around` weight, or its finder, the host's own object, or undefined for none. blits-panel draws
99
+ circles only.
100
+ - `color({ lerp: 'oklch' })` and `color(last(), { lerp: 'oklch' })` run on lanes, where they took
101
+ the general path. The pose is the same to the bit.
102
+ - `mix.pose()` makes a new empty pose for `probe`'s `out`, holding each channel with a rest in kit
103
+ order. Made once and probed into every frame, its numbers are written without allocating, where
104
+ an `out` made as `{}` can box each one: about 310 kB a frame less on a 10,000-subject scene.
105
+ - Named, exported types for option bags and return values that were written inline, so each can
106
+ be read on its own: `LastOptions`, `NumberTuple`, `Layout`, `OverrunOptions`, `MotionOptions`,
107
+ `SpringOptions`, `GlideOptions`, `TweenOptions`, `Spring`, `Glide`, `Tween`, `SpringSpec`,
108
+ `GlideSpec`, `TweenSpec`, `Input`, `Level`, `SlewOptions`, `LagOptions`, `Band`,
109
+ `AnnounceOptions`, `TapeOptions`, `Taken`, `VoiceFade`, `SubjectFade`, `RegistryChange`,
110
+ `WatchedHistory`, `WatchedVoice`, `Probed`, and `Pulled`. `Moving` and `Signal` are interfaces now,
111
+ of the same shape.
112
+
113
+ ### Fixed
114
+
115
+ - A host writing into a pose `probe` handed back without `out` no longer changes where a later
116
+ `from: 'current'` voice starts, or the speed it starts at: the mix keeps a copy of that pose, not
117
+ the object itself.
118
+ - Probes of subjects on lanes cost 4 to 10% less a frame: the copy of laned values into the pose
119
+ stores each channel under its own name, written out once per set of laned channels, where one
120
+ store by a varying name went through V8's generic path. A voice over 10,000 subjects replaced
121
+ every frame (`swap`) takes 1.632 ms a frame against 1.739, its keys form 1.165 against 1.259, and
122
+ a probe then `atRest` of each subject (`probed`) 1.333 against 1.487 (teitou, medians of nine
123
+ runs, each in its own process). A pose the mix makes itself, for a probe without `out`, `atRest`
124
+ or `pull`, is now a plain object of the mix's own shape holding each channel with a rest in kit
125
+ order, so its numbers are stored unboxed. Under a content security policy that refuses
126
+ `Function`, the copy is the loop it was, and the mix asks only once, so a page sees one
127
+ violation at most: each refused `Function` fires one, caught or not.
128
+ - A weight signal on lanes is handed, after a stretch of frames at weight 0, the `dt` since its
129
+ last read, as it is off lanes. It was handed the gap since the subject last had a delta.
130
+ - Tweens read by `pull` cost about a twentieth less a frame: a probe holds its subject for the
131
+ next fill only while fills look subjects up, which bare tweens never do, and a motion voice's
132
+ fill reads each subject's flags once and weighs a shared fade once. 10,000 one-subject tween
133
+ voices (`tweens^`) take 0.285 ms a frame against 0.300, and one tween voice over 10,000 subjects
134
+ (`weasel^`) 0.369 against 0.391 (keiei, medians of seven runs of 5,000 frames, each in its own
135
+ process); other rows are unchanged.
136
+ - 10,000 one-subject tween voices read by `pull` (`tweens^`) cost about 7% less a frame, 0.163 ms
137
+ against 0.175: a crowd row's fill checks once, not every frame, that its stretch is numbered and
138
+ its subject met (teitou, medians of nine runs of 5,000 frames, each in its own process).
139
+ - Starting voices allocates about 400 B less each: a voice's `subjects`, given as an array of
140
+ distinct subjects, is copied without being iterated, and voices of a patch writing one channel
141
+ share their channel slots. 10,000 one-subject voices' first frame takes 20.7 ms against 23.5 for
142
+ tweens and 8.9 against 10.8 for fns (teitou, medians of nine rounds; `bench/start.mjs`).
143
+ - A mix with no tape no longer makes the closures that record the host's calls only to drop them:
144
+ a cue, a `drop`, a motion change and every handle write each made one. A voice cued and replaced
145
+ leaves 11.7 KB of garbage against 12.4 (keiei, medians of three alternated passes of
146
+ `bench/churnwho.mjs 5000`); frame cost is level.
147
+ - A stateless fn voice over every subject cued between frames costs about a quarter less on the
148
+ frame its subjects first meet it: their probe makes its first call on its lane and folds it into
149
+ the lanes' values, where it was folded on the general path. A voice over 10,000 subjects
150
+ replaced every frame among a tween voice per subject (`swap`) takes 2.67 ms a frame against
151
+ 3.67, and allocates 2.0 MB a frame against 2.8 (msb-uai, medians of seven runs of 3,000 frames,
152
+ each in its own process).
153
+ - That frame costs about a sixth less again where every subject was probed the frame before: the
154
+ voice's lane takes every subject when it opens, and the fill makes each one's first call, where
155
+ each probe met the lane and made it. `swap` takes 2.19 ms a frame against 2.64 (msb-uai, medians
156
+ of seven runs of 3,000 frames, each in its own process); other rows are unchanged.
157
+ - A `keys` voice over every subject cued between frames gets both: a probe meeting its lane reads
158
+ it there, and where every subject was probed the frame before, its lane takes them all when it
159
+ opens. A keys voice over 10,000 subjects replaced every frame among a tween voice per subject
160
+ (`swapkeys`) takes 1.21 ms a frame against 2.26 (teitou, medians of seven runs of 3,000 frames,
161
+ each in its own process); other rows are unchanged.
162
+ - A `keys` voice per subject read by `pull` costs about a sixth less a frame: rows whose stops
163
+ share their phases and easings, at the same phase, find their segment and ease it once rather
164
+ than once a row. 10,000 of them (`keyses^`) take 0.270 ms a frame against 0.327, and allocate
165
+ half as much (teitou, medians of nine runs of 5,000 frames, each in its own process). A lane fill
166
+ also reads a subject's last-read stamp only where the weight it keeps for `weightOf` could
167
+ change, which takes `weasel^` from 0.204 to 0.198 ms; other rows are unchanged.
168
+ - A voice weighted by a signal costs a little less a frame on lanes: its setting's `timestamp`,
169
+ `elapsed` and `pass` are filled once for the subjects that share them, not once a subject. A
170
+ pointer glow over 10,000 subjects read by `pull` (`glow^`) takes 1.657 ms a frame against 1.708,
171
+ and three signal-weighted voices over 10,000 (`signal`) 4.511 against 4.641 (keiei, medians of
172
+ seven runs of 5,000 frames, each in its own process); other rows are within their spread.
173
+ - A role, and so a `mix.blend`, costs a further tenth less a frame on lanes where no other voice
174
+ falls between its members in voice order: each subject's members are gathered and folded
175
+ together, at the first member's turn, rather than gathered for every subject and folded in a
176
+ second pass. Three voices in a role over 10,000 subjects (`role`) take 2.555 ms a frame against
177
+ 2.839, and a blend of three (`blend`) 2.841 against 3.055 (teitou, medians of nine runs of 5,000
178
+ frames, each in its own process); three plain voices (`fn`) and three signal-weighted ones
179
+ (`signal`) are unchanged.
180
+ - A role, and so a `mix.blend`, costs about a tenth less a frame on lanes, and allocates no more
181
+ than its members would apart: a member's numbers reach the role without being boxed, 16 bytes
182
+ each a channel a subject. Three voices in a role over 10,000 subjects (`role`) take 4.349 ms a
183
+ frame against 4.810 and allocate 2.4 MB against 4.0, and a blend of three (`blend`) 4.633 against
184
+ 5.128 (keiei, medians of seven runs of 5,000 frames, each in its own process).
185
+ - `project` back under `history` reads a motion voice's subject as it showed. A subject retargeted
186
+ and then faded out of the voice or dropped read back to before it left as though met afresh from
187
+ `from`, and a retarget, push or `set` given a time earlier than the frame it was made in read back
188
+ as in force before that frame. A seek already read both right. A read back no longer numbers a
189
+ subject in the live patch either.
190
+ - A read ahead across many known events of a stateful mix costs a quarter to a third less a frame
191
+ and allocates half as much: each stop hands a subject's record on to the next instead of copying
192
+ it, and a voice starting many stops on asks only the copies that may hold a record for it.
193
+ Reading 5 s ahead past the starts, fade-ins and ends of seven voices over 1,000 subjects (`far`)
194
+ takes 15.1 ms a frame against 20.5 and allocates 25 MB against 53, and over 10,000 subjects 201
195
+ ms against 305; seeking back over the calls that cued them and reading ahead to where the mix
196
+ was (`redo`), 15.5 ms against 21.0 (teitou, medians of five alternated runs of 100 frames against
197
+ `be0aaa4`). With 31 voices starting 150 ms apart, the record handed on reads 154 ms against 182,
198
+ and looking past the copies 109 against 151 (against `03af146`).
199
+ - A voice stopped before its end is let go of when it stops. The mix's schedule kept an entry for
200
+ the time each voice would next change, and one stopped early stayed in it, holding the voice,
201
+ until that time came: 20,000 `patch` voices of 10⁹ ms replaced over 1,000 subjects held 47 MB of
202
+ heap, and now 2.4 (keiei, one run each, `bench/leak.mjs`). A motion patch was never held, as it
203
+ schedules no end.
204
+
7
205
  ## 0.14.0
8
206
 
9
207
  ### Added
package/README.md CHANGED
@@ -151,7 +151,7 @@ do yet.
151
151
 
152
152
  ## Status
153
153
 
154
- v0.10.0, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
154
+ v0.15.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
155
155
  transitions, weasel its animator's tweens, sherpa its seams, and
156
156
  [klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
157
157
  part effects, and the environment a sign is lit by — on a mix each.
@@ -160,8 +160,8 @@ here is the arithmetic a working renderer already had: klieg's suite of 1974 cas
160
160
  screenshot baselines came through it unchanged.
161
161
 
162
162
  One thing the port sent back. klieg's three motion phases layer, so they cue as separate voices; a
163
- `locus` folds its members through each channel's `lerp` instead, which is what a crossfade between
164
- alternatives wants and is not the same number on a multiplicative channel. Reach for a locus when
163
+ `role` folds its members through each channel's `lerp` instead, which is what a crossfade between
164
+ alternatives wants and is not the same number on a multiplicative channel. Reach for a role when
165
165
  one voice should replace another, not when both should be heard.
166
166
 
167
167
  Zero runtime dependencies, ESM only, types included.
@@ -0,0 +1,26 @@
1
+ import { Mixer } from './mixer.js';
2
+ import type { Subject } from './subject.js';
3
+ import { Transport } from './transport.js';
4
+ import type { Voice } from './voice.js';
5
+ /** A live mix's copies at each known event a read ahead stopped at, and the subjects read there. */
6
+ export interface Stages {
7
+ at: Mixer<unknown, unknown>[];
8
+ met: Set<unknown>;
9
+ }
10
+ /**
11
+ * Moves a read ahead's copies to mix time `t`, making the calls the tape holds on the way as a seek
12
+ * there makes them on the mixes. While a voice could carry state across it, it stops at each known
13
+ * event: a call, and every mark of a voice or announced. At each stop the copies are kept for
14
+ * subjects to be read there and copied on, so `copies` ends holding those at `t`. Answers the
15
+ * transport they stand on, and puts the tape back where it stood.
16
+ */
17
+ export declare function playAhead(transport: Transport, c: Transport, copies: Map<Mixer<unknown, unknown>, Mixer<unknown, unknown>>, t: number, earlier: Map<Mixer<unknown, unknown>, Stages>): Transport;
18
+ /**
19
+ * A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one,
20
+ * or from the copy at the known event before.
21
+ */
22
+ export declare function copyAhead<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>): void;
23
+ /** Gives a projection's copy what it reads of the live mix as it is, before its voices are copied. */
24
+ export declare function mirror<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>): void;
25
+ /** A voice's record of a subject, the one it shares among those it does not reach included. */
26
+ export declare function recordOf<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I): Subject<unknown> | undefined;
package/dist/ahead.js ADDED
@@ -0,0 +1,264 @@
1
+ import { ownBlends } from './blend.js';
2
+ import { headOf } from './chain.js';
3
+ import { open, sighted, weightAt } from './everyone.js';
4
+ import { copyHeld, unread } from './history.js';
5
+ import { markOf, voiceMarks } from './marks.js';
6
+ import { Mixer } from './mixer.js';
7
+ import { move } from './move.js';
8
+ import { relink } from './owner.js';
9
+ import { pin, place } from './place.js';
10
+ import { recount } from './roster.js';
11
+ import { hostAhead, replay } from './tape.js';
12
+ import { Transport } from './transport.js';
13
+ import { unreached } from './unreached.js';
14
+ /**
15
+ * Moves a read ahead's copies to mix time `t`, making the calls the tape holds on the way as a seek
16
+ * there makes them on the mixes. While a voice could carry state across it, it stops at each known
17
+ * event: a call, and every mark of a voice or announced. At each stop the copies are kept for
18
+ * subjects to be read there and copied on, so `copies` ends holding those at `t`. Answers the
19
+ * transport they stand on, and puts the tape back where it stood.
20
+ */
21
+ export function playAhead(transport, c, copies, t, earlier) {
22
+ const tape = transport.tape;
23
+ const depth = tape?.undoDepth() ?? 0;
24
+ let played = false;
25
+ try {
26
+ for (;;) {
27
+ const e = nextEvent(tape, c, t);
28
+ if (!(e < t))
29
+ break;
30
+ played = playTo(transport, c, copies, e) || played;
31
+ if (!carries(c))
32
+ continue;
33
+ // Placed now, as the next move would place them, while a target that has just left is here.
34
+ for (const m of c.members)
35
+ if (m.anchored > 0)
36
+ place(m);
37
+ const next = restage(c);
38
+ for (const [mix, copy] of copies) {
39
+ const on = new Mixer(copy.kit, { ...copy.opts, transport: next }, null);
40
+ on.slot = copy.slot;
41
+ on.projecting = true;
42
+ copyAhead(copy, on);
43
+ const stages = earlier.get(mix);
44
+ if (stages === undefined)
45
+ earlier.set(mix, { at: [copy], met: new Set() });
46
+ else
47
+ stages.at.push(copy);
48
+ copies.set(mix, on);
49
+ }
50
+ c = next;
51
+ }
52
+ played = playTo(transport, c, copies, t) || played;
53
+ }
54
+ finally {
55
+ if (played)
56
+ tape?.goto(depth);
57
+ }
58
+ return c;
59
+ }
60
+ /**
61
+ * Whether a voice on a read ahead's copies could carry state from one known event to the next: a
62
+ * patch that steps, or a weight given by a signal, which may keep state. Where none could, a read
63
+ * crosses the events in one go, as fast as one with none.
64
+ */
65
+ function carries(c) {
66
+ for (const m of c.members)
67
+ for (const v of m.cued)
68
+ if (v.state !== 'done' && (v.patch.step !== undefined || typeof v.spec.weight === 'function'))
69
+ return true;
70
+ return false;
71
+ }
72
+ /**
73
+ * The earliest known event a read ahead's copies have yet to reach before `t`, mix time: the next
74
+ * call the tape holds, and while a voice could carry state across one, a mark of a voice or one
75
+ * announced. Infinity where there is none.
76
+ */
77
+ function nextEvent(tape, c, t) {
78
+ const now = c.now;
79
+ let e = Number.POSITIVE_INFINITY;
80
+ const call = tape?.timestampAt(tape.undoDepth());
81
+ if (call !== undefined)
82
+ e = c.pace === null ? call : c.pace.reading(call);
83
+ if (!carries(c))
84
+ return e < t ? e : Number.POSITIVE_INFINITY;
85
+ for (const m of c.members)
86
+ for (const v of m.cued) {
87
+ if (v.state === 'done')
88
+ continue;
89
+ for (const mark of voiceMarks) {
90
+ const at = markOf(m, v, mark);
91
+ if (at !== undefined && at > now && at < e)
92
+ e = at;
93
+ }
94
+ const inner = v.spec.marks;
95
+ if (inner !== undefined)
96
+ for (const mark of Object.keys(inner)) {
97
+ const at = markOf(m, v, mark);
98
+ if (at !== undefined && at > now && at < e)
99
+ e = at;
100
+ }
101
+ }
102
+ for (const a of c.announced) {
103
+ const at = c.pace === null ? a.at : c.pace.at(a.at, c.u);
104
+ if (at > now && at < e)
105
+ e = at;
106
+ }
107
+ return e < t ? e : Number.POSITIVE_INFINITY;
108
+ }
109
+ /**
110
+ * Makes the calls the tape holds up to mix time `to` on a read ahead's copies and moves them
111
+ * there. Answers whether it moved the tape.
112
+ */
113
+ function playTo(transport, c, copies, to) {
114
+ const tape = transport.tape;
115
+ const due = (at) => (c.pace === null ? at : c.pace.reading(at)) <= to;
116
+ const next = tape?.timestampAt(tape.undoDepth());
117
+ c.u = hostAhead(c, to);
118
+ let played = false;
119
+ if (next !== undefined && due(next)) {
120
+ // A call may write what a copy shares with the voice it was copied from.
121
+ for (const copy of c.members)
122
+ for (const v of copy.cued) {
123
+ if (v.parts !== null)
124
+ v.parts = new Map(v.parts);
125
+ if (v.parted !== null)
126
+ v.parted = new Map(v.parted);
127
+ }
128
+ played = true;
129
+ const last = replay(transport, due, c, {
130
+ transport: () => c,
131
+ mix: (of) => copies.get(of),
132
+ });
133
+ c.u = Math.max(last, hostAhead(c, to));
134
+ }
135
+ // A call played at `to` itself moved the copies there before it, as a sync does.
136
+ if (!(c.now >= to))
137
+ for (const copy of c.members)
138
+ move(copy, to);
139
+ return played;
140
+ }
141
+ /** A transport for a read ahead's copies to go on from where those on `c` stand. */
142
+ function restage(c) {
143
+ const next = new Transport(undefined, true);
144
+ next.offset = c.offset;
145
+ next.u = c.u;
146
+ next.nextId = c.nextId;
147
+ next.now = c.now;
148
+ next.pace = c.pace === null ? null : c.pace.until(Number.POSITIVE_INFINITY);
149
+ next.announced = c.announced.map((a) => ({ ...a }));
150
+ // The retargets and pushes the tape made carry their times, so every stop shares one list.
151
+ next.ahead = c.ahead;
152
+ return next;
153
+ }
154
+ /**
155
+ * A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one,
156
+ * or from the copy at the known event before.
157
+ */
158
+ export function copyAhead(mix, c) {
159
+ mirror(mix, c);
160
+ c.cued = mix.cued
161
+ .filter((v) => v.state !== 'done')
162
+ .map((v) => {
163
+ const link = {
164
+ mix,
165
+ voice: v,
166
+ up: links.get(v),
167
+ seen: 0,
168
+ shared: false,
169
+ filled: false,
170
+ };
171
+ const copy = v.copy((subject) => carry(link, subject, c, copy), undefined, true);
172
+ link.seen = copy.seen;
173
+ link.shared = copy.sharing;
174
+ links.set(copy, link);
175
+ const at = mix.pins?.get(v);
176
+ if (at !== undefined)
177
+ pin(c, copy, at);
178
+ return copy;
179
+ });
180
+ c.waking = mix.waking;
181
+ ownBlends(c.cued);
182
+ // A copy keeps no list of owners, so the one before says only by its voices.
183
+ if (mix.owners !== null || (mix.projecting && c.cued.some((v) => v.holding !== null)))
184
+ relink(c.cued);
185
+ recount(c);
186
+ }
187
+ /** Gives a projection's copy what it reads of the live mix as it is, before its voices are copied. */
188
+ export function mirror(mix, c) {
189
+ c.lastPoses = mix.lastPoses;
190
+ c.wantsPose = mix.wantsPose;
191
+ c.reducedNow = mix.reducedNow;
192
+ }
193
+ /** A voice's record of a subject, the one it shares among those it does not reach included. */
194
+ export function recordOf(mix, voice, subject) {
195
+ // A voice sharing one record has that for every subject a chain has asked it about, pointed at
196
+ // the subject for the caller to copy from.
197
+ if (voice.sharing) {
198
+ const slot = headOf(mix, subject)?.slot ?? -1;
199
+ if (!sighted(voice, slot))
200
+ return undefined;
201
+ const all = voice.everyone;
202
+ open(voice, all, slot);
203
+ all.weight = weightAt(voice, slot);
204
+ return all;
205
+ }
206
+ const held = voice.subjects.get(subject);
207
+ if (held !== undefined || voice.unreachedBits === null)
208
+ return held;
209
+ return unreached(voice, headOf(mix, subject)?.slot ?? -1)
210
+ ? voice.unreached
211
+ : undefined;
212
+ }
213
+ /** Each read ahead copy's link, by the copy. */
214
+ const links = new WeakMap();
215
+ /**
216
+ * Whether a read ahead's copy of a voice holds a record only where the copy it was made from does:
217
+ * it has met no subject, taken no record, shares none, and reaches none it was asked about.
218
+ */
219
+ function empty(voice, link) {
220
+ return (voice.seen === link.seen &&
221
+ !link.filled &&
222
+ !link.shared &&
223
+ voice.unreached === null &&
224
+ voice.unreachedBits === null);
225
+ }
226
+ /**
227
+ * Whether a subject's record on a read ahead's copy at a stop can go on to the copy at the next
228
+ * stop as it is: each stop reads a subject once, before the next carries it on, and nothing reads
229
+ * the record there again. Not one a voice shares among subjects, or one the live mix holds.
230
+ */
231
+ function handable(mix, voice, h) {
232
+ return mix.projecting && h.reaches && !voice.sharing && h !== voice.unreached;
233
+ }
234
+ /** A record handed on as `copyHeld` would copy it, its place in the stop's chain let go. */
235
+ function handOn(h) {
236
+ h.restored = null;
237
+ return unread(h);
238
+ }
239
+ /**
240
+ * A projection ahead starts each subject from where the live mix holds it, its `since` worked out
241
+ * again where it was still ahead, since the copy's clock may have moved it.
242
+ */
243
+ function carry(link, subject, c, copy) {
244
+ const mix = link.mix;
245
+ const voice = link.voice;
246
+ // A voice starting many stops on would otherwise ask every copy before it, one by one. Where
247
+ // the first copy that may hold one has none, each copy after it has none, dropped or not.
248
+ let from = link;
249
+ while (from.up !== undefined && empty(from.voice, from.up))
250
+ from = from.up;
251
+ if (from !== link && recordOf(from.mix, from.voice, subject) === undefined)
252
+ return undefined;
253
+ const live = recordOf(mix, voice, subject);
254
+ if (live === undefined)
255
+ return undefined;
256
+ const h = handable(mix, voice, live) ? handOn(live) : copyHeld(voice, live);
257
+ if (h.reaches && !(h.since <= mix.now)) {
258
+ h.since = c.sinceOf(copy, h.delay);
259
+ h.shown = c.shownOf(copy, h.since);
260
+ }
261
+ h.from = h.stepped;
262
+ link.filled = true;
263
+ return h;
264
+ }
@@ -0,0 +1,11 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Circle, Easing, Signal } from './types.js';
3
+ /** The circle a weight carries: an `around` signal's own, or none. */
4
+ export declare const carried: (weight: unknown) => Circle | undefined;
5
+ /**
6
+ * `Mix.around`: 1 at the circle's center, falling with distance over the positions `plot` gave to 0
7
+ * at `r` and beyond, along `ease` read from the edge (0) to the center (1).
8
+ */
9
+ export declare function around<I, O>(this: Mixer<I, O>, circle: Circle, opts?: {
10
+ ease?: Easing;
11
+ }): Signal<I>;
package/dist/around.js ADDED
@@ -0,0 +1,32 @@
1
+ import { curve } from './easing.js';
2
+ /** Each `around` signal's circle, which a voice it weighs is bounded to as if cued `within` it. */
3
+ const circles = new WeakMap();
4
+ /** The circle a weight carries: an `around` signal's own, or none. */
5
+ export const carried = (weight) => typeof weight === 'function' ? circles.get(weight) : undefined;
6
+ /**
7
+ * `Mix.around`: 1 at the circle's center, falling with distance over the positions `plot` gave to 0
8
+ * at `r` and beyond, along `ease` read from the edge (0) to the center (1).
9
+ */
10
+ export function around(circle, opts = {}) {
11
+ if (typeof circle !== 'object' || circle === null)
12
+ throw new TypeError('blits: around takes a circle, { x, y, r }');
13
+ const shape = curve(opts.ease ?? 'linear');
14
+ const read = (subject) => {
15
+ const places = this.places;
16
+ if (places === null)
17
+ return 0;
18
+ const i = places.cur >= 0 ? places.cur : (places.at.get(subject) ?? -1);
19
+ const xy = places.xy;
20
+ if (i < 0 || 2 * i + 1 >= xy.length)
21
+ return 0;
22
+ const r = circle.r;
23
+ const dx = xy[2 * i] - circle.x;
24
+ const dy = xy[2 * i + 1] - circle.y;
25
+ const d2 = dx * dx + dy * dy;
26
+ if (!(r > 0) || !(d2 <= r * r))
27
+ return 0;
28
+ return shape(1 - Math.sqrt(d2) / r);
29
+ };
30
+ circles.set(read, circle);
31
+ return read;
32
+ }
package/dist/bare.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { foldNumber } from './channels.js';
2
2
  import { bare } from './crowd.js';
3
+ import { keepProbed } from './lane.js';
3
4
  import { between, left, progress } from './tweened.js';
4
5
  /** A `Flag.FAST` row's voice time at `now`, before its delay. */
5
6
  export function clock(hot, h, now) {
@@ -26,12 +27,16 @@ export function bareRows(lanes, c, p, id) {
26
27
  const lately = lanes.lately;
27
28
  const now = lanes.now;
28
29
  const op = ch.op;
30
+ // Ids are finite, so a fill running to the end need not read each row's.
31
+ const bounded = id !== Number.POSITIVE_INFINITY;
29
32
  for (; p < list.length; p++) {
30
33
  const h = p * H;
31
- if (hot[h + 7 /* Hot.ID */] >= id || !bare(hot[h + 0 /* Hot.FLAGS */]))
34
+ const f = hot[h + 0 /* Hot.FLAGS */];
35
+ if ((bounded && hot[h + 7 /* Hot.ID */] >= id) || !bare(f))
32
36
  break;
33
37
  const o = p * 8 /* Row.STRIDE */;
34
- if (data[o + 4 /* Row.MSLOT */] < 0 || data[o + 7 /* Row.MET */] === 0)
38
+ const ready = (f & 2048 /* Flag.READY */) !== 0;
39
+ if (!ready && (data[o + 4 /* Row.MSLOT */] < 0 || data[o + 7 /* Row.MET */] === 0))
35
40
  break;
36
41
  const slot = list[p];
37
42
  const q = slot * 6 /* Per.SLOT */;
@@ -43,24 +48,41 @@ export function bareRows(lanes, c, p, id) {
43
48
  !(per[q + 3 /* Per.LANE_PROBE */] > lately ||
44
49
  per[q + 4 /* Per.GENERAL_PROBE */] > lately))
45
50
  break;
46
- const elapsed = clock(hot, h, now) - data[o + 0 /* Row.DELAY */];
47
- if (!(elapsed >= 0))
48
- break;
49
- if (per[q + 5 /* Per.LANE_FILL */] === fills - 1)
50
- data[o + 3 /* Row.PROBED */] = data[o + 2 /* Row.WEIGHT */];
51
+ // Every step from `now` to a sample's progress rounds monotonically, so a row seen landed at a
52
+ // positive rate is landed at any later `now` while it stays ready, and folds its `to` as it is.
53
+ const landed = ready && now >= hot[h + 8 /* Hot.LANDED */];
54
+ let elapsed = 0;
55
+ if (!landed) {
56
+ elapsed = clock(hot, h, now) - data[o + 0 /* Row.DELAY */];
57
+ if (!(elapsed >= 0))
58
+ break;
59
+ }
60
+ keepProbed(data, o, per, q, fills);
51
61
  const w = hot[h + 4 /* Hot.WEIGHT */];
52
62
  data[o + 2 /* Row.WEIGHT */] = w;
53
- if (data[o + 5 /* Row.SAMPLED */] !== -2 /* Sampled.BARE */) {
54
- data[o + 5 /* Row.SAMPLED */] = -2 /* Sampled.BARE */;
55
- c.deltas[p] = null;
63
+ if (!ready) {
64
+ if (data[o + 5 /* Row.SAMPLED */] !== -2 /* Sampled.BARE */) {
65
+ data[o + 5 /* Row.SAMPLED */] = -2 /* Sampled.BARE */;
66
+ c.deltas[p] = null;
67
+ }
68
+ hot[h + 0 /* Hot.FLAGS */] = f | 2048 /* Flag.READY */;
69
+ hot[h + 8 /* Hot.LANDED */] = Number.POSITIVE_INFINITY;
56
70
  }
57
71
  if (w <= 0)
58
72
  continue;
59
- const share = left(c.eases[p], progress(elapsed, hot[h + 8 /* Hot.AT */], hot[h + 9 /* Hot.MS */]));
60
73
  const values = ch.values;
61
74
  const base = slot * n;
62
- const x = h + 10 /* Hot.X0 */;
75
+ const x = h + 11 /* Hot.X0 */;
63
76
  const g = x + 2 * n;
77
+ if (landed) {
78
+ for (let a = 0; a < n; a++)
79
+ values[base + a] = foldNumber(op, values[base + a], hot[g + a], w);
80
+ continue;
81
+ }
82
+ const u = progress(elapsed, hot[h + 9 /* Hot.AT */], hot[h + 10 /* Hot.MS */]);
83
+ const share = left(c.eases[p], u);
84
+ if (u >= 1 && hot[h + 3 /* Hot.RATE */] > 0)
85
+ hot[h + 8 /* Hot.LANDED */] = now;
64
86
  for (let a = 0; a < n; a++)
65
87
  values[base + a] = foldNumber(op, values[base + a], between(hot[x + a], hot[g + a], share), w);
66
88
  }
@@ -77,10 +99,11 @@ export function unbare(c, p) {
77
99
  return;
78
100
  const hot = c.hot;
79
101
  const h = p * c.stride;
102
+ hot[h + 0 /* Hot.FLAGS */] = hot[h + 0 /* Hot.FLAGS */] & ~2048 /* Flag.READY */;
80
103
  const n = c.axes;
81
104
  const elapsed = clock(hot, h, c.bareNow) - data[o + 0 /* Row.DELAY */];
82
- const share = left(c.eases[p], progress(elapsed, hot[h + 8 /* Hot.AT */], hot[h + 9 /* Hot.MS */]));
83
- const x = h + 10 /* Hot.X0 */;
105
+ const share = left(c.eases[p], progress(elapsed, hot[h + 9 /* Hot.AT */], hot[h + 10 /* Hot.MS */]));
106
+ const x = h + 11 /* Hot.X0 */;
84
107
  const g = x + 2 * n;
85
108
  for (let a = 0; a < n; a++)
86
109
  c.samples[p * n + a] = between(hot[x + a], hot[g + a], share);