@solidrt/2d 0.0.56 → 0.0.57

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.
@@ -17,8 +17,8 @@
17
17
  // { follow, spin } or runs { fit: true }, `selected` returns the selected
18
18
  // sprite's world position (null when none), `first` the first sprite's -
19
19
  // the one to aim synthetic taps and drags at.
20
- import { createEffect, displayScale, onFrame, render, windowSize } from "@solidrt/core"
21
- import { addSprite, createAtlas, createCamera2d, createSpriteLayer, fitOversample, grid, setSprite } from "@solidrt/2d"
20
+ import { createEffect, createInputMap, createPointerFeed, decodeImage, displayScale, gamepad, onFrame, render, windowSize } from "@solidrt/core"
21
+ import { addSprite, camera2dActions, camera2dBindings, createAtlas, createCamera2d, createSpriteLayer, feedPointer, fitOversample, grid, setSprite } from "@solidrt/2d"
22
22
  import type { Camera2dHandle, SpriteHandle } from "@solidrt/2d"
23
23
  import { registerDebug } from "srt:dev"
24
24
  import logoBytes from "./logo.png" with { type: "binary" }
@@ -55,15 +55,14 @@ let first: SpriteHandle | null = null
55
55
  let dragging: SpriteHandle | null = null
56
56
 
