@solidrt/core 0.0.64 → 0.0.65
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/AGENTS.md +92 -9
- package/README.md +1 -1
- package/agents/performance.md +7 -2
- package/docs/index.md +9 -0
- package/package.json +7 -5
- package/src/core.ts +6 -1
- package/src/double-tap.ts +2 -2
- package/src/gamepad.ts +32 -1
- package/src/gpu.ts +6 -5
- package/src/image.ts +10 -2
- package/src/index.ts +6 -4
- package/src/input-gamepad-device.ts +38 -14
- package/src/input-gamepad.ts +6 -4
- package/src/input-keyboard.ts +11 -2
- package/src/input-map.ts +12 -1
- package/src/input-pointer.ts +3 -3
- package/src/input-processors.ts +65 -25
- package/src/pan.ts +4 -3
- package/src/renderer.ts +1 -1
- package/src/runtime-modules.d.ts +68 -1
- package/src/swipe.ts +6 -6
- package/src/textures.ts +333 -0
- package/src/transform.ts +16 -7
- package/src/types.d.ts +43 -1
- package/src/velocity.ts +113 -50
- package/src/window.ts +41 -3
package/src/transform.ts
CHANGED
|
@@ -21,8 +21,8 @@ const SPAN_SMOOTH = 0.28
|
|
|
21
21
|
// thresholds is the hysteresis that keeps the gate from chattering.
|
|
22
22
|
// All three step-based constants assume a ~65Hz frame rate (the captured
|
|
23
23
|
// tablet); a 120Hz panel halves per-step deltas. If that ever bites,
|
|
24
|
-
// convert the thresholds to px/s with
|
|
25
|
-
//
|
|
24
|
+
// convert the thresholds to px/s with the pointerFrame terminator's
|
|
25
|
+
// timeStamp.
|
|
26
26
|
const QUIET = 0.3
|
|
27
27
|
const ENGAGE = 0.6
|
|
28
28
|
const QUIET_SMOOTH = 0.19
|
|
@@ -147,6 +147,8 @@ export function createTransform(options: TransformOptions) {
|
|
|
147
147
|
let spanRate = 0
|
|
148
148
|
// Motion arrived this frame; measured and emitted at the terminator.
|
|
149
149
|
let dirty = false
|
|
150
|
+
// A move of the current batch was predicted (see flush).
|
|
151
|
+
let predicted = false
|
|
150
152
|
// The anchor must be retaken at the terminator (activation and set-change
|
|
151
153
|
// rebases): mid-batch the pointer map is mixed-age, so anchoring
|
|
152
154
|
// immediately would bake a jolt into the next delta. A rebase frame
|
|
@@ -208,8 +210,14 @@ export function createTransform(options: TransformOptions) {
|
|
|
208
210
|
let owner: ArenaOwner = { cancel }
|
|
209
211
|
|
|
210
212
|
// The per-frame measure point: runs at the pointerFrame terminator, when
|
|
211
|
-
// every tracked pointer's
|
|
212
|
-
|
|
213
|
+
// every tracked pointer's move of the batch has dispatched: the
|
|
214
|
+
// terminator's timeStamp (the latest of theirs) is the time of what is
|
|
215
|
+
// measured here.
|
|
216
|
+
let flush = (frame: { timeStamp: number }) => {
|
|
217
|
+
// The measure follows a predicted position; the velocity history takes
|
|
218
|
+
// real ones only.
|
|
219
|
+
let real = !predicted
|
|
220
|
+
predicted = false
|
|
213
221
|
if (!active) return
|
|
214
222
|
if (rebase) {
|
|
215
223
|
// Anchor from same-age positions; emits nothing - an activation or
|
|
@@ -219,7 +227,7 @@ export function createTransform(options: TransformOptions) {
|
|
|
219
227
|
let prev = ref
|
|
220
228
|
ref = measure()
|
|
221
229
|
if (prev) tracker.shift(ref.px - prev.px, ref.py - prev.py)
|
|
222
|
-
tracker.push(ref.px, ref.py)
|
|
230
|
+
if (real) tracker.push(ref.px, ref.py, frame.timeStamp)
|
|
223
231
|
spanBase = ref.span
|
|
224
232
|
smoothSpan = ref.span
|
|
225
233
|
rebase = false
|
|
@@ -229,7 +237,7 @@ export function createTransform(options: TransformOptions) {
|
|
|
229
237
|
if (!dirty || !ref) return
|
|
230
238
|
dirty = false
|
|
231
239
|
let m = measure()
|
|
232
|
-
tracker.push(m.px, m.py)
|
|
240
|
+
if (real) tracker.push(m.px, m.py, frame.timeStamp)
|
|
233
241
|
let prevSpan = smoothSpan
|
|
234
242
|
smoothSpan += (m.span - smoothSpan) * SPAN_SMOOTH
|
|
235
243
|
spanRate += (Math.abs(smoothSpan - prevSpan) - spanRate) * QUIET_SMOOTH
|
|
@@ -285,6 +293,7 @@ export function createTransform(options: TransformOptions) {
|
|
|
285
293
|
onPointerMove: (e: PointerEvent) => {
|
|
286
294
|
if (!pointers.has(e.pointerId) || !ref) return
|
|
287
295
|
pointers.set(e.pointerId, at(e))
|
|
296
|
+
if (e.predicted) predicted = true
|
|
288
297
|
if (!active) {
|
|
289
298
|
// Arming runs per event: the slop test tolerates a mixed-age
|
|
290
299
|
// measure (8px against <=1 frame of staleness), and the arena
|
|
@@ -328,7 +337,7 @@ export function createTransform(options: TransformOptions) {
|
|
|
328
337
|
if (pointers.size === 0) {
|
|
329
338
|
// The lift's own position is not measured (no terminator
|
|
330
339
|
// follows it); the fit over the delivered frames is the speed.
|
|
331
|
-
let velocity = flingVelocity(tracker.velocity())
|
|
340
|
+
let velocity = flingVelocity(tracker.velocity(e.timeStamp))
|
|
332
341
|
active = false
|
|
333
342
|
ref = null
|
|
334
343
|
pinch = false
|
package/src/types.d.ts
CHANGED
|
@@ -280,6 +280,33 @@ export interface TransformProps {
|
|
|
280
280
|
// the DOM MouseEvent). pointerType distinguishes mouse from touch; button is the
|
|
281
281
|
// pressed button on down/up (0 = primary); the modifier flags mirror the DOM.
|
|
282
282
|
export interface PointerEvent {
|
|
283
|
+
/**
|
|
284
|
+
* When the event happened, in milliseconds. Every input event carries one,
|
|
285
|
+
* on one clock, so two stamps subtract to the time between two events: a
|
|
286
|
+
* tap's length, the gap of a double tap, the age of the last move at a
|
|
287
|
+
* lift. It is the input's own time, not its delivery's: a move carries the
|
|
288
|
+
* time the pointer was at the position it reports, however late the
|
|
289
|
+
* frame that delivers it, and a down, an up, a wheel or a key the time
|
|
290
|
+
* the platform recorded for it. A `predicted` move runs ahead of its
|
|
291
|
+
* pointer, so the real move after it can carry an earlier time. Use it
|
|
292
|
+
* where you would reach for `performance.now()` in a handler: that one
|
|
293
|
+
* measures when the handler ran, not when the input happened, and it does
|
|
294
|
+
* not follow the dev clock, `srt render` or a test. The origin is
|
|
295
|
+
* arbitrary (compare stamps with each other, not with `performance.now()`,
|
|
296
|
+
* a timer or the `onFrame` tick).
|
|
297
|
+
*/
|
|
298
|
+
timeStamp: number
|
|
299
|
+
/**
|
|
300
|
+
* True for a move whose position the pointer was never reported at. Touch
|
|
301
|
+
* is delivered in batches, and a batch can miss the frame it belongs to;
|
|
302
|
+
* the runtime then bridges the gap with one predicted step, so a drag
|
|
303
|
+
* does not stall and jump. Draw with it, as with any move. Do not measure
|
|
304
|
+
* with it: leave it out of a velocity or a path. If the finger had in
|
|
305
|
+
* fact stopped, the next move reports where it really is, with the time
|
|
306
|
+
* it got there, which is earlier than the predicted move's. False on
|
|
307
|
+
* every other event.
|
|
308
|
+
*/
|
|
309
|
+
predicted: boolean
|
|
283
310
|
clientX: number
|
|
284
311
|
clientY: number
|
|
285
312
|
/**
|
|
@@ -340,6 +367,9 @@ export interface WheelEvent extends PointerEvent {
|
|
|
340
367
|
// to the window root alone. <window onKeyDown> is therefore the app-global
|
|
341
368
|
// shortcut point.
|
|
342
369
|
export interface KeyEvent {
|
|
370
|
+
/** When the key event happened, in milliseconds, on the clock of
|
|
371
|
+
* `PointerEvent.timeStamp`. */
|
|
372
|
+
timeStamp: number
|
|
343
373
|
key: string
|
|
344
374
|
code: string
|
|
345
375
|
repeat: boolean
|
|
@@ -363,7 +393,19 @@ export interface TextEvent {
|
|
|
363
393
|
text: string
|
|
364
394
|
}
|
|
365
395
|
|
|
366
|
-
export interface
|
|
396
|
+
export interface NodeProps {
|
|
397
|
+
/**
|
|
398
|
+
* A stable name for the node, for whoever has to find it again with
|
|
399
|
+
* nothing visible to find it by: a test (`find({ label })` from
|
|
400
|
+
* `@solidrt/test`) or a dev tool (the tree record carries it and the
|
|
401
|
+
* tree query matches it). Never affects layout, paint or input. Prefer
|
|
402
|
+
* finding a node by its text where it has one: such a test breaks when
|
|
403
|
+
* what the user sees breaks.
|
|
404
|
+
*/
|
|
405
|
+
label?: string
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface PointerProps extends NodeProps {
|
|
367
409
|
onPointerDown?: (event: PointerEvent) => void
|
|
368
410
|
onPointerUp?: (event: PointerEvent) => void
|
|
369
411
|
onPointerMove?: (event: PointerEvent) => void
|
package/src/velocity.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// The velocity tracker every recognizer reads at a lift: the pointer's
|
|
2
2
|
// speed from the positions of its last VELOCITY_WINDOW_MS as a
|
|
3
|
-
// least-squares
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// least-squares quadratic read at the newest sample (Flutter's
|
|
4
|
+
// VelocityTracker, Android's LSQ2), not the last two samples. The final
|
|
5
|
+
// sample before a lift is often stationary, and frame batching makes the
|
|
6
|
+
// last delta a whole frame old, so a two-point estimate reads anything from
|
|
7
|
+
// zero to a jolt; a fit over the window reads the finger's actual speed.
|
|
8
|
+
// Quadratic, not a line: a flick accelerates into the lift, and a line
|
|
9
|
+
// reads the window's average speed, well under the speed at the lift. The
|
|
10
|
+
// line stays as the guard: a quadratic overshoots under a hard brake and
|
|
11
|
+
// can point backward, so an axis where the two disagree in direction
|
|
12
|
+
// reads zero. One
|
|
7
13
|
// estimator whatever recognizer asks (createPan, createTransform, the
|
|
8
14
|
// pointer feed), so the 2d camera and a control after it never re-derive
|
|
9
15
|
// their own.
|
|
@@ -11,8 +17,11 @@
|
|
|
11
17
|
// Samples are positions in whatever frame the caller measures (the
|
|
12
18
|
// recognizers push the node's parent frame, so the velocity applies 1:1
|
|
13
19
|
// as their deltas do; a swipe classifier pushes window pixels, the
|
|
14
|
-
// finger's own travel) with
|
|
15
|
-
//
|
|
20
|
+
// finger's own travel), each with the `timeStamp` of the pointer event it
|
|
21
|
+
// came from. The tracker reads no clock: the read at a lift takes the
|
|
22
|
+
// up's `timeStamp`, and a move's stamp is the time of the position it
|
|
23
|
+
// carries, so the age of the last one is how long the pointer had been
|
|
24
|
+
// there, at any frame rate.
|
|
16
25
|
|
|
17
26
|
// Fit horizon: Flutter's and Android's VelocityTracker window.
|
|
18
27
|
const VELOCITY_WINDOW_MS = 100
|
|
@@ -21,11 +30,26 @@ const VELOCITY_WINDOW_MS = 100
|
|
|
21
30
|
const VELOCITY_MAX = 8000
|
|
22
31
|
// A position unchanged for this long at the read means the finger rested
|
|
23
32
|
// before lifting: the velocity is zero, whatever the window still holds.
|
|
24
|
-
// Measured from the
|
|
25
|
-
// the
|
|
26
|
-
//
|
|
27
|
-
// re-delivery must not restart the clock either.
|
|
33
|
+
// Measured from when the pointer reached the position it is at, not from
|
|
34
|
+
// the newest sample's age: a same-position re-delivery must not restart
|
|
35
|
+
// the clock.
|
|
28
36
|
const VELOCITY_REST_MS = 50
|
|
37
|
+
// A gap this long (ms) between two consecutive samples means the pointer
|
|
38
|
+
// stopped in between (Flutter's and Android's assume-stopped time): what
|
|
39
|
+
// came before the gap is an earlier motion and stays out of the fit. A
|
|
40
|
+
// slow drag delivers a sample only when the finger moves, so its history
|
|
41
|
+
// is full of such pauses.
|
|
42
|
+
const VELOCITY_STOP_GAP_MS = 40
|
|
43
|
+
// Travel (px) the fitted samples must cover to be a motion at all. A finger
|
|
44
|
+
// leaving the panel shifts its contact by a pixel or two in its last
|
|
45
|
+
// samples (measured: up to 2.7 px over 8 to 25 ms on a tablet), which a
|
|
46
|
+
// fit over so short a span reads as 60 to 450 px/s. The pan slop's
|
|
47
|
+
// distance: under it nothing was dragged.
|
|
48
|
+
const VELOCITY_MIN_TRAVEL = 8
|
|
49
|
+
// Samples closer in time than this (ms) are one instant to the fit: two
|
|
50
|
+
// samples of one frame carry the same stamp, and a curve pinned by such a
|
|
51
|
+
// pair reads a jolt.
|
|
52
|
+
const VELOCITY_MIN_STEP_MS = 1
|
|
29
53
|
// Ring capacity: a 100 ms window holds 12 samples at 120 Hz; the rest is
|
|
30
54
|
// room for a burst of same-frame samples.
|
|
31
55
|
const VELOCITY_SAMPLES = 20
|
|
@@ -37,16 +61,20 @@ export const FLING_MIN_VELOCITY = 50
|
|
|
37
61
|
export type Velocity = { vx: number; vy: number }
|
|
38
62
|
|
|
39
63
|
export interface VelocityTracker {
|
|
40
|
-
/** Record a position
|
|
41
|
-
|
|
64
|
+
/** Record a position at time `at`, ms: the `timeStamp` of the event
|
|
65
|
+
* that carried it. Real positions only: a `predicted` move is not where
|
|
66
|
+
* the pointer was, and is left out. */
|
|
67
|
+
push(x: number, y: number, at: number): void
|
|
42
68
|
/** Translate every sample: a recognizer rebasing its reference point
|
|
43
69
|
* (a finger joining or leaving a transform) keeps the history
|
|
44
70
|
* continuous instead of dropping it. */
|
|
45
71
|
shift(dx: number, dy: number): void
|
|
46
72
|
reset(): void
|
|
47
|
-
/** The speed at `at` (
|
|
48
|
-
* over the window
|
|
49
|
-
|
|
73
|
+
/** The speed at time `at` (the lift's `timeStamp`), px/s per axis: a
|
|
74
|
+
* least-squares quadratic over the window read at its newest sample,
|
|
75
|
+
* zero after a rest or when the motion since the last pause is a
|
|
76
|
+
* twitch, clamped to VELOCITY_MAX. */
|
|
77
|
+
velocity(at: number): Velocity
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
const ZERO: Velocity = { vx: 0, vy: 0 }
|
|
@@ -61,15 +89,8 @@ export function createVelocityTracker(): VelocityTracker {
|
|
|
61
89
|
// Ring: `count` samples ending at index `head - 1`.
|
|
62
90
|
let head = 0
|
|
63
91
|
let count = 0
|
|
64
|
-
// When the position last changed (the rest rule's clock).
|
|
65
|
-
let movedAt = -Infinity
|
|
66
92
|
return {
|
|
67
|
-
push(x, y, at
|
|
68
|
-
if (count === 0) movedAt = at
|
|
69
|
-
else {
|
|
70
|
-
let last = (head - 1 + VELOCITY_SAMPLES) % VELOCITY_SAMPLES
|
|
71
|
-
if (xs[last] !== x || ys[last] !== y) movedAt = at
|
|
72
|
-
}
|
|
93
|
+
push(x, y, at) {
|
|
73
94
|
xs[head] = x
|
|
74
95
|
ys[head] = y
|
|
75
96
|
ts[head] = at
|
|
@@ -87,41 +108,83 @@ export function createVelocityTracker(): VelocityTracker {
|
|
|
87
108
|
head = 0
|
|
88
109
|
count = 0
|
|
89
110
|
},
|
|
90
|
-
velocity(at
|
|
111
|
+
velocity(at) {
|
|
91
112
|
if (count < 2) return ZERO
|
|
113
|
+
let newest = (head - 1 + VELOCITY_SAMPLES) % VELOCITY_SAMPLES
|
|
114
|
+
// The rest rule's clock: when the pointer reached the position it is
|
|
115
|
+
// at, the oldest of the newest samples that share it.
|
|
116
|
+
let movedAt = ts[newest]!
|
|
117
|
+
for (let i = 1; i < count; i++) {
|
|
118
|
+
let k = (head - 1 - i + VELOCITY_SAMPLES) % VELOCITY_SAMPLES
|
|
119
|
+
if (xs[k] !== xs[newest] || ys[k] !== ys[newest]) break
|
|
120
|
+
movedAt = ts[k]!
|
|
121
|
+
}
|
|
92
122
|
if (at - movedAt > VELOCITY_REST_MS) return ZERO
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let
|
|
123
|
+
// Sums over the window, time and position measured from the newest
|
|
124
|
+
// sample so the fit's slope at t = 0 is the answer: the power sums
|
|
125
|
+
// of t, each axis's moments against them, and how many distinct
|
|
126
|
+
// times (VELOCITY_MIN_STEP_MS apart) the window holds.
|
|
127
|
+
let s0 = 0
|
|
128
|
+
let s1 = 0
|
|
129
|
+
let s2 = 0
|
|
130
|
+
let s3 = 0
|
|
131
|
+
let s4 = 0
|
|
132
|
+
let x0 = 0
|
|
133
|
+
let x1 = 0
|
|
134
|
+
let x2 = 0
|
|
135
|
+
let y0 = 0
|
|
136
|
+
let y1 = 0
|
|
137
|
+
let y2 = 0
|
|
138
|
+
let times = 0
|
|
139
|
+
let prev = Infinity
|
|
140
|
+
// The sample after this one in time, and the fit's oldest position.
|
|
141
|
+
let later = ts[newest]!
|
|
142
|
+
let farX = 0
|
|
143
|
+
let farY = 0
|
|
98
144
|
for (let i = 0; i < count; i++) {
|
|
99
145
|
let k = (head - 1 - i + VELOCITY_SAMPLES) % VELOCITY_SAMPLES
|
|
100
146
|
if (at - ts[k]! > VELOCITY_WINDOW_MS) break
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
if (later - ts[k]! > VELOCITY_STOP_GAP_MS) break
|
|
148
|
+
later = ts[k]!
|
|
149
|
+
let t = ts[k]! - ts[newest]!
|
|
150
|
+
if (prev - t >= VELOCITY_MIN_STEP_MS) {
|
|
151
|
+
times++
|
|
152
|
+
prev = t
|
|
153
|
+
}
|
|
154
|
+
let x = xs[k]! - xs[newest]!
|
|
155
|
+
let y = ys[k]! - ys[newest]!
|
|
156
|
+
farX = x
|
|
157
|
+
farY = y
|
|
158
|
+
let tt = t * t
|
|
159
|
+
s0 += 1
|
|
160
|
+
s1 += t
|
|
161
|
+
s2 += tt
|
|
162
|
+
s3 += tt * t
|
|
163
|
+
s4 += tt * tt
|
|
164
|
+
x0 += x
|
|
165
|
+
x1 += t * x
|
|
166
|
+
x2 += tt * x
|
|
167
|
+
y0 += y
|
|
168
|
+
y1 += t * y
|
|
169
|
+
y2 += tt * y
|
|
119
170
|
}
|
|
120
171
|
// Every sample the same age (one batched frame): no slope to read.
|
|
121
|
-
if (
|
|
172
|
+
if (times < 2) return ZERO
|
|
173
|
+
if (Math.hypot(farX, farY) < VELOCITY_MIN_TRAVEL) return ZERO
|
|
174
|
+
// One axis's slope at t = 0, px per ms: the quadratic
|
|
175
|
+
// c0 + c1 t + c2 t^2 (c1 by Cramer's rule) once three distinct times
|
|
176
|
+
// pin it, the line before that; zero where the quadratic points
|
|
177
|
+
// against the line.
|
|
178
|
+
let det = s0 * (s2 * s4 - s3 * s3) - s1 * (s1 * s4 - s2 * s3) + s2 * (s1 * s3 - s2 * s2)
|
|
179
|
+
let slope = (m0: number, m1: number, m2: number) => {
|
|
180
|
+
let line = (s0 * m1 - s1 * m0) / (s0 * s2 - s1 * s1)
|
|
181
|
+
if (times < 3) return line
|
|
182
|
+
let curve = (s0 * (m1 * s4 - s3 * m2) - m0 * (s1 * s4 - s3 * s2) + s2 * (s1 * m2 - m1 * s2)) / det
|
|
183
|
+
return curve * line < 0 ? 0 : curve
|
|
184
|
+
}
|
|
122
185
|
// Slopes are px per ms; the result is px per second.
|
|
123
|
-
let vx = (
|
|
124
|
-
let vy = (
|
|
186
|
+
let vx = slope(x0, x1, x2) * 1000
|
|
187
|
+
let vy = slope(y0, y1, y2) * 1000
|
|
125
188
|
let speed = Math.hypot(vx, vy)
|
|
126
189
|
if (speed > VELOCITY_MAX) {
|
|
127
190
|
let f = VELOCITY_MAX / speed
|
package/src/window.ts
CHANGED
|
@@ -181,6 +181,21 @@ let animationFrames = new Map<number, Function>()
|
|
|
181
181
|
// callbacks. Defaults to 60 until the first displayRefreshRate event arrives.
|
|
182
182
|
let refreshRate = 60
|
|
183
183
|
|
|
184
|
+
// The tick of the latest frame the runtime delivered (see frameTime).
|
|
185
|
+
let latestTick = 0
|
|
186
|
+
|
|
187
|
+
export interface FrameOptions {
|
|
188
|
+
/**
|
|
189
|
+
* Whether the callback is a request for frames (default true). With
|
|
190
|
+
* `demand: false` it asks for nothing: it still runs before every frame
|
|
191
|
+
* the runtime delivers, painted or not, but it does not keep the app
|
|
192
|
+
* rendering. What it writes requests its frame as any write does, so a
|
|
193
|
+
* loop that changes something a few times a second presents a few times
|
|
194
|
+
* a second.
|
|
195
|
+
*/
|
|
196
|
+
demand?: boolean
|
|
197
|
+
}
|
|
198
|
+
|
|
184
199
|
/**
|
|
185
200
|
* Calls `fn` before every frame is painted: `tick` is the time in ms, `frame` is
|
|
186
201
|
* the present count, and `rate` is the current refresh rate in Hz. `tick`
|
|
@@ -203,8 +218,16 @@ let refreshRate = 60
|
|
|
203
218
|
* one - an effect's apply phase, an event handler - the returned cleanup is
|
|
204
219
|
* the only handle, so return it from the apply (`return onFrame(...)`) to run
|
|
205
220
|
* a loop only while a condition holds.
|
|
221
|
+
*
|
|
222
|
+
* A registered callback is a standing request for the next frame: the app
|
|
223
|
+
* renders and presents every refresh while it is registered, whatever the
|
|
224
|
+
* body does. `{ demand: false }` registers it without that request, for a
|
|
225
|
+
* loop that follows time and only sometimes changes the picture (a sprite
|
|
226
|
+
* clip stepping at 8 fps, a countdown that ticks once a second): it is
|
|
227
|
+
* called on the same ticks, and the app presents only when it writes.
|
|
206
228
|
*/
|
|
207
|
-
export function onFrame(fn: (tick: number, frame: number, rate: number) => void) {
|
|
229
|
+
export function onFrame(fn: (tick: number, frame: number, rate: number) => void, options?: FrameOptions) {
|
|
230
|
+
let demand = options?.demand !== false
|
|
208
231
|
let frameId: number = null!
|
|
209
232
|
// Cancellation is a flag, not map membership: while a frame runs the whole
|
|
210
233
|
// callback map is swapped out, so a cleanup() called from another onFrame
|
|
@@ -219,7 +242,7 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void)
|
|
|
219
242
|
// pending onFrame callback is a standing request for the next frame.
|
|
220
243
|
frameId = nextFrameId++
|
|
221
244
|
animationFrames.set(frameId, extendedFn)
|
|
222
|
-
requestFrame()
|
|
245
|
+
if (demand) requestFrame()
|
|
223
246
|
try {
|
|
224
247
|
fn(tick, frame, rate)
|
|
225
248
|
} catch (err) {
|
|
@@ -229,7 +252,7 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void)
|
|
|
229
252
|
|
|
230
253
|
frameId = nextFrameId++
|
|
231
254
|
animationFrames.set(frameId, extendedFn)
|
|
232
|
-
requestFrame()
|
|
255
|
+
if (demand) requestFrame()
|
|
233
256
|
|
|
234
257
|
let cleanup = () => {
|
|
235
258
|
cancelled = true
|
|
@@ -239,6 +262,20 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void)
|
|
|
239
262
|
return cleanup
|
|
240
263
|
}
|
|
241
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The time of the current frame in ms: the `tick` its onFrame callbacks
|
|
267
|
+
* receive, readable anywhere without registering one. Inside a frame (a
|
|
268
|
+
* frame callback, an effect, the render pass) it is that frame's tick;
|
|
269
|
+
* between frames (an event handler, a timer) it is the latest frame's. It
|
|
270
|
+
* is 0 before the first frame. Not reactive, and reading it requests no
|
|
271
|
+
* frame. Use it to time things against the app's own clock from code that
|
|
272
|
+
* has no tick in hand: it follows the dev tools' clock control, `srt
|
|
273
|
+
* render` and a test, where performance.now() does not.
|
|
274
|
+
*/
|
|
275
|
+
export function frameTime(): number {
|
|
276
|
+
return latestTick
|
|
277
|
+
}
|
|
278
|
+
|
|
242
279
|
// ------ Resize ----------------
|
|
243
280
|
|
|
244
281
|
// Insets: each value is the distance from the corresponding window edge, like
|
|
@@ -538,6 +575,7 @@ export function attachWindow(nodeId: number) {
|
|
|
538
575
|
// registered and run on the first real render event, before the first
|
|
539
576
|
// paint with their writes applied.
|
|
540
577
|
function runFrame(t: number, frame: number, bootstrap = false) {
|
|
578
|
+
if (!bootstrap) latestTick = t
|
|
541
579
|
if (!bootstrap && animationFrames.size > 0) {
|
|
542
580
|
let frames = animationFrames
|
|
543
581
|
animationFrames = new Map()
|