@solidrt/2d 0.0.54 → 0.0.56
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 +241 -57
- package/examples/README.md +22 -5
- package/examples/anim.tsx +86 -0
- package/examples/camera-probe.tsx +200 -0
- package/examples/camera.tsx +190 -0
- package/examples/parity.tsx +4 -4
- package/examples/pick.tsx +50 -33
- package/examples/tiles.tsx +19 -10
- package/package.json +3 -3
- package/src/animation.ts +178 -0
- package/src/camera-motion.ts +532 -0
- package/src/camera.ts +86 -0
- package/src/camera2d.ts +140 -0
- package/src/components.tsx +330 -76
- package/src/dispatch.ts +231 -0
- package/src/index.ts +30 -5
- package/src/layer.ts +454 -186
- package/src/oversample-math.ts +73 -0
- package/src/oversample.ts +39 -11
- package/src/records.ts +132 -27
- package/src/shaders.ts +29 -12
- package/src/tiles.ts +79 -24
package/src/dispatch.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Pointer dispatch behind layer.handlers, shared by both layer kinds: the
|
|
2
|
+
// element event model one tree deeper, with the LAYER as the root of the
|
|
3
|
+
// walk. Down, move, up and wheel dispatch on the topmost hit sprite,
|
|
4
|
+
// bubble through its enclosing groups and end at the layer's listeners;
|
|
5
|
+
// over empty space the walk is the layer alone (`sprite` null). Claiming
|
|
6
|
+
// is stopPropagation: a handler that stops an event keeps every later
|
|
7
|
+
// target, the layer included, from seeing it, and the DOWN decides the
|
|
8
|
+
// press - a pointer whose down never reached the layer stays claimed by
|
|
9
|
+
// the sprite chain for its move, up and tap too, so a sprite that drags
|
|
10
|
+
// itself stops its down once and a camera attached at the root never
|
|
11
|
+
// pans. Capture is per pointer to the press target, the layer included: a
|
|
12
|
+
// drag from empty space keeps delivering to the layer as it crosses
|
|
13
|
+
// sprites, exactly as a drag from a sprite keeps naming that sprite.
|
|
14
|
+
// Enter/leave pair on the sprite alone. Taps are synthesized here (DOM's
|
|
15
|
+
// click, Unity's IPointerClickHandler): a press that releases on the same
|
|
16
|
+
// target within the slop, the only pointer down for its whole press,
|
|
17
|
+
// counted up for repeats (DOM's detail, Unity's clickCount).
|
|
18
|
+
//
|
|
19
|
+
// Pure BY DESIGN (types and camera.ts, nothing with GPU imports) so
|
|
20
|
+
// checks/dispatch-check.ts drives it headless with a fake pick.
|
|
21
|
+
|
|
22
|
+
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
23
|
+
import { unprojectCamera } from "./camera.ts"
|
|
24
|
+
import type { CameraUpdate } from "./camera.ts"
|
|
25
|
+
import type { LayerPointerListener, Sprite, SpriteGroup, SpriteHandlers, SpriteLayer, SpritePointerEvent } from "./layer.ts"
|
|
26
|
+
import type { RecordLayer } from "./records.ts"
|
|
27
|
+
|
|
28
|
+
// Finger travel from the down point, in window pixels, past which a press
|
|
29
|
+
// is a drag and never a tap. Core's pan and transform recognizers engage
|
|
30
|
+
// at the same 8, so a press is never both a tap and a pan.
|
|
31
|
+
const TAP_SLOP = 8
|
|
32
|
+
// Successive taps closer in time than this, on the same target and within
|
|
33
|
+
// TAP_REPEAT_SLOP of each other, count up (Unity's 0.3 s clickCount
|
|
34
|
+
// window, Android's double-tap timeout).
|
|
35
|
+
const TAP_INTERVAL_MS = 300
|
|
36
|
+
// Window pixels a repeat tap may land from the previous one and still
|
|
37
|
+
// count: a thumb repeats within about this (Hammer.js uses 10, Android
|
|
38
|
+
// 100 dp); 20 tolerates touch without a mouse double-click spanning
|
|
39
|
+
// neighbouring sprites.
|
|
40
|
+
const TAP_REPEAT_SLOP = 20
|
|
41
|
+
|
|
42
|
+
export type DispatchDeps = {
|
|
43
|
+
size: () => [number, number]
|
|
44
|
+
camera: () => CameraUpdate
|
|
45
|
+
pick: (x: number, y: number) => Sprite[]
|
|
46
|
+
/** The layer: the walk's root and the listeners' currentTarget. */
|
|
47
|
+
root: SpriteLayer | RecordLayer
|
|
48
|
+
/** The root's listeners, in registration order. */
|
|
49
|
+
listeners: Set<LayerPointerListener>
|
|
50
|
+
/** The clock for tap repeats (default performance.now; checks inject). */
|
|
51
|
+
now?: () => number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type HandlerName = "onPointerDown" | "onPointerMove" | "onPointerUp" | "onWheel" | "onTap"
|
|
55
|
+
|
|
56
|
+
// One runtime event object serves every view (SpritePointerEvent for the
|
|
57
|
+
// sprite chain, LayerPointerEvent at the root, plus the wheel and tap
|
|
58
|
+
// fields); the public types narrow it per handler.
|
|
59
|
+
type InternalEvent = {
|
|
60
|
+
sprite: Sprite | null
|
|
61
|
+
currentTarget: Sprite | SpriteGroup | SpriteLayer | RecordLayer
|
|
62
|
+
x: number
|
|
63
|
+
y: number
|
|
64
|
+
pointerId: number
|
|
65
|
+
pointerType: string
|
|
66
|
+
button?: number
|
|
67
|
+
shiftKey: boolean
|
|
68
|
+
ctrlKey: boolean
|
|
69
|
+
altKey: boolean
|
|
70
|
+
metaKey: boolean
|
|
71
|
+
native: ElementPointerEvent
|
|
72
|
+
deltaX?: number
|
|
73
|
+
deltaY?: number
|
|
74
|
+
tapCount?: number
|
|
75
|
+
_stopped: boolean
|
|
76
|
+
stopPropagation(): void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A pointer between its down and up: what it pressed (null = the layer),
|
|
80
|
+
// whether the down reached the root, the down point for the slop, and
|
|
81
|
+
// whether it has been the only pointer down for its whole press.
|
|
82
|
+
type Press = {
|
|
83
|
+
target: Sprite | null
|
|
84
|
+
claimed: boolean
|
|
85
|
+
cx: number
|
|
86
|
+
cy: number
|
|
87
|
+
moved: boolean
|
|
88
|
+
alone: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function spriteDispatch(deps: DispatchDeps): (layout: (() => { width: number; height: number }) | null) => SpriteHandlers {
|
|
92
|
+
let now = deps.now ?? (() => performance.now())
|
|
93
|
+
let presses = new Map<number, Press>()
|
|
94
|
+
let hover = new Map<number, Sprite>()
|
|
95
|
+
let lastTap: { time: number; cx: number; cy: number; target: Sprite | null; count: number } | null = null
|
|
96
|
+
|
|
97
|
+
// Bubble from the sprite through its groups; then, when the press lets
|
|
98
|
+
// it, the root's listeners (every one of them - the root is the last
|
|
99
|
+
// stop, there is nothing left to claim). Returns whether the root ran.
|
|
100
|
+
let walk = (name: HandlerName, event: InternalEvent, toRoot: boolean): boolean => {
|
|
101
|
+
for (let n: Sprite | SpriteGroup | null = event.sprite; n !== null && !event._stopped; n = n._parent) {
|
|
102
|
+
// An inert handle (removed mid-press) has no handlers to run.
|
|
103
|
+
if (n.layer === null) continue
|
|
104
|
+
let handler = n[name] as ((event: InternalEvent) => void) | undefined
|
|
105
|
+
if (handler) {
|
|
106
|
+
event.currentTarget = n
|
|
107
|
+
handler(event)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (event._stopped || !toRoot) return false
|
|
111
|
+
event.currentTarget = deps.root
|
|
112
|
+
for (let listener of deps.listeners) {
|
|
113
|
+
let handler = listener[name] as ((event: InternalEvent) => void) | undefined
|
|
114
|
+
if (handler) handler(event)
|
|
115
|
+
}
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return layout => {
|
|
120
|
+
let toLayer = (e: ElementPointerEvent): [number, number] => {
|
|
121
|
+
let x = e.localX
|
|
122
|
+
let y = e.localY
|
|
123
|
+
let l = layout?.()
|
|
124
|
+
let [width, height] = deps.size()
|
|
125
|
+
if (l && l.width > 0 && l.height > 0) {
|
|
126
|
+
x *= width / l.width
|
|
127
|
+
y *= height / l.height
|
|
128
|
+
}
|
|
129
|
+
// Undo the camera: screen -> world.
|
|
130
|
+
return unprojectCamera(deps.camera(), x, y)
|
|
131
|
+
}
|
|
132
|
+
let makeEvent = (sprite: Sprite | null, x: number, y: number, e: ElementPointerEvent): InternalEvent => {
|
|
133
|
+
let event: InternalEvent = {
|
|
134
|
+
sprite,
|
|
135
|
+
currentTarget: sprite ?? deps.root,
|
|
136
|
+
x,
|
|
137
|
+
y,
|
|
138
|
+
pointerId: e.pointerId,
|
|
139
|
+
pointerType: e.pointerType,
|
|
140
|
+
button: e.button,
|
|
141
|
+
shiftKey: e.shiftKey,
|
|
142
|
+
ctrlKey: e.ctrlKey,
|
|
143
|
+
altKey: e.altKey,
|
|
144
|
+
metaKey: e.metaKey,
|
|
145
|
+
native: e,
|
|
146
|
+
_stopped: false,
|
|
147
|
+
stopPropagation() {
|
|
148
|
+
event._stopped = true
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
return event
|
|
152
|
+
}
|
|
153
|
+
let topmost = (x: number, y: number): Sprite | null => deps.pick(x, y)[0] ?? null
|
|
154
|
+
// Enter/leave go to one sprite directly, outside the walk: the event
|
|
155
|
+
// in its sprite view.
|
|
156
|
+
let hoverEvent = (sprite: Sprite, x: number, y: number, e: ElementPointerEvent): SpritePointerEvent =>
|
|
157
|
+
makeEvent(sprite, x, y, e) as unknown as SpritePointerEvent
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
onPointerDown(e) {
|
|
161
|
+
let [x, y] = toLayer(e)
|
|
162
|
+
let hit = topmost(x, y)
|
|
163
|
+
// A second pointer landing ends "alone" for every press in flight.
|
|
164
|
+
for (let p of presses.values()) p.alone = false
|
|
165
|
+
let press: Press = { target: hit, claimed: false, cx: e.clientX, cy: e.clientY, moved: false, alone: presses.size === 0 }
|
|
166
|
+
presses.set(e.pointerId, press)
|
|
167
|
+
press.claimed = !walk("onPointerDown", makeEvent(hit, x, y, e), true)
|
|
168
|
+
},
|
|
169
|
+
onPointerMove(e) {
|
|
170
|
+
let [x, y] = toLayer(e)
|
|
171
|
+
let press = presses.get(e.pointerId)
|
|
172
|
+
if (press) {
|
|
173
|
+
if (!press.moved && Math.hypot(e.clientX - press.cx, e.clientY - press.cy) >= TAP_SLOP) press.moved = true
|
|
174
|
+
walk("onPointerMove", makeEvent(press.target, x, y, e), !press.claimed)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
let hit = topmost(x, y)
|
|
178
|
+
let prev = hover.get(e.pointerId) ?? null
|
|
179
|
+
if (prev !== hit) {
|
|
180
|
+
if (prev && prev.layer) prev.onPointerLeave?.(hoverEvent(prev, x, y, e))
|
|
181
|
+
if (hit) hit.onPointerEnter?.(hoverEvent(hit, x, y, e))
|
|
182
|
+
if (hit) hover.set(e.pointerId, hit)
|
|
183
|
+
else hover.delete(e.pointerId)
|
|
184
|
+
}
|
|
185
|
+
walk("onPointerMove", makeEvent(hit, x, y, e), true)
|
|
186
|
+
},
|
|
187
|
+
onPointerUp(e) {
|
|
188
|
+
let [x, y] = toLayer(e)
|
|
189
|
+
let press = presses.get(e.pointerId)
|
|
190
|
+
if (!press) {
|
|
191
|
+
// A press this layer never saw go down: deliver to what is
|
|
192
|
+
// under it, as the element model does.
|
|
193
|
+
walk("onPointerUp", makeEvent(topmost(x, y), x, y, e), true)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
presses.delete(e.pointerId)
|
|
197
|
+
walk("onPointerUp", makeEvent(press.target, x, y, e), !press.claimed)
|
|
198
|
+
// The tap rule: no travel past the slop, alone for the whole
|
|
199
|
+
// press, released over the target it pressed (empty space for the
|
|
200
|
+
// layer itself).
|
|
201
|
+
if (press.moved || !press.alone || topmost(x, y) !== press.target) return
|
|
202
|
+
let time = now()
|
|
203
|
+
let repeat =
|
|
204
|
+
lastTap !== null &&
|
|
205
|
+
time - lastTap.time <= TAP_INTERVAL_MS &&
|
|
206
|
+
lastTap.target === press.target &&
|
|
207
|
+
Math.hypot(e.clientX - lastTap.cx, e.clientY - lastTap.cy) <= TAP_REPEAT_SLOP
|
|
208
|
+
let count = repeat ? lastTap!.count + 1 : 1
|
|
209
|
+
lastTap = { time, cx: e.clientX, cy: e.clientY, target: press.target, count }
|
|
210
|
+
let tap = makeEvent(press.target, x, y, e)
|
|
211
|
+
tap.tapCount = count
|
|
212
|
+
walk("onTap", tap, !press.claimed)
|
|
213
|
+
},
|
|
214
|
+
onWheel(e) {
|
|
215
|
+
let [x, y] = toLayer(e)
|
|
216
|
+
let event = makeEvent(topmost(x, y), x, y, e)
|
|
217
|
+
event.deltaX = e.deltaX
|
|
218
|
+
event.deltaY = e.deltaY
|
|
219
|
+
walk("onWheel", event, true)
|
|
220
|
+
},
|
|
221
|
+
onPointerLeave(e) {
|
|
222
|
+
let [x, y] = toLayer(e)
|
|
223
|
+
let prev = hover.get(e.pointerId)
|
|
224
|
+
if (prev) {
|
|
225
|
+
hover.delete(e.pointerId)
|
|
226
|
+
if (prev.layer) prev.onPointerLeave?.(hoverEvent(prev, x, y, e))
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,18 +12,43 @@
|
|
|
12
12
|
// without Solid components) and the components (SpriteLayer/Sprite/Group/
|
|
13
13
|
// TileLayer) on top. See AGENTS.md for the model and the traps.
|
|
14
14
|
|
|
15
|
-
export { addGroup, addSprite, createSpriteLayer, getSprite, removeGroup, removeSprite, setGroup, setGroupTransition, setSprite, setSpriteParent, setSpriteTransition, POSE_FLOATS, STYLE_FLOATS } from "./layer.ts"
|
|
15
|
+
export { addGroup, addSprite, createSpriteLayer, getSprite, removeGroup, removeSprite, setGroup, setGroupTransition, setSprite, setSpriteParent, setSpriteTransition, worldPosition, POSE_FLOATS, STYLE_FLOATS } from "./layer.ts"
|
|
16
16
|
export { createRecordLayer, FLOATS_PER_SPRITE } from "./records.ts"
|
|
17
|
-
export type { RecordLayer as RecordLayerHandle } from "./records.ts"
|
|
17
|
+
export type { RecordLayer as RecordLayerHandle, RecordLayerOptions } from "./records.ts"
|
|
18
18
|
export { pointInSprite } from "./pick.ts"
|
|
19
|
-
export
|
|
19
|
+
export { projectCamera, unprojectCamera } from "./camera.ts"
|
|
20
|
+
export type { CameraState, CameraUpdate } from "./camera.ts"
|
|
21
|
+
export { createCamera2d } from "./camera2d.ts"
|
|
22
|
+
export type { Camera2d as Camera2dHandle, Camera2dOptions } from "./camera2d.ts"
|
|
23
|
+
export type { Camera2dPose, Camera2dTarget, Rect2d } from "./camera-motion.ts"
|
|
24
|
+
export type {
|
|
25
|
+
AddSpriteOptions,
|
|
26
|
+
GroupOptions,
|
|
27
|
+
LayerPointerEvent,
|
|
28
|
+
LayerPointerListener,
|
|
29
|
+
LayerTapEvent,
|
|
30
|
+
LayerWheelEvent,
|
|
31
|
+
Sprite as SpriteHandle,
|
|
32
|
+
SpriteGroup,
|
|
33
|
+
SpriteHandlers,
|
|
34
|
+
SpriteLayer as SpriteLayerHandle,
|
|
35
|
+
SpriteLayerOptions,
|
|
36
|
+
SpriteOptions,
|
|
37
|
+
SpritePointerEvent,
|
|
38
|
+
SpriteTapEvent,
|
|
39
|
+
SpriteWheelEvent,
|
|
40
|
+
TransitionEndEvent,
|
|
41
|
+
} from "./layer.ts"
|
|
20
42
|
export { createTileLayer } from "./tiles.ts"
|
|
21
43
|
export type { TileChunk, TileLayer as TileLayerHandle, TileLayerOptions } from "./tiles.ts"
|
|
22
44
|
|
|
23
45
|
export type { NodeTransition, NodeTransitionSpec } from "flux:spatial"
|
|
24
46
|
export { grid, namedFrames, FULL_FRAME } from "./frames.ts"
|
|
25
47
|
export type { Frame, GridOptions } from "./frames.ts"
|
|
48
|
+
export { createAnimation } from "./animation.ts"
|
|
49
|
+
export type { AnimationOptions, SpriteAnimation } from "./animation.ts"
|
|
50
|
+
export { fitOversample } from "./oversample.ts"
|
|
26
51
|
export { createAtlas } from "./atlas.ts"
|
|
27
52
|
export type { Atlas, AtlasOptions } from "./atlas.ts"
|
|
28
|
-
export { Group, Sprite, SpriteLayer, TileLayer, useSpriteLayer } from "./components.tsx"
|
|
29
|
-
export type { GroupProps, SpriteLayerProps, SpritePointerProps, SpriteProps, TileCamera, TileLayerProps } from "./components.tsx"
|
|
53
|
+
export { Camera2d, Group, Sprite, SpriteLayer, TileLayer, useSpriteLayer } from "./components.tsx"
|
|
54
|
+
export type { Camera2dProps, GroupProps, LayerPointerProps, SpriteLayerProps, SpritePointerProps, SpriteProps, TileCamera, TileLayerProps } from "./components.tsx"
|