@msb235/blits 0.13.0 → 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 +29 -0
- package/README.md +17 -0
- package/dist/closed.d.ts +1 -0
- package/dist/closed.js +45 -1
- package/dist/cue.js +8 -1
- package/dist/fling.d.ts +66 -0
- package/dist/fling.js +162 -0
- package/dist/fold.js +1 -0
- package/dist/handle.d.ts +1 -0
- package/dist/handle.js +5 -0
- package/dist/history.js +2 -0
- package/dist/mixer.d.ts +8 -0
- package/dist/mixer.js +13 -0
- 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 +24 -1
- 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/seek.js +10 -2
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/strays.js +1 -1
- package/dist/types/mix.d.ts +4 -2
- package/dist/types/patch.d.ts +2 -0
- package/dist/types/voice.d.ts +10 -3
- package/dist/voice.d.ts +4 -0
- package/dist/voice.js +2 -0
- package/docs/schema.html +11 -3
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ 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
|
+
|
|
7
36
|
## 0.13.0
|
|
8
37
|
|
|
9
38
|
### Breaking
|
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/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/cue.js
CHANGED
|
@@ -44,7 +44,10 @@ export function cue(mix, given) {
|
|
|
44
44
|
? mixAt(mix, spec.start - mix.offset)
|
|
45
45
|
: Number.isNaN(mix.now)
|
|
46
46
|
? 0
|
|
47
|
-
: 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;
|
|
48
51
|
const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? noFade, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
|
|
49
52
|
if (patch === ownerPatch && mix.fitting !== null)
|
|
50
53
|
voice.fitting = mix.fitting;
|
|
@@ -56,6 +59,10 @@ export function cue(mix, given) {
|
|
|
56
59
|
checkChild(spec, voice);
|
|
57
60
|
if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
|
|
58
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
|
+
}
|
|
59
66
|
enter(mix, voice);
|
|
60
67
|
const h = handle(mix, voice);
|
|
61
68
|
voice.handle = h;
|
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
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
|
@@ -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/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;
|
package/dist/mixer.d.ts
CHANGED
|
@@ -114,6 +114,13 @@ export declare class Mixer<I, O> implements Mix<I, O> {
|
|
|
114
114
|
reducedNow: boolean;
|
|
115
115
|
/** Something was changed since the last sync, so the next frame may differ from this one. */
|
|
116
116
|
stirred: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* The host read `inert` as true since the last sync, so its frame loop may be asleep until a
|
|
119
|
+
* change wakes it; a host that never asks keeps no loop asleep on it.
|
|
120
|
+
*/
|
|
121
|
+
asleep: boolean;
|
|
122
|
+
/** Some voice cued while the mix slept waits for the next sync to set its start. */
|
|
123
|
+
waking: boolean;
|
|
117
124
|
/** Counts the changes `stir` records, which a frame's record of a probe is current only before. */
|
|
118
125
|
stirs: number;
|
|
119
126
|
syncing: boolean;
|
|
@@ -259,6 +266,7 @@ export declare class Mixer<I, O> implements Mix<I, O> {
|
|
|
259
266
|
voices(tag?: string): Handle<I>[];
|
|
260
267
|
get live(): boolean;
|
|
261
268
|
get inert(): boolean;
|
|
269
|
+
private restful;
|
|
262
270
|
get rate(): number;
|
|
263
271
|
set rate(r: number);
|
|
264
272
|
ramp(rate: number, over: number): void;
|
package/dist/mixer.js
CHANGED
|
@@ -105,6 +105,13 @@ export class Mixer {
|
|
|
105
105
|
reducedNow = false;
|
|
106
106
|
/** Something was changed since the last sync, so the next frame may differ from this one. */
|
|
107
107
|
stirred = false;
|
|
108
|
+
/**
|
|
109
|
+
* The host read `inert` as true since the last sync, so its frame loop may be asleep until a
|
|
110
|
+
* change wakes it; a host that never asks keeps no loop asleep on it.
|
|
111
|
+
*/
|
|
112
|
+
asleep = false;
|
|
113
|
+
/** Some voice cued while the mix slept waits for the next sync to set its start. */
|
|
114
|
+
waking = false;
|
|
108
115
|
/** Counts the changes `stir` records, which a frame's record of a probe is current only before. */
|
|
109
116
|
stirs = 0;
|
|
110
117
|
syncing = false;
|
|
@@ -474,6 +481,12 @@ export class Mixer {
|
|
|
474
481
|
get inert() {
|
|
475
482
|
if (this.stirred)
|
|
476
483
|
return false;
|
|
484
|
+
const inert = this.restful();
|
|
485
|
+
if (inert && !Number.isNaN(this.now))
|
|
486
|
+
this.asleep = true;
|
|
487
|
+
return inert;
|
|
488
|
+
}
|
|
489
|
+
restful() {
|
|
477
490
|
// Standing still, a voice changes no pose but by its weight signal, or by an anchor or start
|
|
478
491
|
// that host time may yet reach.
|
|
479
492
|
if (this.pace?.stopped(this.u))
|
package/dist/motion.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export type Moving<I, O, V extends Value> = Patch<I, O, void> & {
|
|
|
44
44
|
* back at the latest frame too, however long it is before it is next read.
|
|
45
45
|
*/
|
|
46
46
|
push(subject: I, velocity: V, at?: number): void;
|
|
47
|
+
/**
|
|
48
|
+
* Puts `subject` at `value` from voice time `at`, keeping its velocity, with `at` defaulting as
|
|
49
|
+
* `push`'s does: a spring or tween heads for its target from there, a glide coasts on from there.
|
|
50
|
+
*/
|
|
51
|
+
set(subject: I, value: V, at?: number): void;
|
|
47
52
|
};
|
|
48
53
|
/**
|
|
49
54
|
* The state behind a `'motion'` patch, for an engine that reads it directly; a patch another copy
|
|
@@ -90,6 +95,11 @@ export declare function spring<I, O, V extends Value = number>(writes: keyof O,
|
|
|
90
95
|
* `ms`, and comes to rest `velocity · ms / 1000` past where it was released. `push` sets it moving
|
|
91
96
|
* again from wherever it is. Closed form, so frame rate does not change where it stops.
|
|
92
97
|
*
|
|
98
|
+
* With `bounds`, a coast that would stop past an edge springs back to it from where it crosses, at
|
|
99
|
+
* the speed it crosses at, on a critically damped spring of `stiffness`; one let go past an edge
|
|
100
|
+
* springs back from there, unless it coasts back inside. A `set` past an edge leaves the subject
|
|
101
|
+
* there, coasting on, until the next `push`, so a drag can hold it out.
|
|
102
|
+
*
|
|
93
103
|
* @category patch
|
|
94
104
|
*/
|
|
95
105
|
export declare function glide<I, O, V extends Value = number>(writes: keyof O, opts: Common<I, V> & {
|
|
@@ -97,7 +107,14 @@ export declare function glide<I, O, V extends Value = number>(writes: keyof O, o
|
|
|
97
107
|
from: PerSubject<I, V>;
|
|
98
108
|
/** The friction's time constant, ms. Default 325, a touch scroll's. */
|
|
99
109
|
ms?: number;
|
|
100
|
-
|
|
110
|
+
/** The least and greatest value a coast comes to rest at, per axis. Default none. */
|
|
111
|
+
bounds?: PerSubject<I, readonly [min: V, max: V]>;
|
|
112
|
+
/** The spring back's, per second squared, with `bounds`. Default 170, as a spring's. */
|
|
113
|
+
stiffness?: number;
|
|
114
|
+
}): Moving<I, O, V> & {
|
|
115
|
+
/** `subject`'s bounds, undefined for a glide without them. */
|
|
116
|
+
bounds(subject: I): readonly [min: V, max: V] | undefined;
|
|
117
|
+
};
|
|
101
118
|
/**
|
|
102
119
|
* From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
|
|
103
120
|
* change where a subject is. `to(subject, target)` retargets one subject mid-flight: the new stretch
|
package/dist/motion.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { COAST, CRITICAL, EASED, OVER, UNDER } from './closed.js';
|
|
1
|
+
import { BOUND, COAST, CRITICAL, EASED, OVER, UNDER } from './closed.js';
|
|
2
2
|
import { curve } from './easing.js';
|
|
3
3
|
import { Motions } from './motions.js';
|
|
4
4
|
import { release } from './shared.js';
|
|
@@ -44,6 +44,9 @@ class MotionPatch {
|
|
|
44
44
|
state.sample(s, setting.elapsed, state.xs, state.vs);
|
|
45
45
|
return { [this.writes[0]]: state.value(s, state.xs) };
|
|
46
46
|
}
|
|
47
|
+
set(subject, value, at) {
|
|
48
|
+
this.motions.change(subject, { at, x: axes(value) });
|
|
49
|
+
}
|
|
47
50
|
read(subject, at) {
|
|
48
51
|
const state = this.motions;
|
|
49
52
|
const r = state.read(subject, at);
|
|
@@ -63,9 +66,18 @@ class SpringPatch extends TweenPatch {
|
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
class GlidePatch extends MotionPatch {
|
|
69
|
+
edges;
|
|
70
|
+
constructor(writes, motions, edges) {
|
|
71
|
+
super(writes, motions);
|
|
72
|
+
this.edges = edges;
|
|
73
|
+
}
|
|
66
74
|
push(subject, velocity, at) {
|
|
67
75
|
this.motions.change(subject, { at, v: axes(velocity) });
|
|
68
76
|
}
|
|
77
|
+
bounds(subject) {
|
|
78
|
+
const b = this.edges;
|
|
79
|
+
return b === undefined ? undefined : per(b, subject);
|
|
80
|
+
}
|
|
69
81
|
}
|
|
70
82
|
/**
|
|
71
83
|
* The state behind a `'motion'` patch, for an engine that reads it directly; a patch another copy
|
|
@@ -124,12 +136,14 @@ class GlideShape {
|
|
|
124
136
|
ms = undefined;
|
|
125
137
|
start;
|
|
126
138
|
speed;
|
|
139
|
+
edges;
|
|
127
140
|
constructor(law, motion, opts, tau) {
|
|
128
141
|
this.law = law;
|
|
129
142
|
this.motion = motion;
|
|
130
143
|
this.tau = tau;
|
|
131
144
|
this.start = opts.from;
|
|
132
145
|
this.speed = opts.velocity;
|
|
146
|
+
this.edges = opts.bounds;
|
|
133
147
|
}
|
|
134
148
|
spec() {
|
|
135
149
|
return this.motion;
|
|
@@ -141,8 +155,24 @@ class GlideShape {
|
|
|
141
155
|
const v = this.speed;
|
|
142
156
|
return v === undefined ? still(x) : axes(per(v, s));
|
|
143
157
|
}
|
|
144
|
-
aim(x, v) {
|
|
145
|
-
|
|
158
|
+
aim(x, v, _was, s, put) {
|
|
159
|
+
const rest = x.map((xi, i) => xi + v[i] * this.tau);
|
|
160
|
+
if (this.edges === undefined)
|
|
161
|
+
return rest;
|
|
162
|
+
const [lo, hi] = per(this.edges, s);
|
|
163
|
+
const min = axes(lo);
|
|
164
|
+
const max = axes(hi);
|
|
165
|
+
if (min.length !== rest.length || max.length !== rest.length)
|
|
166
|
+
throw new Error('blits: bounds on a glide have as many axes as its subjects');
|
|
167
|
+
return rest.map((r, i) => {
|
|
168
|
+
const a = min[i];
|
|
169
|
+
const b = max[i];
|
|
170
|
+
const xi = x[i];
|
|
171
|
+
// A set past an edge stays there: a drag holds it out, and only a push lets it spring back.
|
|
172
|
+
if (put && (xi < a || xi > b))
|
|
173
|
+
return r;
|
|
174
|
+
return r < a ? a : r > b ? b : r;
|
|
175
|
+
});
|
|
146
176
|
}
|
|
147
177
|
scalar(s) {
|
|
148
178
|
return typeof per(this.start, s) === 'number';
|
|
@@ -226,16 +256,25 @@ export function spring(writes, opts) {
|
|
|
226
256
|
* `ms`, and comes to rest `velocity · ms / 1000` past where it was released. `push` sets it moving
|
|
227
257
|
* again from wherever it is. Closed form, so frame rate does not change where it stops.
|
|
228
258
|
*
|
|
259
|
+
* With `bounds`, a coast that would stop past an edge springs back to it from where it crosses, at
|
|
260
|
+
* the speed it crosses at, on a critically damped spring of `stiffness`; one let go past an edge
|
|
261
|
+
* springs back from there, unless it coasts back inside. A `set` past an edge leaves the subject
|
|
262
|
+
* there, coasting on, until the next `push`, so a drag can hold it out.
|
|
263
|
+
*
|
|
229
264
|
* @category patch
|
|
230
265
|
*/
|
|
231
266
|
export function glide(writes, opts) {
|
|
232
267
|
const ms = opts.ms ?? 325;
|
|
233
268
|
if (!(ms > 0))
|
|
234
269
|
throw new RangeError('blits: a glide takes a positive ms');
|
|
270
|
+
const k = opts.stiffness ?? 170;
|
|
271
|
+
if (!(k > 0))
|
|
272
|
+
throw new RangeError('blits: a glide takes a positive stiffness');
|
|
235
273
|
const tau = ms / 1000;
|
|
236
274
|
const settle = opts.settle ?? 1e-4;
|
|
237
|
-
const
|
|
238
|
-
|
|
275
|
+
const bounded = opts.bounds !== undefined;
|
|
276
|
+
const shape = new GlideShape(bounded ? [BOUND, settle, tau, Math.sqrt(k), 0] : [COAST, settle, tau, 0, 0], bounded ? { kind: 'glide', ms, settle, stiffness: k } : { kind: 'glide', ms, settle }, opts, tau);
|
|
277
|
+
return new GlidePatch(writes, new Motions(shape), opts.bounds);
|
|
239
278
|
}
|
|
240
279
|
/**
|
|
241
280
|
* From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
|
package/dist/motions.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface Change {
|
|
|
20
20
|
*/
|
|
21
21
|
at?: number;
|
|
22
22
|
to?: number[];
|
|
23
|
+
/** Where it moves from, in place of where its stretch has it then. */
|
|
24
|
+
x?: number[];
|
|
23
25
|
v?: number[];
|
|
24
26
|
/** The mix clock when it was made, NaN where no mix was playing the patch. */
|
|
25
27
|
made?: number;
|
|
@@ -73,8 +75,11 @@ export interface Shape<I> {
|
|
|
73
75
|
from: (subject: I) => number[];
|
|
74
76
|
/** How fast each axis starts moving, given where it starts: still unless the options say. */
|
|
75
77
|
velocity: (subject: I, x: number[]) => number[];
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Where it heads when released from `x` at `v`, given where it was heading; `put` for a stretch a
|
|
80
|
+
* `set` released.
|
|
81
|
+
*/
|
|
82
|
+
aim: (x: number[], v: number[], was: number[] | null, subject: I, put: boolean) => number[];
|
|
78
83
|
/** `[form, settle, k1, k2, k3]`. */
|
|
79
84
|
law: readonly number[];
|
|
80
85
|
/** Whether a subject's value is a number rather than an array. */
|
|
@@ -238,7 +243,7 @@ export declare class Motions<I> {
|
|
|
238
243
|
s: number;
|
|
239
244
|
} | undefined;
|
|
240
245
|
/**
|
|
241
|
-
* Queues a retarget or push, untimed at the latest frame, for `subject`'s next read past its time.
|
|
246
|
+
* Queues a retarget, set or push, untimed at the latest frame, for `subject`'s next read past its time.
|
|
242
247
|
* The queue keeps the changes with a time in time order, ahead of any still waiting for one.
|
|
243
248
|
*/
|
|
244
249
|
change(subject: I, c: Change): void;
|
package/dist/motions.js
CHANGED
|
@@ -126,7 +126,7 @@ export class Motions {
|
|
|
126
126
|
return known;
|
|
127
127
|
const x = this.shape.from(subject);
|
|
128
128
|
const v = this.shape.velocity(subject, x);
|
|
129
|
-
const to = this.shape.aim(x, v, null, subject);
|
|
129
|
+
const to = this.shape.aim(x, v, null, subject, false);
|
|
130
130
|
const n = this.n < 0 ? x.length : this.n;
|
|
131
131
|
this.check(x, n);
|
|
132
132
|
this.check(v, n);
|
|
@@ -465,13 +465,14 @@ export class Motions {
|
|
|
465
465
|
return { x, v, s };
|
|
466
466
|
}
|
|
467
467
|
/**
|
|
468
|
-
* Queues a retarget or push, untimed at the latest frame, for `subject`'s next read past its time.
|
|
468
|
+
* Queues a retarget, set or push, untimed at the latest frame, for `subject`'s next read past its time.
|
|
469
469
|
* The queue keeps the changes with a time in time order, ahead of any still waiting for one.
|
|
470
470
|
*/
|
|
471
471
|
change(subject, c) {
|
|
472
472
|
const back = this.owner === null ? Number.NaN : this.owner.revive(this.ownerId, subject);
|
|
473
473
|
const s = this.slot(subject);
|
|
474
474
|
this.check(c.to, this.n);
|
|
475
|
+
this.check(c.x, this.n);
|
|
475
476
|
this.check(c.v, this.n);
|
|
476
477
|
c.made = this.owner === null ? Number.NaN : this.owner.now();
|
|
477
478
|
c.seq = this.owner === null ? Number.NaN : this.owner.seq();
|
|
@@ -494,7 +495,7 @@ export class Motions {
|
|
|
494
495
|
else
|
|
495
496
|
this.queue(list, c, c.at);
|
|
496
497
|
reading.moved++;
|
|
497
|
-
const made = { at: c.at, to: c.to, v: c.v };
|
|
498
|
+
const made = { at: c.at, to: c.to, x: c.x, v: c.v };
|
|
498
499
|
this.owner?.changed(this.ownerId, this, subject, made);
|
|
499
500
|
}
|
|
500
501
|
/** Files a change with a time after every one due by then, ahead of those still waiting for one. */
|
|
@@ -671,11 +672,12 @@ export class Motions {
|
|
|
671
672
|
landing = { x: new Float64Array(n), v: new Float64Array(n) };
|
|
672
673
|
const { x, v } = landing;
|
|
673
674
|
this.evaluateSegment(seg, at, x, v);
|
|
674
|
-
const xs = numbers(x, 0, n);
|
|
675
|
+
const xs = change.x ?? numbers(x, 0, n);
|
|
675
676
|
const vs = change.v ?? numbers(v, 0, n);
|
|
676
677
|
const subject = this.numbers.subject(s);
|
|
677
678
|
const known = subject === absent ? undefined : subject;
|
|
678
|
-
const
|
|
679
|
+
const put = change.x !== undefined && change.v === undefined;
|
|
680
|
+
const to = this.shape.aim(xs, vs, change.to ?? seg.to, known, put);
|
|
679
681
|
const ms = subject === absent ? seg.ms : (this.shape.ms?.(known) ?? 0);
|
|
680
682
|
return { at, ms, x0: xs, v0: vs, to, change };
|
|
681
683
|
}
|
package/dist/move.js
CHANGED
|
@@ -9,7 +9,7 @@ import { begin } from './held.js';
|
|
|
9
9
|
import { noted } from './history.js';
|
|
10
10
|
import { leave as leaveClock } from './origin.js';
|
|
11
11
|
import { ownerReading } from './owner.js';
|
|
12
|
-
import { mixAt, place, repin, startOf } from './place.js';
|
|
12
|
+
import { mixAt, place, repin, startAt, startOf } from './place.js';
|
|
13
13
|
import { pageVoice } from './revive.js';
|
|
14
14
|
import { delist } from './roster.js';
|
|
15
15
|
import { scoredLeft, scoredRemove } from './scored.js';
|
|
@@ -47,6 +47,8 @@ function moveTo(mix, now) {
|
|
|
47
47
|
for (const a of mix.announced)
|
|
48
48
|
if (Number.isNaN(a.at))
|
|
49
49
|
a.at = mix.u;
|
|
50
|
+
if (mix.waking)
|
|
51
|
+
wake(mix, now);
|
|
50
52
|
if (mix.pins !== null && mix.pins.size > 0)
|
|
51
53
|
repin(mix);
|
|
52
54
|
if (mix.anchored > 0)
|
|
@@ -137,10 +139,31 @@ function moveTo(mix, now) {
|
|
|
137
139
|
if (mix.sharers.length > 0 && !mix.projecting)
|
|
138
140
|
outgrow(mix);
|
|
139
141
|
mix.stirred = false;
|
|
142
|
+
mix.asleep = false;
|
|
140
143
|
if (mix.retired.length > 0)
|
|
141
144
|
prune(mix);
|
|
142
145
|
forget(mix);
|
|
143
146
|
}
|
|
147
|
+
/** Starts each voice cued while the mix slept at `now`, the first sync after its cue. */
|
|
148
|
+
function wake(mix, now) {
|
|
149
|
+
mix.waking = false;
|
|
150
|
+
for (const voice of mix.cued) {
|
|
151
|
+
if (!voice.waking)
|
|
152
|
+
continue;
|
|
153
|
+
voice.waking = false;
|
|
154
|
+
if (voice.state !== 'pending')
|
|
155
|
+
continue;
|
|
156
|
+
startAt(mix, voice, now);
|
|
157
|
+
// Where a seek back before the cue puts it, so the tape's cue again starts it here.
|
|
158
|
+
const first = voice.first;
|
|
159
|
+
if (first !== null) {
|
|
160
|
+
first.start = now;
|
|
161
|
+
first.anchorNow = now;
|
|
162
|
+
}
|
|
163
|
+
if (!mix.projecting)
|
|
164
|
+
schedule(mix, voice);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
144
167
|
/**
|
|
145
168
|
* Which way a voice's clock is running at mix time `now`, through every owner above it: above 0
|
|
146
169
|
* forward, below 0 back, 0 standing still.
|
package/dist/pack.d.ts
CHANGED
package/dist/pack.js
CHANGED
|
@@ -11,6 +11,7 @@ export function packHeld(voice, h) {
|
|
|
11
11
|
shown: h.shown,
|
|
12
12
|
weight: h.weight,
|
|
13
13
|
rested: h.rested,
|
|
14
|
+
restedAt: h.restedAt,
|
|
14
15
|
bands: h.bands === null ? null : Array.from(h.bands),
|
|
15
16
|
state: h.state === undefined ? undefined : patch.pack ? patch.pack(h.state) : clone(h.state),
|
|
16
17
|
stepped: h.stepped,
|
|
@@ -40,6 +41,7 @@ export function unpackHeld(voice, p) {
|
|
|
40
41
|
h.shown = p.shown;
|
|
41
42
|
h.weight = p.weight;
|
|
42
43
|
h.rested = p.rested;
|
|
44
|
+
h.restedAt = p.restedAt;
|
|
43
45
|
h.bands = bands;
|
|
44
46
|
h.stepped = p.stepped;
|
|
45
47
|
h.ticks = p.ticks;
|
package/dist/project.js
CHANGED
|
@@ -291,6 +291,7 @@ function copyAhead(mix, c) {
|
|
|
291
291
|
pin(c, copy, at);
|
|
292
292
|
return copy;
|
|
293
293
|
});
|
|
294
|
+
c.waking = mix.waking;
|
|
294
295
|
ownBlends(c.cued);
|
|
295
296
|
// A copy keeps no list of owners, so the one before says only by its voices.
|
|
296
297
|
if (mix.owners !== null || (mix.projecting && c.cued.some((v) => v.holding !== null)))
|
package/dist/record.js
CHANGED
package/dist/seek.js
CHANGED
|
@@ -33,8 +33,7 @@ function restore(mix, voice, live, at, every, mine) {
|
|
|
33
33
|
const h = mine && live.reaches ? unread(live) : copyHeld(voice, live);
|
|
34
34
|
if (h.stepped > t)
|
|
35
35
|
h.stepped = t;
|
|
36
|
-
|
|
37
|
-
h.rested = false;
|
|
36
|
+
restedBy(h, live, t);
|
|
38
37
|
h.weight = 0;
|
|
39
38
|
h.inputs = inputs;
|
|
40
39
|
if (!(h.since <= t))
|
|
@@ -63,6 +62,7 @@ function restore(mix, voice, live, at, every, mine) {
|
|
|
63
62
|
else
|
|
64
63
|
h = copyHeld(voice, snap.held, true);
|
|
65
64
|
h.weight = 0;
|
|
65
|
+
restedBy(h, live, t);
|
|
66
66
|
if (snap.at < t)
|
|
67
67
|
h.copied = true;
|
|
68
68
|
const kept = i + 1;
|
|
@@ -108,6 +108,14 @@ function restore(mix, voice, live, at, every, mine) {
|
|
|
108
108
|
}
|
|
109
109
|
return h;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether a record put back for mix time `t` had rested by then, from when the live one did: a
|
|
113
|
+
* subject may have rested and moved on since, so a probe at `t` would not find it at rest again.
|
|
114
|
+
*/
|
|
115
|
+
function restedBy(h, live, t) {
|
|
116
|
+
h.rested = live.restedAt <= t;
|
|
117
|
+
h.restedAt = h.rested ? live.restedAt : Number.NaN;
|
|
118
|
+
}
|
|
111
119
|
/** Cuts a record's list of copies to its first `kept`, keeping those no other list holds as spares. */
|
|
112
120
|
function trim(voice, snaps, kept) {
|
|
113
121
|
while (snaps.length > kept) {
|
package/dist/shared.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
3
|
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
4
|
*/
|
|
5
|
-
export declare const release = "0.
|
|
5
|
+
export declare const release = "0.14.0";
|
|
6
6
|
/**
|
|
7
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
8
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/shared.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
3
|
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
4
|
*/
|
|
5
|
-
export const release = '0.
|
|
5
|
+
export const release = '0.14.0';
|
|
6
6
|
/**
|
|
7
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
8
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/strays.js
CHANGED
|
@@ -117,7 +117,7 @@ function changeAhead(to, id, motion, given, change) {
|
|
|
117
117
|
made.set(motion, bySubject);
|
|
118
118
|
}
|
|
119
119
|
const list = bySubject.get(subject);
|
|
120
|
-
const c = { at, to: change.to, v: change.v };
|
|
120
|
+
const c = { at, to: change.to, x: change.x, v: change.v };
|
|
121
121
|
if (list === undefined)
|
|
122
122
|
bySubject.set(subject, [c]);
|
|
123
123
|
else {
|
package/dist/types/mix.d.ts
CHANGED
|
@@ -134,7 +134,7 @@ export interface Mix<I, O, H = unknown> {
|
|
|
134
134
|
* state keeps it once, not per member. One `spec` cues them all, and a member given as a
|
|
135
135
|
* `BlendMember` carries its own `name`, `tags` and `hits` in place of the spec's.
|
|
136
136
|
*/
|
|
137
|
-
blend(members: readonly (Patch<I, O,
|
|
137
|
+
blend(members: readonly (Patch<I, O, any, H> | BlendMember<I, O, H>)[], by: Signal<I, H>, spec?: Omit<VoiceSpec<I, O, H>, 'patch' | 'weight' | 'locus'>): Handle<I>[];
|
|
138
138
|
/**
|
|
139
139
|
* Cues an owner: a voice with no patch that holds the voices cued with `owner` set to its handle,
|
|
140
140
|
* and plays them on its own clock, so they can be placed, timed and faded as one. Its handle acts
|
|
@@ -308,7 +308,9 @@ export interface Mix<I, O, H = unknown> {
|
|
|
308
308
|
* Another frame would change no pose, so a host's frame loop may sleep: every voice is done, frozen
|
|
309
309
|
* at a plain weight, or a motion whose every subject has landed on its target. At rate 0 it is
|
|
310
310
|
* enough that every voice not done has started, at a plain weight, with no anchor. A change made
|
|
311
|
-
* since the last sync, a retarget or a rate included, makes it false until the next.
|
|
311
|
+
* since the last sync, a retarget or a rate included, makes it false until the next. Read as true,
|
|
312
|
+
* it tells the mix the host may sleep: a voice cued with no `start` before the next sync starts
|
|
313
|
+
* at that sync.
|
|
312
314
|
*/
|
|
313
315
|
readonly inert: boolean;
|
|
314
316
|
/**
|
package/dist/types/patch.d.ts
CHANGED
|
@@ -110,6 +110,8 @@ export type MotionSpec = {
|
|
|
110
110
|
/** The friction's time constant, ms. */
|
|
111
111
|
readonly ms: number;
|
|
112
112
|
readonly settle: number;
|
|
113
|
+
/** The spring back's, per second squared; only on a glide with bounds. */
|
|
114
|
+
readonly stiffness?: number;
|
|
113
115
|
} | {
|
|
114
116
|
readonly kind: 'tween';
|
|
115
117
|
/** How long a stretch takes; undefined where it varies by subject, kept on the patch then. */
|
package/dist/types/voice.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface SpanHints {
|
|
|
47
47
|
* @category voice
|
|
48
48
|
*/
|
|
49
49
|
export interface VoiceSpec<I, O, H = unknown> extends SpanHints {
|
|
50
|
-
patch: Patch<I, O,
|
|
50
|
+
patch: Patch<I, O, any, H>;
|
|
51
51
|
/**
|
|
52
52
|
* What `history.revive` rebuilds this spec from, so a history `store` can page the voice out once
|
|
53
53
|
* it has left. A voice cued without it stays in memory for as long as history reaches it.
|
|
@@ -72,7 +72,9 @@ export interface VoiceSpec<I, O, H = unknown> extends SpanHints {
|
|
|
72
72
|
* When the voice starts, in ms on the host's clock, the one it passes `sync`: a rAF timestamp and
|
|
73
73
|
* `performance.now()` share an origin, `Date.now()` does not. Default: the time the mix last
|
|
74
74
|
* synced to, and 0 for a voice cued before the first sync, which that sync finds already as far
|
|
75
|
-
* in as its timestamp.
|
|
75
|
+
* in as its timestamp. Where the host read `inert` as true since the last sync, its frame loop
|
|
76
|
+
* may be asleep, and the default is the next sync instead, so a voice cued after a long pause
|
|
77
|
+
* starts there rather than that far in.
|
|
76
78
|
*/
|
|
77
79
|
start?: number;
|
|
78
80
|
/**
|
|
@@ -175,7 +177,7 @@ export interface VoiceSpec<I, O, H = unknown> extends SpanHints {
|
|
|
175
177
|
* @category blending
|
|
176
178
|
*/
|
|
177
179
|
export interface BlendMember<I, O, H = unknown> {
|
|
178
|
-
patch: Patch<I, O,
|
|
180
|
+
patch: Patch<I, O, any, H>;
|
|
179
181
|
/** What other voices call this member by. Default: the spec's `name`. */
|
|
180
182
|
name?: string;
|
|
181
183
|
/** Default: the spec's `tags`. */
|
|
@@ -384,6 +386,11 @@ export interface Handle<I = unknown> {
|
|
|
384
386
|
* `glide`: the patch's own `push`. Throws for any other voice.
|
|
385
387
|
*/
|
|
386
388
|
push(subject: I, velocity: Value, at?: number): void;
|
|
389
|
+
/**
|
|
390
|
+
* Puts `subject` at `value`, keeping its velocity, for a voice playing any motion patch: the
|
|
391
|
+
* patch's own `set`. Throws for any other voice.
|
|
392
|
+
*/
|
|
393
|
+
set(subject: I, value: Value, at?: number): void;
|
|
387
394
|
/**
|
|
388
395
|
* Where `subject` is and how fast it moves, for a voice playing a motion patch: the patch's own
|
|
389
396
|
* `read`. Undefined for any other voice, and wherever the patch's `read` is.
|
package/dist/voice.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface Subject<S> {
|
|
|
86
86
|
weight: number;
|
|
87
87
|
/** Left at rest during a handover, so it stops contributing. */
|
|
88
88
|
rested: boolean;
|
|
89
|
+
/** The mix time it rested at, which a seek reads to know whether it had by then; NaN until it does. */
|
|
90
|
+
restedAt: number;
|
|
89
91
|
/**
|
|
90
92
|
* Per written channel, whether a rest-less contribution is on: 0 unknown, 1 on, 2 off; null until
|
|
91
93
|
* a rest-less channel asks.
|
|
@@ -333,6 +335,8 @@ export declare class Voice<I, O> {
|
|
|
333
335
|
outSet: boolean;
|
|
334
336
|
/** The host time a pinned start was given at, NaN for none: a seek back pins it again. */
|
|
335
337
|
pinned: number;
|
|
338
|
+
/** Cued with no start while its mix slept, so the next sync sets its start. */
|
|
339
|
+
waking: boolean;
|
|
336
340
|
/** Whether its start is still to be fixed by an anchor, so it waits pending. */
|
|
337
341
|
placing: boolean;
|
|
338
342
|
/** What each member of a join last answered, kept for once its target has left. */
|
package/dist/voice.js
CHANGED
|
@@ -226,6 +226,8 @@ export class Voice {
|
|
|
226
226
|
outSet = false;
|
|
227
227
|
/** The host time a pinned start was given at, NaN for none: a seek back pins it again. */
|
|
228
228
|
pinned = Number.NaN;
|
|
229
|
+
/** Cued with no start while its mix slept, so the next sync sets its start. */
|
|
230
|
+
waking = false;
|
|
229
231
|
/** Whether its start is still to be fixed by an anchor, so it waits pending. */
|
|
230
232
|
placing = false;
|
|
231
233
|
/** What each member of a join last answered, kept for once its target has left. */
|
package/docs/schema.html
CHANGED
|
@@ -512,12 +512,13 @@ interface Handle<I = unknown> {
|
|
|
512
512
|
<span class="cm">/** The weight this voice gave `subject` the last frame it was probed: after fades and its signal, before a
|
|
513
513
|
locus folds it. 0 where it gave none. One number on a record the mix keeps anyway, so free unasked. */</span>
|
|
514
514
|
weightOf(subject: I): number;
|
|
515
|
-
<span class="cm">/** A motion patch's own `to`, `push`, and `read`, reached without holding the patch; `at` is a voice time. `to`
|
|
516
|
-
throws for a voice not playing a `spring` or a `tween`,
|
|
517
|
-
`read` gives undefined for a voice playing no motion patch. `Value` is a number or an array of them, and
|
|
515
|
+
<span class="cm">/** A motion patch's own `to`, `push`, `set`, and `read`, reached without holding the patch; `at` is a voice time. `to`
|
|
516
|
+
throws for a voice not playing a `spring` or a `tween`, `push` for one not playing a `spring` or a `glide`, and
|
|
517
|
+
`set` for one playing no motion patch; `read` gives undefined for a voice playing no motion patch. `Value` is a number or an array of them, and
|
|
518
518
|
`Motion` is `{ value, velocity }`. See Springs, glides and tweens. */</span>
|
|
519
519
|
to(subject: I, target: Value, at?: number): void;
|
|
520
520
|
push(subject: I, velocity: Value, at?: number): void;
|
|
521
|
+
set(subject: I, value: Value, at?: number): void;
|
|
521
522
|
read(subject: I, at?: number): Motion<Value> | undefined;
|
|
522
523
|
<span class="cm">/** Resolves when the voice has been removed from the mix, however that happened. */</span>
|
|
523
524
|
readonly done: Promise<void>;
|
|
@@ -755,9 +756,14 @@ mix.book({
|
|
|
755
756
|
mix.cue({ patch: s });
|
|
756
757
|
s.to(node, 40); <span class="cm">// heads elsewhere from wherever it is, keeping its velocity</span>
|
|
757
758
|
s.push(node, 600); <span class="cm">// a kick: same target, new velocity</span>
|
|
759
|
+
s.set(node, 20); <span class="cm">// moved there: same target, same velocity</span>
|
|
758
760
|
s.read(node); <span class="cm">// { value, velocity }, for handing the motion to another patch</span>
|
|
759
761
|
|
|
760
762
|
const g = glide<Node, Pose>('x', { from: 0, velocity: 1200, ms: 325 }); <span class="cm">// a fling: rests 390 past the start</span>
|
|
763
|
+
g.set(node, 80); <span class="cm">// under a dragging pointer</span>
|
|
764
|
+
g.push(node, -900); <span class="cm">// let go: coasts on from there</span>
|
|
765
|
+
|
|
766
|
+
const list = glide<Node, Pose>('y', { from: 0, bounds: [-2400, 0] }); <span class="cm">// springs back from an edge it coasts past</span>
|
|
761
767
|
|
|
762
768
|
const t = tween<Node, Pose>('x', { from: (n) => n.x, to: (n) => n.goal, ms: 400, ease: 'ease-out' });
|
|
763
769
|
t.to(node, 40); <span class="cm">// from wherever it is, over a full 400 ms again</span></code></pre>
|
|
@@ -765,6 +771,8 @@ t.to(node, 40); <span class="cm">// from wherever it is, over a ful
|
|
|
765
771
|
<ul>
|
|
766
772
|
<li><b>Time is the voice's</b>, so <code>rate</code> and <code>seek</code> apply. A change takes effect at the voice time it names, or, named none, at the subject's own voice time at the mix's latest frame, whether or not the host probed it then; naming one is what makes a retarget land the same at any frame rate. A subject no frame of its voice has met yet, or still inside its stagger, takes an unnamed change at its first read, after any named change due by then. <code>read</code> with no time answers at that same latest frame, counting every change due by then, and answers nothing once the voice is gone.</li>
|
|
767
773
|
<li><b>History is kept to the mix's horizon.</b> Each subject keeps every stretch a read may still reach, so a read earlier than a change finds the stretch that was playing then. A mix without <code>history</code> keeps only the stretch playing now.</li>
|
|
774
|
+
<li id="fling"><b>Dragging and flinging.</b> <code>@msb235/blits/fling</code> wires pointer events to a motion patch: <code>fling(element, glide, subject, { axis, scale })</code> stops the subject where it is on a press, <code>set</code>s it under the pointer on each move, and <code>push</code>es it on release at the pointer’s velocity, which <code>track</code> reads as the least-squares slope of the last 100 ms of samples, or zero if the pointer sat still for 40 ms before letting go. <code>track</code> has no listeners, so anything with a time and a position can feed it. It is a subpath because it is the package’s only code that knows a page exists, and it asks only for <code>addEventListener</code>, <code>removeEventListener</code> and <code>setPointerCapture</code>, so the package builds without the DOM’s types. Since the coast is a voice like any other, other voices read it: a signal over <code>glide.read(subject)</code> drives parallax off its value or a blur off its velocity. Past an edge of a glide's <code>bounds</code>, a drag carries the subject less and less of the way, never more than <code>give</code> (default 120 units) past: at the edge it follows 0.55 of the drag, as iOS's scroll views do, and <code>give: 0</code> holds it at the edge. A press on a subject springing back past an edge holds it where it is. Added 2026-10-24.</li>
|
|
775
|
+
<li id="bounds"><b>Bounds.</b> <code>glide(…, { bounds: [min, max], stiffness })</code> gives each axis the least and greatest value a coast comes to rest at, the same for every subject or one pair per subject; <code>g.bounds(subject)</code> reads them back. A coast that would stop inside them is the glide without bounds, to the bit. One that would stop past an edge coasts until it crosses it, then springs back on a critically damped spring of <code>stiffness</code> (default 170, a spring’s), starting at the speed it crossed at, so neither where it is nor how fast it moves jumps. One let go already past an edge springs back from there, unless its coast carries it back inside, when it coasts with no spring at all. A <code>set</code> past an edge leaves the subject there, coasting on, until the next <code>push</code>: that is how a drag holds it out, and why a <code>push(subject, 0)</code> lets it go. It needed no new state: a stretch still holds where it starts, how fast, and where it heads, which is now the edge when the coast stops past one, and the closed form tells the three cases apart from those numbers alone, so lanes, seeks and history carry it as they carry any glide. Added 2026-10-24.</li>
|
|
768
776
|
<li><b>A spring or a glide settles.</b> Within <code>settle</code> (1e-4) of where it heads, and that slow, it lands there exactly, so <code>atRest</code> sees it. <code>settle: 0</code> keeps the curve exact. A tween needs none: it lands on its target exactly at <code>ms</code>.</li>
|
|
769
777
|
<li><b>The state is the patch's own</b>, per subject, rather than the mix's, because the host retargets through the patch, or through <code>handle.to</code>, <code>push</code>, and <code>read</code>, which reach it. Cue each one on one voice. A patch moves every subject on the same number of axes, and throws on a subject, a <code>to</code> or a <code>push</code> with another, leaving the subject as it was. Its values are the kind its channel holds, a number for a number and an array for an array: one every subject shares is refused at <code>cue</code>, one given per subject on that subject's first sample.</li>
|
|
770
778
|
<li><b>The constants are data.</b> <code>patch.motion</code>, a <code>MotionSpec</code>, is <code>{ kind: 'spring', stiffness, damping, mass, settle }</code> or <code>{ kind: 'glide', ms, settle }</code> or <code>{ kind: 'tween', ms, ease }</code>, with <code>ms</code> undefined where it varies by subject, so an engine can run one without calling <code>at</code>. Where each subject starts and heads stays on the patch, since either may be a function of the subject.</li>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msb235/blits",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Concurrent effects: each on its own clock with its own state and weight, folded into one value per subject per frame",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"animation",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"./lean": {
|
|
35
35
|
"types": "./dist/lean.d.ts",
|
|
36
36
|
"default": "./dist/lean.js"
|
|
37
|
+
},
|
|
38
|
+
"./fling": {
|
|
39
|
+
"types": "./dist/fling.d.ts",
|
|
40
|
+
"default": "./dist/fling.js"
|
|
37
41
|
}
|
|
38
42
|
},
|
|
39
43
|
"files": [
|