@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.
@@ -0,0 +1,140 @@
1
+ // The 2d camera: Godot's Camera2D and Three's MapControls in one control
2
+ // over the layers' shared CameraUpdate - drag to pan with inertia, wheel
3
+ // and pinch to zoom about the pointer, eased glides, fit-to-rect, follow a
4
+ // moving point through a dead zone with damping, rotation about the pivot,
5
+ // world bounds - extracted so apps stop rebuilding the same hundred
6
+ // lines. The motion itself is camera-motion.ts, pure and
7
+ // headless-checked; this file adds the input and the reactive face.
8
+ //
9
+ // Input goes through core's merged transform recognizer (createTransform),
10
+ // the same path as @solidrt/3d's orbit camera: the drag participates in
11
+ // the gesture arena (a layer inside a scroller arbitrates instead of both
12
+ // panning), the slop is swallowed, and one delta arrives per frame. One
13
+ // finger or a mouse drag pans; two fingers pan by their focal point and
14
+ // zoom by their span about it; the wheel zooms directly through an eased
15
+ // glide. Taps are not the camera's business: the layer's dispatch
16
+ // synthesizes them (onTap on sprites, groups and the root), as the DOM
17
+ // does for the elements a map control sits on.
18
+ //
19
+ // Two ways in. `attach(layer)` listens at the layer's root, so the camera
20
+ // sees exactly the presses the sprites let through: a sprite that drags
21
+ // itself stops its down and the camera never pans it, a drag on empty
22
+ // space or on a sprite without a claim pans, a wheel anywhere zooms.
23
+ // `handlers` spread straight onto a leaf for the layers without a
24
+ // dispatch (a tile world on its own). Either way the pose reaches the
25
+ // layers synchronously on input, so a drag needs no frame loop and the
26
+ // next paint carries the new camera.
27
+ //
28
+ // Coordinates: everything arrives in the input element's own frame. The
29
+ // recognizer reports the pinch focal in the element's local pixels (layer
30
+ // pixels on the layer's leaf) and the drag deltas in its parent frame, and
31
+ // the wheel uses the event's localX/localY - so a leaf under a designSize
32
+ // fit or any scaled ancestor pans and zooms correctly.
33
+ //
34
+ // Pose is plain mutable state advanced by update(dt) from a frame loop;
35
+ // `active()` is the reactive gate for that loop (true while a glide,
36
+ // fling, fit or follow needs frames; false at rest, when the camera costs
37
+ // nothing), which is how <Camera2d> runs frames only while something
38
+ // moves. update() pushes the pose to the layers only when it changed (one
39
+ // setCamera per driven layer: a shared-params write, however many sprites
40
+ // exist) and reports that, so per-frame dependents (labels re-projected
41
+ // via projectCamera) can follow the camera without recomputing every
42
+ // frame.
43
+
44
+ import { createSignal, createTransform } from "@solidrt/core"
45
+ import type { PointerEvent, WheelEvent } from "@solidrt/core"
46
+ import { createCameraMotion } from "./camera-motion.ts"
47
+ import type { Camera2dMotion, Camera2dMotionOptions, Camera2dTarget } from "./camera-motion.ts"
48
+ import type { LayerPointerListener } from "./layer.ts"
49
+
50
+ export type Camera2dOptions = Camera2dMotionOptions
51
+
52
+ export type Camera2d = Camera2dMotion & {
53
+ /** Reactive: whether update(dt) needs frames (see Camera2dMotion.active).
54
+ * Track it to run a frame loop only while the camera moves. */
55
+ active: () => boolean
56
+ /** Spread onto the element that receives input where there is no sprite
57
+ * dispatch to attach to - a tile world's leaf. Layers with a dispatch
58
+ * take `attach`. */
59
+ handlers: {
60
+ onPointerDown(e: PointerEvent): void
61
+ onPointerMove(e: PointerEvent): void
62
+ onPointerUp(e: PointerEvent): void
63
+ onWheel(e: WheelEvent): void
64
+ }
65
+ /**
66
+ * Take input from a layer's root (LayerBase.listen): the presses the
67
+ * sprites let through pan and pinch, every wheel zooms. A sprite that
68
+ * claims a press (stopPropagation on its down) keeps the camera out of
69
+ * that drag. Returns the detach. The layer must be one the camera
70
+ * drives, or its events arrive in a frame the camera does not show.
71
+ */
72
+ attach(layer: { listen(listener: LayerPointerListener): () => void }): () => void
73
+ }
74
+
75
+ /**
76
+ * Create a 2d camera driving `target`'s camera, where `target` is a sprite
77
+ * or record layer (or several: a layer and its overlay layer share one
78
+ * camera), or anything else with the layers' `setCamera`, such as a signal
79
+ * setter feeding `<TileLayer camera>`. The initial pose applies
80
+ * immediately: the world fitted when `world` is given and no `zoom`.
81
+ * Create it from an owned scope (a component body, like the orbit
82
+ * camera): the input recognizer registers its cleanup with the owner.
83
+ */
84
+ export function createCamera2d(target: Camera2dTarget | Camera2dTarget[], options: Camera2dOptions): Camera2d {
85
+ // Written by the motion from wherever it is driven - handlers, frames,
86
+ // a set() in a component body - hence the owned-write opt-in.
87
+ let [active, setActive] = createSignal(false, { ownedWrite: true })
88
+ let motion = createCameraMotion(target, options, setActive)
89
+
90
+ let transform = createTransform({
91
+ onTransformMove: (t) => {
92
+ if (t.scale !== 1) motion.zoomAt(t.x, t.y, t.scale)
93
+ if (t.dx !== 0 || t.dy !== 0) motion.panBy(t.dx, t.dy)
94
+ },
95
+ onTransformEnd: () => motion.release(),
96
+ })
97
+
98
+ let handlers: Camera2d["handlers"] = {
99
+ onPointerDown(e) {
100
+ if (e.button != null && e.button !== 0) return
101
+ // A finger landing on a gliding view stops it where it is.
102
+ motion.interrupt()
103
+ transform.handlers.onPointerDown(e)
104
+ },
105
+ onPointerMove(e) {
106
+ transform.handlers.onPointerMove(e)
107
+ },
108
+ onPointerUp(e) {
109
+ transform.handlers.onPointerUp(e)
110
+ },
111
+ onWheel(e) {
112
+ motion.wheel(e.localX, e.localY, e.deltaY)
113
+ },
114
+ }
115
+
116
+ return {
117
+ ...motion,
118
+ active,
119
+ handlers,
120
+ attach(layer) {
121
+ // Input pushes the pose synchronously (update(0)), so a drag needs
122
+ // no frame loop and the next paint carries the new camera.
123
+ return layer.listen({
124
+ onPointerDown: e => {
125
+ handlers.onPointerDown(e.native)
126
+ motion.update(0)
127
+ },
128
+ onPointerMove: e => {
129
+ handlers.onPointerMove(e.native)
130
+ motion.update(0)
131
+ },
132
+ onPointerUp: e => handlers.onPointerUp(e.native),
133
+ onWheel: e => {
134
+ handlers.onWheel(e.native)
135
+ motion.update(0)
136
+ },
137
+ })
138
+ },
139
+ }
140
+ }