@solidrt/2d 0.0.58 → 0.0.59
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 +55 -23
- package/examples/anim.tsx +3 -3
- package/examples/parity.tsx +4 -4
- package/examples/springs.tsx +29 -7
- package/package.json +3 -3
- package/src/animation.ts +5 -9
- package/src/camera2d.ts +20 -3
- package/src/components/group.tsx +6 -6
- package/src/components/sprite-layer.tsx +8 -0
- package/src/components/sprite.tsx +6 -6
- package/src/components/view2d.tsx +8 -0
- package/src/dispatch.ts +2 -2
- package/src/index.ts +6 -3
- package/src/layer.ts +231 -52
- package/src/records.ts +6 -6
- package/src/shaders.ts +22 -18
- package/src/tiles.ts +4 -4
- package/src/views.ts +13 -13
package/AGENTS.md
CHANGED
|
@@ -55,7 +55,7 @@ Contents:
|
|
|
55
55
|
### Three faces
|
|
56
56
|
|
|
57
57
|
THREE faces, layered: the node-backed live layer (layer.ts:
|
|
58
|
-
`createSpriteLayer`/`addSprite`/`setSprite`/`
|
|
58
|
+
`createSpriteLayer`/`addSprite`/`setSprite`/`destroySprite` plus
|
|
59
59
|
`addGroup`/`setGroup`/`setSpriteParent`/`setSpriteTransition`/
|
|
60
60
|
`setGroupTransition` - plain objects, no signals, usable without
|
|
61
61
|
components), the records layer (records.ts: `createRecordLayer` - the
|
|
@@ -95,7 +95,7 @@ real draw order says so with `orderBy` ("y" or "renderOrder" - see below).
|
|
|
95
95
|
|
|
96
96
|
Growth (past `capacity`, doubling): pose sinks move in ONE core
|
|
97
97
|
`retargetRecords` call (full republish next flush), style re-uploads,
|
|
98
|
-
`
|
|
98
|
+
`setDrawBuffers({ buffers })` swaps both, old buffers destroyed.
|
|
99
99
|
|
|
100
100
|
### Picking
|
|
101
101
|
|
|
@@ -139,11 +139,14 @@ the mover stay in the plane with no 2d narrowphase of their own.
|
|
|
139
139
|
Groups (`addGroup`/`<Group>`) are plain arena nodes (x, y, rotation,
|
|
140
140
|
UNIFORM scale - a group is a frame, never a sprite size; sprite w/h
|
|
141
141
|
lives in the sprite node's scale, which is why sprites cannot parent
|
|
142
|
-
sprites). Child sprite pose fields are local to the group. `
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
sprites). Child sprite pose fields are local to the group. `destroyGroup`
|
|
143
|
+
destroys the SUBTREE: child sprites and groups die with it (re-parent a
|
|
144
|
+
child out first to keep it), children first so each plays its own
|
|
145
|
+
`exit` and the group stays their frame until the last has settled. One
|
|
146
|
+
removal verb, the same word with the same meaning as @solidrt/3d's
|
|
147
|
+
`destroy`: a sprite cannot exist outside its layer, so there is no
|
|
148
|
+
detach here (3d's `remove`), and a sprite that should come back is
|
|
149
|
+
hidden (`visible: false`).
|
|
147
150
|
A handle's x/y are local to its group; `worldPosition(sprite | group)`
|
|
148
151
|
reads the layer-pixel position composed through every enclosing group
|
|
149
152
|
from the core's world matrix (@solidrt/3d's worldPosition).
|
|
@@ -186,7 +189,7 @@ the WRITE path only - whatever computes the motion is excluded and is
|
|
|
186
189
|
usually the dominant cost, e.g. a 24k-particle sim measured ~25ms with
|
|
187
190
|
a near-free publish) - the axis is
|
|
188
191
|
WHERE MOTION IS COMPUTED, not retained-vs-dynamic. The sprite functions
|
|
189
|
-
(addSprite/setSprite/getSprite/
|
|
192
|
+
(addSprite/setSprite/getSprite/destroySprite) work on both layer kinds;
|
|
190
193
|
record sprites have `node: null` and no groups.
|
|
191
194
|
|
|
192
195
|
### Layer space
|
|
@@ -216,7 +219,7 @@ functions over it.
|
|
|
216
219
|
Camera control: `createCamera2d(view | views, { viewport: () =>
|
|
217
220
|
({ width, height }), world?: { width, height }, min/maxZoom?, pivot?,
|
|
218
221
|
deadZone?: { width, height }, panSpeed?, zoomSpeed?, rollSpeed?,
|
|
219
|
-
followSpeed?, inertia?, x?, y?, zoom?, rotation? })` - Godot's Camera2D
|
|
222
|
+
damping?, followSpeed?, inertia?, x?, y?, zoom?, rotation? })` - Godot's Camera2D
|
|
220
223
|
and Three's MapControls in one control over the shared CameraUpdate:
|
|
221
224
|
pan with inertia on release, zoom about a point, eased `glideTo`/`fit`,
|
|
222
225
|
`follow(x, y)` through a dead zone with damping, roll about the pivot,
|
|
@@ -242,7 +245,9 @@ finger; a rate slides at one viewport height per second), `zoom` (axis,
|
|
|
242
245
|
octaves, positive in: a delta bracketed by a gesture - a pinch - applies
|
|
243
246
|
at once about its focal point, an unbracketed one - a wheel notch -
|
|
244
247
|
retargets an eased glide, which is how the control tells a finger from
|
|
245
|
-
an impulse; notches compound on the pending target
|
|
248
|
+
an impulse; notches compound on the pending target, and `damping` scales
|
|
249
|
+
the settle time - 0 applies a notch at once - the 3d controls' knob) and
|
|
250
|
+
`roll` (axis,
|
|
246
251
|
turns); a pan gesture's begin stops any glide (a finger landing on a
|
|
247
252
|
gliding view holds it) and its end flings with the drag's velocity. The
|
|
248
253
|
verbs, each pushing the pose at once: `panBy(dx, dy)` screen pixels,
|
|
@@ -274,7 +279,8 @@ inertia and the axis rates, pushes one setCamera per driven view when
|
|
|
274
279
|
the pose changed and reports that), gated on the reactive
|
|
275
280
|
`cam.active()` (true while a glide, fling, fit, follow or pan gesture
|
|
276
281
|
needs frames, or a rate drives; false at rest), read `cam.camera()` for
|
|
277
|
-
projectCamera
|
|
282
|
+
projectCamera and `cam.pose()` for the control's own four values (the
|
|
283
|
+
shape `set()` takes, the 3d controls' `pose()`). The camera has NO tap of its own: taps are the
|
|
278
284
|
dispatch's (`onTap` on the root with `e.sprite` null is "tap on empty
|
|
279
285
|
space"). `<Camera2d input={input}>` inside `<SpriteLayer>` is all of
|
|
280
286
|
that wired through context, the 3d `<OrbitCamera>` shape: options read
|
|
@@ -357,13 +363,36 @@ moving the same population imperatively). Retargeting every frame is
|
|
|
357
363
|
also a legitimate pattern, not an abuse: rewriting a spring's TARGET
|
|
358
364
|
each frame to chase a moving point (a follow-camera trailing a moving
|
|
359
365
|
sprite) rides the spring's smoothing for free - a spring keeps its
|
|
360
|
-
velocity through retargets, so the chase stays fluid.
|
|
361
|
-
component
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
366
|
+
velocity through retargets, so the chase stays fluid. A mount pose snaps
|
|
367
|
+
unless a component's `from` (2d units: `position: { from: [x, y] }`,
|
|
368
|
+
`rotation: { from: angle }`, `scale: { from: [w, h] }`, a group's
|
|
369
|
+
`from: s`) animates the sprite in from there - once, and only when the
|
|
370
|
+
declaration lands in the tick that added the sprite (the `<Sprite>` prop
|
|
371
|
+
or a setSpriteTransition right after addSprite; a later one animates
|
|
372
|
+
writes only). A component's `exit` (same units) is where it animates to
|
|
373
|
+
when destroySprite/destroyGroup lets go of it: the sprite stays drawn
|
|
374
|
+
while it leaves and is a GHOST meanwhile - no pick, raycast, overlap,
|
|
375
|
+
sweep or pointer event sees it - then frees when the last exit settles
|
|
376
|
+
(a bullet never hits a corpse; an entering sprite is live from its
|
|
377
|
+
first frame, whatever `from` it passes through). Either endpoint takes
|
|
378
|
+
the object form `{ value, duration?, curve?, bounce?, delay? }` to own
|
|
379
|
+
its direction's motion (an ease-out enter, an ease-in exit), and `delay`
|
|
380
|
+
on an entry holds its writes that long on the animation clock (a late
|
|
381
|
+
frame catches up, so a hitch never shifts a held start). `stagger` (ms)
|
|
382
|
+
goes on a GROUP's declaration (`setGroupTransition` / `<Group
|
|
383
|
+
transition>`), never on the sprites: it spaces the enters and exits of
|
|
384
|
+
everything under the group that begin in one frame by `index * stagger`
|
|
385
|
+
(add order in, tree order out whatever the destroy order; nearest
|
|
386
|
+
declaring group wins), and
|
|
387
|
+
cascades nothing unless the sprites declare `from`/`exit` - a hand of
|
|
388
|
+
cards fanning in, a menu popping in, is one `stagger` on the group. For
|
|
389
|
+
the sprites straight under the layer (no group) the layer's root carries
|
|
390
|
+
it: `createSpriteLayer(texture, { stagger })` / `layer.setStagger(ms)`.
|
|
391
|
+
Each natural settle calls the handle's `onTransitionEnd` (plain field,
|
|
392
|
+
or the `<Sprite>`/`<Group>` prop) with `{ component }` - target-only,
|
|
393
|
+
never on a cancel, snap or exit; the raw "spatialTransitionEnd" engine
|
|
394
|
+
event (srt:events, node = sprite.node) stays for flux:spatial consumers.
|
|
395
|
+
See examples/springs.tsx (tap a sprite: it leaves through its `exit`).
|
|
367
396
|
|
|
368
397
|
### Frame-rate motion
|
|
369
398
|
|
|
@@ -378,8 +407,9 @@ re-runs an effect per sprite per frame for nothing.
|
|
|
378
407
|
|
|
379
408
|
Frame animation (animation.ts): `createAnimation(frames, fps, { loop })`
|
|
380
409
|
is a clip with a shared wall-clock timer stepping every attached sprite
|
|
381
|
-
(`anim.add(sprite)` / `remove`; `play`/`pause`; `
|
|
382
|
-
|
|
410
|
+
(`anim.add(sprite)` / `remove`; `play`/`pause`; `frame()`/`playing()`
|
|
411
|
+
read the clock, methods like the 3d mixer's `playing()`; `loop: false`
|
|
412
|
+
holds the last frame and fires `onFinish`, the mixer's name for it in
|
|
383
413
|
@solidrt/3d). One timer per playing clip, one setSprite
|
|
384
414
|
per sprite per STEP - an 8fps cycle is 8 publishes a second regardless of
|
|
385
415
|
display rate, and a paused clip costs nothing. A sprite belongs to at
|
|
@@ -482,7 +512,9 @@ on approach, evict) - okf/backlog/2d-baked-layers.md.
|
|
|
482
512
|
|
|
483
513
|
`SpriteLayer` owns the layer and its own view, rendered as the built-in
|
|
484
514
|
`<texture>` leaf carrying the view's pointer handlers (opt out with
|
|
485
|
-
`events={false}
|
|
515
|
+
`events={false}`, which throws at mount together with a `pointer` feed -
|
|
516
|
+
the feed would listen at a root no event reaches; the same on `<View2d>`;
|
|
517
|
+
compose yourself with `output`, then spread
|
|
486
518
|
`useSpriteLayer().viewport.handlers` onto your leaf; `output={false}` for
|
|
487
519
|
no own view at all). `useSpriteLayer()` returns `{ layer, parent,
|
|
488
520
|
viewport, pointer }` - the same shape as `useScene()` in `@solidrt/3d` -
|
|
@@ -611,7 +643,7 @@ hover, wheel and tap rules headless.
|
|
|
611
643
|
a hoisted reference becomes a dead copy whose writes publish nothing.
|
|
612
644
|
`layer.withRecords(fn)` is the hoist-proof read; a bare `layer.records`
|
|
613
645
|
at use time is equally live.
|
|
614
|
-
- Records layer: record order is draw order: `
|
|
646
|
+
- Records layer: record order is draw order: `destroySprite` shifts every
|
|
615
647
|
later sprite down one slot (copyWithin + index fixup, O(later
|
|
616
648
|
sprites)). Its flush publishes the WHOLE live prefix, not a dirty
|
|
617
649
|
range: one moved sprite re-publishes count x 52 bytes - a single
|
|
@@ -650,7 +682,7 @@ hover, wheel and tap rules headless.
|
|
|
650
682
|
### Nodes, transitions and queries
|
|
651
683
|
|
|
652
684
|
- Node layer: `sprite.node` is public FOR BINDING PRODUCERS, not for
|
|
653
|
-
lifecycle - never destroyNode it yourself (
|
|
685
|
+
lifecycle - never destroyNode it yourself (destroySprite owns that), and
|
|
654
686
|
a transform written through flux:spatial directly bypasses the sprite's
|
|
655
687
|
pose mirror, so a later setSprite with the old x wins (its compare sees
|
|
656
688
|
no change to skip, but partial writes compose from the mirror).
|
package/examples/anim.tsx
CHANGED
|
@@ -62,15 +62,15 @@ function App() {
|
|
|
62
62
|
}, period / 2)
|
|
63
63
|
setTimeout(() => {
|
|
64
64
|
check(shownIndex(looped[1]!) === 1, "loop advanced to frame 1")
|
|
65
|
-
check(looping.frame === shownIndex(looped[1]!), "anim.frame matches the sprite's shown frame")
|
|
65
|
+
check(looping.frame() === shownIndex(looped[1]!), "anim.frame matches the sprite's shown frame")
|
|
66
66
|
}, period * 1.5)
|
|
67
67
|
setTimeout(() => {
|
|
68
68
|
check(shownIndex(looped[2]!) === 0, "loop wrapped back to frame 0 after a full cycle")
|
|
69
69
|
// One-shot: 4 frames at 5fps end at 800ms - long since done here.
|
|
70
70
|
check(shownIndex(shotSprite) === frames.length - 1, "one-shot holds its last frame")
|
|
71
|
-
check(!shot.playing, "one-shot stopped playing")
|
|
71
|
+
check(!shot.playing(), "one-shot stopped playing")
|
|
72
72
|
check(ended === 1, `onFinish fired once, got ${ended}`)
|
|
73
|
-
check(looping.playing, "the looping clip is still playing")
|
|
73
|
+
check(looping.playing(), "the looping clip is still playing")
|
|
74
74
|
console.log(failures.length === 0 ? "ANIM-OK" : `ANIM-FAIL: ${failures.join("; ")}`)
|
|
75
75
|
}, period * 4.5)
|
|
76
76
|
|
package/examples/parity.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
createSpriteLayer,
|
|
18
18
|
getSprite,
|
|
19
19
|
grid,
|
|
20
|
-
|
|
20
|
+
destroySprite,
|
|
21
21
|
setGroup,
|
|
22
22
|
setSprite,
|
|
23
23
|
} from "@solidrt/2d"
|
|
@@ -83,11 +83,11 @@ function App() {
|
|
|
83
83
|
{
|
|
84
84
|
let extra = addSprite(nodes, { x: 10, y: 10, w: 8, h: 8 })
|
|
85
85
|
let slotBefore = extra._slot
|
|
86
|
-
|
|
86
|
+
destroySprite(extra)
|
|
87
87
|
let reused = addSprite(nodes, { x: -100, y: -100, w: 0, h: 0 })
|
|
88
88
|
check(reused._slot === slotBefore, "removed slot recycles to the next add")
|
|
89
89
|
check(getSprite(extra) === null, "removed handle is inert")
|
|
90
|
-
|
|
90
|
+
destroySprite(reused)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
let pixelParity = () => {
|
|
@@ -158,7 +158,7 @@ function App() {
|
|
|
158
158
|
check(nodes.pick(150, 100)[0] !== child, "the ungrouped position no longer hits")
|
|
159
159
|
let f = getSprite(child)!
|
|
160
160
|
check(f.x === 50 && f.y === 0, "getSprite reads the local pose")
|
|
161
|
-
|
|
161
|
+
destroySprite(child)
|
|
162
162
|
// The group node stays for the layer to dispose.
|
|
163
163
|
})
|
|
164
164
|
}
|
package/examples/springs.tsx
CHANGED
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
// random angle always takes the short arc, and the position spring keeps
|
|
14
14
|
// its velocity when a shuffle lands mid-flight - retarget as fast as you
|
|
15
15
|
// like, the motion stays continuous.
|
|
16
|
+
//
|
|
17
|
+
// Tap a sprite and it LEAVES: destroySprite lets go of the handle, and the
|
|
18
|
+
// `exit` on its scale entry shrinks it away on an ease-in of its own while
|
|
19
|
+
// the sprite is a ghost - drawn, but no tap or pick reaches it - then the
|
|
20
|
+
// core frees it. A fresh sprite pops into the empty slot a moment later,
|
|
21
|
+
// entering from scale zero (`from`); no settle callback, no bookkeeping.
|
|
16
22
|
import { decodeImage, render } from "@solidrt/core"
|
|
17
|
-
import { addSprite, createAtlas, createSpriteLayer, grid, setSprite, setSpriteTransition } from "@solidrt/2d"
|
|
23
|
+
import { addSprite, createAtlas, createSpriteLayer, destroySprite, grid, setSprite, setSpriteTransition } from "@solidrt/2d"
|
|
24
|
+
import type { SpriteHandle, SpriteTransition } from "@solidrt/2d"
|
|
18
25
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
19
26
|
|
|
20
27
|
const COLS = 20
|
|
@@ -23,6 +30,16 @@ const COUNT = COLS * ROWS
|
|
|
23
30
|
const W = 720
|
|
24
31
|
const H = 720
|
|
25
32
|
const SPRITE = 30
|
|
33
|
+
// The pop-in / shrink-out of a sprite's size, and the pause before a
|
|
34
|
+
// tapped-away sprite is replaced.
|
|
35
|
+
const POP_MS = 300
|
|
36
|
+
const RESPAWN_MS = 600
|
|
37
|
+
|
|
38
|
+
const TRANSITION: SpriteTransition = {
|
|
39
|
+
position: { duration: 700, bounce: 0.3 },
|
|
40
|
+
rotation: { duration: 700 },
|
|
41
|
+
scale: { duration: POP_MS, bounce: 0.4, from: [0, 0], exit: { value: [0, 0], curve: "ease-in", duration: POP_MS / 2 } },
|
|
42
|
+
}
|
|
26
43
|
|
|
27
44
|
function App() {
|
|
28
45
|
let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
|
|
@@ -35,12 +52,17 @@ function App() {
|
|
|
35
52
|
|
|
36
53
|
// Sprite k sits at grid slot slots[k]; each shuffle re-deals the slots.
|
|
37
54
|
let slots = Array.from({ length: COUNT }, (_, i) => i)
|
|
38
|
-
let
|
|
39
|
-
addSprite(layer, { x: slotX(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
let spawn = (k: number): SpriteHandle => {
|
|
56
|
+
let sprite = addSprite(layer, { x: slotX(slots[k]!), y: slotY(slots[k]!), w: SPRITE, h: SPRITE, frame: frames[k % 4] })
|
|
57
|
+
// Declared in the adding tick, so the `from` plays: the sprite pops in.
|
|
58
|
+
setSpriteTransition(sprite, TRANSITION)
|
|
59
|
+
sprite.onTap = () => {
|
|
60
|
+
destroySprite(sprite)
|
|
61
|
+
setTimeout(() => (sprites[k] = spawn(k)), RESPAWN_MS)
|
|
62
|
+
}
|
|
63
|
+
return sprite
|
|
43
64
|
}
|
|
65
|
+
let sprites = slots.map((_, k) => spawn(k))
|
|
44
66
|
|
|
45
67
|
let shuffle = () => {
|
|
46
68
|
for (let i = slots.length - 1; i > 0; i--) {
|
|
@@ -58,7 +80,7 @@ function App() {
|
|
|
58
80
|
|
|
59
81
|
return (
|
|
60
82
|
<window alignItems="center" justifyContent="center">
|
|
61
|
-
<texture src={view.texture} width={W} height={H} />
|
|
83
|
+
<texture src={view.texture} width={W} height={H} {...view.handlers} />
|
|
62
84
|
</window>
|
|
63
85
|
)
|
|
64
86
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.59",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"AGENTS.md"
|
|
16
16
|
],
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
19
|
-
"@solidrt/core": "0.0.
|
|
18
|
+
"@solidjs/signals": "2.0.0-rc.8",
|
|
19
|
+
"@solidrt/core": "0.0.59"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/src/animation.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// playing clip and one frame write per attached sprite per STEP (not per
|
|
4
4
|
// display frame), so an 8fps walk cycle generates 8 publishes a second and
|
|
5
5
|
// a paused clip costs nothing - the demand-gate story unchanged. The clip
|
|
6
|
-
// does not own its sprites (
|
|
6
|
+
// does not own its sprites (destroySprite prunes lazily on the next step),
|
|
7
7
|
// and a sprite belongs to at most one animation. Plain JS over setSprite,
|
|
8
8
|
// so it works on both layer kinds and composes with <Sprite> via ref (leave
|
|
9
9
|
// the frame prop off - the clip owns that field).
|
|
@@ -28,10 +28,10 @@ export type AnimationOptions = {
|
|
|
28
28
|
|
|
29
29
|
export type SpriteAnimation = {
|
|
30
30
|
/** Current frame index into the clip. */
|
|
31
|
-
|
|
31
|
+
frame(): number
|
|
32
32
|
/** True while the clock advances: born playing, false after pause() and
|
|
33
33
|
* after a one-shot ends. */
|
|
34
|
-
|
|
34
|
+
playing(): boolean
|
|
35
35
|
/**
|
|
36
36
|
* Attach a sprite: it shows the clip's current frame immediately and
|
|
37
37
|
* steps with the shared clock (every attached sprite shows the SAME
|
|
@@ -127,12 +127,8 @@ export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOp
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
let handle: SpriteAnimation = {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
get playing() {
|
|
134
|
-
return playing
|
|
135
|
-
},
|
|
130
|
+
frame: () => current,
|
|
131
|
+
playing: () => playing,
|
|
136
132
|
onFinish: undefined,
|
|
137
133
|
add(sprite) {
|
|
138
134
|
if (disposed) throw new Error("createAnimation: add on a disposed animation")
|
package/src/camera2d.ts
CHANGED
|
@@ -53,7 +53,8 @@ import { projectCamera, unprojectCamera } from "./camera.ts"
|
|
|
53
53
|
import type { CameraUpdate } from "./camera.ts"
|
|
54
54
|
|
|
55
55
|
// Glide rate toward a pending zoom or pose, e-foldings per second: high
|
|
56
|
-
// enough that a wheel notch reads as one push, low enough to look smooth
|
|
56
|
+
// enough that a wheel notch reads as one push, low enough to look smooth
|
|
57
|
+
// (the 3d controls' rate, motion.ts there); `damping` scales a notch's.
|
|
57
58
|
const GLIDE_EASE = 9
|
|
58
59
|
// A glide lands when the remaining zoom gap is under this (relative) ...
|
|
59
60
|
const GLIDE_EPSILON = 0.002
|
|
@@ -86,6 +87,7 @@ const ROLL_RATE = 0.5
|
|
|
86
87
|
/** What a 2d camera drives: anything with the layers' `setCamera`. */
|
|
87
88
|
export type Camera2dTarget = { setCamera(update: CameraUpdate): void }
|
|
88
89
|
|
|
90
|
+
/** The control's parameters: what set() takes and pose() returns filled. */
|
|
89
91
|
export type Camera2dPose = {
|
|
90
92
|
x?: number
|
|
91
93
|
y?: number
|
|
@@ -121,6 +123,11 @@ export type Camera2dOptions = Camera2dPose & {
|
|
|
121
123
|
panSpeed?: number
|
|
122
124
|
zoomSpeed?: number
|
|
123
125
|
rollSpeed?: number
|
|
126
|
+
/** How long an unbracketed zoom delta (a wheel notch) takes to ease in,
|
|
127
|
+
* as a multiple of the built-in settle time: 1 (the default) is the
|
|
128
|
+
* built-in ease, 2 coasts twice as long, 0 applies notches at once, as
|
|
129
|
+
* a pinch does. The 3d controls' knob, same meaning. */
|
|
130
|
+
damping?: number
|
|
124
131
|
/** Multiplier over the built-in follow damping. */
|
|
125
132
|
followSpeed?: number
|
|
126
133
|
/** Whether a drag release keeps the view gliding (default true). */
|
|
@@ -133,6 +140,9 @@ export type Camera2d = {
|
|
|
133
140
|
/** The pose as the layers receive it (a fresh object per call): the
|
|
134
141
|
* argument for projectCamera/unprojectCamera. */
|
|
135
142
|
camera(): CameraUpdate
|
|
143
|
+
/** Pose snapshot - the control's own four values, the shape set() takes
|
|
144
|
+
* (camera() adds the pivot the layers are told). */
|
|
145
|
+
pose(): Required<Camera2dPose>
|
|
136
146
|
/** Merge a pose in (clamps apply) and push it. A snap: an x/y/zoom
|
|
137
147
|
* write cancels a glide or fling in flight; rotation alone leaves them
|
|
138
148
|
* running. */
|
|
@@ -222,6 +232,9 @@ function checkOptions(options: Camera2dOptions): void {
|
|
|
222
232
|
}
|
|
223
233
|
finite("pivot.x", options.pivot?.x)
|
|
224
234
|
finite("pivot.y", options.pivot?.y)
|
|
235
|
+
if (options.damping !== undefined && !(Number.isFinite(options.damping) && options.damping >= 0)) {
|
|
236
|
+
throw new Error(`createCamera2d: damping must be a non-negative number, got ${options.damping}`)
|
|
237
|
+
}
|
|
225
238
|
checkFraction("deadZone.width", options.deadZone?.width ?? 0)
|
|
226
239
|
checkFraction("deadZone.height", options.deadZone?.height ?? 0)
|
|
227
240
|
}
|
|
@@ -257,6 +270,7 @@ export function createCamera2d(target: Camera2dTarget | Camera2dTarget[], option
|
|
|
257
270
|
let panSpeed = () => options.panSpeed ?? 1
|
|
258
271
|
let zoomExponent = () => options.zoomSpeed ?? 1
|
|
259
272
|
let rollSpeed = () => options.rollSpeed ?? 1
|
|
273
|
+
let damping = () => options.damping ?? 1
|
|
260
274
|
let followEase = () => FOLLOW_EASE * (options.followSpeed ?? 1)
|
|
261
275
|
let inertia = () => options.inertia ?? true
|
|
262
276
|
|
|
@@ -443,7 +457,7 @@ export function createCamera2d(target: Camera2dTarget | Camera2dTarget[], option
|
|
|
443
457
|
let zoomAt = (sx: number, sy: number, factor: number, ease: boolean) => {
|
|
444
458
|
positive("zoomAt factor", factor)
|
|
445
459
|
readViewport()
|
|
446
|
-
if (!ease) {
|
|
460
|
+
if (!ease || damping() <= 0) {
|
|
447
461
|
interrupt()
|
|
448
462
|
zoomTo(sx, sy, zoom * factor)
|
|
449
463
|
return
|
|
@@ -512,6 +526,7 @@ export function createCamera2d(target: Camera2dTarget | Camera2dTarget[], option
|
|
|
512
526
|
|
|
513
527
|
return {
|
|
514
528
|
camera,
|
|
529
|
+
pose: () => ({ x, y, zoom, rotation }),
|
|
515
530
|
active,
|
|
516
531
|
axes,
|
|
517
532
|
set(pose) {
|
|
@@ -654,7 +669,9 @@ export function createCamera2d(target: Camera2dTarget | Camera2dTarget[], option
|
|
|
654
669
|
touch()
|
|
655
670
|
}
|
|
656
671
|
if (glide !== null && dt > 0) {
|
|
657
|
-
|
|
672
|
+
// A wheel notch's glide coasts as `damping` says; a commanded
|
|
673
|
+
// glide (glideTo, fit) at the built-in rate.
|
|
674
|
+
let k = 1 - Math.exp(-(glide.kind === "anchor" ? GLIDE_EASE / damping() : GLIDE_EASE) * dt)
|
|
658
675
|
if (glide.kind === "anchor") {
|
|
659
676
|
let gap = Math.log(glide.target / zoom)
|
|
660
677
|
zoom = Math.abs(gap) < GLIDE_EPSILON ? glide.target : zoom * Math.exp(gap * k)
|
package/src/components/group.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
2
|
import type { ParentComponent } from "@solidrt/core"
|
|
3
|
-
import { addGroup,
|
|
3
|
+
import { addGroup, destroyGroup, setGroup, setGroupTransition } from "../layer.ts"
|
|
4
4
|
import type { SpriteGroup, SpritePointerEvent, SpriteTapEvent, SpriteWheelEvent, TransitionEndEvent } from "../layer.ts"
|
|
5
|
-
import type {
|
|
5
|
+
import type { SpriteTransition } from "../layer.ts"
|
|
6
6
|
import { GroupContext, LayerContext } from "./context.ts"
|
|
7
7
|
|
|
8
8
|
export type GroupProps = {
|
|
@@ -23,9 +23,9 @@ export type GroupProps = {
|
|
|
23
23
|
onPointerUp?: (event: SpritePointerEvent) => void
|
|
24
24
|
onWheel?: (event: SpriteWheelEvent) => void
|
|
25
25
|
onTap?: (event: SpriteTapEvent) => void
|
|
26
|
-
/** How pose-prop changes animate (see setGroupTransition)
|
|
27
|
-
* pose
|
|
28
|
-
transition?:
|
|
26
|
+
/** How pose-prop changes animate (see setGroupTransition). The mount
|
|
27
|
+
* pose snaps, unless a component's `from` animates it in from there. */
|
|
28
|
+
transition?: SpriteTransition | string | null
|
|
29
29
|
/** A declared transition settled on one component. */
|
|
30
30
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
31
31
|
ref?: (group: SpriteGroup) => void
|
|
@@ -63,6 +63,6 @@ export let Group: ParentComponent<GroupProps> = props => {
|
|
|
63
63
|
},
|
|
64
64
|
)
|
|
65
65
|
untrack(() => props.ref)?.(group)
|
|
66
|
-
onCleanup(() =>
|
|
66
|
+
onCleanup(() => destroyGroup(group))
|
|
67
67
|
return <GroupContext value={group}>{props.children}</GroupContext>
|
|
68
68
|
}
|
|
@@ -241,6 +241,14 @@ export let SpriteLayer: ParentComponent<SpriteLayerProps> = props => {
|
|
|
241
241
|
}),
|
|
242
242
|
)
|
|
243
243
|
let events = untrack(() => props.events) !== false
|
|
244
|
+
// A feed on a built-in leaf carrying no handlers listens at a root no
|
|
245
|
+
// event reaches: a contradiction, so it throws (the dev validation
|
|
246
|
+
// policy) instead of feeding nothing.
|
|
247
|
+
if (pointer !== null && !events && !output) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"SpriteLayer: a pointer feed with events={false} receives nothing - the feed listens at the view's root, which only the leaf's handlers reach; keep events on, or compose the leaf with output and spread useSpriteLayer().viewport.handlers on it",
|
|
250
|
+
)
|
|
251
|
+
}
|
|
244
252
|
let leaf: { id: number } | undefined
|
|
245
253
|
// The view's pixel size: the props in fixed mode, the built-in leaf's
|
|
246
254
|
// laid-out box in fill mode (getLayoutBox, the untransformed read, so a
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
2
|
import type { VoidComponent } from "@solidrt/core"
|
|
3
|
-
import { addSprite,
|
|
3
|
+
import { addSprite, destroySprite, setSprite, setSpriteTransition } from "../layer.ts"
|
|
4
4
|
import type { Sprite as SpriteHandle, SpriteOptions, SpritePointerEvent, SpriteTapEvent, SpriteWheelEvent, TransitionEndEvent } from "../layer.ts"
|
|
5
|
-
import type {
|
|
5
|
+
import type { SpriteTransition } from "../layer.ts"
|
|
6
6
|
import { GroupContext, LayerContext } from "./context.ts"
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -29,9 +29,9 @@ export type SpritePointerProps = {
|
|
|
29
29
|
|
|
30
30
|
export type SpriteProps = SpriteOptions &
|
|
31
31
|
SpritePointerProps & {
|
|
32
|
-
/** How pose-prop changes animate (see setSpriteTransition)
|
|
33
|
-
* pose
|
|
34
|
-
transition?:
|
|
32
|
+
/** How pose-prop changes animate (see setSpriteTransition). The mount
|
|
33
|
+
* pose snaps, unless a component's `from` animates it in from there. */
|
|
34
|
+
transition?: SpriteTransition | string | null
|
|
35
35
|
/** A declared transition settled on one component. */
|
|
36
36
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
37
37
|
ref?: (sprite: SpriteHandle) => void
|
|
@@ -67,6 +67,6 @@ export let Sprite: VoidComponent<SpriteProps> = props => {
|
|
|
67
67
|
},
|
|
68
68
|
)
|
|
69
69
|
untrack(() => props.ref)?.(sprite)
|
|
70
|
-
onCleanup(() =>
|
|
70
|
+
onCleanup(() => destroySprite(sprite))
|
|
71
71
|
return null
|
|
72
72
|
}
|
|
@@ -113,6 +113,14 @@ export let View2d: ParentComponent<View2dProps> = props => {
|
|
|
113
113
|
)
|
|
114
114
|
let output = untrack(() => props.output)
|
|
115
115
|
let events = untrack(() => props.events) !== false
|
|
116
|
+
// A feed on a built-in leaf carrying no handlers listens at a root no
|
|
117
|
+
// event reaches: a contradiction, so it throws (the dev validation
|
|
118
|
+
// policy) instead of feeding nothing.
|
|
119
|
+
if (pointer !== null && !events && !output) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"View2d: a pointer feed with events={false} receives nothing - the feed listens at the view's root, which only the leaf's handlers reach; keep events on, or compose the leaf with output and spread view.handlers on it",
|
|
122
|
+
)
|
|
123
|
+
}
|
|
116
124
|
let leaf: { id: number } | undefined
|
|
117
125
|
// Auto oversample from the built-in leaf's window box, in device pixels,
|
|
118
126
|
// per view pixel: the same pick as SpriteLayer's fixed mode.
|
package/src/dispatch.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
23
23
|
import { unprojectCamera } from "./camera.ts"
|
|
24
24
|
import type { CameraUpdate } from "./camera.ts"
|
|
25
|
-
import type { LayerPointerListener, Sprite, SpriteGroup,
|
|
25
|
+
import type { LayerPointerListener, Sprite, SpriteGroup, LayerHandlers, SpritePointerEvent } from "./layer.ts"
|
|
26
26
|
import type { ViewHandle } from "./views.ts"
|
|
27
27
|
|
|
28
28
|
// Finger travel from the down point, in window pixels, past which a press
|
|
@@ -88,7 +88,7 @@ type Press = {
|
|
|
88
88
|
alone: boolean
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function spriteDispatch(deps: DispatchDeps): (layout: (() => { width: number; height: number }) | null) =>
|
|
91
|
+
export function spriteDispatch(deps: DispatchDeps): (layout: (() => { width: number; height: number }) | null) => LayerHandlers {
|
|
92
92
|
let now = deps.now ?? (() => performance.now())
|
|
93
93
|
let presses = new Map<number, Press>()
|
|
94
94
|
let hover = new Map<number, Sprite>()
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// without Solid components) and the components (SpriteLayer/Sprite/Group/
|
|
16
16
|
// View2d/TileLayer) on top. See AGENTS.md for the model and the traps.
|
|
17
17
|
|
|
18
|
-
export { addGroup, addSprite, createSpriteLayer, getSprite,
|
|
18
|
+
export { addGroup, addSprite, createSpriteLayer, getSprite, destroyGroup, destroySprite, setGroup, setGroupTransition, setSprite, setSpriteParent, setSpriteTransition, worldPosition, POSE_FLOATS, STYLE_FLOATS } from "./layer.ts"
|
|
19
19
|
export { createRecordLayer, FLOATS_PER_SPRITE } from "./records.ts"
|
|
20
20
|
export type { RecordLayer as RecordLayerHandle, RecordLayerOptions } from "./records.ts"
|
|
21
21
|
export { pointInSprite } from "./pick.ts"
|
|
@@ -35,7 +35,7 @@ export type {
|
|
|
35
35
|
MoveResult,
|
|
36
36
|
Overlap,
|
|
37
37
|
QueryOptions,
|
|
38
|
-
|
|
38
|
+
Hit,
|
|
39
39
|
Rect,
|
|
40
40
|
LayerPointerEvent,
|
|
41
41
|
LayerPointerListener,
|
|
@@ -43,12 +43,15 @@ export type {
|
|
|
43
43
|
LayerWheelEvent,
|
|
44
44
|
Sprite as SpriteHandle,
|
|
45
45
|
SpriteGroup,
|
|
46
|
-
|
|
46
|
+
LayerHandlers,
|
|
47
47
|
SpriteLayer as SpriteLayerHandle,
|
|
48
48
|
SpriteLayerOptions,
|
|
49
49
|
SpriteOptions,
|
|
50
50
|
SpritePointerEvent,
|
|
51
51
|
SpriteTapEvent,
|
|
52
|
+
SpriteEndpoint,
|
|
53
|
+
SpriteTransition,
|
|
54
|
+
SpriteTransitionSpec,
|
|
52
55
|
SpriteWheelEvent,
|
|
53
56
|
TransitionEndEvent,
|
|
54
57
|
Volume,
|