@msb235/blits 0.12.1 → 0.14.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.
- package/CHANGELOG.md +55 -0
- package/README.md +17 -0
- package/dist/channels.d.ts +5 -0
- package/dist/channels.js +2 -0
- package/dist/closed.d.ts +1 -0
- package/dist/closed.js +45 -1
- package/dist/crowd.d.ts +2 -0
- package/dist/crowd.js +9 -1
- package/dist/cue.d.ts +1 -1
- package/dist/cue.js +26 -2
- package/dist/fling.d.ts +66 -0
- package/dist/fling.js +162 -0
- package/dist/fold.js +14 -0
- package/dist/handle.d.ts +1 -0
- package/dist/handle.js +6 -1
- package/dist/held.d.ts +2 -0
- package/dist/held.js +21 -13
- package/dist/history.js +2 -0
- package/dist/mixer.d.ts +8 -0
- package/dist/mixer.js +15 -2
- package/dist/motion.d.ts +18 -1
- package/dist/motion.js +44 -5
- package/dist/motions.d.ts +8 -3
- package/dist/motions.js +7 -5
- package/dist/move.js +40 -10
- package/dist/origin.d.ts +3 -1
- package/dist/origin.js +16 -3
- package/dist/pack.d.ts +1 -0
- package/dist/pack.js +2 -0
- package/dist/project.js +1 -0
- package/dist/record.js +1 -0
- package/dist/rows.js +3 -1
- package/dist/seek.js +19 -3
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/stack.d.ts +2 -2
- package/dist/stack.js +4 -5
- package/dist/strays.js +1 -1
- package/dist/transport.d.ts +5 -0
- package/dist/transport.js +2 -0
- package/dist/types/mix.d.ts +11 -4
- package/dist/types/patch.d.ts +2 -0
- package/dist/types/voice.d.ts +12 -4
- package/dist/voice.d.ts +4 -0
- package/dist/voice.js +2 -0
- package/docs/schema.html +15 -8
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,61 @@ 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.14.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `set(subject, value, at?)` on `spring`, `glide`, `tween`, and a voice's handle puts a subject at a
|
|
12
|
+
value and keeps its velocity: a spring or tween heads for its target from there, a glide coasts
|
|
13
|
+
on. A seek plays it again, as it does a push.
|
|
14
|
+
- `@msb235/blits/fling`: `fling(element, motion, subject, opts)` drags a subject by pointer and
|
|
15
|
+
flings it on release, and `track` reads a pointer's velocity from its samples with no listeners
|
|
16
|
+
of its own. The package still builds without the DOM's types.
|
|
17
|
+
- `glide` takes `bounds: [min, max]`, per axis and optionally per subject: a coast that would stop
|
|
18
|
+
past an edge springs back to it from where it crosses, on a critically damped spring of
|
|
19
|
+
`stiffness` (default 170). A `set` past an edge stays there until the next push. `g.bounds(subject)`
|
|
20
|
+
reads them, and `Patch.motion` reports the `stiffness` of a glide that has them.
|
|
21
|
+
- `fling` resists a drag past a glide's bounds, carrying the subject at most `give` (default 120)
|
|
22
|
+
past an edge, and a press holds a subject springing back past one where it is.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- A voice cued with no `start` into a mix whose frame loop sleeps on `inert`, as `ticker` does,
|
|
27
|
+
starts at the next sync. It started at the last one, so an effect cued after a pause had already
|
|
28
|
+
played that far, and a short one was over before its first frame.
|
|
29
|
+
- A patch that names no host type can be cued and blended in a mix that declares one, so a patch
|
|
30
|
+
library's patches type-check in a host-typed mix. `VoiceSpec.patch`, `BlendMember.patch`, and
|
|
31
|
+
`blend`'s members take a patch keeping any state.
|
|
32
|
+
- A seek back keeps out a subject that had already left a voice at rest by then. After a fade with
|
|
33
|
+
`at: 'rest'`, a seek to a moment where that subject was moving again put it back in the voice,
|
|
34
|
+
playing.
|
|
35
|
+
|
|
36
|
+
## 0.13.0
|
|
37
|
+
|
|
38
|
+
### Breaking
|
|
39
|
+
|
|
40
|
+
- A mix's stock `band` is on at any weight above 0 and off at 0, where it was on above 0.6 and off
|
|
41
|
+
below 0.4. A channel with no rest, such as `last()`, now shows its value through a whole fade in or
|
|
42
|
+
out. Pass `band: { on: 0.6, off: 0.4 }` for the old behavior. `fold` follows the same default.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- A mix's `loci`: tags that act as loci, so a voice carrying one folds with the others carrying it,
|
|
47
|
+
as if its `locus` named the tag.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- A seek with `state: 'keep'` that sends a subject back before its start now starts it afresh there.
|
|
52
|
+
It kept the state it had stepped to, so what it showed, and how it played on from its start,
|
|
53
|
+
depended on how often the host had read it, and a mix seek could put back the state from before.
|
|
54
|
+
- A mix sought back and then ahead to where it stood reads the same `now` on its next sync as one
|
|
55
|
+
never sought. At some mix rates it read one rounding step off, and every later sync kept the error.
|
|
56
|
+
- Replacing voices costs less a frame: a crowd catches up only the rows marked stale, where it
|
|
57
|
+
scanned every row, and a few retired voices are spliced out of the list. One subject leaving for
|
|
58
|
+
good each frame and a new one coming (`turnover^`, 10,000 subjects) takes 0.286 ms a frame
|
|
59
|
+
against 0.315, and one voice replaced in place each frame (`churn^`) 0.224 against 0.252 (teitou,
|
|
60
|
+
medians of nine runs of 5,000 frames, each in its own process); steady voices are unchanged.
|
|
61
|
+
|
|
7
62
|
## 0.12.1
|
|
8
63
|
|
|
9
64
|
### Added
|
package/README.md
CHANGED
|
@@ -102,6 +102,23 @@ import { kit, mix, patch, sum } from '@msb235/blits/lean';
|
|
|
102
102
|
|
|
103
103
|
Everything else is the same and so are the poses. `lanes: true` is refused there.
|
|
104
104
|
|
|
105
|
+
## Dragging and flinging
|
|
106
|
+
|
|
107
|
+
`@msb235/blits/fling` drags a motion patch's subject by pointer and flings it on release:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { fling } from '@msb235/blits/fling';
|
|
111
|
+
|
|
112
|
+
const scroll = glide<Page, Pose>('y', { from: 0 });
|
|
113
|
+
mix.cue({ patch: scroll });
|
|
114
|
+
const off = fling(element, scroll, page, { scale: -1 }); // content moves against the drag
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A press stops the glide where it is, each move puts it under the pointer, and letting go pushes it at
|
|
118
|
+
the pointer's velocity. `track` reads that velocity from any `(t, x)` samples, with no listeners of
|
|
119
|
+
its own. Anything else in the mix can read the glide's position and velocity, so parallax, a header
|
|
120
|
+
that shrinks or a blur that grows with speed are voices driven by a signal over `scroll.read(page)`.
|
|
121
|
+
|
|
105
122
|
## Writing patches for blits in another library
|
|
106
123
|
|
|
107
124
|
A library that ships patches exports them with the channels they write, as a `Kit` of its own
|
package/dist/channels.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Channel, Kit } from './types.js';
|
|
2
|
+
/** The band a mix switches a rest-less channel's contribution across where its options name none. */
|
|
3
|
+
export declare const STOCK_BAND: {
|
|
4
|
+
readonly on: number;
|
|
5
|
+
readonly off: 0;
|
|
6
|
+
};
|
|
2
7
|
/** The stock numeric channels' `lerp`, which `vec` applies per axis and lanes call directly. */
|
|
3
8
|
export declare const lerpNumber: (a: number, b: number, u: number) => number;
|
|
4
9
|
/**
|
package/dist/channels.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { shared } from './shared.js';
|
|
2
|
+
/** The band a mix switches a rest-less channel's contribution across where its options name none. */
|
|
3
|
+
export const STOCK_BAND = { on: Number.MIN_VALUE, off: 0 };
|
|
2
4
|
/** The stock numeric channels' `lerp`, which `vec` applies per axis and lanes call directly. */
|
|
3
5
|
export const lerpNumber = (a, b, u) => a + (b - a) * u;
|
|
4
6
|
const mix = lerpNumber;
|
package/dist/closed.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const CRITICAL = 1;
|
|
|
5
5
|
export declare const OVER = 2;
|
|
6
6
|
export declare const COAST = 3;
|
|
7
7
|
export declare const EASED = 4;
|
|
8
|
+
export declare const BOUND = 5;
|
|
8
9
|
/**
|
|
9
10
|
* A stretch at voice time `t`, released at `at` from `x0` moving at `v0` toward `to` (each read
|
|
10
11
|
* from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
|
package/dist/closed.js
CHANGED
|
@@ -6,6 +6,7 @@ export const CRITICAL = 1;
|
|
|
6
6
|
export const OVER = 2;
|
|
7
7
|
export const COAST = 3;
|
|
8
8
|
export const EASED = 4;
|
|
9
|
+
export const BOUND = 5;
|
|
9
10
|
const solved = { y: 0, dy: 0 };
|
|
10
11
|
/**
|
|
11
12
|
* The terms of a stretch that depend only on how long since its release, the same on every axis, so
|
|
@@ -114,6 +115,46 @@ function solveGlide(law, y0) {
|
|
|
114
115
|
solved.y = y0 * e;
|
|
115
116
|
solved.dy = (-y0 / tau) * e;
|
|
116
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* A glide with bounds, heading for `goal`: the rest point itself where the coast stops inside them,
|
|
120
|
+
* else the edge it springs back to. Which one is read off the stretch, so no stretch carries more
|
|
121
|
+
* than an unbounded one: the coast's own rest, worked out as the glide's `aim` does, is `goal`
|
|
122
|
+
* exactly when it stops inside; past it on the side `x0` is on too, the subject was let go past the
|
|
123
|
+
* edge and springs back at once; else it coasts until it crosses the edge and springs from there,
|
|
124
|
+
* at the speed it crossed at. The spring is critically damped, its ω₀ the law's `k2`.
|
|
125
|
+
*/
|
|
126
|
+
function solveBound(law, x0, goal, v0, t) {
|
|
127
|
+
const tau = law[2];
|
|
128
|
+
const y0 = x0 - goal;
|
|
129
|
+
const over = x0 + v0 * tau - goal;
|
|
130
|
+
if (over === 0) {
|
|
131
|
+
solveGlide(law, y0);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const w0 = law[3];
|
|
135
|
+
if (y0 * over > 0) {
|
|
136
|
+
const e = Math.exp(-w0 * t);
|
|
137
|
+
const b = v0 + w0 * y0;
|
|
138
|
+
const y = e * (y0 + b * t);
|
|
139
|
+
solved.y = y;
|
|
140
|
+
solved.dy = e * b - w0 * y;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// e^(−t/τ) where the coast meets the edge: the fraction of its speed it still has there.
|
|
144
|
+
const left = 1 + y0 / (v0 * tau);
|
|
145
|
+
const cross = left > 0 ? -tau * Math.log(left) : Number.POSITIVE_INFINITY;
|
|
146
|
+
if (t < cross) {
|
|
147
|
+
const e = timed.e;
|
|
148
|
+
solved.y = y0 + v0 * tau * (1 - e);
|
|
149
|
+
solved.dy = v0 * e;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const s = t - cross;
|
|
153
|
+
const vc = v0 * left;
|
|
154
|
+
const e = Math.exp(-w0 * s);
|
|
155
|
+
solved.y = e * vc * s;
|
|
156
|
+
solved.dy = e * vc * (1 - w0 * s);
|
|
157
|
+
}
|
|
117
158
|
/**
|
|
118
159
|
* A stretch at voice time `t`, released at `at` from `x0` moving at `v0` toward `to` (each read
|
|
119
160
|
* from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
|
|
@@ -130,7 +171,10 @@ export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
|
|
|
130
171
|
prepare(law, dt);
|
|
131
172
|
for (let i = 0; i < n; i++) {
|
|
132
173
|
const goal = to[g + i];
|
|
133
|
-
|
|
174
|
+
if (law[0] === BOUND)
|
|
175
|
+
solveBound(law, x0[x + i], goal, v0[v + i], dt);
|
|
176
|
+
else
|
|
177
|
+
solve(law, x0[x + i] - goal, v0[v + i], dt);
|
|
134
178
|
xo[i] = goal + solved.y;
|
|
135
179
|
vo[i] = solved.dy;
|
|
136
180
|
if (Math.abs(solved.y) > settle || Math.abs(solved.dy) > settle)
|
package/dist/crowd.d.ts
CHANGED
|
@@ -92,6 +92,8 @@ export declare class Crowd<I, O> implements Positions<I, O> {
|
|
|
92
92
|
dead: number;
|
|
93
93
|
/** Whether some row is `Flag.STALE`, for `freshen` to copy before the fill's loop. */
|
|
94
94
|
stale: boolean;
|
|
95
|
+
/** The rows marked stale since the last `freshen`, which checks each is still stale. */
|
|
96
|
+
staleRows: number[];
|
|
95
97
|
readonly xs: Float64Array;
|
|
96
98
|
readonly vs: Float64Array;
|
|
97
99
|
/** What a keys row reads into, folded before the next row reads. */
|
package/dist/crowd.js
CHANGED
|
@@ -54,6 +54,8 @@ export class Crowd {
|
|
|
54
54
|
dead = 0;
|
|
55
55
|
/** Whether some row is `Flag.STALE`, for `freshen` to copy before the fill's loop. */
|
|
56
56
|
stale = false;
|
|
57
|
+
/** The rows marked stale since the last `freshen`, which checks each is still stale. */
|
|
58
|
+
staleRows = [];
|
|
57
59
|
xs;
|
|
58
60
|
vs;
|
|
59
61
|
/** What a keys row reads into, folded before the next row reads. */
|
|
@@ -82,6 +84,7 @@ export class Crowd {
|
|
|
82
84
|
restale(p, flags) {
|
|
83
85
|
this.hot[p * this.stride + 0 /* Hot.FLAGS */] = flags | 4 /* Flag.STALE */;
|
|
84
86
|
this.stale = true;
|
|
87
|
+
this.staleRows.push(p);
|
|
85
88
|
}
|
|
86
89
|
/** Samples of another width than the channel's, which `samples` cannot hold, by row. */
|
|
87
90
|
odd = null;
|
|
@@ -347,7 +350,12 @@ export function freshen(c) {
|
|
|
347
350
|
c.stale = false;
|
|
348
351
|
const hot = c.hot;
|
|
349
352
|
const H = c.stride;
|
|
350
|
-
|
|
353
|
+
const rows = c.staleRows;
|
|
354
|
+
c.staleRows = [];
|
|
355
|
+
for (let i = 0; i < rows.length; i++) {
|
|
356
|
+
const p = rows[i];
|
|
357
|
+
if (p >= c.list.length)
|
|
358
|
+
continue;
|
|
351
359
|
const f = hot[p * H + 0 /* Hot.FLAGS */];
|
|
352
360
|
if ((f & 4 /* Flag.STALE */) === 0 || (f & 128 /* Flag.MOTION */) === 0)
|
|
353
361
|
continue;
|
package/dist/cue.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
2
|
import type { Handle, Kit, Patch, VoiceSpec } from './types.js';
|
|
3
|
-
export declare function cue<I, O>(mix: Mixer<I, O>,
|
|
3
|
+
export declare function cue<I, O>(mix: Mixer<I, O>, given: VoiceSpec<I, O>): Handle<I>;
|
|
4
4
|
/** Refuses a patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
|
|
5
5
|
export declare function fitsKit<I, O, S, H>(patch: Patch<I, O, S, H>, kit: Kit<O>): void;
|
|
6
6
|
/** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
|
package/dist/cue.js
CHANGED
|
@@ -17,7 +17,8 @@ import { checkChild, refit } from './spans.js';
|
|
|
17
17
|
import { cuedById, motionOwner } from './strays.js';
|
|
18
18
|
import { record, unplayable } from './tape.js';
|
|
19
19
|
import { Voice } from './voice.js';
|
|
20
|
-
export function cue(mix,
|
|
20
|
+
export function cue(mix, given) {
|
|
21
|
+
const spec = locusOf(mix, given);
|
|
21
22
|
const patch = spec.patch;
|
|
22
23
|
playable(mix, spec);
|
|
23
24
|
if (spec.from === 'current' && patch.form !== 'keys')
|
|
@@ -43,7 +44,10 @@ export function cue(mix, spec) {
|
|
|
43
44
|
? mixAt(mix, spec.start - mix.offset)
|
|
44
45
|
: Number.isNaN(mix.now)
|
|
45
46
|
? 0
|
|
46
|
-
: mix.
|
|
47
|
+
: // A loop asleep on an inert mix syncs next when it wakes, which is the cue's "now".
|
|
48
|
+
mix.asleep
|
|
49
|
+
? Number.POSITIVE_INFINITY
|
|
50
|
+
: mix.now;
|
|
47
51
|
const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? noFade, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
|
|
48
52
|
if (patch === ownerPatch && mix.fitting !== null)
|
|
49
53
|
voice.fitting = mix.fitting;
|
|
@@ -55,6 +59,10 @@ export function cue(mix, spec) {
|
|
|
55
59
|
checkChild(spec, voice);
|
|
56
60
|
if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
|
|
57
61
|
voice.pinned = spec.start - mix.offset;
|
|
62
|
+
if (start === Number.POSITIVE_INFINITY && !anchored && !owner?.fitting) {
|
|
63
|
+
voice.waking = true;
|
|
64
|
+
mix.waking = true;
|
|
65
|
+
}
|
|
58
66
|
enter(mix, voice);
|
|
59
67
|
const h = handle(mix, voice);
|
|
60
68
|
voice.handle = h;
|
|
@@ -88,6 +96,22 @@ export function fitsKit(patch, kit) {
|
|
|
88
96
|
throw new Error(`blits: channel ${String(channel)} is ${here.kind ?? 'a custom channel'} in this kit, but the patch was written for ${wanted.kind ?? 'a custom channel'}`);
|
|
89
97
|
}
|
|
90
98
|
}
|
|
99
|
+
/** The spec with its `locus` set from a tag the mix declares one, where it carries one. */
|
|
100
|
+
function locusOf(mix, spec) {
|
|
101
|
+
const loci = mix.opts.loci;
|
|
102
|
+
const tags = spec.tags;
|
|
103
|
+
if (loci === undefined || tags === undefined)
|
|
104
|
+
return spec;
|
|
105
|
+
let locus = spec.locus;
|
|
106
|
+
for (const tag of tags) {
|
|
107
|
+
if (!loci.includes(tag) || tag === locus)
|
|
108
|
+
continue;
|
|
109
|
+
if (locus !== undefined)
|
|
110
|
+
throw new Error(`blits: a voice folds in one locus, not both ${locus} and ${tag}`);
|
|
111
|
+
locus = tag;
|
|
112
|
+
}
|
|
113
|
+
return locus === spec.locus ? spec : { ...spec, locus };
|
|
114
|
+
}
|
|
91
115
|
/** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
|
|
92
116
|
export function playable(mix, spec) {
|
|
93
117
|
const patch = spec.patch;
|
package/dist/fling.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Moving, Value } from './motion.js';
|
|
2
|
+
/**
|
|
3
|
+
* A pointer's recent path, for the velocity it is let go at.
|
|
4
|
+
*
|
|
5
|
+
* @category fling
|
|
6
|
+
*/
|
|
7
|
+
export interface Track<V extends Value = number> {
|
|
8
|
+
/** Where it was at `t`, ms; samples must come in time order. */
|
|
9
|
+
add(t: number, x: V): void;
|
|
10
|
+
/**
|
|
11
|
+
* Units per second at `at`, default the latest sample: the least-squares slope of the samples
|
|
12
|
+
* within `ms` of the latest, or zero once the latest is more than `stop` ms before `at`, or with
|
|
13
|
+
* fewer than two samples.
|
|
14
|
+
*/
|
|
15
|
+
velocity(at?: number): V;
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Tracks a pointer's velocity from its samples, with no listeners of its own: anything that can give
|
|
20
|
+
* a time and a position can feed it.
|
|
21
|
+
*
|
|
22
|
+
* @category fling
|
|
23
|
+
*/
|
|
24
|
+
export declare function track<V extends Value = number>(opts?: {
|
|
25
|
+
/** How far back the slope reads, ms. Default 100. */
|
|
26
|
+
ms?: number;
|
|
27
|
+
/** How long a pointer can sit still before letting go and still fling, ms. Default 40. */
|
|
28
|
+
stop?: number;
|
|
29
|
+
}): Track<V>;
|
|
30
|
+
/** The part of a pointer event `fling` reads. */
|
|
31
|
+
export interface PointerLike {
|
|
32
|
+
pointerId: number;
|
|
33
|
+
clientX: number;
|
|
34
|
+
clientY: number;
|
|
35
|
+
timeStamp: number;
|
|
36
|
+
}
|
|
37
|
+
/** Anything pointer events come from: an element, or a stand-in for one. */
|
|
38
|
+
export interface PointerTarget {
|
|
39
|
+
addEventListener(type: string, fn: (e: PointerLike) => void): void;
|
|
40
|
+
removeEventListener(type: string, fn: (e: PointerLike) => void): void;
|
|
41
|
+
setPointerCapture?(pointerId: number): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Drags `subject` of a motion patch, usually a `glide`, by pointer on `target`, and flings it on
|
|
45
|
+
* release at the pointer's velocity: a press stops it where it is, each move `set`s it under the
|
|
46
|
+
* pointer, and letting go `push`es it. Past the edge of a glide's `bounds` it follows less and less
|
|
47
|
+
* of the drag, never more than `give` past. One pointer at a time; a canceled one lets go at rest.
|
|
48
|
+
* Returns the function that removes its listeners.
|
|
49
|
+
*
|
|
50
|
+
* @category fling
|
|
51
|
+
*/
|
|
52
|
+
export declare function fling<I, O, V extends Value = number>(target: PointerTarget, motion: Pick<Moving<I, O, V>, 'read' | 'set' | 'push'> & {
|
|
53
|
+
bounds?(subject: I): readonly [min: V, max: V] | undefined;
|
|
54
|
+
}, subject: I, opts?: {
|
|
55
|
+
/** Which pointer axes it follows: `'xy'` moves a subject on two axes. Default `'y'`. */
|
|
56
|
+
axis?: 'x' | 'y' | 'xy';
|
|
57
|
+
/** Units per pixel of pointer travel; -1 scrolls content against the drag. Default 1. */
|
|
58
|
+
scale?: number;
|
|
59
|
+
/**
|
|
60
|
+
* How far past an edge of the glide's bounds a drag can carry the subject, in its units; 0
|
|
61
|
+
* holds it at the edge. Default 120.
|
|
62
|
+
*/
|
|
63
|
+
give?: number;
|
|
64
|
+
ms?: number;
|
|
65
|
+
stop?: number;
|
|
66
|
+
}): () => void;
|
package/dist/fling.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks a pointer's velocity from its samples, with no listeners of its own: anything that can give
|
|
3
|
+
* a time and a position can feed it.
|
|
4
|
+
*
|
|
5
|
+
* @category fling
|
|
6
|
+
*/
|
|
7
|
+
export function track(opts = {}) {
|
|
8
|
+
const span = opts.ms ?? 100;
|
|
9
|
+
const stop = opts.stop ?? 40;
|
|
10
|
+
if (!(span > 0) || !(stop >= 0))
|
|
11
|
+
throw new RangeError('blits: a track takes a positive ms and a stop of 0 or more');
|
|
12
|
+
const ts = [];
|
|
13
|
+
const xs = [];
|
|
14
|
+
let scalar = true;
|
|
15
|
+
return {
|
|
16
|
+
add(t, x) {
|
|
17
|
+
scalar = typeof x === 'number';
|
|
18
|
+
ts.push(t);
|
|
19
|
+
xs.push(typeof x === 'number' ? [x] : [...x]);
|
|
20
|
+
let drop = 0;
|
|
21
|
+
while (t - ts[drop] > span)
|
|
22
|
+
drop++;
|
|
23
|
+
if (drop > 0) {
|
|
24
|
+
ts.splice(0, drop);
|
|
25
|
+
xs.splice(0, drop);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
velocity(at) {
|
|
29
|
+
const n = ts.length;
|
|
30
|
+
const axes = n === 0 ? 1 : xs[0].length;
|
|
31
|
+
const out = new Array(axes).fill(0);
|
|
32
|
+
const last = ts[n - 1];
|
|
33
|
+
if (n >= 2 && !((at ?? last) - last > stop)) {
|
|
34
|
+
let mt = 0;
|
|
35
|
+
for (const t of ts)
|
|
36
|
+
mt += t;
|
|
37
|
+
mt /= n;
|
|
38
|
+
let tt = 0;
|
|
39
|
+
for (const t of ts)
|
|
40
|
+
tt += (t - mt) ** 2;
|
|
41
|
+
if (tt > 0)
|
|
42
|
+
for (let k = 0; k < axes; k++) {
|
|
43
|
+
let mx = 0;
|
|
44
|
+
for (const x of xs)
|
|
45
|
+
mx += x[k];
|
|
46
|
+
mx /= n;
|
|
47
|
+
let tx = 0;
|
|
48
|
+
for (let i = 0; i < n; i++)
|
|
49
|
+
tx += (ts[i] - mt) * (xs[i][k] - mx);
|
|
50
|
+
out[k] = (tx / tt) * 1000;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return (scalar ? out[0] : out);
|
|
54
|
+
},
|
|
55
|
+
clear() {
|
|
56
|
+
ts.length = 0;
|
|
57
|
+
xs.length = 0;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** How much of a drag past an edge the subject follows, right at the edge. */
|
|
62
|
+
const PULL = 0.55;
|
|
63
|
+
/** How far a drag `d` past an edge carries the subject, never as far as `give`. */
|
|
64
|
+
const resist = (d, give) => give > 0 ? give * (1 - 1 / ((PULL * d) / give + 1)) : 0;
|
|
65
|
+
/** The drag past an edge that carries the subject `r` past it, as `resist` maps it. */
|
|
66
|
+
const unresist = (r, give) => {
|
|
67
|
+
const held = Math.min(r, give * 0.99);
|
|
68
|
+
return give > 0 ? (give * held) / (PULL * (give - held)) : 0;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Drags `subject` of a motion patch, usually a `glide`, by pointer on `target`, and flings it on
|
|
72
|
+
* release at the pointer's velocity: a press stops it where it is, each move `set`s it under the
|
|
73
|
+
* pointer, and letting go `push`es it. Past the edge of a glide's `bounds` it follows less and less
|
|
74
|
+
* of the drag, never more than `give` past. One pointer at a time; a canceled one lets go at rest.
|
|
75
|
+
* Returns the function that removes its listeners.
|
|
76
|
+
*
|
|
77
|
+
* @category fling
|
|
78
|
+
*/
|
|
79
|
+
export function fling(target, motion, subject, opts = {}) {
|
|
80
|
+
const axis = opts.axis ?? 'y';
|
|
81
|
+
const scale = opts.scale ?? 1;
|
|
82
|
+
const give = opts.give ?? 120;
|
|
83
|
+
if (!(give >= 0))
|
|
84
|
+
throw new RangeError('blits: a fling takes a give of 0 or more');
|
|
85
|
+
let lo;
|
|
86
|
+
let hi;
|
|
87
|
+
/** Where the drag `x` puts the subject, past an edge carried by `resist`. */
|
|
88
|
+
const held = (x) => lo === undefined || hi === undefined
|
|
89
|
+
? x
|
|
90
|
+
: x.map((xi, k) => {
|
|
91
|
+
const a = lo[k];
|
|
92
|
+
const b = hi[k];
|
|
93
|
+
return xi < a ? a - resist(a - xi, give) : xi > b ? b + resist(xi - b, give) : xi;
|
|
94
|
+
});
|
|
95
|
+
/** The drag that puts the subject at `x`, as `held` maps it. */
|
|
96
|
+
const dragged = (x) => lo === undefined || hi === undefined
|
|
97
|
+
? x
|
|
98
|
+
: x.map((xi, k) => {
|
|
99
|
+
const a = lo[k];
|
|
100
|
+
const b = hi[k];
|
|
101
|
+
return xi < a ? a - unresist(a - xi, give) : xi > b ? b + unresist(xi - b, give) : xi;
|
|
102
|
+
});
|
|
103
|
+
const path = track(opts);
|
|
104
|
+
let id;
|
|
105
|
+
let from = [];
|
|
106
|
+
let p0 = [];
|
|
107
|
+
const at = (e) => axis === 'x' ? [e.clientX] : axis === 'y' ? [e.clientY] : [e.clientX, e.clientY];
|
|
108
|
+
const value = (x) => (axis === 'xy' ? x : x[0]);
|
|
109
|
+
const flat = (v) => (typeof v === 'number' ? [v] : [...v]);
|
|
110
|
+
const zero = () => value(from.map(() => 0));
|
|
111
|
+
const down = (e) => {
|
|
112
|
+
if (id !== undefined)
|
|
113
|
+
return;
|
|
114
|
+
const now = motion.read(subject);
|
|
115
|
+
if (now === undefined)
|
|
116
|
+
return;
|
|
117
|
+
id = e.pointerId;
|
|
118
|
+
target.setPointerCapture?.(id);
|
|
119
|
+
const edges = motion.bounds?.(subject);
|
|
120
|
+
lo = edges === undefined ? undefined : flat(edges[0]);
|
|
121
|
+
hi = edges === undefined ? undefined : flat(edges[1]);
|
|
122
|
+
const here = flat(now.value);
|
|
123
|
+
from = dragged(here);
|
|
124
|
+
p0 = at(e);
|
|
125
|
+
motion.push(subject, zero());
|
|
126
|
+
// The push alone would spring a subject caught past an edge back; a set there holds it.
|
|
127
|
+
motion.set(subject, value(here));
|
|
128
|
+
path.clear();
|
|
129
|
+
path.add(e.timeStamp, here);
|
|
130
|
+
};
|
|
131
|
+
const move = (e) => {
|
|
132
|
+
if (e.pointerId !== id)
|
|
133
|
+
return;
|
|
134
|
+
const x = held(at(e).map((p, k) => from[k] + (p - p0[k]) * scale));
|
|
135
|
+
motion.set(subject, value(x));
|
|
136
|
+
path.add(e.timeStamp, x);
|
|
137
|
+
};
|
|
138
|
+
const up = (e) => {
|
|
139
|
+
if (e.pointerId !== id)
|
|
140
|
+
return;
|
|
141
|
+
id = undefined;
|
|
142
|
+
motion.push(subject, value(path.velocity(e.timeStamp)));
|
|
143
|
+
};
|
|
144
|
+
const cancel = (e) => {
|
|
145
|
+
if (e.pointerId !== id)
|
|
146
|
+
return;
|
|
147
|
+
id = undefined;
|
|
148
|
+
motion.push(subject, zero());
|
|
149
|
+
};
|
|
150
|
+
const on = [
|
|
151
|
+
['pointerdown', down],
|
|
152
|
+
['pointermove', move],
|
|
153
|
+
['pointerup', up],
|
|
154
|
+
['pointercancel', cancel],
|
|
155
|
+
];
|
|
156
|
+
for (const [type, fn] of on)
|
|
157
|
+
target.addEventListener(type, fn);
|
|
158
|
+
return () => {
|
|
159
|
+
for (const [type, fn] of on)
|
|
160
|
+
target.removeEventListener(type, fn);
|
|
161
|
+
};
|
|
162
|
+
}
|
package/dist/fold.js
CHANGED
|
@@ -2,6 +2,7 @@ import { foldNumber } from './channels.js';
|
|
|
2
2
|
import { passAt, phaseAt, phaseBefore, silent, stepOf } from './clock.js';
|
|
3
3
|
import { copy as copyValue, first } from './clone.js';
|
|
4
4
|
import { open as openRecord, shut as shutRecord, weighAt } from './everyone.js';
|
|
5
|
+
import { waits } from './held.js';
|
|
5
6
|
import { apart, copyState, recordHost, remember } from './history.js';
|
|
6
7
|
import { stateful } from './hosts.js';
|
|
7
8
|
import { locusScratch } from './locus.js';
|
|
@@ -59,6 +60,13 @@ function rebuild(mix, voice, subject, now, held) {
|
|
|
59
60
|
held.stepped = held.since < now ? held.since : now;
|
|
60
61
|
held.probed = Number.NaN;
|
|
61
62
|
}
|
|
63
|
+
/** Whether a seek since its last read moved a stateful record's origin, as one back before it does. */
|
|
64
|
+
function sentBack(mix, voice, held) {
|
|
65
|
+
const p = voice.patch;
|
|
66
|
+
if (p.state === undefined && p.step === undefined && !voice.keeping)
|
|
67
|
+
return false;
|
|
68
|
+
return mix.sinceOf(voice, held.delay) !== held.since;
|
|
69
|
+
}
|
|
62
70
|
/**
|
|
63
71
|
* One live voice's delta for one subject this frame, through its record for the subject, or null
|
|
64
72
|
* when it does not reach. Its weight is left in `w`.
|
|
@@ -71,6 +79,11 @@ export function contribution(voice, subject, now, held) {
|
|
|
71
79
|
return null;
|
|
72
80
|
if (held.rebuilt !== voice.rebuilds)
|
|
73
81
|
rebuild(this, voice, subject, now, held);
|
|
82
|
+
else if (held.seeks !== voice.seeks && sentBack(this, voice, held)) {
|
|
83
|
+
// A seek sent it back before its start, so it starts afresh there, whatever state it kept.
|
|
84
|
+
rebuild(this, voice, subject, now, held);
|
|
85
|
+
waits(voice, held, now);
|
|
86
|
+
}
|
|
74
87
|
const raw = voice.rawAt(now, held.delay);
|
|
75
88
|
const early = voice.before(raw, now);
|
|
76
89
|
if (early && !voice.holdsFirst)
|
|
@@ -177,6 +190,7 @@ export function contribution(voice, subject, now, held) {
|
|
|
177
190
|
if (voice.out?.rest && this.isRest(delta)) {
|
|
178
191
|
held.weight = 0;
|
|
179
192
|
held.rested = true;
|
|
193
|
+
held.restedAt = now;
|
|
180
194
|
voice.restedCount++;
|
|
181
195
|
return null;
|
|
182
196
|
}
|
package/dist/handle.d.ts
CHANGED
|
@@ -68,5 +68,6 @@ export declare class VoiceHandle<I, O> implements Handle<I> {
|
|
|
68
68
|
weightOf(subject: I): number;
|
|
69
69
|
to(subject: I, target: Value, at?: number): void;
|
|
70
70
|
push(subject: I, velocity: Value, at?: number): void;
|
|
71
|
+
set(subject: I, value: Value, at?: number): void;
|
|
71
72
|
read(subject: I, at?: number): Motion<Value> | undefined;
|
|
72
73
|
}
|
package/dist/handle.js
CHANGED
|
@@ -115,7 +115,7 @@ export class VoiceHandle {
|
|
|
115
115
|
// A pending voice's clock is anchored at its start, so it starts there on time.
|
|
116
116
|
if (voice.state !== 'pending') {
|
|
117
117
|
const u = this.#host.nowFor(voice);
|
|
118
|
-
leave(voice, u);
|
|
118
|
+
leave(voice, u, true);
|
|
119
119
|
voice.rebase(u);
|
|
120
120
|
}
|
|
121
121
|
voice.anchorElapsed = elapsed;
|
|
@@ -164,6 +164,11 @@ export class VoiceHandle {
|
|
|
164
164
|
if (voice !== null)
|
|
165
165
|
moves(voice.patch, 'push', 'a spring or a glide').push(subject, velocity, at);
|
|
166
166
|
}
|
|
167
|
+
set(subject, value, at) {
|
|
168
|
+
const voice = this.#voice;
|
|
169
|
+
if (voice !== null)
|
|
170
|
+
moves(voice.patch, 'set', 'a motion patch').set(subject, value, at);
|
|
171
|
+
}
|
|
167
172
|
read(subject, at) {
|
|
168
173
|
const voice = this.#voice;
|
|
169
174
|
if (voice === null || motionOf(voice.patch) === undefined)
|
package/dist/held.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function sinceOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, del
|
|
|
24
24
|
* the voice first showed, for one freezing before.
|
|
25
25
|
*/
|
|
26
26
|
export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, since: number): number;
|
|
27
|
+
/** Lists a record whose origin is still ahead of `now`, for a later change to the clock to move. */
|
|
28
|
+
export declare function waits<I, O>(voice: Voice<I, O>, held: Subject<unknown>, now: number): void;
|
|
27
29
|
/**
|
|
28
30
|
* A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
|
|
29
31
|
* while it waited is made again for where its clock is.
|
package/dist/held.js
CHANGED
|
@@ -64,19 +64,7 @@ export function held(voice, subject, slot, guess = null) {
|
|
|
64
64
|
held.unknown = (this.backward && voice.spec.from === 'current') || fresh;
|
|
65
65
|
}
|
|
66
66
|
voice.subjects.set(subject, held);
|
|
67
|
-
|
|
68
|
-
voice.early ??= [];
|
|
69
|
-
const early = voice.early;
|
|
70
|
-
// Prunes at each doubling, so records whose origin has passed are not kept to the voice's end.
|
|
71
|
-
if (early.length >= 64 && (early.length & (early.length - 1)) === 0) {
|
|
72
|
-
let kept = 0;
|
|
73
|
-
for (const h of early)
|
|
74
|
-
if (!(h.since <= now))
|
|
75
|
-
early[kept++] = h;
|
|
76
|
-
early.length = kept;
|
|
77
|
-
}
|
|
78
|
-
early.push(held);
|
|
79
|
-
}
|
|
67
|
+
waits(voice, held, now);
|
|
80
68
|
// A subject a voice saw while it shared one record was counted then.
|
|
81
69
|
if (sighted(voice, slot))
|
|
82
70
|
inherit(voice, held, slot);
|
|
@@ -137,6 +125,22 @@ export function shownOf(voice, since) {
|
|
|
137
125
|
const from = since < start ? start : since;
|
|
138
126
|
return voice.freezesBefore && voice.opened < from ? voice.opened : from;
|
|
139
127
|
}
|
|
128
|
+
/** Lists a record whose origin is still ahead of `now`, for a later change to the clock to move. */
|
|
129
|
+
export function waits(voice, held, now) {
|
|
130
|
+
if (held.since <= now)
|
|
131
|
+
return;
|
|
132
|
+
voice.early ??= [];
|
|
133
|
+
const early = voice.early;
|
|
134
|
+
// Prunes at each doubling, so records whose origin has passed are not kept to the voice's end.
|
|
135
|
+
if (early.length >= 64 && (early.length & (early.length - 1)) === 0) {
|
|
136
|
+
let kept = 0;
|
|
137
|
+
for (const h of early)
|
|
138
|
+
if (!(h.since <= now))
|
|
139
|
+
early[kept++] = h;
|
|
140
|
+
early.length = kept;
|
|
141
|
+
}
|
|
142
|
+
early.push(held);
|
|
143
|
+
}
|
|
140
144
|
/**
|
|
141
145
|
* A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
|
|
142
146
|
* while it waited is made again for where its clock is.
|
|
@@ -154,7 +158,11 @@ export function begin(mix, voice, now) {
|
|
|
154
158
|
}
|
|
155
159
|
/** Counts a record whose `since` was still ahead from where its voice's clock now puts it. */
|
|
156
160
|
export function origin(mix, voice, held) {
|
|
161
|
+
const was = held.since;
|
|
157
162
|
held.since = mix.sinceOf(voice, held.delay);
|
|
163
|
+
// Sent back before a start it had passed: its next read starts it afresh.
|
|
164
|
+
if (held.since !== was && (held.ticks > 0 || held.stepped > was))
|
|
165
|
+
held.rebuilt = -1;
|
|
158
166
|
held.shown = mix.shownOf(voice, held.since);
|
|
159
167
|
if (held.ticks === 0 && held.stepped > held.since)
|
|
160
168
|
held.stepped = held.since;
|
package/dist/history.js
CHANGED
|
@@ -122,6 +122,7 @@ export function copyHeld(voice, h, shared = false) {
|
|
|
122
122
|
shown: h.shown,
|
|
123
123
|
weight: h.weight,
|
|
124
124
|
rested: h.rested,
|
|
125
|
+
restedAt: h.restedAt,
|
|
125
126
|
bands: h.bands === null ? null : h.bands.slice(),
|
|
126
127
|
state: h.state === undefined || shared ? h.state : copyState(voice, h.state),
|
|
127
128
|
stepped: h.stepped,
|
|
@@ -172,6 +173,7 @@ export function copyHeldInto(voice, h, into, shared = false) {
|
|
|
172
173
|
into.shown = h.shown;
|
|
173
174
|
into.weight = h.weight;
|
|
174
175
|
into.rested = h.rested;
|
|
176
|
+
into.restedAt = h.restedAt;
|
|
175
177
|
into.bands = h.bands === null ? null : h.bands.slice();
|
|
176
178
|
into.state = h.state === undefined || shared ? h.state : copyState(voice, h.state);
|
|
177
179
|
into.stepped = h.stepped;
|