57
57
  function App() {
58
- let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
59
- let frames = grid(2, 2, { width: atlas.width, height: atlas.height })
58
+ let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
59
+ let frames = grid(atlas, 2, 2)
60
60
  // The window's logical size, mirrored for the layer and the camera.
61
61
  let win = { width: 1, height: 1 }
62
- let layer = createSpriteLayer(win.width, win.height, atlas.texture, {
63
- capacity: COUNT + 1,
64
- clearColor: [0.05, 0.05, 0.09, 1],
65
- label: "camera",
66
- })
62
+ let layer = createSpriteLayer(atlas.texture, { capacity: COUNT + 1, label: "camera" })
63
+ // The layer's one view here: the window. The camera drives it and
64
+ // listens at its root.
65
+ let view = layer.createView({ width: win.width, height: win.height, clearColor: [0.05, 0.05, 0.09, 1] })
67
66
  let select = (sprite: SpriteHandle | null) => {
68
67
  if (selected) setSprite(selected, { tint: TINT })
69
68
  selected = sprite
@@ -94,16 +93,23 @@ function App() {
94
93
  }
95
94
  let roamer = addSprite(layer, { x: WORLD.width / 2, y: WORLD.height / 2, w: ROAMER, h: ROAMER, frame: frames[0], tint: [1, 0.8, 0.3, 1] })
96
95
 
97
- cam = createCamera2d(layer, {
96
+ cam = createCamera2d(view, {
98
97
  viewport: () => win,
99
98
  world: WORLD,
100
99
  maxZoom: MAX_ZOOM,
101
100
  deadZone: DEAD_ZONE,
102
101
  })
103
- cam.attach(layer)
102
+ // The camera's input: the view's pointer feed (fed from the view's root,
103
+ // after the sprites' claims) and any pad, bound to the control's actions
104
+ // (ARCHITECTURE.md: the app wires devices, the control takes the map).
105
+ let pointer = createPointerFeed()
106
+ feedPointer(view, pointer)
107
+ let input = createInputMap(camera2dActions)
108
+ input.bind(camera2dBindings({ pointer, gamepad: gamepad() }))
109
+ input.drive(cam.axes)
104
110
  // A tap that reached the root landed on empty space (a sprite's tap
105
111
  // still bubbles here with e.sprite set, so the miss is the null case).
106
- layer.listen({
112
+ view.listen({
107
113
  onTap: e => {
108
114
  if (e.sprite) return
109
115
  select(null)
@@ -117,8 +123,8 @@ function App() {
117
123
  ({ size, scale }) => {
118
124
  win.width = Math.max(1, size.width)
119
125
  win.height = Math.max(1, size.height)
120
- layer.setSize(win.width, win.height)
121
- layer.setOversample(fitOversample(scale, win.width, win.height, win.width * win.height * scale * scale))
126
+ view.setSize(win.width, win.height)
127
+ view.setOversample(fitOversample(scale, win.width, win.height, win.width * win.height * scale * scale))
122
128
  },
123
129
  )
124
130
 
@@ -152,7 +158,7 @@ function App() {
152
158
  }
153
159
  }}
154
160
  >
155
- <texture src={layer.texture} position="absolute" left={0} top={0} width={windowSize().width} height={windowSize().height} {...layer.handlers} />
161
+ <texture src={view.texture} position="absolute" left={0} top={0} width={windowSize().width} height={windowSize().height} {...view.handlers} />
156
162
  <view pointerEvents="none" gap={6} padding={20}>
157
163
  <text color="#eef4ff" fontSize={24} fontWeight={700}>
158
164
  Camera
@@ -7,7 +7,7 @@
7
7
  // picking at random points, exercises pickRect, groups and slot recycling,
8
8
  // and benches addSprite plus a move-everything frame on both layers. Watch
9
9
  // the logs for PARITY/BENCH lines ending in PARITY-OK.
10
- import { onFrame, render } from "@solidrt/core"
10
+ import { decodeImage, onFrame, render } from "@solidrt/core"
11
11
  import { readTexture } from "@solidrt/core/gpu"
12
12
  import {
13
13
  addGroup,
@@ -42,10 +42,12 @@ function rng(seed: number): () => number {
42
42
  }
43
43
 
44
44
  function App() {
45
- let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
46
- let frames = grid(2, 2, { width: atlas.width, height: atlas.height })
47
- let nodes = createSpriteLayer(W, H, atlas.texture, { capacity: 64, clearColor: [0.05, 0.05, 0.09, 1], label: "nodes" })
48
- let records = createRecordLayer(W, H, atlas.texture, { capacity: 64, clearColor: [0.05, 0.05, 0.09, 1], label: "records" })
45
+ let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
46
+ let frames = grid(atlas, 2, 2)
47
+ let nodes = createSpriteLayer(atlas.texture, { capacity: 64, label: "nodes" })
48
+ let records = createRecordLayer(atlas.texture, { capacity: 64, label: "records" })
49
+ let nodeView = nodes.createView({ width: W, height: H, clearColor: [0.05, 0.05, 0.09, 1], label: "nodes" })
50
+ let recordView = records.createView({ width: W, height: H, clearColor: [0.05, 0.05, 0.09, 1], label: "records" })
49
51
 
50
52
  let fields = (r: () => number) => ({
51
53
  x: 20 + r() * (W - 40),
@@ -89,8 +91,8 @@ function App() {
89
91
  }
90
92
 
91
93
  let pixelParity = () => {
92
- let a = readTexture(nodes.texture)
93
- let b = readTexture(records.texture)
94
+ let a = readTexture(nodeView.texture)
95
+ let b = readTexture(recordView.texture)
94
96
  if (a.width !== b.width || a.height !== b.height) {
95
97
  check(false, `sizes differ: ${a.width}x${a.height} vs ${b.width}x${b.height}`)
96
98
  return
@@ -195,8 +197,8 @@ function App() {
195
197
 
196
198
  return (
197
199
  <window flexDirection="row" alignItems="center" justifyContent="center" gap={8}>
198
- <texture src={nodes.texture} width={W} height={H} />
199
- <texture src={records.texture} width={W} height={H} />
200
+ <texture src={nodeView.texture} width={W} height={H} />
201
+ <texture src={recordView.texture} width={W} height={H} />
200
202
  </window>
201
203
  )
202
204
  }
package/examples/pick.tsx CHANGED
@@ -10,10 +10,16 @@
10
10
  // and waits for the next add); structure lives in a signal, so <For>
11
11
  // unmounts the removed <Sprite>. A tap on empty space reaches the layer
12
12
  // with e.sprite null. The layer FILLS the window (no width/height): layer
13
- // pixels are the leaf's own coordinates. The `state` debug command
14
- // returns the camera pose and every sprite's world position.
15
- import { createSignal, render, For } from "@solidrt/core"
16
- import { Camera2d, createAtlas, grid, setSprite, Sprite, SpriteLayer } from "@solidrt/2d"
13
+ // pixels are the leaf's own coordinates. A corner <View2d> renders the
14
+ // same sprites again under a <Camera2d> of its own: drag or wheel there
15
+ // moves only the inset's camera, while a sprite tapped or dragged in the
16
+ // inset responds exactly as in the main view (the same handlers, the
17
+ // view's camera undone). The `state` debug command returns both camera
18
+ // poses and every sprite's world position; `maxZoom` sets the main
19
+ // <Camera2d maxZoom> prop live (a tighter range re-clamps the pose at
20
+ // once - the props are live, only the pose props are initial values).
21
+ import { createInputMap, createPointerFeed, createSignal, decodeImage, render, For } from "@solidrt/core"
22
+ import { Camera2d, camera2dActions, camera2dBindings, createAtlas, grid, setSprite, Sprite, SpriteLayer, View2d } from "@solidrt/2d"
17
23
  import type { Camera2dHandle, Frame, SpriteHandle } from "@solidrt/2d"
18
24
  import { registerDebug } from "srt:dev"
19
25
  import logoBytes from "./logo.png" with { type: "binary" }
@@ -25,21 +31,37 @@ const TINTS: [number, number, number, number][] = [
25
31
  [0.55, 0.7, 1, 1],
26
32
  ]
27
33
  const MAX_ZOOM = 4
34
+ // The inset: view pixels, its margin from the window's corner, and the
35
+ // zoom that shows the four authored sprites whole inside it.
36
+ const INSET_WIDTH = 240
37
+ const INSET_HEIGHT = 160
38
+ const INSET_MARGIN = 16
39
+ const INSET_ZOOM = 0.25
28
40
 
29
41
  type Item = { id: number; x: number; y: number; frame: Frame }
30
42
 
31
43
  let cam: Camera2dHandle | undefined
44
+ let insetCam: Camera2dHandle | undefined
32
45
  let handles = new Map<number, SpriteHandle>()
46
+ let [maxZoom, setMaxZoom] = createSignal(MAX_ZOOM)
33
47
 
34
48
  function App() {
35
- let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
36
- let frames = grid(2, 2, { width: atlas.width, height: atlas.height })
49
+ let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
50
+ let frames = grid(atlas, 2, 2)
37
51
  let [items, setItems] = createSignal<Item[]>([
38
52
  { id: 0, x: 200, y: 240, frame: frames[0]! },
39
53
  { id: 1, x: 420, y: 300, frame: frames[1]! },
40
54
  { id: 2, x: 300, y: 470, frame: frames[2]! },
41
55
  { id: 3, x: 520, y: 500, frame: frames[3]! },
42
56
  ])
57
+ // One pointer feed and map per view: the layer's own view and the inset
58
+ // each drive their <Camera2d> from their own gestures and no other's.
59
+ let pointer = createPointerFeed()
60
+ let input = createInputMap(camera2dActions)
61
+ input.bind(camera2dBindings({ pointer }))
62
+ let insetPointer = createPointerFeed()
63
+ let insetInput = createInputMap(camera2dActions)
64
+ insetInput.bind(camera2dBindings({ pointer: insetPointer }))
43
65
 
44
66
  return (
45
67
  <window>
@@ -47,13 +69,14 @@ function App() {
47
69
  atlas={atlas.texture}
48
70
  capacity={64}
49
71
  clearColor={[0.05, 0.05, 0.09, 1]}
72
+ pointer={pointer}
50
73
  onTap={e => {
51
74
  if (e.sprite === null) console.log(`tap on empty space at ${e.x.toFixed(0)}, ${e.y.toFixed(0)} (x${e.tapCount})`)
52
75
  }}
53
76
  >
54
77
  {/* A top-left pivot keeps world (0,0) at the leaf's corner, so the
55
78
  authored positions read as window coordinates until panned. */}
56
- <Camera2d maxZoom={MAX_ZOOM} pivot={{ x: 0, y: 0 }} ref={c => (cam = c)} />
79
+ <Camera2d input={input} maxZoom={maxZoom()} pivot={{ x: 0, y: 0 }} ref={c => (cam = c)} />
57
80
  <For each={items()}>
58
81
  {item => {
59
82
  let tintIndex = 0
@@ -94,6 +117,13 @@ function App() {
94
117
  )
95
118
  }}
96
119
  </For>
120
+ {/* A laid-out element among the layer's children renders where it
121
+ sits; the <View2d> inside stays a layer child through context. */}
122
+ <view position="absolute" right={INSET_MARGIN} bottom={INSET_MARGIN}>
123
+ <View2d width={INSET_WIDTH} height={INSET_HEIGHT} clearColor={[0.12, 0.1, 0.16, 1]} label="inset" pointer={insetPointer}>
124
+ <Camera2d input={insetInput} zoom={INSET_ZOOM} maxZoom={MAX_ZOOM} pivot={{ x: 0, y: 0 }} ref={c => (insetCam = c)} />
125
+ </View2d>
126
+ </view>
97
127
  </SpriteLayer>
98
128
  </window>
99
129
  )
@@ -101,7 +131,13 @@ function App() {
101
131
 
102
132
  render(() => <App />)
103
133
 
134
+ registerDebug("maxZoom", (args?: { value?: number }) => {
135
+ if (typeof args?.value === "number") setMaxZoom(args.value)
136
+ return maxZoom()
137
+ })
138
+
104
139
  registerDebug("state", () => ({
105
140
  camera: cam?.camera() ?? null,
141
+ inset: insetCam?.camera() ?? null,
106
142
  items: [...handles].filter(([, s]) => s.layer !== null).map(([id, s]) => ({ id, x: s._x, y: s._y })),
107
143
  }))
@@ -0,0 +1,111 @@
1
+ // Split-screen: ONE layer of sprites shown through two views side by
2
+ // side, each a full viewport with a camera of its own - the layer has no
3
+ // output but its views (a Unity scene renders only through Cameras, a
4
+ // Godot World2D only through Viewports). The <SpriteLayer output={false}>
5
+ // owns the sprites and no leaf; its two <View2d> children fill the window
6
+ // half each and follow one roaming sprite apiece through a <Camera2d>
7
+ // with a dead zone, so the two poses differ every frame. Wheel inside a
8
+ // pane zooms that pane alone (the follow keeps tracking under the zoom),
9
+ // and so does the pad that joined it (press any button on it first);
10
+ // a tap on a sprite in either pane tints it in both, since there is only
11
+ // one sprite. The `cameras` debug command reads both poses back.
12
+ import { createInputMap, createPointerFeed, decodeImage, gamepad, onFrame, render, windowSize, For } from "@solidrt/core"
13
+ import { Camera2d, camera2dActions, camera2dBindings, createAtlas, grid, setSprite, Sprite, SpriteLayer, View2d } from "@solidrt/2d"
14
+ import type { Camera2dHandle, Frame, SpriteHandle } from "@solidrt/2d"
15
+ import { registerDebug } from "srt:dev"
16
+ import logoBytes from "./logo.png" with { type: "binary" }
17
+
18
+ const WORLD = { width: 2400, height: 1600 }
19
+ const COUNT = 200
20
+ const SPRITE = 48
21
+ const ROAMER = 72
22
+ // Roaming paths: angular rates (rad/s) and the fraction of the world
23
+ // each roamer sweeps.
24
+ const ROAM_RATE = [0.23, 0.31, 0.17, 0.29]
25
+ const ROAM_SPAN = 0.4
26
+ // The follow dead zone, as viewport fractions.
27
+ const DEAD_ZONE = { width: 0.2, height: 0.25 }
28
+ const PANE_ZOOM = 1.5
29
+ const MAX_ZOOM = 4
30
+ const TINT: [number, number, number, number] = [0.8, 0.85, 0.95, 1]
31
+ const HIT: [number, number, number, number] = [1, 0.5, 0.5, 1]
32
+
33
+ type Item = { id: number; x: number; y: number; frame: Frame }
34
+
35
+ let cams: (Camera2dHandle | undefined)[] = [undefined, undefined]
36
+ let roamers: (SpriteHandle | undefined)[] = [undefined, undefined]
37
+
38
+ function App() {
39
+ let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
40
+ let frames = grid(atlas, 2, 2)
41
+ let items: Item[] = []
42
+ for (let i = 0; i < COUNT; i++) {
43
+ items.push({ id: i, x: Math.random() * WORLD.width, y: Math.random() * WORLD.height, frame: frames[i % 4]! })
44
+ }
45
+ // The window halves, live through windowSize (its first read is 0x0,
46
+ // hence the floor of 1): the views resize with the window.
47
+ let pane = () => ({ width: Math.max(1, Math.floor(windowSize().width / 2)), height: Math.max(1, windowSize().height) })
48
+ // The roamers move at frame rate; each <Camera2d> follows its own and
49
+ // runs its frames itself while the follow is settling.
50
+ onFrame(tick => {
51
+ let t = tick / 1000
52
+ for (let i = 0; i < 2; i++) {
53
+ let x = WORLD.width / 2 + Math.sin(t * ROAM_RATE[i * 2]!) * WORLD.width * ROAM_SPAN
54
+ let y = WORLD.height / 2 + Math.cos(t * ROAM_RATE[i * 2 + 1]!) * WORLD.height * ROAM_SPAN
55
+ let roamer = roamers[i]
56
+ if (roamer) setSprite(roamer, { x, y, rotation: t })
57
+ cams[i]?.follow(x, y)
58
+ }
59
+ })
60
+ // Two players, two maps: each pane's <Camera2d> takes its own pane's
61
+ // gestures and its own pad, and nothing crosses over. The pads seat in
62
+ // pick-up order: the first pad to press any button joins pane 0, the
63
+ // next joins pane 1 (gamepad.next(); gamepad(i) would pin the slots).
64
+ let paneView = (i: number) => {
65
+ let pointer = createPointerFeed()
66
+ let input = createInputMap(camera2dActions)
67
+ input.bind(camera2dBindings({ pointer, gamepad: gamepad.next() }))
68
+ return (
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} deadZone={DEAD_ZONE} ref={c => (cams[i] = c)} />
71
+ </View2d>
72
+ )
73
+ }
74
+ return (
75
+ <window>
76
+ <SpriteLayer atlas={atlas.texture} capacity={COUNT + 2} output={false}>
77
+ <view flexDirection="row" width="100%" height="100%">
78
+ {paneView(0)}
79
+ {paneView(1)}
80
+ </view>
81
+ <For each={items}>
82
+ {item => {
83
+ let handle: SpriteHandle | undefined
84
+ let hit = false
85
+ return (
86
+ <Sprite
87
+ ref={s => (handle = s)}
88
+ x={item.x}
89
+ y={item.y}
90
+ w={SPRITE}
91
+ h={SPRITE}
92
+ frame={item.frame}
93
+ tint={TINT}
94
+ onTap={() => {
95
+ hit = !hit
96
+ if (handle) setSprite(handle, { tint: hit ? HIT : TINT })
97
+ }}
98
+ />
99
+ )
100
+ }}
101
+ </For>
102
+ <Sprite ref={s => (roamers[0] = s)} x={WORLD.width / 2} y={WORLD.height / 2} w={ROAMER} h={ROAMER} frame={frames[0]!} tint={[1, 0.8, 0.3, 1]} />
103
+ <Sprite ref={s => (roamers[1] = s)} x={WORLD.width / 2} y={WORLD.height / 2} w={ROAMER} h={ROAMER} frame={frames[3]!} tint={[0.4, 0.9, 1, 1]} />
104
+ </SpriteLayer>
105
+ </window>
106
+ )
107
+ }
108
+
109
+ render(() => <App />)
110
+
111
+ registerDebug("cameras", () => cams.map(c => c?.camera() ?? null))
@@ -13,7 +13,7 @@
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
- import { render } from "@solidrt/core"
16
+ import { decodeImage, render } from "@solidrt/core"
17
17
  import { addSprite, createAtlas, createSpriteLayer, grid, setSprite, setSpriteTransition } from "@solidrt/2d"
18
18
  import logoBytes from "./logo.png" with { type: "binary" }
19
19
 
@@ -25,13 +25,10 @@ const H = 720
25
25
  const SPRITE = 30
26
26
 
27
27
  function App() {
28
- let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
29
- let frames = grid(2, 2, { width: atlas.width, height: atlas.height })
30
- let layer = createSpriteLayer(W, H, atlas.texture, {
31
- capacity: COUNT,
32
- clearColor: [0.05, 0.05, 0.09, 1],
33
- label: "springs",
34
- })
28
+ let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
29
+ let frames = grid(atlas, 2, 2)
30
+ let layer = createSpriteLayer(atlas.texture, { capacity: COUNT, label: "springs" })
31
+ let view = layer.createView({ width: W, height: H, clearColor: [0.05, 0.05, 0.09, 1] })
35
32
 
36
33
  let slotX = (slot: number) => ((slot % COLS) + 0.5) * (W / COLS)
37
34
  let slotY = (slot: number) => (Math.floor(slot / COLS) + 0.5) * (H / ROWS)
@@ -61,7 +58,7 @@ function App() {
61
58
 
62
59
  return (
63
60
  <window alignItems="center" justifyContent="center">
64
- <texture src={layer.texture} width={W} height={H} />
61
+ <texture src={view.texture} width={W} height={H} />
65
62
  </window>
66
63
  )
67
64
  }
@@ -8,10 +8,10 @@
8
8
  // instance-buffer write per frame.
9
9
  //
10
10
  // The atlas is a real image (core's logo) sliced 2x2 by grid(): four frames,
11
- // each sprite drawing one quarter. An atlas from raw pixel bytes would use
12
- // createTexture directly; this path (createAtlas) decodes PNG bytes imported
13
- // with { type: "binary" }.
14
- import { onFrame, render } from "@solidrt/core"
11
+ // each sprite drawing one quarter. Pixels built in code go to createAtlas as
12
+ // they are; this path decodes PNG bytes imported with { type: "binary" }
13
+ // first.
14
+ import { decodeImage, onFrame, render } from "@solidrt/core"
15
15
  import { addSprite, createAtlas, createSpriteLayer, grid, setSprite } from "@solidrt/2d"
16
16
  import logoBytes from "./logo.png" with { type: "binary" }
17
17
 
@@ -21,13 +21,10 @@ const H = 720
21
21
  const SPRITE = 48
22
22
 
23
23
  function App() {
24
- let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
25
- let frames = grid(2, 2, { width: atlas.width, height: atlas.height })
26
- let layer = createSpriteLayer(W, H, atlas.texture, {
27
- capacity: COUNT,
28
- clearColor: [0.05, 0.05, 0.09, 1],
29
- label: "bounce",
30
- })
24
+ let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
25
+ let frames = grid(atlas, 2, 2)
26
+ let layer = createSpriteLayer(atlas.texture, { capacity: COUNT, label: "bounce" })
27
+ let view = layer.createView({ width: W, height: H, clearColor: [0.05, 0.05, 0.09, 1] })
31
28
 
32
29
  // Simulation state lives in plain arrays; the layer holds the published
33
30
  // snapshot of it.
@@ -73,7 +70,7 @@ function App() {
73
70
 
74
71
  return (
75
72
  <window alignItems="center" justifyContent="center">
76
- <texture src={layer.texture} width={W} height={H} />
73
+ <texture src={view.texture} width={W} height={H} />
77
74
  </window>
78
75
  )
79
76
  }
@@ -7,15 +7,23 @@
7
7
  // The camera flies a ship-style path: a fixed screen pivot near the bottom
8
8
  // of the viewport, the world panning and ROTATING under it so the flight
9
9
  // heading always points up - the <TileLayer> camera prop with rotation and
10
- // pivot (a leaf transform, never a re-bake). A timer edits tiles while it
11
- // runs: beacon markers along the road's center line blink, and each blink's
12
- // batch of setTile calls re-bakes only the chunks the beacons land in.
10
+ // pivot (a leaf transform, never a re-bake). The world is seeded by ONE
11
+ // setTiles rect write over a Uint16Array of frame indices (-1 where the
12
+ // world is empty), the shape a generator or a worker produces; a timer
13
+ // edits tiles while it runs: beacon markers along the road's center line
14
+ // blink, and each blink's batch of setTile calls re-bakes only the chunks
15
+ // the beacons land in.
13
16
  //
14
17
  // The atlas is the core logo sliced 2x2 by grid(); a real game would slice
15
- // a tileset sheet the same way.
16
- import { createSignal, onFrame, pct, render, windowSize } from "@solidrt/core"
18
+ // a tileset sheet the same way, and the array is the layer's `frames`
19
+ // table the indices name.
20
+ //
21
+ // Debug commands: `state` (resident chunk count) and `cell` ({ col, row }:
22
+ // the frame at a cell, null when empty).
23
+ import { createSignal, decodeImage, onFrame, pct, render, windowSize } from "@solidrt/core"
17
24
  import { createAtlas, grid, TileLayer } from "@solidrt/2d"
18
25
  import type { TileCamera, TileLayerHandle } from "@solidrt/2d"
26
+ import { registerDebug } from "srt:dev"
19
27
  import logoBytes from "./logo.png" with { type: "binary" }
20
28
 
21
29
  const COLS = 128
@@ -29,23 +37,29 @@ const WORLD = COLS * TILE
29
37
  const HITCH_FACTOR = 1.5
30
38
 
31
39
  function App() {
32
- let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
33
- let frames = grid(2, 2, { width: atlas.width, height: atlas.height })
40
+ let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
41
+ let frames = grid(atlas, 2, 2)
34
42
 
35
43
  let layer!: TileLayerHandle
36
44
  let seed = (l: TileLayerHandle) => {
37
45
  layer = l
38
46
  // A solid ring "road" around the world center: continuous under the
39
47
  // flight path, empty everywhere else - the empty regions are the point,
40
- // their chunks never allocate.
48
+ // their chunks never allocate, since a rect write allocates a chunk
49
+ // only for the first frame it carries there. -1 is the clear (a
50
+ // Uint16Array holds it as 0xffff, the same bits).
41
51
  let c = COLS / 2
52
+ let cells = new Uint16Array(COLS * ROWS).fill(-1)
42
53
  for (let row = 0; row < ROWS; row++) {
43
54
  for (let col = 0; col < COLS; col++) {
44
55
  let d = Math.hypot(col - c, row - c)
45
- if (d > 40 && d < 52) l.setTile(col, row, frames[(col ^ row) % 4]!)
56
+ if (d > 40 && d < 52) cells[row * COLS + col] = (col ^ row) % 4
46
57
  }
47
58
  }
59
+ l.setTiles(0, 0, COLS, ROWS, cells)
48
60
  }
61
+ registerDebug("state", () => ({ chunks: layer.chunks.length }))
62
+ registerDebug("cell", (args?: { col?: number; row?: number }) => layer.getTile(args?.col ?? 0, args?.row ?? 0))
49
63
 
50
64
  // Beacon cells on the road's center line every 7.5 degrees; the interval
51
65
  // below blinks them between a marker frame and the road pattern.
@@ -99,6 +113,7 @@ function App() {
99
113
  tileW={TILE}
100
114
  tileH={TILE}
101
115
  atlas={atlas.texture}
116
+ frames={frames}
102
117
  camera={camera()}
103
118
  label="tile-world"
104
119
  ref={seed}