@solidrt/core 0.0.63 → 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/camera-control.ts +9 -2
- package/src/core.ts +6 -1
- package/src/double-tap.ts +2 -2
- package/src/gamepad.ts +32 -1
- package/src/gpu.ts +25 -10
- package/src/image.ts +10 -2
- package/src/index.ts +8 -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 +66 -12
- package/src/velocity.ts +113 -50
- package/src/window.ts +41 -3
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()
|