@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.
@@ -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
- * a dead zone or a rate follows its prop, and a bounds or pivot change
37
- * re-clamps and pushes the pose at once - the `<OrbitCamera>` rule.
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 pivot prop change re-clamps and pushes the pose at once
71
- // (set({}) applies the clamps). Deferred: the creation already clamped
72
- // and pushed the initial pose. Untracked: the control reads the options
73
- // through the getters while it applies them, and the apply wants the
74
- // values of that moment (the compute above is what tracks them).
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
  )
@@ -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 twist
30
- * rolls; the left stick scrolls, the triggers zoom, the right stick's x
31
- * rolls; the arrow keys scroll and minus/equals zoom.
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?: NodeTransitionSpec
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
- let checkCell = (col: number, row: number, verb: string): void => {
328
- if (!(Number.isInteger(col) && Number.isInteger(row) && col >= 0 && col < cols && row >= 0 && row < rows)) {
329
- throw new Error(`${verb}: cell ${col}, ${row} outside the ${cols} x ${rows} grid`)
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, "setTile")
397
- let index = chunkOf(col, row)
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
- 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)) {
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
- let cr0 = Math.floor(row / chunkTiles)
444
- let cr1 = Math.floor((row + h - 1) / chunkTiles)
445
- let cc0 = Math.floor(col / chunkTiles)
446
- let cc1 = Math.floor((col + w - 1) / chunkTiles)
447
- for (let cr = cr0; cr <= cr1; cr++) {
448
- let rowA = Math.max(row, cr * chunkTiles)
449
- let rowB = Math.min(row + h, (cr + 1) * chunkTiles)
450
- for (let cc = cc0; cc <= cc1; cc++) {
451
- let colA = Math.max(col, cc * chunkTiles)
452
- let colB = Math.min(col + w, (cc + 1) * chunkTiles)
453
- let index = cr * chunkCols + cc
454
- let chunk = resident.get(index)
455
- let r = chunk ? chunk.records : null
456
- for (let y = rowA; y < rowB; y++) {
457
- let at = slot(colA, y)
458
- let i = (y - row) * w + (colA - col)
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
- if (r === null) {
487
- chunk = allocate(index)
488
- r = chunk.records
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
- writeCell(r, at, x, y, u0, v0, u1, v1, cellTint)
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, "getTile")
499
- let chunk = resident.get(chunkOf(col, row))
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