@solidrt/2d 0.0.60 → 0.0.61
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
|
@@ -33,8 +33,9 @@ const MAX_CAMERA_DT = 0.1
|
|
|
33
33
|
* change the pose at runtime through `ref`'s set/glideTo/fit/follow and
|
|
34
34
|
* the verbs. Every other prop is live, forwarded to the control as a
|
|
35
35
|
* getter and read where it applies, so a world, a zoom range, a pivot,
|
|
36
|
-
*
|
|
37
|
-
* re-clamps and pushes the pose at once -
|
|
36
|
+
* the follow's zones, the offset lane or a rate follows its prop, and a
|
|
37
|
+
* bounds, pivot or offset change re-clamps and pushes the pose at once -
|
|
38
|
+
* the `<OrbitCamera>` rule.
|
|
38
39
|
* `viewport` defaults to the view's own size. Frames run only while the
|
|
39
40
|
* camera moves (`active()`), so a resting camera leaves the app
|
|
40
41
|
* demand-driven idle.
|
|
@@ -67,13 +68,14 @@ export let Camera2d: VoidComponent<Camera2dProps> = props => {
|
|
|
67
68
|
})
|
|
68
69
|
},
|
|
69
70
|
)
|
|
70
|
-
// A bounds or
|
|
71
|
-
// (set({}) applies the clamps). Deferred: the
|
|
72
|
-
// and pushed the initial pose. Untracked: the
|
|
73
|
-
// through the getters while it applies
|
|
74
|
-
// values of that moment (the compute
|
|
71
|
+
// A bounds, pivot or offset prop change re-clamps and pushes the pose
|
|
72
|
+
// at once (set({}) applies the clamps and the lanes). Deferred: the
|
|
73
|
+
// creation already clamped and pushed the initial pose. Untracked: the
|
|
74
|
+
// control reads the options through the getters while it applies
|
|
75
|
+
// them, and the apply wants the values of that moment (the compute
|
|
76
|
+
// above is what tracks them).
|
|
75
77
|
createEffect(
|
|
76
|
-
() => [options.world, options.minZoom, options.maxZoom, options.pivot],
|
|
78
|
+
() => [options.world, options.minZoom, options.maxZoom, options.pivot, options.offset],
|
|
77
79
|
() => untrack(() => cam.set({})),
|
|
78
80
|
{ defer: true },
|
|
79
81
|
)
|
package/src/components/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ export { Sprite } from "./sprite.tsx"
|
|
|
22
22
|
export type { SpritePointerProps, SpriteProps } from "./sprite.tsx"
|
|
23
23
|
export { Camera2d } from "./camera2d.tsx"
|
|
24
24
|
export type { Camera2dProps } from "./camera2d.tsx"
|
|
25
|
+
export { Shot, Shots } from "./shots.tsx"
|
|
26
|
+
export type { ShotProps, ShotsProps } from "./shots.tsx"
|
|
25
27
|
export { View2d } from "./view2d.tsx"
|
|
26
28
|
export type { View2dProps } from "./view2d.tsx"
|
|
27
29
|
export { TileLayer } from "./tile-layer.tsx"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createContext, createEffect, onCleanup, onFrame, untrack, useContext } from "@solidrt/core"
|
|
2
|
+
import type { Element, ParentComponent } from "@solidrt/core"
|
|
3
|
+
import { ViewportContext, useSpriteLayer } from "./context.ts"
|
|
4
|
+
import { createShots } from "../shots.ts"
|
|
5
|
+
import type { ShotsHandle } from "../shots.ts"
|
|
6
|
+
import type { ViewHandle } from "../views.ts"
|
|
7
|
+
|
|
8
|
+
// Cap on the blend loop's per-frame dt in seconds, so the first tick
|
|
9
|
+
// after a suspended stretch cannot leap a blend to its end.
|
|
10
|
+
const MAX_BLEND_DT = 0.1
|
|
11
|
+
|
|
12
|
+
let ShotsContext = createContext<ShotsHandle>()
|
|
13
|
+
|
|
14
|
+
export type ShotsProps = {
|
|
15
|
+
/** The default blend time in seconds when the live shot changes
|
|
16
|
+
* (0.5); 0 cuts. */
|
|
17
|
+
blend?: number
|
|
18
|
+
/** The blender's handle (activate/deactivate/live/camera). */
|
|
19
|
+
ref?: (shots: ShotsHandle) => void
|
|
20
|
+
children?: Element
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ShotProps = {
|
|
24
|
+
name: string
|
|
25
|
+
/** The enabled shot with the highest priority is live (default 0;
|
|
26
|
+
* the most recently enabled wins a tie). Read at mount. */
|
|
27
|
+
priority?: number
|
|
28
|
+
/** Whether the shot takes part (default true); live: false withdraws
|
|
29
|
+
* it and the next by priority goes live, blended. */
|
|
30
|
+
active?: boolean
|
|
31
|
+
/** This shot's own blend time when it goes live or withdraws
|
|
32
|
+
* (default: the container's). */
|
|
33
|
+
blend?: number
|
|
34
|
+
children?: Element
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* createShots as a SpriteLayer (or View2d) child: owns the nearest
|
|
39
|
+
* view's camera and blends between its `<Shot>` children (Cinemachine's
|
|
40
|
+
* virtual cameras), each of which provides the view with its camera
|
|
41
|
+
* redirected to the shot's recording target - so a `<Camera2d>` inside
|
|
42
|
+
* a `<Shot>` drives that shot, not the view, while its size, handlers
|
|
43
|
+
* and everything else stay the view's. The blend runs on frames only
|
|
44
|
+
* while one is in flight.
|
|
45
|
+
*/
|
|
46
|
+
export let Shots: ParentComponent<ShotsProps> = props => {
|
|
47
|
+
let view = useSpriteLayer().viewport
|
|
48
|
+
let shots = untrack(() => createShots(view, { blend: props.blend }))
|
|
49
|
+
createEffect(
|
|
50
|
+
() => shots.active(),
|
|
51
|
+
on => {
|
|
52
|
+
if (!on) return
|
|
53
|
+
let last: number | null = null
|
|
54
|
+
return onFrame(tick => {
|
|
55
|
+
let now = tick / 1000
|
|
56
|
+
let dt = last === null ? 0 : Math.min(now - last, MAX_BLEND_DT)
|
|
57
|
+
last = now
|
|
58
|
+
shots.update(dt)
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
untrack(() => props.ref)?.(shots)
|
|
63
|
+
return <ShotsContext value={shots}>{props.children}</ShotsContext>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export let Shot: ParentComponent<ShotProps> = props => {
|
|
67
|
+
let shots = useContext(ShotsContext)
|
|
68
|
+
let view = useSpriteLayer().viewport
|
|
69
|
+
let name = untrack(() => props.name)
|
|
70
|
+
let target = untrack(() => shots.shot(name, { priority: props.priority }))
|
|
71
|
+
// The view with its camera redirected: a Proxy keeps the view's live
|
|
72
|
+
// fields (width, height, handlers) live.
|
|
73
|
+
let redirected = new Proxy(view, {
|
|
74
|
+
get(t, key, receiver) {
|
|
75
|
+
if (key === "setCamera") return target.setCamera
|
|
76
|
+
if (key === "camera") return target.camera
|
|
77
|
+
return Reflect.get(t, key, receiver)
|
|
78
|
+
},
|
|
79
|
+
}) as ViewHandle
|
|
80
|
+
createEffect(
|
|
81
|
+
() => props.active ?? true,
|
|
82
|
+
on => {
|
|
83
|
+
let blend = untrack(() => props.blend)
|
|
84
|
+
if (on) shots.activate(name, { blend })
|
|
85
|
+
else shots.deactivate(name, { blend })
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
onCleanup(() => shots.remove(name, { blend: untrack(() => props.blend) }))
|
|
89
|
+
return <ViewportContext value={redirected}>{props.children}</ViewportContext>
|
|
90
|
+
}
|
|
@@ -79,6 +79,13 @@ export type SpriteLayerProps = LayerPointerProps & {
|
|
|
79
79
|
* SpriteLayerOptions.orderBy).
|
|
80
80
|
*/
|
|
81
81
|
orderBy?: "y" | "renderOrder"
|
|
82
|
+
/**
|
|
83
|
+
* Stagger (ms) on the layer's root: the enters and exits of the sprites
|
|
84
|
+
* and groups straight under the layer that begin in one frame are spaced
|
|
85
|
+
* by `index * stagger` (a `<Group>` declaring its own wins for what is
|
|
86
|
+
* under it). Live: a change applies to the next frame's cascade.
|
|
87
|
+
*/
|
|
88
|
+
stagger?: number
|
|
82
89
|
label?: string
|
|
83
90
|
/** The layer: sprites, tint, pick, createView. */
|
|
84
91
|
ref?: (layer: LayerHandle) => void
|
|
@@ -156,6 +163,7 @@ export let SpriteLayer: ParentComponent<SpriteLayerProps> = props => {
|
|
|
156
163
|
capacity: props.capacity,
|
|
157
164
|
tint: props.tint,
|
|
158
165
|
orderBy: props.orderBy,
|
|
166
|
+
stagger: props.stagger,
|
|
159
167
|
label: props.label,
|
|
160
168
|
}),
|
|
161
169
|
)
|
|
@@ -165,6 +173,11 @@ export let SpriteLayer: ParentComponent<SpriteLayerProps> = props => {
|
|
|
165
173
|
if (tint !== undefined) layer.setTint(tint)
|
|
166
174
|
},
|
|
167
175
|
)
|
|
176
|
+
createEffect(
|
|
177
|
+
() => props.stagger,
|
|
178
|
+
ms => layer.setStagger(ms ?? null),
|
|
179
|
+
{ defer: true },
|
|
180
|
+
)
|
|
168
181
|
untrack(() => props.ref)?.(layer)
|
|
169
182
|
let output = untrack(() => props.output)
|
|
170
183
|
if (output === false) {
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,8 @@ export { projectCamera, unprojectCamera } from "./camera.ts"
|
|
|
23
23
|
export type { CameraState, CameraUpdate } from "./camera.ts"
|
|
24
24
|
export { createCamera2d } from "./camera2d.ts"
|
|
25
25
|
export type { Camera2d as Camera2dHandle, Camera2dAxes, Camera2dOptions, Camera2dPose, Camera2dTarget, Rect2d } from "./camera2d.ts"
|
|
26
|
+
export { createShots, mixCamera2d } from "./shots.ts"
|
|
27
|
+
export type { ShotTarget, ShotsHandle } from "./shots.ts"
|
|
26
28
|
export { camera2dActions, camera2dBindings } from "./input.ts"
|
|
27
29
|
export type { CameraDevices } from "./input.ts"
|
|
28
30
|
export type {
|
|
@@ -69,5 +71,5 @@ export type { AnimationOptions, SpriteAnimation } from "./animation.ts"
|
|
|
69
71
|
export { fitOversample } from "./oversample.ts"
|
|
70
72
|
export { createAtlas } from "./atlas.ts"
|
|
71
73
|
export type { Atlas, AtlasOptions } from "./atlas.ts"
|
|
72
|
-
export { Camera2d, Group, Sprite, SpriteLayer, TileLayer, View2d, useSpriteLayer } from "./components/index.ts"
|
|
73
|
-
export type { Camera2dProps, GroupProps, LayerPointerProps, SpriteLayerProps, SpritePointerProps, SpriteProps, TileCamera, TileLayerProps, View2dProps } from "./components/index.ts"
|
|
74
|
+
export { Camera2d, Group, Sprite, SpriteLayer, TileLayer, View2d, useSpriteLayer, Shot, Shots } from "./components/index.ts"
|
|
75
|
+
export type { Camera2dProps, GroupProps, LayerPointerProps, SpriteLayerProps, SpritePointerProps, SpriteProps, TileCamera, TileLayerProps, View2dProps, ShotProps, ShotsProps } from "./components/index.ts"
|
package/src/input.ts
CHANGED
|
@@ -26,9 +26,10 @@ export let camera2dActions = { pan: "vec2", zoom: "axis", roll: "axis" } as cons
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* The 2d camera's standard bindings: one finger or a mouse drag pans and
|
|
29
|
-
* so do two fingers, a pinch and the wheel zoom about the pointer, a
|
|
30
|
-
*
|
|
31
|
-
* rolls; the
|
|
29
|
+
* so do two fingers, a pinch and the wheel zoom about the pointer, a
|
|
30
|
+
* double tap zooms one octave in at its point (the map convention), a
|
|
31
|
+
* twist rolls; the left stick scrolls, the triggers zoom, the right
|
|
32
|
+
* stick's x rolls; the arrow keys scroll and minus/equals zoom.
|
|
32
33
|
*/
|
|
33
34
|
export function camera2dBindings(devices: CameraDevices): Binding[] {
|
|
34
35
|
let out: Binding[] = []
|
|
@@ -39,6 +40,7 @@ export function camera2dBindings(devices: CameraDevices): Binding[] {
|
|
|
39
40
|
{ action: "pan", source: pointer.pan },
|
|
40
41
|
{ action: "zoom", source: pointer.pinch },
|
|
41
42
|
{ action: "zoom", source: pointer.wheel },
|
|
43
|
+
{ action: "zoom", source: pointer.doubleTap },
|
|
42
44
|
{ action: "roll", source: pointer.twist },
|
|
43
45
|
)
|
|
44
46
|
}
|
package/src/layer.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
MoveOptions as CoreMoveOptions,
|
|
36
36
|
NodeEndpoint,
|
|
37
37
|
NodeId,
|
|
38
|
+
NodeMotionSpec,
|
|
38
39
|
NodeTransition,
|
|
39
40
|
NodeTransitionSpec,
|
|
40
41
|
QueryFilter,
|
|
@@ -1265,7 +1266,9 @@ export type SpriteTransition = {
|
|
|
1265
1266
|
position?: SpriteTransitionSpec<[number, number]>
|
|
1266
1267
|
rotation?: SpriteTransitionSpec<number>
|
|
1267
1268
|
scale?: SpriteTransitionSpec<[number, number] | number>
|
|
1268
|
-
all
|
|
1269
|
+
/** The catch-all: a motion alone, no `from`/`exit` (those name a
|
|
1270
|
+
* component's units, so they live on the per-component specs). */
|
|
1271
|
+
all?: NodeMotionSpec
|
|
1269
1272
|
stagger?: number
|
|
1270
1273
|
}
|
|
1271
1274
|
|
package/src/shots.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Shots and blends over the 2d camera (okf/design/camera-controls.md):
|
|
2
|
+
// core's blender specialized to the layers' CameraState - a control
|
|
3
|
+
// drives a shot's recording target instead of the view, the blender
|
|
4
|
+
// owns the view's setCamera, and a switch blends x/y/rotation/pivot
|
|
5
|
+
// linearly and the zoom in log space (a zoom blend in linear space
|
|
6
|
+
// races in and crawls out).
|
|
7
|
+
|
|
8
|
+
import { createShotBlend } from "@solidrt/core/camera-control"
|
|
9
|
+
import type { ShotBlend, ShotBlendOptions, ShotTarget as RecordingTarget } from "@solidrt/core/camera-control"
|
|
10
|
+
import type { CameraState } from "./camera.ts"
|
|
11
|
+
import type { Camera2dTarget } from "./camera2d.ts"
|
|
12
|
+
|
|
13
|
+
// The camera every shot starts from: the layers' own defaults (world 0,0
|
|
14
|
+
// at the top-left, pixel for pixel).
|
|
15
|
+
const INITIAL: CameraState = { x: 0, y: 0, zoom: 1, rotation: 0, pivotX: 0, pivotY: 0 }
|
|
16
|
+
|
|
17
|
+
let lerp = (a: number, b: number, t: number) => a + (b - a) * t
|
|
18
|
+
|
|
19
|
+
/** Interpolate two 2d cameras: everything linear, the zoom in log space. */
|
|
20
|
+
export let mixCamera2d = (a: CameraState, b: CameraState, t: number): CameraState => ({
|
|
21
|
+
x: lerp(a.x, b.x, t),
|
|
22
|
+
y: lerp(a.y, b.y, t),
|
|
23
|
+
zoom: a.zoom * Math.pow(b.zoom / a.zoom, t),
|
|
24
|
+
rotation: lerp(a.rotation, b.rotation, t),
|
|
25
|
+
pivotX: lerp(a.pivotX, b.pivotX, t),
|
|
26
|
+
pivotY: lerp(a.pivotY, b.pivotY, t),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export type ShotsHandle = ShotBlend<CameraState>
|
|
30
|
+
export type ShotTarget = RecordingTarget<CameraState> & Camera2dTarget
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Shots over a view (or several: one output over a scene's layers):
|
|
34
|
+
* `shot(name, { priority? })` returns the target a `createCamera2d` (or
|
|
35
|
+
* a `<Camera2d>` through a custom viewport) drives; `activate(name, {
|
|
36
|
+
* blend? })` makes it live when its priority wins, blending from the
|
|
37
|
+
* current output; `update(dt)` from a frame loop while `active()`.
|
|
38
|
+
*/
|
|
39
|
+
export function createShots(target: Camera2dTarget | Camera2dTarget[], options: ShotBlendOptions = {}): ShotsHandle {
|
|
40
|
+
let targets = Array.isArray(target) ? target : [target]
|
|
41
|
+
for (let t of targets) if (!t || typeof t.setCamera !== "function") throw new Error("createShots: every target needs setCamera() (a view)")
|
|
42
|
+
return createShotBlend<CameraState>(
|
|
43
|
+
camera => {
|
|
44
|
+
for (let t of targets) t.setCamera(camera)
|
|
45
|
+
},
|
|
46
|
+
mixCamera2d,
|
|
47
|
+
INITIAL,
|
|
48
|
+
options,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The pure tile-grid math: cell and rect validation, a cell's chunk and
|
|
2
|
+
// record slot, and the walk of a rect chunk by chunk - plain arithmetic
|
|
3
|
+
// with no GPU imports, so the headless check (checks/tiles-check.ts)
|
|
4
|
+
// drives it directly; tiles.ts wraps it with the chunk textures.
|
|
5
|
+
|
|
6
|
+
/** Throw unless (col, row) is a cell of the cols x rows grid. */
|
|
7
|
+
export function checkCell(verb: string, col: number, row: number, cols: number, rows: number): void {
|
|
8
|
+
if (!(Number.isInteger(col) && Number.isInteger(row) && col >= 0 && col < cols && row >= 0 && row < rows)) {
|
|
9
|
+
throw new Error(`${verb}: cell ${col}, ${row} outside the ${cols} x ${rows} grid`)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Throw unless the w x h rect at (col, row) is non-empty and lies inside
|
|
14
|
+
* the cols x rows grid. */
|
|
15
|
+
export function checkRect(verb: string, col: number, row: number, w: number, h: number, cols: number, rows: number): void {
|
|
16
|
+
if (!(Number.isInteger(col) && Number.isInteger(row) && Number.isInteger(w) && Number.isInteger(h) && w > 0 && h > 0 && col >= 0 && row >= 0 && col + w <= cols && row + h <= rows)) {
|
|
17
|
+
throw new Error(`${verb}: rect ${col}, ${row} of ${w} x ${h} outside the ${cols} x ${rows} grid`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A cell's chunk index in a grid chunked chunkTiles x chunkTiles, with
|
|
22
|
+
* chunkCols chunks across: chunks are numbered row-major. */
|
|
23
|
+
export function chunkOf(col: number, row: number, chunkTiles: number, chunkCols: number): number {
|
|
24
|
+
return Math.floor(row / chunkTiles) * chunkCols + Math.floor(col / chunkTiles)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A cell's record offset inside its chunk's records: cells are row-major
|
|
28
|
+
* within the chunk, `floatsPerCell` floats each. */
|
|
29
|
+
export function slotOf(col: number, row: number, chunkTiles: number, floatsPerCell: number): number {
|
|
30
|
+
return ((row % chunkTiles) * chunkTiles + (col % chunkTiles)) * floatsPerCell
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Walk the w x h rect at (col, row) chunk by chunk: `visit` is called once
|
|
35
|
+
* per chunk the rect touches with the chunk's index and the rect's slice
|
|
36
|
+
* of it, the cells [colA, colB) x [rowA, rowB) - chunk rows outer, chunk
|
|
37
|
+
* columns inner, so a caller writing records advances through each
|
|
38
|
+
* chunk's slice row by row.
|
|
39
|
+
*/
|
|
40
|
+
export function eachChunkSlice(
|
|
41
|
+
col: number,
|
|
42
|
+
row: number,
|
|
43
|
+
w: number,
|
|
44
|
+
h: number,
|
|
45
|
+
chunkTiles: number,
|
|
46
|
+
chunkCols: number,
|
|
47
|
+
visit: (index: number, colA: number, colB: number, rowA: number, rowB: number) => void,
|
|
48
|
+
): void {
|
|
49
|
+
let cr0 = Math.floor(row / chunkTiles)
|
|
50
|
+
let cr1 = Math.floor((row + h - 1) / chunkTiles)
|
|
51
|
+
let cc0 = Math.floor(col / chunkTiles)
|
|
52
|
+
let cc1 = Math.floor((col + w - 1) / chunkTiles)
|
|
53
|
+
for (let cr = cr0; cr <= cr1; cr++) {
|
|
54
|
+
let rowA = Math.max(row, cr * chunkTiles)
|
|
55
|
+
let rowB = Math.min(row + h, (cr + 1) * chunkTiles)
|
|
56
|
+
for (let cc = cc0; cc <= cc1; cc++) {
|
|
57
|
+
let colA = Math.max(col, cc * chunkTiles)
|
|
58
|
+
let colB = Math.min(col + w, (cc + 1) * chunkTiles)
|
|
59
|
+
visit(cr * chunkCols + cc, colA, colB, rowA, rowB)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/tiles.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { checkTint } from "./layer.ts"
|
|
|
39
39
|
import { checkOversample, thrashSentinel } from "./oversample.ts"
|
|
40
40
|
import { FLOATS_PER_SPRITE } from "./records.ts"
|
|
41
41
|
import { FRAGMENT, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
|
|
42
|
+
import { checkCell, checkRect, chunkOf, eachChunkSlice, slotOf } from "./tiles-math.ts"
|
|
42
43
|
|
|
43
44
|
const RESOLVED = Promise.resolve()
|
|
44
45
|
|
|
@@ -324,14 +325,10 @@ export function createTileLayer(
|
|
|
324
325
|
return chunk
|
|
325
326
|
}
|
|
326
327
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
// A cell's chunk index, and its record offset inside that chunk.
|
|
333
|
-
let chunkOf = (col: number, row: number): number => Math.floor(row / chunkTiles) * chunkCols + Math.floor(col / chunkTiles)
|
|
334
|
-
let slot = (col: number, row: number): number => ((row % chunkTiles) * chunkTiles + (col % chunkTiles)) * FLOATS_PER_SPRITE
|
|
328
|
+
// A cell's chunk index, and its record offset inside that chunk
|
|
329
|
+
// (tiles-math.ts, bound to this layer's grid).
|
|
330
|
+
let chunkAt = (col: number, row: number): number => chunkOf(col, row, chunkTiles, chunkCols)
|
|
331
|
+
let slot = (col: number, row: number): number => slotOf(col, row, chunkTiles, FLOATS_PER_SPRITE)
|
|
335
332
|
// Write one cell's record: the quad at the cell, the frame's UVs, and
|
|
336
333
|
// the tint - the one given, else the default when the cell comes up from
|
|
337
334
|
// empty (a re-set keeps its tint: absent keys keep their values, like
|
|
@@ -393,8 +390,8 @@ export function createTileLayer(
|
|
|
393
390
|
},
|
|
394
391
|
setTile(col, row, frame, opts) {
|
|
395
392
|
if (disposed) return
|
|
396
|
-
checkCell(col, row,
|
|
397
|
-
let index =
|
|
393
|
+
checkCell("setTile", col, row, cols, rows)
|
|
394
|
+
let index = chunkAt(col, row)
|
|
398
395
|
let at = slot(col, row)
|
|
399
396
|
let chunk = resident.get(index)
|
|
400
397
|
if (frame === null) {
|
|
@@ -411,9 +408,7 @@ export function createTileLayer(
|
|
|
411
408
|
},
|
|
412
409
|
setTiles(col, row, w, h, cells, opts) {
|
|
413
410
|
if (disposed) return
|
|
414
|
-
|
|
415
|
-
throw new Error(`setTiles: rect ${col}, ${row} of ${w} x ${h} outside the ${cols} x ${rows} grid`)
|
|
416
|
-
}
|
|
411
|
+
checkRect("setTiles", col, row, w, h, cols, rows)
|
|
417
412
|
if (cells.length !== w * h) throw new Error(`setTiles: a ${w} x ${h} rect takes ${w * h} cells, got ${cells.length}`)
|
|
418
413
|
if (opts?.tint !== undefined) checkTint("setTiles", opts.tint)
|
|
419
414
|
let cellTint = opts?.tint
|
|
@@ -440,63 +435,52 @@ export function createTileLayer(
|
|
|
440
435
|
// sub-rect walked row by row with the record offset advancing, one
|
|
441
436
|
// dirty mark at the end. A chunk the slice only clears is never
|
|
442
437
|
// allocated.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
for (let x = colA; x < colB; x++, at += FLOATS_PER_SPRITE, i++) {
|
|
460
|
-
let u0: number
|
|
461
|
-
let v0: number
|
|
462
|
-
let u1: number
|
|
463
|
-
let v1: number
|
|
464
|
-
if (idx !== null) {
|
|
465
|
-
let k = idx[i]!
|
|
466
|
-
if (k === -1 || k === CLEAR_INDEX) {
|
|
467
|
-
if (r !== null) clearCell(r, at)
|
|
468
|
-
continue
|
|
469
|
-
}
|
|
470
|
-
let b = k * 4
|
|
471
|
-
u0 = uv![b]!
|
|
472
|
-
v0 = uv![b + 1]!
|
|
473
|
-
u1 = uv![b + 2]!
|
|
474
|
-
v1 = uv![b + 3]!
|
|
475
|
-
} else {
|
|
476
|
-
let f = objs![i]
|
|
477
|
-
if (f === null || f === undefined) {
|
|
478
|
-
if (r !== null) clearCell(r, at)
|
|
479
|
-
continue
|
|
480
|
-
}
|
|
481
|
-
u0 = f.u0
|
|
482
|
-
v0 = f.v0
|
|
483
|
-
u1 = f.u1
|
|
484
|
-
v1 = f.v1
|
|
438
|
+
eachChunkSlice(col, row, w, h, chunkTiles, chunkCols, (index, colA, colB, rowA, rowB) => {
|
|
439
|
+
let chunk = resident.get(index)
|
|
440
|
+
let r = chunk ? chunk.records : null
|
|
441
|
+
for (let y = rowA; y < rowB; y++) {
|
|
442
|
+
let at = slot(colA, y)
|
|
443
|
+
let i = (y - row) * w + (colA - col)
|
|
444
|
+
for (let x = colA; x < colB; x++, at += FLOATS_PER_SPRITE, i++) {
|
|
445
|
+
let u0: number
|
|
446
|
+
let v0: number
|
|
447
|
+
let u1: number
|
|
448
|
+
let v1: number
|
|
449
|
+
if (idx !== null) {
|
|
450
|
+
let k = idx[i]!
|
|
451
|
+
if (k === -1 || k === CLEAR_INDEX) {
|
|
452
|
+
if (r !== null) clearCell(r, at)
|
|
453
|
+
continue
|
|
485
454
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
455
|
+
let b = k * 4
|
|
456
|
+
u0 = uv![b]!
|
|
457
|
+
v0 = uv![b + 1]!
|
|
458
|
+
u1 = uv![b + 2]!
|
|
459
|
+
v1 = uv![b + 3]!
|
|
460
|
+
} else {
|
|
461
|
+
let f = objs![i]
|
|
462
|
+
if (f === null || f === undefined) {
|
|
463
|
+
if (r !== null) clearCell(r, at)
|
|
464
|
+
continue
|
|
489
465
|
}
|
|
490
|
-
|
|
466
|
+
u0 = f.u0
|
|
467
|
+
v0 = f.v0
|
|
468
|
+
u1 = f.u1
|
|
469
|
+
v1 = f.v1
|
|
491
470
|
}
|
|
471
|
+
if (r === null) {
|
|
472
|
+
chunk = allocate(index)
|
|
473
|
+
r = chunk.records
|
|
474
|
+
}
|
|
475
|
+
writeCell(r, at, x, y, u0, v0, u1, v1, cellTint)
|
|
492
476
|
}
|
|
493
|
-
if (chunk) touch(chunk)
|
|
494
477
|
}
|
|
495
|
-
|
|
478
|
+
if (chunk) touch(chunk)
|
|
479
|
+
})
|
|
496
480
|
},
|
|
497
481
|
getTile(col, row) {
|
|
498
|
-
checkCell(col, row,
|
|
499
|
-
let chunk = resident.get(
|
|
482
|
+
checkCell("getTile", col, row, cols, rows)
|
|
483
|
+
let chunk = resident.get(chunkAt(col, row))
|
|
500
484
|
let at = slot(col, row)
|
|
501
485
|
if (!chunk || chunk.records[at + 2] === 0) return null
|
|
502
486
|
let r = chunk.records
|