@solidrt/core 0.0.56 → 0.0.57

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.
@@ -0,0 +1,213 @@
1
+ // A pointer feed: one element's pointer events turned into gesture
2
+ // sources for an input map. The pointer is the one device that is not
3
+ // global - the tree holds UI and scene together, so a drag belongs to the
4
+ // element it started on (a slider beside a viewport must not turn the
5
+ // camera) - hence a feed per element: spread `feed.handlers` on the leaf
6
+ // (a Scene, View3d, SpriteLayer or View2d takes it as its `pointer` prop
7
+ // and spreads it on the built-in leaf) and bind its sources.
8
+ //
9
+ // The sources are gestures, not raw events, because a pointer only means
10
+ // something once a gesture is recognized: a move without a press is
11
+ // nothing, one finger dragging is `drag`, two fingers moving together is
12
+ // `pan`, spreading is `pinch`, turning is `twist`, and the wheel is its
13
+ // own thing. One merged recognizer (createTransform) serves all of them,
14
+ // so they arbitrate in the app-wide gesture arena as one - a viewport
15
+ // inside a scroller never double-handles - and they never fight each
16
+ // other over the same fingers. `mouseDelta` is the pointer-lock path:
17
+ // raw mouse motion while the pointer is locked, when the cursor does not
18
+ // move and no drag can arm.
19
+ //
20
+ // Units are device-free (the input map's contract): drag and pan travel
21
+ // in the element's own heights (a drag across the element is 1 whatever
22
+ // the window size - Three's OrbitControls convention), pinch and wheel
23
+ // in octaves (log2 of the scale change, one notch about a fifth), twist
24
+ // in turns, mouse motion in view-height-equivalents through a fixed
25
+ // reference. The focal point of a pinch or a wheel rides along as a
26
+ // fraction of the element (0..1), the anchor a zoom-about-the-cursor
27
+ // consumer maps to its own space. Screen convention: x right, y down.
28
+ //
29
+ // Gesture brackets: `drag` begins when the first pointer lands and ends
30
+ // when the last lifts (or when a second lands, which begins `pan`,
31
+ // `pinch` and `twist` until the count drops back to one); the wheel
32
+ // sends no brackets, so a consumer can tell a notch from a finger.
33
+ // Brackets fire on the press itself, before the recognizer's slop, so a
34
+ // glide stops the moment a finger lands.
35
+ //
36
+ // The element's laid-out box normalizes the travel: read at the press
37
+ // through getLayoutBox (the untransformed read, so a designSize fit or
38
+ // an ancestor transform never skews it), or from `layout` when given -
39
+ // required for a detached d-* leaf, which has no layout box (the feed
40
+ // throws rather than treat pixels as heights), and handy for a headless
41
+ // check with no tree. A zero-height box passes pixels through unscaled.
42
+ //
43
+ // Create a feed in an owned scope (a component body): the recognizer
44
+ // registers its cleanup with the owner.
45
+
46
+ import { createTransform } from "./transform"
47
+ import { getLayoutBox } from "./core"
48
+ import { pointerLocked } from "./window"
49
+ import type { PointerEvent, WheelEvent } from "./types"
50
+ import type { DeltaSink, InputSource } from "./input-map"
51
+ import type { Vec2 } from "./input-axes"
52
+
53
+ // Octaves per wheel-delta unit: the zoom exponent both cameras used
54
+ // (0.0015 per unit) taken in log2, so a 100-unit notch zooms about a
55
+ // fifth of an octave and a fast scroll of ten notches doubles.
56
+ const WHEEL_OCTAVES = 0.0015 / Math.LN2
57
+ // Mouse travel per delta unit under pointer lock, in logical pixels: one
58
+ // unit is what a drag across the element's height means to a consumer (a
59
+ // half turn of look in the first-person control), and 1500 px per half
60
+ // turn is the ~0.002 rad/px of Three's PointerLockControls.
61
+ const MOUSE_PX_PER_UNIT = 1500
62
+
63
+ export interface PointerFeed {
64
+ /** Spread on the element whose pointer events feed the sources. */
65
+ handlers: {
66
+ onPointerDown(event: PointerEvent): void
67
+ onPointerMove(event: PointerEvent): void
68
+ onPointerUp(event: PointerEvent): void
69
+ onPointerLeave(event: PointerEvent): void
70
+ onWheel(event: WheelEvent): void
71
+ }
72
+ /** One pointer's travel, in element heights; bracketed. */
73
+ drag: InputSource<"vec2">
74
+ /** Two or more pointers' focal travel, in element heights; bracketed. */
75
+ pan: InputSource<"vec2">
76
+ /** Span change of two pointers, in octaves (spreading positive), with
77
+ * the focal point; bracketed. */
78
+ pinch: InputSource<"axis">
79
+ /** Rotation of the pointer pair, in turns, with the focal point; bracketed. */
80
+ twist: InputSource<"axis">
81
+ /** Wheel notches in octaves (wheel up positive), with the cursor as the
82
+ * focal point; unbracketed. */
83
+ wheel: InputSource<"axis">
84
+ /** Raw mouse motion while the pointer is locked, in view-height
85
+ * equivalents (see MOUSE_PX_PER_UNIT); unbracketed. */
86
+ mouseDelta: InputSource<"vec2">
87
+ }
88
+
89
+ export interface PointerFeedOptions {
90
+ /** The element's size, when the app knows it - required for a detached
91
+ * d-* leaf, which has no layout box; default reads getLayoutBox at each
92
+ * press. */
93
+ layout?: () => { width: number; height: number } | null
94
+ }
95
+
96
+ type Sinks = Set<DeltaSink>
97
+
98
+ function source<K extends "axis" | "vec2">(kind: K, label: string, sinks: Sinks): InputSource<K> {
99
+ return {
100
+ kind,
101
+ label,
102
+ deltas(sink) {
103
+ sinks.add(sink)
104
+ return () => {
105
+ sinks.delete(sink)
106
+ }
107
+ },
108
+ }
109
+ }
110
+
111
+ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed {
112
+ let sinks = {
113
+ drag: new Set<DeltaSink>(),
114
+ pan: new Set<DeltaSink>(),
115
+ pinch: new Set<DeltaSink>(),
116
+ twist: new Set<DeltaSink>(),
117
+ wheel: new Set<DeltaSink>(),
118
+ mouseDelta: new Set<DeltaSink>(),
119
+ }
120
+ let begin = (s: Sinks) => s.forEach(k => k.begin())
121
+ let end = (s: Sinks) => s.forEach(k => k.end())
122
+ let delta = (s: Sinks, value: number | Vec2, focal?: Vec2) => s.forEach(k => k.delta(value, focal))
123
+
124
+ // The element's box, read at each press (see the header). A detached
125
+ // leaf has no layout box and must bring `layout`: normalizing by
126
+ // nothing would turn a 100 px drag into 100 element heights, so that
127
+ // is a throw (the dev validation policy), while a zero-size box is a
128
+ // degenerate layout and passes pixels through.
129
+ let size = (event: { currentTarget: number }): { width: number; height: number } => {
130
+ let box = options.layout ? options.layout() : getLayoutBox({ id: event.currentTarget })
131
+ if (box === null && !options.layout) {
132
+ throw new Error("createPointerFeed: the element has no layout box (a detached d-* leaf, or before its first layout); create the feed with { layout: () => ({ width, height }) } for it")
133
+ }
134
+ return box && box.width > 0 && box.height > 0 ? { width: box.width, height: box.height } : { width: 0, height: 0 }
135
+ }
136
+ let box = { width: 0, height: 0 }
137
+ let unit = () => (box.height > 0 ? box.height : 1)
138
+ let focalOf = (x: number, y: number): Vec2 | undefined => (box.width > 0 && box.height > 0 ? [x / box.width, y / box.height] : undefined)
139
+
140
+ let transform = createTransform({
141
+ onTransformMove: t => {
142
+ let h = unit()
143
+ let travel: Vec2 = [t.dx / h, t.dy / h]
144
+ if (t.pointers >= 2) {
145
+ if (t.dx !== 0 || t.dy !== 0) delta(sinks.pan, travel)
146
+ let focal = focalOf(t.x, t.y)
147
+ if (t.scale !== 1) delta(sinks.pinch, Math.log2(t.scale), focal)
148
+ if (t.rotation !== 0) delta(sinks.twist, t.rotation / (2 * Math.PI), focal)
149
+ } else if (t.dx !== 0 || t.dy !== 0) {
150
+ delta(sinks.drag, travel)
151
+ }
152
+ },
153
+ })
154
+
155
+ // Pointers down on the element, for the gesture brackets: the first
156
+ // press opens drag, a second closes it and opens the two-pointer
157
+ // gestures, and the count coming back down reverses that.
158
+ let downs = new Set<number>()
159
+ let landed = (id: number) => {
160
+ if (downs.has(id)) return
161
+ downs.add(id)
162
+ if (downs.size === 1) begin(sinks.drag)
163
+ if (downs.size === 2) {
164
+ end(sinks.drag)
165
+ begin(sinks.pan)
166
+ begin(sinks.pinch)
167
+ begin(sinks.twist)
168
+ }
169
+ }
170
+ let lifted = (id: number) => {
171
+ if (!downs.delete(id)) return
172
+ if (downs.size === 1) {
173
+ end(sinks.pan)
174
+ end(sinks.pinch)
175
+ end(sinks.twist)
176
+ begin(sinks.drag)
177
+ }
178
+ if (downs.size === 0) end(sinks.drag)
179
+ }
180
+
181
+ return {
182
+ handlers: {
183
+ onPointerDown(e) {
184
+ if (e.button != null && e.button !== 0) return
185
+ if (downs.size === 0) box = size(e)
186
+ landed(e.pointerId)
187
+ transform.handlers.onPointerDown(e)
188
+ },
189
+ onPointerMove(e) {
190
+ if (pointerLocked() && e.pointerType === "mouse" && (e.movementX !== 0 || e.movementY !== 0)) {
191
+ delta(sinks.mouseDelta, [e.movementX / MOUSE_PX_PER_UNIT, e.movementY / MOUSE_PX_PER_UNIT])
192
+ }
193
+ transform.handlers.onPointerMove(e)
194
+ },
195
+ onPointerUp(e) {
196
+ transform.handlers.onPointerUp(e)
197
+ lifted(e.pointerId)
198
+ },
199
+ onPointerLeave() {},
200
+ onWheel(e) {
201
+ if (e.deltaY === 0) return
202
+ box = size(e)
203
+ delta(sinks.wheel, -e.deltaY * WHEEL_OCTAVES, focalOf(e.localX, e.localY))
204
+ },
205
+ },
206
+ drag: source("vec2", "pointer drag", sinks.drag),
207
+ pan: source("vec2", "pointer two-finger pan", sinks.pan),
208
+ pinch: source("axis", "pointer pinch", sinks.pinch),
209
+ twist: source("axis", "pointer twist", sinks.twist),
210
+ wheel: source("axis", "pointer wheel", sinks.wheel),
211
+ mouseDelta: source("vec2", "mouse motion (pointer locked)", sinks.mouseDelta),
212
+ }
213
+ }
package/src/input.ts ADDED
@@ -0,0 +1,12 @@
1
+ // The runtime-free half of the input surface, importable as
2
+ // `@solidrt/core/input` by modules that must stay headless (the camera
3
+ // controls and their checks run on the bare flux binary, which has no
4
+ // event bus). The map, the control-side axes contract and the keyboard
5
+ // device are pure; the gamepad device and the pointer feed need the
6
+ // runtime and live on the main entry.
7
+ export { createInputMap, invert, scale } from "./input-map"
8
+ export type { ActionKind, ActionValue, ActionsDecl, Binding, DeltaSink, GestureListener, InputMap, InputSource } from "./input-map"
9
+ export { createAxes, combineRates } from "./input-axes"
10
+ export type { Axes, AxesDecl, AxesHooks, AxisKind, AxisValue, Vec2 } from "./input-axes"
11
+ export { keyboard } from "./input-keyboard"
12
+ export type { KeyboardDevice, KeyboardVec2Keys } from "./input-keyboard"
package/src/types.d.ts CHANGED
@@ -590,9 +590,10 @@ export interface TransitionProps {
590
590
  * `all` covers every animatable property the element has, and a bare
591
591
  * string is shorthand for it: `transition="300ms ease-out"`. Only
592
592
  * properties the element carries animate (a d-rect has x, a view's x is
593
- * its transform); the initial value never animates unless the entry sets
594
- * `from` (an enter animation), and a non-numeric write (e.g. null)
595
- * cancels the running animation and snaps. `null` clears the
593
+ * its transform); the initial values (every write before the element's
594
+ * first paint) never animate unless the entry sets `from` (an enter
595
+ * animation), and a non-numeric write (e.g. null) cancels the running
596
+ * animation and snaps. `null` clears the
596
597
  * declaration; already-running animations finish. A spec built in a
597
598
  * conditional widens `curve` to `string` for TypeScript; write
598
599
  * `satisfies Transition` on the branch.