@solidrt/core 0.0.64 → 0.0.66

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.
@@ -17,7 +17,9 @@
17
17
  //
18
18
  // Held state is a count behind a signal, so a source's rate is reactive
19
19
  // (a held key wakes a control's frame loop) and a blur - the up that never
20
- // arrives once focus has left - clears it. Composites clamp nothing: the
20
+ // arrives once focus has left - clears it. A source also reports when its
21
+ // keys last changed (the key event's timeStamp; nothing after a blur,
22
+ // which is no release by the player). Composites clamp nothing: the
21
23
  // map's combination does (W and D together read [1, -1] before the
22
24
  // vec2 clamp to unit length, so diagonals walk at the same speed).
23
25
  //
@@ -60,9 +62,11 @@ let matches = (event: KeyEvent, key: string): boolean => {
60
62
  return key.length === 4 && key.startsWith("Key") && event.key.toLowerCase() === key[3]!.toLowerCase()
61
63
  }
62
64
 
63
- // A set of specs with a reactive "how many are held" count.
65
+ // A set of specs with a reactive "how many are held" count, and the time
66
+ // of the key event that last changed which are held.
64
67
  function held(specs: Spec[]) {
65
68
  let down = new Set<string>()
69
+ let changedAt: number | null = null
66
70
  // ownedWrite: a map forwards key events from wherever its handlers sit.
67
71
  let [count, setCount] = createSignal(0, { ownedWrite: true })
68
72
  return {
@@ -72,14 +76,18 @@ function held(specs: Spec[]) {
72
76
  // on its most specific matches (a repeat under a newly held Shift
73
77
  // moves "Tab" to "Shift+Tab" and back), an up frees them all.
74
78
  let onKey = specs.filter(s => matches(event, s.key))
79
+ let before = [...down].join()
75
80
  for (let s of onKey) down.delete(s.text)
76
81
  if (isDown) for (let s of mostSpecific(onKey, s => s.mods, event)) down.add(s.text)
82
+ if ([...down].join() !== before) changedAt = event.timeStamp
77
83
  setCount(down.size)
78
84
  },
79
85
  blur() {
86
+ if (down.size > 0) changedAt = null
80
87
  down.clear()
81
88
  setCount(0)
82
89
  },
90
+ changedAt: () => changedAt,
83
91
  has(text: string) {
84
92
  return down.has(text)
85
93
  },
@@ -95,6 +103,7 @@ function key(spec: string): InputSource<"button"> {
95
103
  id: `${DEVICE}:key:${spec}`,
96
104
  device: DEVICE,
97
105
  rate: () => state.count() > 0,
106
+ changedAt: state.changedAt,
98
107
  key: state.key,
99
108
  blur: state.blur,
100
109
  }
package/src/input-map.ts CHANGED
@@ -76,7 +76,8 @@ export type DeviceName = "keyboard" | "gamepad" | "pointer"
76
76
  * One device control as an input source. `rate` reads its current value
77
77
  * (reactive); `deltas` subscribes a sink to the amounts a gesture
78
78
  * produces; `key`/`blur` receive the key events a map forwards
79
- * (InputMap.handlers). A source may carry any subset.
79
+ * (InputMap.handlers); `changedAt` says when the rate last changed. A
80
+ * source may carry any subset.
80
81
  */
81
82
  export interface InputSource<K extends ActionKind = ActionKind> {
82
83
  readonly kind: K
@@ -90,6 +91,16 @@ export interface InputSource<K extends ActionKind = ActionKind> {
90
91
  * may leave it out and never counts as the active device. */
91
92
  readonly device?: DeviceName
92
93
  rate?: () => ActionValue<K>
94
+ /**
95
+ * When `rate` last changed: the `timeStamp` (ms) of the input event that
96
+ * changed it, or null when the value was dropped and not let go by the
97
+ * player (a blur releases every held key with no event), and before any
98
+ * change. What `tap` and `doubleTap` measure with, so a button source
99
+ * has to carry it to sit under one: the time travels with the value,
100
+ * since whoever sees the change sees it after the event is gone. Not
101
+ * reactive.
102
+ */
103
+ changedAt?: () => number | null
93
104
  deltas?: (sink: DeltaSink) => () => void
94
105
  key?: (event: KeyEvent, down: boolean) => void
95
106
  blur?: () => void
@@ -454,7 +454,7 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
454
454
  if (downs.size === 1) {
455
455
  opening = { id: e.pointerId, x: e.clientX, y: e.clientY, mods: mods(e), alone: true }
456
456
  finger.reset()
457
- finger.push(e.clientX, e.clientY)
457
+ finger.push(e.clientX, e.clientY, e.timeStamp)
458
458
  } else if (opening) opening.alone = false
459
459
  }
460
460
  },
@@ -465,7 +465,7 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
465
465
  transform.handlers.onPointerMove(e)
466
466
  hold.move(e)
467
467
  taps.move(e)
468
- if (opening && opening.id === e.pointerId) finger.push(e.clientX, e.clientY)
468
+ if (opening && opening.id === e.pointerId && !e.predicted) finger.push(e.clientX, e.clientY, e.timeStamp)
469
469
  },
470
470
  onPointerUp(e) {
471
471
  if (downs.get(e.pointerId) !== (e.button ?? PRIMARY)) return
@@ -475,7 +475,7 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
475
475
  taps.up(e)
476
476
  if (opening && opening.id === e.pointerId) {
477
477
  if (opening.alone && downs.size === 0) {
478
- let direction: SwipeDirection | null = classifySwipe(finger.velocity(), { dx: e.clientX - opening.x, dy: e.clientY - opening.y })
478
+ let direction: SwipeDirection | null = classifySwipe(finger.velocity(e.timeStamp), { dx: e.clientX - opening.x, dy: e.clientY - opening.y })
479
479
  if (direction) swipe.fire(opening.mods, direction)
480
480
  }
481
481
  opening = null
@@ -6,11 +6,15 @@
6
6
  // the plain press on another action, and the consumer reads one bool.
7
7
  //
8
8
  // Timing: an interaction watches its source's rate under a root of its
9
- // own and keeps time with setTimeout, so it works headless and needs no
10
- // frame loop; the root is disposed with the creating scope (a component
11
- // body) and lives for the session without one. `tap` and `doubleTap`
12
- // are edges, not levels: they read pressed for one task, long enough
13
- // for a map's onPress to fire, then release on their own.
9
+ // own, so it works headless and needs no frame loop; the root is disposed
10
+ // with the creating scope (a component body) and lives for the session
11
+ // without one. `hold` keeps time with setTimeout. `tap` and `doubleTap`
12
+ // measure between the source's edges with the source's own time
13
+ // (`changedAt`, the timeStamp of the input event behind each edge): they
14
+ // see an edge at the flush, after the event is gone, so they read no
15
+ // clock of their own, and a source without `changedAt` cannot sit under
16
+ // them. They are edges, not levels: they read pressed for one task, long
17
+ // enough for a map's onPress to fire, then release on their own.
14
18
  //
15
19
  // Every processor composes the source's id (`invert(gamepad:leftStick)`,
16
20
  // `hold(400,keyboard:key:Space)`), so a saved binding restores through
@@ -49,6 +53,13 @@ let checkButtonSource = (what: string, source: InputSource): void => {
49
53
  if (source.kind !== "button") throw new Error(`${what}: "${source.label}" is a ${source.kind}, not a button`)
50
54
  }
51
55
 
56
+ let checkTimedSource = (what: string, source: InputSource): void => {
57
+ checkButtonSource(what, source)
58
+ if (typeof source.changedAt !== "function") {
59
+ throw new Error(`${what}: "${source.label}" does not say when it changes (no changedAt), so its presses cannot be timed`)
60
+ }
61
+ }
62
+
52
63
  let checkMs = (what: string, ms: number): void => {
53
64
  if (!Number.isFinite(ms) || ms < 0) throw new Error(`${what}: ms must be a non-negative number, got ${String(ms)}`)
54
65
  }
@@ -98,11 +109,13 @@ export function scale<K extends "axis" | "vec2">(source: InputSource<K>, factor:
98
109
  }
99
110
 
100
111
  // A button source derived from one button source's press and release
101
- // edges: `edge(down, now)` runs on each, with `set` for the derived
102
- // state and `later` for a timer that the disposal clears.
112
+ // edges: `edge(down, at)` runs on each, with `set` for the derived
113
+ // state and `later` for a timer that the disposal clears. `at` is the
114
+ // source's time for the edge (its changedAt), null when it has none: a
115
+ // source that carries no time, or a value that was dropped (a blur).
103
116
  type EdgeHooks = { set: (pressed: boolean) => void; later: (ms: number, run: () => void) => void; clearLater: () => void }
104
117
 
105
- function derived(source: InputSource<"button">, label: string, id: string, edge: (down: boolean, now: number, hooks: EdgeHooks) => void): InputSource<"button"> {
118
+ function derived(source: InputSource<"button">, label: string, id: string, edge: (down: boolean, at: number | null, hooks: EdgeHooks) => void): InputSource<"button"> {
106
119
  // ownedWrite: the edges land inside an effect and timers.
107
120
  let [pressed, setPressed] = createSignal(false, { ownedWrite: true })
108
121
  let timer: ReturnType<typeof setTimeout> | null = null
@@ -124,7 +137,7 @@ function derived(source: InputSource<"button">, label: string, id: string, edge:
124
137
  createEffect(
125
138
  () => source.rate!(),
126
139
  (down, prev) => {
127
- if (down !== prev) edge(down, performance.now(), hooks)
140
+ if (down !== prev) edge(down, source.changedAt?.() ?? null, hooks)
128
141
  },
129
142
  { defer: true },
130
143
  )
@@ -151,7 +164,7 @@ let pulse = (hooks: EdgeHooks): void => {
151
164
  export function hold(source: InputSource<"button">, ms = HOLD_MS): InputSource<"button"> {
152
165
  checkButtonSource("hold", source)
153
166
  checkMs("hold", ms)
154
- return derived(source, `${source.label} (hold ${ms} ms)`, `hold(${ms},${source.id})`, (down, _now, hooks) => {
167
+ return derived(source, `${source.label} (hold ${ms} ms)`, `hold(${ms},${source.id})`, (down, _at, hooks) => {
155
168
  if (down) hooks.later(ms, () => hooks.set(true))
156
169
  else {
157
170
  hooks.clearLater()
@@ -161,40 +174,55 @@ export function hold(source: InputSource<"button">, ms = HOLD_MS): InputSource<"
161
174
  }
162
175
 
163
176
  /** A press released within `ms` (default 200): pressed for one task on the
164
- * release, so onPress fires once per tap and a long press is not one. */
177
+ * release, so onPress fires once per tap and a long press is not one. A
178
+ * press the player did not let go (a blur dropped it) is no tap. The
179
+ * source has to say when it changes (`changedAt`). */
165
180
  export function tap(source: InputSource<"button">, ms = TAP_MS): InputSource<"button"> {
166
- checkButtonSource("tap", source)
181
+ checkTimedSource("tap", source)
167
182
  checkMs("tap", ms)
168
- let downAt = 0
169
- return derived(source, `${source.label} (tap)`, `tap(${ms},${source.id})`, (down, now, hooks) => {
170
- if (down) downAt = now
171
- else if (now - downAt <= ms) pulse(hooks)
183
+ let downAt: number | null = null
184
+ return derived(source, `${source.label} (tap)`, `tap(${ms},${source.id})`, (down, at, hooks) => {
185
+ if (down) downAt = at
186
+ else {
187
+ if (at !== null && downAt !== null && at - downAt <= ms) pulse(hooks)
188
+ downAt = null
189
+ }
172
190
  })
173
191
  }
174
192
 
175
193
  /** Two taps (each within `tapMs`, default 200) at most `gapMs` (default
176
- * 300) apart: pressed for one task on the second release. */
194
+ * 300) apart: pressed for one task on the second release. A press the
195
+ * player did not let go (a blur dropped it) is no tap and ends the pair.
196
+ * The source has to say when it changes (`changedAt`). */
177
197
  export function doubleTap(source: InputSource<"button">, gapMs = DOUBLE_TAP_GAP_MS, tapMs = TAP_MS): InputSource<"button"> {
178
- checkButtonSource("doubleTap", source)
198
+ checkTimedSource("doubleTap", source)
179
199
  checkMs("doubleTap", gapMs)
180
200
  checkMs("doubleTap", tapMs)
181
- let downAt = 0
201
+ let downAt: number | null = null
182
202
  let lastTap = -Infinity
183
- return derived(source, `${source.label} (double tap)`, `doubleTap(${gapMs},${tapMs},${source.id})`, (down, now, hooks) => {
203
+ return derived(source, `${source.label} (double tap)`, `doubleTap(${gapMs},${tapMs},${source.id})`, (down, at, hooks) => {
184
204
  if (down) {
185
- downAt = now
205
+ downAt = at
186
206
  return
187
207
  }
188
- if (now - downAt > tapMs) return
189
- if (now - lastTap <= gapMs) {
208
+ let pressedAt = downAt
209
+ downAt = null
210
+ if (at === null || pressedAt === null) {
211
+ lastTap = -Infinity
212
+ return
213
+ }
214
+ if (at - pressedAt > tapMs) return
215
+ if (at - lastTap <= gapMs) {
190
216
  lastTap = -Infinity
191
217
  pulse(hooks)
192
- } else lastTap = now
218
+ } else lastTap = at
193
219
  })
194
220
  }
195
221
 
196
222
  /** Pressed while every source is: Shift+click on one action, a two-button
197
- * combo on a pad. Key events reach each keyboard source among them. */
223
+ * combo on a pad. Key events reach each keyboard source among them. It
224
+ * changes when the last of its sources to change did (`changedAt`, when
225
+ * each of them carries one). */
198
226
  export function chord(...sources: InputSource<"button">[]): InputSource<"button"> {
199
227
  if (sources.length < 2) throw new Error("chord: needs at least two button sources")
200
228
  for (let s of sources) checkButtonSource("chord", s)
@@ -204,6 +232,18 @@ export function chord(...sources: InputSource<"button">[]): InputSource<"button"
204
232
  id: `chord(${sources.map(s => s.id).join(",")})`,
205
233
  device: sources[0]!.device,
206
234
  rate: () => sources.every(s => s.rate?.() === true),
235
+ changedAt: sources.every(s => s.changedAt)
236
+ ? () => {
237
+ let latest: number | null = null
238
+ for (let s of sources) {
239
+ let at = s.changedAt!()
240
+ // One source dropped (a blur): so was the chord.
241
+ if (at === null) return null
242
+ if (latest === null || at > latest) latest = at
243
+ }
244
+ return latest
245
+ }
246
+ : undefined,
207
247
  key: (event, down) => {
208
248
  for (let s of sources) s.key?.(event, down)
209
249
  },
package/src/pan.ts CHANGED
@@ -108,7 +108,7 @@ export function createPan(options: PanOptions) {
108
108
  armed = null
109
109
  origin = { x: e.parentX, y: e.parentY }
110
110
  tracker.reset()
111
- tracker.push(e.parentX, e.parentY)
111
+ if (!e.predicted) tracker.push(e.parentX, e.parentY, e.timeStamp)
112
112
  options.onPanStart?.()
113
113
  } else {
114
114
  // The arena is resolved against us; the drag belongs elsewhere.
@@ -117,7 +117,8 @@ export function createPan(options: PanOptions) {
117
117
  return
118
118
  }
119
119
  if (active === e.pointerId && origin) {
120
- tracker.push(e.parentX, e.parentY)
120
+ // The pan follows a predicted position; its velocity does not.
121
+ if (!e.predicted) tracker.push(e.parentX, e.parentY, e.timeStamp)
121
122
  options.onPanMove?.(e.parentX - origin.x, e.parentY - origin.y)
122
123
  origin = { x: e.parentX, y: e.parentY }
123
124
  }
@@ -126,7 +127,7 @@ export function createPan(options: PanOptions) {
126
127
  if (active === e.pointerId) {
127
128
  // The up is not a sample: it sits where the last move left the
128
129
  // pointer, and its age is what tells a rest from a flick.
129
- let velocity = flingVelocity(tracker.velocity())
130
+ let velocity = flingVelocity(tracker.velocity(e.timeStamp))
130
131
  reset()
131
132
  options.onPanEnd?.(velocity)
132
133
  } else if (armed === e.pointerId) {
package/src/renderer.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // createErrorBoundary comes from signals, not solid-js: solid-js marks it
2
2
  // @internal (an app writes <Errored>, a renderer reaches for the primitive)
3
- // and strips it from its .d.ts, so the solid-js re-export types as any.
3
+ // and leaves it out of its public types.
4
4
  import { createErrorBoundary, createRoot, onCleanup, NotReadyError, untrack } from "@solidjs/signals"
5
5
  import { createRenderer } from "@solidjs/universal"
6
6
  import type { Element } from "solid-js"
@@ -86,7 +86,7 @@ function flushDestroy(): void {
86
86
  function removeNode(parent: ProxyNode, node: ProxyNode): void {
87
87
  if (!node || !parent) return
88
88
 
89
- // console.debug("[srt] removeNode", parent.id, node.id)
89
+ // console.debug("[sol] removeNode", parent.id, node.id)
90
90
 
91
91
  // Update JS tree references
92
92
  let index = parent.children.indexOf(node)
@@ -113,7 +113,7 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
113
113
  // bookkeeping on the hot create/insert paths, orphans are derived from the
114
114
  // proxy map itself: parentless, not the window root, and not awaiting the
115
115
  // destroy sweep. window.ts runs the scan on a rendered frame every few
116
- // seconds; dev bundles only (srt always defines import.meta.env.DEV, so a
116
+ // seconds; dev bundles only (sol always defines import.meta.env.DEV, so a
117
117
  // production bundle folds the check into a constant early return).
118
118
  const SENTINEL_INTERVAL_MS = 5000
119
119
  let sentinelDue = 0
@@ -208,7 +208,7 @@ function routeFor(name: string): number {
208
208
  function applyProp<T>(node: ProxyNode, name: string, value: T): void {
209
209
  if (!node) return
210
210
 
211
- // console.debug("[srt] applyProp", node.id, name, value)
211
+ // console.debug("[sol] applyProp", node.id, name, value)
212
212
 
213
213
  switch (routeFor(name)) {
214
214
  case ROUTE_EVENT:
@@ -234,7 +234,7 @@ let renderer = createRenderer<ProxyNode>({
234
234
  createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
235
235
  let proxy = createProxyNode(elementType)
236
236
 
237
- // console.debug("[srt] createElement", proxy.id, elementType)
237
+ // console.debug("[sol] createElement", proxy.id, elementType)
238
238
 
239
239
  if (elementType === "window") tree.createRoot(proxy.id)
240
240
  else tree.createNode(proxy.id, elementType)
@@ -257,20 +257,20 @@ let renderer = createRenderer<ProxyNode>({
257
257
  // no element form of its own (the DOM's "#text" node name).
258
258
  createTextNode: (value: string): ProxyNode => {
259
259
  let proxy = createProxyNode("#text")
260
- // console.debug("[srt] createTextNode", proxy.id, value)
260
+ // console.debug("[sol] createTextNode", proxy.id, value)
261
261
  tree.createNode(proxy.id, "#text")
262
262
  tree.setProperty(proxy.id, "text", "" + value)
263
263
  return proxy
264
264
  },
265
265
 
266
266
  replaceText: (node: ProxyNode, value: string): void => {
267
- // console.debug("[srt] replaceText", node.id, value)
267
+ // console.debug("[sol] replaceText", node.id, value)
268
268
  tree.setProperty(node.id, "text", "" + value)
269
269
  },
270
270
 
271
271
  isTextNode: (node: ProxyNode): boolean => node?.elementType === "#text",
272
272
  setProperty: <T>(node: ProxyNode, name: string, value: T): void => {
273
- // console.debug("[srt] setProperty", node.id, name, value)
273
+ // console.debug("[sol] setProperty", node.id, name, value)
274
274
  applyProp(node, name, value)
275
275
  },
276
276
 
@@ -294,7 +294,7 @@ let renderer = createRenderer<ProxyNode>({
294
294
  pendingDestroy.delete(node.id)
295
295
 
296
296
  if (parent) {
297
- // console.debug("[srt] insertNode", parent.id, node.id, anchor?.id ?? "")
297
+ // console.debug("[sol] insertNode", parent.id, node.id, anchor?.id ?? "")
298
298
 
299
299
  // Native first: the tree refuses a laid-out element under a d-* parent
300
300
  // (it throws, naming both tags), and the mirror must not record a child
@@ -1,4 +1,4 @@
1
- // Lattice runner builtin modules (the "srt:*" surface). These are ambient
1
+ // Lattice runner builtin modules (the "sol:*" surface). These are ambient
2
2
  // module declarations, so they live in this global-script .d.ts (no top-level
3
3
  // import/export) rather than in types.d.ts: an ambient `declare module` only
4
4
  // becomes globally visible to consumers from a non-module declaration file.
@@ -51,7 +51,7 @@ declare module "*.glb" {
51
51
  const bytes: Uint8Array
52
52
  export default bytes
53
53
  }
54
- declare module "*.srtm" {
54
+ declare module "*.sol3m" {
55
55
  const bytes: Uint8Array
56
56
  export default bytes
57
57
  }
@@ -61,14 +61,15 @@ declare module "*.srtm" {
61
61
  // stream ("pointerMove"/"pointerDown"/... consumed by window.ts),
62
62
  // "pointerFrame" (the move-batch terminator: fires after all of a frame's
63
63
  // pointer moves have dispatched, every pointer the same age - multi-pointer
64
- // recognizers measure there), and "render" (the per-frame signal).
65
- declare module "srt:events" {
64
+ // recognizers measure there; its `timeStamp` is theirs), and "render" (the
65
+ // per-frame signal).
66
+ declare module "sol:events" {
66
67
  export function on(event: string, callback: (data: any) => void): () => void
67
68
  export function once(event: string, callback: (data: any) => void): () => void
68
69
  }
69
70
 
70
71
  // The running application's own surface (lattice), present in every build.
71
- declare module "srt:app" {
72
+ declare module "sol:app" {
72
73
  /**
73
74
  * End the current app now, without running the quit hooks: back to the
74
75
  * player in a dev client, quit when standalone or at the player root (on
@@ -99,7 +100,7 @@ declare module "srt:app" {
99
100
 
100
101
  // Dev-server control surface (lattice). Present only in dev/go builds; in other
101
102
  // builds `available` is false and the functions are no-ops.
102
- declare module "srt:dev" {
103
+ declare module "sol:dev" {
103
104
  export const available: boolean
104
105
  export const canDiscover: boolean
105
106
  export const recents: string[]
@@ -138,7 +139,7 @@ declare module "srt:dev" {
138
139
  // Installed-app management (lattice), the player's surface over the client's
139
140
  // version store. Present only in go/dev client builds; elsewhere `available`
140
141
  // is false, `list` returns [], and launch/remove are no-ops.
141
- declare module "srt:apps" {
142
+ declare module "sol:apps" {
142
143
  export const available: boolean
143
144
  /**
144
145
  * An installed app: id, display name (the installed manifest's displayName,
@@ -232,6 +233,72 @@ declare module "srt:apps" {
232
233
  // submits the display list. To schedule a future frame instead, use
233
234
  // requestFrame() from "flux:rendertree". The tree-building surface itself is
234
235
  // "flux:rendertree" (from @solidrt/flux-types).
235
- declare module "srt:render" {
236
+ declare module "sol:render" {
236
237
  export function renderFrame(): void
237
238
  }
239
+
240
+ // The engine verbs of an app test (lattice, dev client only), which
241
+ // @solidrt/test builds its surface on; tests import that, not this.
242
+ // The verbs work only in an engine a test host built (`sol test`) and throw
243
+ // anywhere else. App time there is frame / frameRate and nothing else: no
244
+ // frame runs unless one is asked for.
245
+ declare module "sol:test" {
246
+ /**
247
+ * Runs one frame: timers due by its time fire, then frame callbacks and
248
+ * the flush, then the draw if anything demanded one. Fulfills once the
249
+ * frame has run. One frame at a time: throws while the previous one is
250
+ * still pending.
251
+ */
252
+ export function frame(): Promise<void>
253
+ /** The frames per second this engine steps at (60 unless set). */
254
+ export function frameRate(): number
255
+ /** Another frame rate, a positive integer. Throws once a frame has run. */
256
+ export function setFrameRate(fps: number): void
257
+ /** App time as of the last frame, in milliseconds; 0 before the first. */
258
+ export function time(): number
259
+ /**
260
+ * Fulfills once the window's size has reached the engine, at once when
261
+ * it already has. No frame runs for it and no time passes: a mounted
262
+ * window builds its first frame on that size.
263
+ */
264
+ export function windowReady(): Promise<void>
265
+ /**
266
+ * Expands `events` (JSON text, the control API's `/input` event shape)
267
+ * into the steps that are sent and returns what passes before each:
268
+ * `[ms, frames]` per step, at least that much time and that many frames.
269
+ * Throws on an invalid event, before anything is sent. The steps are then
270
+ * sent with `inputStep`, in order.
271
+ */
272
+ export function inputPlan(events: string): [ms: number, frames: number][]
273
+ /**
274
+ * Sends step `index` of the current plan into the input pipeline: a
275
+ * down, an up, a key or a wheel is dispatched on arrival, ahead of the
276
+ * next frame; a move with that frame.
277
+ */
278
+ export function inputStep(index: number): void
279
+ /**
280
+ * Runs frames until the app is at rest: nothing in flight, no timer due,
281
+ * no frame demanded. Work in flight is waited for with no app time
282
+ * passing. Rejects once `maxMs` of app time have passed without rest,
283
+ * with what is left in the message.
284
+ */
285
+ export function settle(maxMs: number): Promise<void>
286
+ /**
287
+ * Delivers a link the way an OS-routed one arrives (the raw string, on
288
+ * the `link` event, ahead of the next frame). Returns whether anything
289
+ * listens for links.
290
+ */
291
+ export function link(link: string): boolean
292
+ /**
293
+ * Calls a debug command the app registered with `registerDebug`. `args`
294
+ * and the result are JSON text (null for no args). Throws on an unknown
295
+ * name, a throwing command and a result JSON cannot carry.
296
+ */
297
+ export function debug(name: string, args: string | null): string
298
+ /**
299
+ * The pixels a node's subtree paints: RGBA8, premultiplied, rows top to
300
+ * bottom, at the display scale. Drawn at once from the tree as it is; no
301
+ * frame runs and no app time passes.
302
+ */
303
+ export function capture(node: number): { width: number; height: number; data: Uint8Array }
304
+ }
package/src/swipe.ts CHANGED
@@ -80,8 +80,8 @@ export function createSwipe(options: SwipeOptions) {
80
80
  // The down's window position and the finger's window-px velocity, over
81
81
  // every event the wrapped pan sees.
82
82
  let down: { id: number; x: number; y: number } | null = null
83
- // The lift's window position, set just before the pan's up runs.
84
- let lift: { x: number; y: number } | null = null
83
+ // The lift's window position and time, set just before the pan's up runs.
84
+ let lift: { x: number; y: number; at: number } | null = null
85
85
  let finger = createVelocityTracker()
86
86
  let pan = createPan({
87
87
  get axis() {
@@ -92,7 +92,7 @@ export function createSwipe(options: SwipeOptions) {
92
92
  onPanEnd: velocity => {
93
93
  let direction: SwipeDirection | null = null
94
94
  if (down && lift) {
95
- direction = classifySwipe(finger.velocity(), { dx: lift.x - down.x, dy: lift.y - down.y }, directions())
95
+ direction = classifySwipe(finger.velocity(lift.at), { dx: lift.x - down.x, dy: lift.y - down.y }, directions())
96
96
  }
97
97
  if (direction) options.onSwipe?.(direction, velocity)
98
98
  options.onSwipeEnd?.(velocity)
@@ -104,16 +104,16 @@ export function createSwipe(options: SwipeOptions) {
104
104
  if (down === null && (e.button == null || e.button === 0)) {
105
105
  down = { id: e.pointerId, x: e.clientX, y: e.clientY }
106
106
  finger.reset()
107
- finger.push(e.clientX, e.clientY)
107
+ finger.push(e.clientX, e.clientY, e.timeStamp)
108
108
  }
109
109
  pan.handlers.onPointerDown(e)
110
110
  },
111
111
  onPointerMove: (e: PointerEvent) => {
112
- if (down && down.id === e.pointerId) finger.push(e.clientX, e.clientY)
112
+ if (down && down.id === e.pointerId && !e.predicted) finger.push(e.clientX, e.clientY, e.timeStamp)
113
113
  pan.handlers.onPointerMove(e)
114
114
  },
115
115
  onPointerUp: (e: PointerEvent) => {
116
- if (down && down.id === e.pointerId) lift = { x: e.clientX, y: e.clientY }
116
+ if (down && down.id === e.pointerId) lift = { x: e.clientX, y: e.clientY, at: e.timeStamp }
117
117
  pan.handlers.onPointerUp(e)
118
118
  if (down && down.id === e.pointerId) {
119
119
  down = null