@solidrt/2d 0.0.60 → 0.0.62
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 +119 -48
- package/examples/camera.tsx +60 -22
- package/examples/split-screen.tsx +1 -1
- package/package.json +3 -3
- package/src/camera2d.ts +282 -198
- package/src/components/camera2d.tsx +10 -8
- package/src/components/index.ts +2 -0
- package/src/components/shots.tsx +90 -0
- package/src/components/sprite-layer.tsx +13 -0
- package/src/index.ts +4 -2
- package/src/input.ts +5 -3
- package/src/layer.ts +4 -1
- package/src/shots.ts +50 -0
- package/src/tiles-math.ts +62 -0
- package/src/tiles.ts +47 -63
package/AGENTS.md
CHANGED
|
@@ -216,24 +216,79 @@ functions over it.
|
|
|
216
216
|
|
|
217
217
|
### Camera control
|
|
218
218
|
|
|
219
|
+
The camera controls run one pipeline (okf/design/camera-controls.md):
|
|
220
|
+
a SOURCE (an input nudge, a verb, the follow) writes the pose, the
|
|
221
|
+
FRAMING eases a followed point into its zones, the LANES (a screen
|
|
222
|
+
offset, shakes) are summed on top of the pose, the CONSTRAINTS (the zoom
|
|
223
|
+
range, the contain clamp) apply to pose plus lanes and move the pose,
|
|
224
|
+
and the PUSH writes the final camera. `pose()` is the pose alone;
|
|
225
|
+
`camera()` the final camera as the layers received it.
|
|
226
|
+
|
|
219
227
|
Camera control: `createCamera2d(view | views, { viewport: () =>
|
|
220
|
-
({ width, height }), world?: { width, height }, min/maxZoom?,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
and Three's MapControls in one
|
|
224
|
-
pan with inertia on release, zoom
|
|
225
|
-
`follow(x, y)` through
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
the
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
228
|
+
({ width, height }), world?: { width, height, damping? }, min/maxZoom?,
|
|
229
|
+
pivot?, follow?: { damping?, deadZone?, hardLimits?, lookahead? },
|
|
230
|
+
offset?, panSpeed?, zoomSpeed?, rollSpeed?, damping?, inertia?, x?, y?,
|
|
231
|
+
zoom?, rotation? })` - Godot's Camera2D and Three's MapControls in one
|
|
232
|
+
control over the shared CameraUpdate: pan with inertia on release, zoom
|
|
233
|
+
about a point, eased `glideTo`/`fit`, `follow(x, y)` through
|
|
234
|
+
Cinemachine's framing, roll about the pivot, and world bounds that
|
|
235
|
+
CONTAIN the view (an axis whose view is wider than the world centers -
|
|
236
|
+
so at the fit zoom a pan is a no-op, there is nothing to pan; limits
|
|
237
|
+
ignore rotation, Godot's rule; a `world.damping` eases a MOTION into
|
|
238
|
+
the bounds instead of cutting it at the edge, Godot's limit_smoothed,
|
|
239
|
+
while a direct write always clamps at once). The pose is "world point
|
|
240
|
+
at the pivot", the pivot a viewport fraction defaulting to the center,
|
|
241
|
+
so `camera().x/y` is the view center and glideTo/follow land there
|
|
242
|
+
(without a `world`, the default pose puts world 0,0 at the pivot: a
|
|
243
|
+
fill layer that wants world = screen at rest takes `pivot: { x: 0, y:
|
|
244
|
+
0 }`). The first argument is anything with a view's `setCamera` (a view
|
|
245
|
+
of a sprite layer, of a record layer, several at once - one camera over
|
|
246
|
+
a scene's layers - or a signal setter feeding `<TileLayer camera>`).
|
|
247
|
+
|
|
248
|
+
**Follow.** `follow(x, y)` chases a world point through the framing in
|
|
249
|
+
`follow` (core's camera-control, shared with the 3d orbit camera):
|
|
250
|
+
damping per axis (`{ x, y }`: a platformer's tight horizontal and lazy
|
|
251
|
+
vertical; a number for both; 0 snaps), the dead zone and hard limits as
|
|
252
|
+
fractions of the viewport centred on the pivot (inside the dead zone
|
|
253
|
+
the point roams free; in the band up to the hard limits it is eased
|
|
254
|
+
back, Godot's drag margins; beyond them it is clamped inside at once,
|
|
255
|
+
whatever the damping - the guarantee a fast target never leaves the
|
|
256
|
+
screen; Cinemachine's game default is 0.8 x 0.8), and lookahead by
|
|
257
|
+
`time` seconds of the point's velocity smoothed over `smoothing`
|
|
258
|
+
seconds. Call it every frame for a moving point; the control settles
|
|
259
|
+
once the point rests inside the zones. A pose glide yields to it, an
|
|
260
|
+
anchor glide (a wheel zoom) keeps running under it; `unfollow()`, or a
|
|
261
|
+
verb that writes x/y/zoom, ends it.
|
|
262
|
+
|
|
263
|
+
**Lanes.** `offset` is where the pose's world point is shown, in
|
|
264
|
+
viewport HEIGHTS from the pivot (x right, y down: `[0, -0.25]` shows it
|
|
265
|
+
a quarter of the height above the pivot - Godot's `offset`, Phaser's
|
|
266
|
+
followOffset); `shake(strength, duration, { frequency?, direction? })`
|
|
267
|
+
adds decaying oscillations (viewport heights of peak displacement) that
|
|
268
|
+
sum. Both are added on top of the pose at push and never enter
|
|
269
|
+
`pose()`; the bounds contain pose plus offset by moving the pose, and
|
|
270
|
+
clip a shake at push, so a shake at the world's edge never shows the
|
|
271
|
+
outside and never disturbs the pose.
|
|
272
|
+
|
|
273
|
+
**Shots.** `createShots(view | views, { blend? })` (core's blender over
|
|
274
|
+
the layers' CameraState, Cinemachine's virtual cameras): `shot(name, {
|
|
275
|
+
priority? })` returns a target a `createCamera2d` drives instead of the
|
|
276
|
+
view - a wide camera and a close one, each with its own control - and
|
|
277
|
+
`activate(name, { blend? })` makes the highest-priority enabled shot
|
|
278
|
+
live, blending the view from the current output over `blend` seconds
|
|
279
|
+
(default 0.5; 0 cuts; x/y/rotation/pivot linear, the zoom in log
|
|
280
|
+
space, `mixCamera2d`); `deactivate` falls back by priority. A live
|
|
281
|
+
shot's pushes reach the view at once; a blend needs `update(dt)` while
|
|
282
|
+
`active()`, the controls' rule. `live()` and `camera()` read the state.
|
|
283
|
+
A switch mid-blend starts from the output of that moment, so a quick
|
|
284
|
+
back-and-forth never jumps. The component form: `<Shots blend? ref?>`
|
|
285
|
+
under `<SpriteLayer>` (or `<View2d>`) with `<Shot name priority?
|
|
286
|
+
active? blend?>` children, each providing the view with its camera
|
|
287
|
+
redirected to the shot, so a `<Camera2d>` inside a `<Shot>` drives that
|
|
288
|
+
shot while its size and handlers stay the view's; `active` (default
|
|
289
|
+
true, live) withdraws and restores a shot, an unmount removes it, the
|
|
290
|
+
container runs the blend on frames while one is in flight.
|
|
291
|
+
probes/shots-2d-probe.tsx is the live guard.
|
|
237
292
|
|
|
238
293
|
#### Input and bindings
|
|
239
294
|
|
|
@@ -249,10 +304,14 @@ an impulse; notches compound on the pending target, and `damping` scales
|
|
|
249
304
|
the settle time - 0 applies a notch at once - the 3d controls' knob) and
|
|
250
305
|
`roll` (axis,
|
|
251
306
|
turns); a pan gesture's begin stops any glide (a finger landing on a
|
|
252
|
-
gliding view holds it) and its end flings with the
|
|
307
|
+
gliding view holds it) and its end flings with the release velocity the
|
|
308
|
+
gesture measured (viewport heights per second from the pointer feed;
|
|
309
|
+
the control has no estimator of its own). The
|
|
253
310
|
verbs, each pushing the pose at once: `panBy(dx, dy)` screen pixels,
|
|
254
311
|
`zoomAt(sx, sy, factor, { glide? })`, `rollBy(angle)`, `set(pose)`,
|
|
255
|
-
`glideTo
|
|
312
|
+
`glideTo(x, y, zoom?, rotation?)` (eased, a rotation glide included),
|
|
313
|
+
`fit`, `follow`/`unfollow`, `shake`, `interrupt`, `release([vx, vy])`
|
|
314
|
+
(screen pixels per second of content travel). An input
|
|
256
315
|
map (core AGENTS.md) drives the axes by name and the APP binds devices
|
|
257
316
|
to it: the view's pointer feed (`createPointerFeed()`, handed to
|
|
258
317
|
`<SpriteLayer pointer>` / `<View2d pointer>`, or bridged with
|
|
@@ -275,34 +334,37 @@ input.drive(cam.axes)
|
|
|
275
334
|
```
|
|
276
335
|
|
|
277
336
|
Call `cam.update(dt)` from a frame loop (it advances glides, follow,
|
|
278
|
-
inertia and the axis rates, pushes one setCamera per driven
|
|
279
|
-
the
|
|
280
|
-
`cam.active()` (true while a glide, fling, fit, follow or
|
|
281
|
-
needs frames, or a rate drives; false at rest), read
|
|
282
|
-
projectCamera and `cam.pose()` for the control's own
|
|
283
|
-
shape `set()` takes, the 3d controls' `pose()`). The
|
|
284
|
-
dispatch's (`onTap` on the
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
337
|
+
inertia, the lanes and the axis rates, pushes one setCamera per driven
|
|
338
|
+
view when the camera changed and reports that), gated on the reactive
|
|
339
|
+
`cam.active()` (true while a glide, fling, fit, follow, shake or damped
|
|
340
|
+
bound needs frames, or a rate drives; false at rest), read
|
|
341
|
+
`cam.camera()` for projectCamera and `cam.pose()` for the control's own
|
|
342
|
+
four values (the shape `set()` takes, the 3d controls' `pose()`). The
|
|
343
|
+
camera has NO tap of its own: taps are the dispatch's (`onTap` on the
|
|
344
|
+
root with `e.sprite` null is "tap on empty space"). `<Camera2d
|
|
345
|
+
input={input}>` inside `<SpriteLayer>` is all of that wired through
|
|
346
|
+
context, the 3d `<OrbitCamera>` shape: options read at mount, driving
|
|
347
|
+
the nearest view (the `<SpriteLayer>`'s own or the enclosing
|
|
348
|
+
`<View2d>`), `viewport` defaulting to that view's size, the map's
|
|
349
|
+
`pan`/`zoom`/`roll` actions (or the names in `actions`) driving the
|
|
350
|
+
axes, frames only while `active()`. A view's feed normalizes a drag by
|
|
351
|
+
the leaf's own box, so a leaf under a designSize fit pans and zooms
|
|
352
|
+
correctly. camera2d.ts imports `@solidrt/core/input` and
|
|
353
|
+
`@solidrt/core/camera-control` only, so checks/camera2d-check.ts pins
|
|
354
|
+
the clamp, anchoring, glides, the framing, the lanes, damped bounds,
|
|
294
355
|
inertia and the axes headless; examples/camera.tsx (function face) and
|
|
295
356
|
examples/pick.tsx (`<Camera2d>`) are the live guards. The shared
|
|
296
357
|
vocabulary with @solidrt/3d, one kind and unit per word: `pan` vec2,
|
|
297
358
|
`zoom` axis (octaves), `roll` axis (turns) here; `rotate`, `look`,
|
|
298
|
-
`move`, `rise` there. The pose options are initial values;
|
|
299
|
-
option (world, zoom range, pivot,
|
|
300
|
-
read where it applies, never
|
|
301
|
-
live - forwarded as getters, a
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
pinned in the check's
|
|
305
|
-
|
|
359
|
+
`move`, `rise`, `focus` there. The pose options are initial values;
|
|
360
|
+
every other option (world, zoom range, pivot, the follow's zones, the
|
|
361
|
+
offset, the speeds, inertia) is read where it applies, never
|
|
362
|
+
snapshotted, so `<Camera2d>` props are live - forwarded as getters, a
|
|
363
|
+
bounds, pivot or offset change re-clamping and pushing the pose at once
|
|
364
|
+
through `set({})` - and a function-face caller mutates the options
|
|
365
|
+
object it passed (the `<OrbitCamera>` rule, pinned in the check's
|
|
366
|
+
live-options case). Not yet, additive: a contain origin other than
|
|
367
|
+
center.
|
|
306
368
|
|
|
307
369
|
### Layer tint
|
|
308
370
|
|
|
@@ -421,9 +483,10 @@ keeps, so other props stay reactive).
|
|
|
421
483
|
|
|
422
484
|
### Pure pieces
|
|
423
485
|
|
|
424
|
-
frames.ts, pick.ts, camera.ts, camera-motion.ts
|
|
425
|
-
|
|
426
|
-
|
|
486
|
+
frames.ts, pick.ts, camera.ts, camera-motion.ts, dispatch.ts,
|
|
487
|
+
oversample-math.ts and tiles-math.ts (the tile grid's chunk and slot
|
|
488
|
+
math, checks/tiles-check.ts) are pure (no GPU imports) BY DESIGN so they
|
|
489
|
+
can be checked headless; keep them that way.
|
|
427
490
|
|
|
428
491
|
## The baked tile layer (tiles.ts)
|
|
429
492
|
|
|
@@ -501,10 +564,10 @@ on approach, evict) - okf/backlog/2d-baked-layers.md.
|
|
|
501
564
|
|
|
502
565
|
| Component | Props |
|
|
503
566
|
|---|---|
|
|
504
|
-
| `SpriteLayer` | atlas (TextureId), capacity?, tint? ([r,g,b,a] 0..1, over the whole layer in every view), orderBy?, label?, ref?(layer) - the layer; plus its OWN VIEW, unless `output={false}`: width?, height? (view pixels - both, or neither = FILL: the leaf lays out at 100% of its sized parent and the view follows its box, so view pixels are the leaf's own coordinates; mount-fixed, a function `output` requires explicit sizes, matching `<Scene>` in @solidrt/3d), clearColor?, camera?, oversample?, maxOversample?, viewRef?(view), output? (a function composes the leaf from the texture id; `false` = no own view, the layer shows only through `<View2d>` children and the view props throw), events?, pointer? (a createPointerFeed fed from the view's root, what a map over this view binds; `useSpriteLayer().pointer` inside), onPointer{Down,Move,Up}?, onWheel?, onTap? (the view's root: `event.sprite` is the hit sprite or null over empty space) |
|
|
567
|
+
| `SpriteLayer` | atlas (TextureId), capacity?, tint? ([r,g,b,a] 0..1, over the whole layer in every view), orderBy?, stagger? (ms, the layer root's enter/exit spacing), label?, ref?(layer) - the layer; plus its OWN VIEW, unless `output={false}`: width?, height? (view pixels - both, or neither = FILL: the leaf lays out at 100% of its sized parent and the view follows its box, so view pixels are the leaf's own coordinates; mount-fixed, a function `output` requires explicit sizes, matching `<Scene>` in @solidrt/3d), clearColor?, camera?, oversample?, maxOversample?, viewRef?(view), output? (a function composes the leaf from the texture id; `false` = no own view, the layer shows only through `<View2d>` children and the view props throw), events?, pointer? (a createPointerFeed fed from the view's root, what a map over this view binds; `useSpriteLayer().pointer` inside), onPointer{Down,Move,Up}?, onWheel?, onTap? (the view's root: `event.sprite` is the hit sprite or null over empty space) |
|
|
505
568
|
| `Sprite` | x, y (center; local to the enclosing `<Group>`), w, h, frame?, rotation? (radians, clockwise), tint? ([r,g,b,a] 0..1), visible?, transition?, onPointer{Down,Move,Up,Enter,Leave}?, onWheel?, onTap?, ref? |
|
|
506
569
|
| `Group` | x?, y?, rotation?, scale? (uniform, scales the subtree), visible? (the whole subtree), transition?, onPointer{Down,Move,Up}?, onWheel?, onTap? (bubbled from hit child sprites), ref? |
|
|
507
|
-
| `Camera2d` | createCamera2d's options minus `viewport` (world?, min/maxZoom?, pivot?,
|
|
570
|
+
| `Camera2d` | createCamera2d's options minus `viewport` (world?, min/maxZoom?, pivot?, follow?, offset?, panSpeed?, zoomSpeed?, rollSpeed?, damping?, inertia?, x?, y?, zoom?, rotation?), viewport? (`() => { width, height }`, default: the driven viewport's size), input? (the input map driving its `pan`/`zoom`/`roll` axes; live), actions? (action names per axis when the map's differ), ref? - a `<SpriteLayer>` child driving the nearest view's camera (the `<SpriteLayer>`'s own, or inside a `<View2d>` that view) from the map, nothing else; read at mount; throws under `output={false}` outside a `<View2d>` |
|
|
508
571
|
| `View2d` | a `<SpriteLayer>` child: one more view of the layer from a camera of its own (layer.createView as a component): width, height (view pixels, live; fixed-size only for now), camera? (partial CameraUpdate on the view's camera, live; the same state a `<Camera2d>` child writes), oversample?, maxOversample? (the auto-pick, as SpriteLayer's), clearColor?, label? (createView's, fixed), ref?(view), output?(texture) (else a built-in `<texture>` leaf at the view size carrying the view's handlers), events?, pointer? (this view's feed, fed from its root), onPointer{Down,Move,Up}?, onWheel?, onTap? (the view's root: `event.sprite` null over empty space); a `<Camera2d>` child drives the VIEW from its map (inside, `useSpriteLayer()` reports the view as `viewport` and the feed as `pointer`); `<Sprite>`/`<Group>` children mount to the layer as outside |
|
|
509
572
|
| `TileLayer` | cols, rows, tileW, tileH, atlas (TextureId), frames? (the tileset `setTiles` indices name), chunkClearColor?, filter?, chunkTiles?, tint? ([r,g,b,a] 0..1, over the whole layer), oversample?, maxOversample?, camera? (TileCamera: x, y, zoom, rotation, pivotX, pivotY), label?, ref? |
|
|
510
573
|
|
|
@@ -593,6 +656,14 @@ hover, wheel and tap rules headless.
|
|
|
593
656
|
edges before sampling: pass `inset` to `grid`/`namedFrames` (2^k texels
|
|
594
657
|
keeps mip level k clean, losing that border) or pack a gutter as
|
|
595
658
|
`spacing`, until extrusion lands (okf/backlog/2d-atlas-extrude.md).
|
|
659
|
+
- The colour contract: tint multiplies the atlas texel as stored -
|
|
660
|
+
premultiplied sRGB, no decode - and the layer tint multiplies over
|
|
661
|
+
that, so a tint of `[0.5, 0.5, 0.5, 1]` halves the ENCODED value (about
|
|
662
|
+
a fifth of the light, not half). Right for an unlit pipeline (the
|
|
663
|
+
texel is the author's colour and comes out as it went in), but it is
|
|
664
|
+
not @solidrt/3d's contract, whose color.ts documents sRGB in, linear
|
|
665
|
+
shading: the same tint triple is a different brightness in the two
|
|
666
|
+
packages, so a palette shared across them needs its own conversion.
|
|
596
667
|
- Tint multiplies the sampled texel (`texture * tint`) and the pipeline
|
|
597
668
|
blends with `blend: "alpha"` in record order, the premultiplied composite.
|
|
598
669
|
The atlas is premultiplied because `decodeImage` premultiplies by default;
|
package/examples/camera.tsx
CHANGED
|
@@ -5,19 +5,22 @@
|
|
|
5
5
|
// walk: a tap on one selects it (tint), a drag on one moves it - the
|
|
6
6
|
// sprite stops its down, which claims the press, so the camera never
|
|
7
7
|
// pans under a sprite drag. Keys switch modes - F follows the roaming
|
|
8
|
-
// sprite through a dead zone
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// camera
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// sprite through the framing (a dead zone, hard limits, a lazy vertical
|
|
9
|
+
// damping, lookahead; the zones are drawn while it follows), S shakes
|
|
10
|
+
// the view, R spins it about its center, Space fits the whole world.
|
|
11
|
+
// The camera is one setCamera write per changed frame (a shared-params
|
|
12
|
+
// write); no sprite moves for the camera's sake. The layer fills the
|
|
13
|
+
// window through the function face (starlings' shape): the app owns the
|
|
14
|
+
// <texture> leaf and spreads the LAYER's handlers on it; the camera
|
|
15
|
+
// listens at the root through attach.
|
|
14
16
|
//
|
|
15
17
|
// Debug commands, for driving it from the control API: `camera` returns
|
|
16
|
-
// the
|
|
17
|
-
//
|
|
18
|
+
// the final camera (and parks the pose when given x/y/zoom/rotation),
|
|
19
|
+
// `pose` the control's own values, `mode` sets { follow, spin } or runs
|
|
20
|
+
// { fit: true } / { shake: true }, `selected` returns the selected
|
|
18
21
|
// sprite's world position (null when none), `first` the first sprite's -
|
|
19
22
|
// the one to aim synthetic taps and drags at.
|
|
20
|
-
import { createEffect, createInputMap, createPointerFeed, decodeImage, displayScale, gamepad, onFrame, render, windowSize } from "@solidrt/core"
|
|
23
|
+
import { createEffect, createInputMap, createPointerFeed, createSignal, decodeImage, displayScale, gamepad, onFrame, render, Show, windowSize } from "@solidrt/core"
|
|
21
24
|
import { addSprite, camera2dActions, camera2dBindings, createAtlas, createCamera2d, createSpriteLayer, feedPointer, fitOversample, grid, setSprite } from "@solidrt/2d"
|
|
22
25
|
import type { Camera2dHandle, SpriteHandle } from "@solidrt/2d"
|
|
23
26
|
import { registerDebug } from "srt:dev"
|
|
@@ -35,8 +38,19 @@ const ROAM_RATE_Y = 0.31
|
|
|
35
38
|
const ROAM_SPAN = 0.4
|
|
36
39
|
// Spin rate while R is on, radians per second.
|
|
37
40
|
const SPIN_RATE = 0.4
|
|
38
|
-
// The follow
|
|
41
|
+
// The follow's zones, fractions of the viewport: the roamer moves freely
|
|
42
|
+
// inside the dead zone, is eased back within the hard limits, and is
|
|
43
|
+
// never let past them.
|
|
39
44
|
const DEAD_ZONE = { width: 0.3, height: 0.3 }
|
|
45
|
+
const HARD_LIMITS = { width: 0.8, height: 0.7 }
|
|
46
|
+
// A lazy vertical follow (a platformer's), tight horizontal.
|
|
47
|
+
const FOLLOW_DAMPING = { x: 1, y: 2.5 }
|
|
48
|
+
// Lookahead along the roamer's velocity, seconds, and its smoothing.
|
|
49
|
+
const LOOKAHEAD = { time: 0.35, smoothing: 0.15 }
|
|
50
|
+
// A shake: peak displacement in viewport heights, and seconds.
|
|
51
|
+
const SHAKE_STRENGTH = 0.03
|
|
52
|
+
const SHAKE_SECONDS = 0.4
|
|
53
|
+
const ZONE_STROKE = 2
|
|
40
54
|
// Largest frame step, seconds: a stall eases on from here instead of
|
|
41
55
|
// teleporting.
|
|
42
56
|
const MAX_DT = 0.1
|
|
@@ -44,7 +58,7 @@ const TINT: [number, number, number, number] = [0.6, 0.7, 0.9, 1]
|
|
|
44
58
|
const SELECTED_TINT: [number, number, number, number] = [1, 0.95, 0.5, 1]
|
|
45
59
|
|
|
46
60
|
let cam!: Camera2dHandle
|
|
47
|
-
let following = false
|
|
61
|
+
let [following, setFollowing] = createSignal(false)
|
|
48
62
|
let spinning = false
|
|
49
63
|
// The roamer's world position, for the `mode` debug command.
|
|
50
64
|
let roamerAt = { x: 0, y: 0 }
|
|
@@ -97,7 +111,7 @@ function App() {
|
|
|
97
111
|
viewport: () => win,
|
|
98
112
|
world: WORLD,
|
|
99
113
|
maxZoom: MAX_ZOOM,
|
|
100
|
-
deadZone: DEAD_ZONE,
|
|
114
|
+
follow: { deadZone: DEAD_ZONE, hardLimits: HARD_LIMITS, damping: FOLLOW_DAMPING, lookahead: LOOKAHEAD },
|
|
101
115
|
})
|
|
102
116
|
// The camera's input: the view's pointer feed (fed from the view's root,
|
|
103
117
|
// after the sprites' claims) and any pad, bound to the control's actions
|
|
@@ -113,7 +127,7 @@ function App() {
|
|
|
113
127
|
onTap: e => {
|
|
114
128
|
if (e.sprite) return
|
|
115
129
|
select(null)
|
|
116
|
-
|
|
130
|
+
setFollowing(false)
|
|
117
131
|
cam.glideTo(e.x, e.y)
|
|
118
132
|
},
|
|
119
133
|
})
|
|
@@ -138,33 +152,54 @@ function App() {
|
|
|
138
152
|
setSprite(roamer, { x: rx, y: ry, rotation: t })
|
|
139
153
|
roamerAt.x = rx
|
|
140
154
|
roamerAt.y = ry
|
|
141
|
-
if (following) cam.follow(rx, ry)
|
|
142
|
-
if (spinning) cam.set({ rotation: cam.
|
|
155
|
+
if (following()) cam.follow(rx, ry)
|
|
156
|
+
if (spinning) cam.set({ rotation: cam.pose().rotation + SPIN_RATE * dt })
|
|
143
157
|
cam.update(dt)
|
|
144
158
|
})
|
|
159
|
+
// A zone outline centered in the window, sized by its fractions.
|
|
160
|
+
let zone = (size: { width: number; height: number }, color: string) => (
|
|
161
|
+
<rect
|
|
162
|
+
position="absolute"
|
|
163
|
+
left={(windowSize().width * (1 - size.width)) / 2}
|
|
164
|
+
top={(windowSize().height * (1 - size.height)) / 2}
|
|
165
|
+
width={windowSize().width * size.width}
|
|
166
|
+
height={windowSize().height * size.height}
|
|
167
|
+
drawStyle="stroke"
|
|
168
|
+
strokeWidth={ZONE_STROKE}
|
|
169
|
+
color={color}
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
145
172
|
|
|
146
173
|
return (
|
|
147
174
|
<window
|
|
148
175
|
onKeyDown={e => {
|
|
149
176
|
if (e.key === "f") {
|
|
150
|
-
|
|
151
|
-
if (!following) cam.unfollow()
|
|
177
|
+
setFollowing(!following())
|
|
178
|
+
if (!following()) cam.unfollow()
|
|
179
|
+
} else if (e.key === "s") {
|
|
180
|
+
cam.shake(SHAKE_STRENGTH, SHAKE_SECONDS)
|
|
152
181
|
} else if (e.key === "r") {
|
|
153
182
|
spinning = !spinning
|
|
154
183
|
} else if (e.key === " ") {
|
|
155
|
-
|
|
184
|
+
setFollowing(false)
|
|
156
185
|
cam.unfollow()
|
|
157
186
|
cam.fit(undefined, { glide: true })
|
|
158
187
|
}
|
|
159
188
|
}}
|
|
160
189
|
>
|
|
161
190
|
<texture src={view.texture} position="absolute" left={0} top={0} width={windowSize().width} height={windowSize().height} {...view.handlers} />
|
|
191
|
+
<Show when={following()}>
|
|
192
|
+
<view pointerEvents="none" position="absolute" left={0} top={0} width={windowSize().width} height={windowSize().height}>
|
|
193
|
+
{zone(DEAD_ZONE, "#7fd18a99")}
|
|
194
|
+
{zone(HARD_LIMITS, "#e0704e99")}
|
|
195
|
+
</view>
|
|
196
|
+
</Show>
|
|
162
197
|
<view pointerEvents="none" gap={6} padding={20}>
|
|
163
198
|
<text color="#eef4ff" fontSize={24} fontWeight={700}>
|
|
164
199
|
Camera
|
|
165
200
|
</text>
|
|
166
201
|
<text color="#a9bcd6" fontSize={15}>
|
|
167
|
-
drag empty space to pan (flick for inertia) - wheel or pinch to zoom - tap empty space to glide there - tap a sprite to select, drag it to move - F follow - R spin - Space fit
|
|
202
|
+
drag empty space to pan (flick for inertia) - wheel or pinch to zoom - tap empty space to glide there - tap a sprite to select, drag it to move - F follow (zones drawn) - S shake - R spin - Space fit
|
|
168
203
|
</text>
|
|
169
204
|
</view>
|
|
170
205
|
</window>
|
|
@@ -182,14 +217,17 @@ registerDebug("camera", (args?: Record<string, unknown>) => {
|
|
|
182
217
|
return cam.camera()
|
|
183
218
|
})
|
|
184
219
|
|
|
220
|
+
registerDebug("pose", () => cam.pose())
|
|
221
|
+
|
|
185
222
|
registerDebug("mode", (args?: Record<string, unknown>) => {
|
|
186
223
|
if (typeof args?.follow === "boolean") {
|
|
187
|
-
|
|
188
|
-
if (!
|
|
224
|
+
setFollowing(args.follow)
|
|
225
|
+
if (!args.follow) cam.unfollow()
|
|
189
226
|
}
|
|
190
227
|
if (typeof args?.spin === "boolean") spinning = args.spin
|
|
191
228
|
if (args?.fit === true) cam.fit(undefined, { glide: true })
|
|
192
|
-
|
|
229
|
+
if (args?.shake === true) cam.shake(SHAKE_STRENGTH, SHAKE_SECONDS)
|
|
230
|
+
return { following: following(), spinning, roamer: { ...roamerAt } }
|
|
193
231
|
})
|
|
194
232
|
|
|
195
233
|
registerDebug("selected", () => (selected ? { x: selected._x, y: selected._y } : null))
|
|
@@ -67,7 +67,7 @@ function App() {
|
|
|
67
67
|
input.bind(camera2dBindings({ pointer, gamepad: gamepad.next() }))
|
|
68
68
|
return (
|
|
69
69
|
<View2d width={pane().width} height={pane().height} clearColor={i === 0 ? [0.05, 0.05, 0.09, 1] : [0.09, 0.05, 0.05, 1]} label={`pane-${i}`} pointer={pointer}>
|
|
70
|
-
<Camera2d input={input} world={WORLD} zoom={PANE_ZOOM} maxZoom={MAX_ZOOM}
|
|
70
|
+
<Camera2d input={input} world={WORLD} zoom={PANE_ZOOM} maxZoom={MAX_ZOOM} follow={{ deadZone: DEAD_ZONE }} ref={c => (cams[i] = c)} />
|
|
71
71
|
</View2d>
|
|
72
72
|
)
|
|
73
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.62",
|
|
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.9",
|
|
19
|
+
"@solidrt/core": "0.0.62"
|
|
20
20
|
}
|
|
21
21
|
}
|