@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.
- package/AGENTS.md +248 -85
- package/README.md +3 -3
- package/examples/README.md +38 -13
- package/examples/anim.tsx +6 -8
- package/examples/camera-probe.tsx +16 -14
- package/examples/camera.tsx +21 -15
- package/examples/parity.tsx +11 -9
- package/examples/pick.tsx +43 -7
- package/examples/split-screen.tsx +111 -0
- package/examples/springs.tsx +6 -9
- package/examples/sprites.tsx +9 -12
- package/examples/tiles.tsx +24 -9
- package/examples/views.tsx +177 -0
- package/package.json +2 -2
- package/src/atlas.ts +35 -33
- package/src/camera.ts +26 -1
- package/src/camera2d.ts +692 -119
- package/src/components/auto-oversample.ts +27 -0
- package/src/components/camera2d.tsx +82 -0
- package/src/components/context.ts +48 -0
- package/src/components/group.tsx +68 -0
- package/src/components/index.ts +28 -0
- package/src/components/sprite-layer.tsx +312 -0
- package/src/components/sprite.tsx +72 -0
- package/src/components/tile-layer.tsx +160 -0
- package/src/components/view2d.tsx +151 -0
- package/src/dispatch.ts +7 -7
- package/src/frames.ts +63 -14
- package/src/index.ts +27 -11
- package/src/input.ts +50 -0
- package/src/layer.ts +384 -245
- package/src/oversample-math.ts +1 -1
- package/src/records.ts +33 -138
- package/src/shaders.ts +65 -3
- package/src/tiles.ts +195 -42
- package/src/views.ts +294 -0
- package/src/camera-motion.ts +0 -532
- package/src/components.tsx +0 -611
package/AGENTS.md
CHANGED
|
@@ -17,8 +17,16 @@ moved subtrees in Rust, and picking walks the core BVH.
|
|
|
17
17
|
`addGroup`/`setGroup`/`setSpriteParent`/`setSpriteTransition`/
|
|
18
18
|
`setGroupTransition` - plain objects, no signals, usable without
|
|
19
19
|
components), the records layer (records.ts: `createRecordLayer` - the
|
|
20
|
-
raw escape hatch, below), and the component face (components
|
|
21
|
-
`SpriteLayer`/`Sprite`/`Group`/`Camera2d` over context).
|
|
20
|
+
raw escape hatch, below), and the component face (components/:
|
|
21
|
+
`SpriteLayer`/`Sprite`/`Group`/`View2d`/`Camera2d` over context).
|
|
22
|
+
- A layer holds sprites and renders NOTHING by itself: it shows through
|
|
23
|
+
its VIEWS (`layer.createView({ width, height, camera?, oversample?,
|
|
24
|
+
clearColor?, label? })`, `<View2d>`, or the `<SpriteLayer>`'s own leaf,
|
|
25
|
+
which is one), each a draw target with a camera, a size and pointer
|
|
26
|
+
dispatch of its own - a Unity scene renders only through Cameras, a
|
|
27
|
+
Godot World2D only through Viewports, and there is no privileged first
|
|
28
|
+
one. The window view, the minimap, two split-screen panes are the same
|
|
29
|
+
`ViewHandle`. Details under Views below.
|
|
22
30
|
- Node layer ownership split, two instance-buffer slots on one pipeline:
|
|
23
31
|
slot 0 is the POSE buffer `[x, y, angle, sx, sy]` written ONLY by the
|
|
24
32
|
core (each sprite node's Pose2D record sink; one coalesced buffer write
|
|
@@ -37,9 +45,34 @@ moved subtrees in Rust, and picking walks the core BVH.
|
|
|
37
45
|
- Picking is the core index: `pick` raycasts [x, y, -1] along +z (exact
|
|
38
46
|
rotated-rect via the node's local box) and returns ALL hits topmost
|
|
39
47
|
first (highest slot) - the all-hits shape of @solidrt/3d's pick, so
|
|
40
|
-
`pick(x, y)[0]` is the topmost; `pickRect` is
|
|
41
|
-
(exact for rotated sprites, the marquee),
|
|
42
|
-
|
|
48
|
+
`pick(x, y)[0]` is the topmost; `pickRect` is `overlap` over an
|
|
49
|
+
unrotated rect, sprites only (exact for rotated sprites, the marquee),
|
|
50
|
+
unordered. Every query passes the core a filter with the layer's root
|
|
51
|
+
node (sprites and groups without a parent hang off it), so the arena
|
|
52
|
+
being shared with e.g. a 3d scene costs nothing in JS.
|
|
53
|
+
- Spatial queries (`checks/collision-check.tsx` pins the contract): the
|
|
54
|
+
3d scene's trio one dimension down, Godot's PhysicsDirectSpaceState2D
|
|
55
|
+
and CharacterBody2D in their names. `layer.raycast(x, y, dx, dy,
|
|
56
|
+
opts?)` is every shown sprite the ray strikes, nearest first, with
|
|
57
|
+
distance, edge point and edge normal; `layer.overlap(volume, opts?)`
|
|
58
|
+
every sprite a volume touches with its deepest contact `{ sprite,
|
|
59
|
+
point, normal, depth }`, unordered; `layer.sweep(volume, dx, dy,
|
|
60
|
+
opts?)` every sprite the moving volume first touches `{ sprite, time,
|
|
61
|
+
point, normal }`, earliest first; `layer.moveAndSlide(volume, dx, dy,
|
|
62
|
+
opts?)` the character mover over them, one core call per body per
|
|
63
|
+
frame (the depenetration, slide and floor-snap loop runs in the spatial
|
|
64
|
+
core, the same one @solidrt/3d's mover uses), returning `{ motion,
|
|
65
|
+
floor, wall, ceiling, hits }` with `up` defaulting to [0, -1] (y-down).
|
|
66
|
+
A `Volume` is a `Circle` `{ x, y, radius }`, a `Capsule` `{ ax, ay,
|
|
67
|
+
bx, by, radius }` or a `Rect` `{ x, y, width, height, rotation? }`;
|
|
68
|
+
`QueryOptions.sprites` is an include-list. Results name sprites, never
|
|
69
|
+
node ids, and every query runs the pending batch first, like pick.
|
|
70
|
+
The trap behind the design: a sprite is a COLUMN in the index
|
|
71
|
+
(`SPRITE_DEPTH` half-depth along z), not a flat quad. Against a flat
|
|
72
|
+
triangle the core's contact for a circle whose center lies over the
|
|
73
|
+
sprite is the plane normal (z) with depth = radius; against a column
|
|
74
|
+
every volume at z = 0 meets only side faces, so contacts, sweeps and
|
|
75
|
+
the mover stay in the plane with no 2d narrowphase of their own.
|
|
43
76
|
- Groups (`addGroup`/`<Group>`) are plain arena nodes (x, y, rotation,
|
|
44
77
|
UNIFORM scale - a group is a frame, never a sprite size; sprite w/h
|
|
45
78
|
lives in the sprite node's scale, which is why sprites cannot parent
|
|
@@ -83,68 +116,139 @@ moved subtrees in Rust, and picking walks the core BVH.
|
|
|
83
116
|
- Layer space is pixels, top-left origin, y-down - the render tree's frame.
|
|
84
117
|
The pipeline's clip space is y-down too (core gpu.ts pixel contract), so
|
|
85
118
|
the vertex stage carries NO flip anywhere. Do not add one.
|
|
86
|
-
- The camera (`setCamera`/the `camera` prop) is ONE `CameraUpdate`
|
|
87
|
-
across both
|
|
88
|
-
documents the semantics and the heading-upward
|
|
89
|
-
`rotation = -h - pi/2`).
|
|
90
|
-
|
|
91
|
-
however many sprites exist; `projectCamera`/`unprojectCamera`
|
|
92
|
-
the world <-> screen mapping as pure functions, and pointer
|
|
93
|
-
undoes the camera with the latter, so events arrive in
|
|
94
|
-
pixels. Picking itself works in world space and never
|
|
95
|
-
`
|
|
96
|
-
`CameraState` snapshot, @solidrt/3d's scene.camera())
|
|
97
|
-
`
|
|
119
|
+
- The camera (`view.setCamera`/the `camera` prop) is ONE `CameraUpdate`
|
|
120
|
+
type across both layer kinds' views: offset, zoom, and rotation about a
|
|
121
|
+
pivot (camera.ts documents the semantics and the heading-upward
|
|
122
|
+
convention, `rotation = -h - pi/2`). It is a shared-params write on
|
|
123
|
+
the view's target (`uCamera` + `uCameraRot`, the rotation in-shader),
|
|
124
|
+
one call however many sprites exist; `projectCamera`/`unprojectCamera`
|
|
125
|
+
export the world <-> screen mapping as pure functions, and pointer
|
|
126
|
+
dispatch undoes the view's camera with the latter, so events arrive in
|
|
127
|
+
world (layer) pixels. Picking itself works in world space and never
|
|
128
|
+
sees a camera. `view.camera()` reads the camera back (every field
|
|
129
|
+
present - the `CameraState` snapshot, @solidrt/3d's scene.camera())
|
|
130
|
+
and `view.project(x, y)` / `view.unproject(x, y)` are the two pure
|
|
98
131
|
functions over it.
|
|
99
|
-
- Camera control: `createCamera2d(
|
|
132
|
+
- Camera control: `createCamera2d(view | views, { viewport: () =>
|
|
100
133
|
({ width, height }), world?: { width, height }, min/maxZoom?, pivot?,
|
|
101
|
-
deadZone?: { width, height },
|
|
102
|
-
x?, y?, zoom?, rotation? })` - Godot's Camera2D
|
|
103
|
-
MapControls in one control over the shared CameraUpdate:
|
|
104
|
-
with inertia on release,
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
rotation, Godot's rule). The pose is
|
|
109
|
-
pivot a viewport fraction defaulting to
|
|
110
|
-
is the view center and glideTo/follow
|
|
111
|
-
the default pose puts world 0,0 at the
|
|
112
|
-
world = screen at rest takes `pivot: {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
camera
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
its
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
deadZone?: { width, height }, panSpeed?, zoomSpeed?, rollSpeed?,
|
|
135
|
+
followSpeed?, inertia?, x?, y?, zoom?, rotation? })` - Godot's Camera2D
|
|
136
|
+
and Three's MapControls in one control over the shared CameraUpdate:
|
|
137
|
+
pan with inertia on release, zoom about a point, eased `glideTo`/`fit`,
|
|
138
|
+
`follow(x, y)` through a dead zone with damping, roll about the pivot,
|
|
139
|
+
and world bounds that CONTAIN the view (an axis whose view is wider
|
|
140
|
+
than the world centers - so at the fit zoom a pan is a no-op, there is
|
|
141
|
+
nothing to pan; limits ignore rotation, Godot's rule). The pose is
|
|
142
|
+
"world point at the pivot", the pivot a viewport fraction defaulting to
|
|
143
|
+
the center, so `camera().x/y` is the view center and glideTo/follow
|
|
144
|
+
land there (without a `world`, the default pose puts world 0,0 at the
|
|
145
|
+
pivot: a fill layer that wants world = screen at rest takes `pivot: {
|
|
146
|
+
x: 0, y: 0 }`). The first argument is anything with a view's
|
|
147
|
+
`setCamera` (a view of a sprite layer, of a record layer, several at
|
|
148
|
+
once - one camera over a scene's layers - or a signal setter feeding
|
|
149
|
+
`<TileLayer camera>`).
|
|
150
|
+
Input, the rule (ARCHITECTURE.md): the control consumes a device-free
|
|
151
|
+
abstraction and never handles events itself. Its `axes` (core's
|
|
152
|
+
createAxes contract) are `pan` (vec2: a delta is finger travel in
|
|
153
|
+
viewport heights, applied as content travel - the world follows the
|
|
154
|
+
finger; a rate slides at one viewport height per second), `zoom` (axis,
|
|
155
|
+
octaves, positive in: a delta bracketed by a gesture - a pinch - applies
|
|
156
|
+
at once about its focal point, an unbracketed one - a wheel notch -
|
|
157
|
+
retargets an eased glide, which is how the control tells a finger from
|
|
158
|
+
an impulse; notches compound on the pending target) and `roll` (axis,
|
|
159
|
+
turns); a pan gesture's begin stops any glide (a finger landing on a
|
|
160
|
+
gliding view holds it) and its end flings with the drag's velocity. The
|
|
161
|
+
verbs, each pushing the pose at once: `panBy(dx, dy)` screen pixels,
|
|
162
|
+
`zoomAt(sx, sy, factor, { glide? })`, `rollBy(angle)`, `set(pose)`,
|
|
163
|
+
`glideTo`, `fit`, `follow`/`unfollow`, `interrupt`, `release`. An input
|
|
164
|
+
map (core AGENTS.md) drives the axes by name and the APP binds devices
|
|
165
|
+
to it: the view's pointer feed (`createPointerFeed()`, handed to
|
|
166
|
+
`<SpriteLayer pointer>` / `<View2d pointer>`, or bridged with
|
|
167
|
+
`feedPointer(view, feed)` for an imperative view - fed from the view's
|
|
168
|
+
ROOT, so a sprite that claims its press keeps the camera out of that
|
|
169
|
+
drag and a wheel anywhere zooms), a pad, the keyboard. Nothing binds
|
|
170
|
+
by default; `camera2dBindings({ pointer, gamepad?, keyboard? })` is the
|
|
171
|
+
standard set (drag and two fingers pan, pinch and wheel zoom, twist
|
|
172
|
+
rolls; the left stick and the arrows scroll the VIEW - bound through
|
|
173
|
+
invert(), since keys and sticks move the camera where a drag moves the
|
|
174
|
+
content - the triggers zoom, the right stick's x rolls) and
|
|
175
|
+
`camera2dActions` the declarations:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
let pointer = createPointerFeed()
|
|
179
|
+
feedPointer(view, pointer)
|
|
180
|
+
let input = createInputMap(camera2dActions)
|
|
181
|
+
input.bind(camera2dBindings({ pointer, gamepad: gamepad() }))
|
|
182
|
+
input.drive(cam.axes)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Call `cam.update(dt)` from a frame loop (it advances glides, follow,
|
|
186
|
+
inertia and the axis rates, pushes one setCamera per driven view when
|
|
187
|
+
the pose changed and reports that), gated on the reactive
|
|
188
|
+
`cam.active()` (true while a glide, fling, fit, follow or pan gesture
|
|
189
|
+
needs frames, or a rate drives; false at rest), read `cam.camera()` for
|
|
127
190
|
projectCamera. The camera has NO tap of its own: taps are the
|
|
128
191
|
dispatch's (`onTap` on the root with `e.sprite` null is "tap on empty
|
|
129
|
-
space"). `<Camera2d>` inside `<SpriteLayer>` is all of
|
|
130
|
-
through context, the 3d `<OrbitCamera>` shape: options read
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
192
|
+
space"). `<Camera2d input={input}>` inside `<SpriteLayer>` is all of
|
|
193
|
+
that wired through context, the 3d `<OrbitCamera>` shape: options read
|
|
194
|
+
at mount, driving the nearest view (the `<SpriteLayer>`'s own or the
|
|
195
|
+
enclosing `<View2d>`), `viewport` defaulting to that view's size, the
|
|
196
|
+
map's `pan`/`zoom`/`roll` actions (or the names in `actions`) driving
|
|
197
|
+
the axes, frames only while `active()`. A view's feed normalizes a drag
|
|
198
|
+
by the leaf's own box, so a leaf under a designSize fit pans and zooms
|
|
199
|
+
correctly. camera2d.ts imports `@solidrt/core/input` only, so
|
|
200
|
+
checks/camera2d-check.ts pins the clamp, anchoring, glides, follow,
|
|
201
|
+
inertia and the axes headless; examples/camera.tsx (function face) and
|
|
202
|
+
examples/pick.tsx (`<Camera2d>`) are the live guards. The shared
|
|
203
|
+
vocabulary with @solidrt/3d, one kind and unit per word: `pan` vec2,
|
|
204
|
+
`zoom` axis (octaves), `roll` axis (turns) here; `rotate`, `look`,
|
|
205
|
+
`move`, `rise` there. The pose options are initial values; every other
|
|
206
|
+
option (world, zoom range, pivot, dead zone, the speeds, inertia) is
|
|
207
|
+
read where it applies, never snapshotted, so `<Camera2d>` props are
|
|
208
|
+
live - forwarded as getters, a bounds or pivot change re-clamping and
|
|
209
|
+
pushing the pose at once through `set({})` - and a function-face
|
|
210
|
+
caller mutates the options object it passed (the `<OrbitCamera>` rule,
|
|
211
|
+
pinned in the check's live-options case). Not yet, all additive:
|
|
212
|
+
rotation glides, a contain origin other than center.
|
|
142
213
|
- Layer tint (`setTint(rgba)`/the `tint` option and prop, both layer
|
|
143
214
|
kinds): one `uTint` shared-params write multiplied over every sprite's
|
|
144
215
|
own tint - day/night, a dimmed parallax plane, a fade-in. Cheap to
|
|
145
216
|
animate (no record touches), unlike TileLayer's, which re-renders
|
|
146
217
|
resident chunks. The same contract as TileLayer.setTint, so one signal
|
|
147
218
|
drives a whole scene across layer kinds.
|
|
219
|
+
- Views (`layer.createView({ width, height, camera?, oversample?,
|
|
220
|
+
clearColor?, label? })`, both layer kinds): the ONLY output a layer
|
|
221
|
+
has - the window-filling main view, a minimap, a radar strip, a zoomed
|
|
222
|
+
inset, two split-screen panes - @solidrt/3d's scene.createView one
|
|
223
|
+
dimension down, as Unity renders a scene only through Cameras and
|
|
224
|
+
Godot a World2D only through Viewports. Each view is one draw target
|
|
225
|
+
holding one entry over the layer's pipeline and instance buffers
|
|
226
|
+
(views.ts): no sprite is mirrored, no record is written twice, and key
|
|
227
|
+
order (`orderBy`) comes along, since the core gathers the buffers
|
|
228
|
+
themselves at publish - ONE live entry declares the order (the first
|
|
229
|
+
view's; when that view is disposed the next live view re-adds its
|
|
230
|
+
entry ordered), every other entry reads them sorted. Growth, the
|
|
231
|
+
instance count and the layer tint fan out to every view; the camera
|
|
232
|
+
and the viewport are the view's own params, so a view costs no
|
|
233
|
+
per-frame JS beyond its camera writes. The `ViewHandle` is the
|
|
234
|
+
viewport contract (texture, width/height/setSize,
|
|
235
|
+
oversample/setOversample, setCamera/camera/project/unproject,
|
|
236
|
+
listen/handlers/handlersFor, dispose - views also die with the layer);
|
|
237
|
+
the sprites stay the layer's (`addSprite`, `pick`, `setTint`,
|
|
238
|
+
`createView`, `dispose`). Pointer events on a view leaf run the
|
|
239
|
+
dispatch with the VIEW's camera undone over the layer's pick, the view
|
|
240
|
+
as the root of the walk: a sprite under a minimap gets its ordinary
|
|
241
|
+
handlers, `view.listen` is the last stop (a tap there with `e.x`/`e.y`
|
|
242
|
+
in world pixels is "glide the main camera here"), and
|
|
243
|
+
a camera bound to a feed `feedPointer(view, feed)` bridges drives any
|
|
244
|
+
view. `<View2d>` is the
|
|
245
|
+
component form; the `<SpriteLayer>`'s built-in leaf is a view too, and
|
|
246
|
+
`<SpriteLayer output={false}>` has none, showing only through its
|
|
247
|
+
`<View2d>` children (examples/split-screen.tsx). examples/views.tsx is
|
|
248
|
+
the live guard. Not yet, all additive: tile-layer views, a `layers`
|
|
249
|
+
bitmask (a markers-only minimap), `into` tiling, per-view tint, and a
|
|
250
|
+
viewport over SEVERAL layers under one camera (their scene-level
|
|
251
|
+
camera) - okf/backlog/2d-layer-views-additive.md.
|
|
148
252
|
- Retargeted motion is NATIVE: `setSpriteTransition(sprite, { position:
|
|
149
253
|
{ duration: 700, bounce: 0.3 }, ... })` (or the `transition` prop) makes
|
|
150
254
|
setSprite writes TARGETS the core animates toward - position/scale
|
|
@@ -217,6 +321,29 @@ empty chunks cost nothing). Tiles are data, not
|
|
|
217
321
|
children: there is no `<Tile>` component on purpose - write cells through
|
|
218
322
|
`ref` with `setTile`.
|
|
219
323
|
|
|
324
|
+
Bulk writes: `setTiles(col, row, cols, rows, cells)` writes a rect at
|
|
325
|
+
once - `cells` row-major, `cols * rows` long, either frames (null clears)
|
|
326
|
+
or, with a `frames` table given at creation (`createTileLayer(..., {
|
|
327
|
+
frames })`, the `<TileLayer frames>` prop: a tileset, `grid()`'s array,
|
|
328
|
+
up to 65534 entries), indices into it (-1 clears; a Uint16Array holds -1
|
|
329
|
+
as 0xffff, the same bits) - Unity's SetTilesBlock, Phaser's putTilesAt
|
|
330
|
+
over the map's tileset. One locate and one dirty mark per chunk the rect
|
|
331
|
+
touches, the per-cell loop inside the layer, the same flush; a chunk the
|
|
332
|
+
rect only clears never allocates, so a whole-world write of a sparse
|
|
333
|
+
world allocates exactly the chunks the per-cell seed would. A generated
|
|
334
|
+
or worker-built world is a Uint16Array of indices, transferable, never an
|
|
335
|
+
array of frame objects; a fill is `new Uint16Array(n).fill(i)`. `tint`
|
|
336
|
+
in the options applies to every cell set. Every entry is validated before
|
|
337
|
+
the first write, so a bad one changes nothing; the table is copied at
|
|
338
|
+
creation. The rect form is the shape for worker transfer and chunk
|
|
339
|
+
re-fill, not a large speedup: the per-cell floor is the record write
|
|
340
|
+
itself, so a 16k-cell solid rect re-writes in ~14 ms against ~25 ms as a
|
|
341
|
+
setTile loop and a sparse rect is at parity with the loop, while a first
|
|
342
|
+
seed is dominated by chunk allocation at 1-2 ms per chunk
|
|
343
|
+
(probes/2d-tiles-bulk-bench.tsx). probes/2d-tiles-bulk-probe.tsx holds
|
|
344
|
+
the three seeds (setTile, index rect, frame rect) to the same cells,
|
|
345
|
+
chunks and bakes.
|
|
346
|
+
|
|
220
347
|
Tinting, two levels: `setTile(col, row, frame, { tint })` writes the
|
|
221
348
|
cell's record tint (same `[r, g, b, a]` 0..1 semantics as a sprite's
|
|
222
349
|
tint; absent keys keep their values, a cell set from empty starts at
|
|
@@ -232,37 +359,50 @@ on approach, evict) - okf/backlog/2d-baked-layers.md.
|
|
|
232
359
|
|
|
233
360
|
| Component | Props |
|
|
234
361
|
|---|---|
|
|
235
|
-
| `SpriteLayer` | width?, height? (
|
|
362
|
+
| `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) |
|
|
236
363
|
| `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? |
|
|
237
364
|
| `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? |
|
|
238
|
-
| `Camera2d` | createCamera2d's options minus `viewport` (world?, min/maxZoom?, pivot?, deadZone?, zoomSpeed?, followSpeed?, inertia?, x?, y?, zoom?, rotation?), viewport? (`() => { width, height }`, default: the
|
|
239
|
-
| `
|
|
365
|
+
| `Camera2d` | createCamera2d's options minus `viewport` (world?, min/maxZoom?, pivot?, deadZone?, panSpeed?, zoomSpeed?, rollSpeed?, followSpeed?, 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>` |
|
|
366
|
+
| `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 |
|
|
367
|
+
| `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? |
|
|
240
368
|
|
|
241
|
-
`SpriteLayer` owns the layer and
|
|
242
|
-
carrying the
|
|
243
|
-
yourself with `output`, then spread
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
`
|
|
369
|
+
`SpriteLayer` owns the layer and its own view, rendered as the built-in
|
|
370
|
+
`<texture>` leaf carrying the view's pointer handlers (opt out with
|
|
371
|
+
`events={false}`; compose yourself with `output`, then spread
|
|
372
|
+
`useSpriteLayer().viewport.handlers` onto your leaf; `output={false}` for
|
|
373
|
+
no own view at all). `useSpriteLayer()` returns `{ layer, parent,
|
|
374
|
+
viewport, pointer }` - the same shape as `useScene()` in `@solidrt/3d` -
|
|
375
|
+
where `parent` is the enclosing `<Group>`'s handle (null at the layer
|
|
376
|
+
root), so imperative `addSprite(layer, { parent })` mounts where the JSX
|
|
377
|
+
sits, `viewport` is the nearest view (the `<SpriteLayer>`'s own, or
|
|
378
|
+
inside a `<View2d>` that view), what a `<Camera2d>` drives, and
|
|
379
|
+
`pointer` that view's feed (the owner's `pointer` prop, null without
|
|
380
|
+
one); read under `output={false}` outside a `<View2d>` `viewport` throws. A FILL-mode
|
|
381
|
+
view is 1x1 until the first layout: `ref` fires at mount, before it, so
|
|
382
|
+
`viewport.width`/`height` read 1 there. Sprites added and positioned from
|
|
383
|
+
`ref` (the pool, the opening scene) are fine - the first layout resizes
|
|
384
|
+
the view before the first paint - but anything that needs the real size
|
|
385
|
+
(centering on the view) waits for `onLayout`, or works in a `designSize`
|
|
386
|
+
that is known up front. `Sprite` renders
|
|
248
387
|
nothing - it allocates a record through context and syncs props into it.
|
|
249
388
|
`GroupContext` is `createContext<SpriteGroup | null>(null)` on purpose: an
|
|
250
389
|
optional parent needs a non-undefined default, since Solid 2 throws on a
|
|
251
390
|
resolved `undefined` even when one was passed as the default.
|
|
252
391
|
|
|
253
392
|
Pointer events (dispatch.ts, both layer kinds): the DOM event model one
|
|
254
|
-
tree deeper, with the
|
|
393
|
+
tree deeper, with the VIEW as the root of the walk. Exact rotated-rect
|
|
255
394
|
containment, topmost sprite first; down/move/up/wheel dispatch on the hit
|
|
256
|
-
sprite, bubble through its enclosing groups and END AT THE
|
|
257
|
-
listeners (`
|
|
258
|
-
onTap
|
|
259
|
-
`event.sprite` null. `event.sprite` stays the hit,
|
|
260
|
-
handle whose handler runs (the
|
|
261
|
-
stops the walk, and a stopped DOWN claims the whole
|
|
262
|
-
move, up and tap never reach the
|
|
395
|
+
sprite, bubble through its enclosing groups and END AT THE VIEW's
|
|
396
|
+
listeners (`view.listen({...})`, the `<SpriteLayer>`/`<View2d>`
|
|
397
|
+
`onPointer*/onWheel/onTap` props) - over empty space the walk is the view
|
|
398
|
+
alone, with `event.sprite` null. `event.sprite` stays the hit,
|
|
399
|
+
`currentTarget` the handle whose handler runs (the view at the end),
|
|
400
|
+
`stopPropagation()` stops the walk, and a stopped DOWN claims the whole
|
|
401
|
+
press: that pointer's move, up and tap never reach the view either (the
|
|
402
|
+
chain still bubbles).
|
|
263
403
|
That one rule is how a sprite drags itself under a `<Camera2d>` without
|
|
264
404
|
the view panning: stop the down, own the captured moves. Capture is per
|
|
265
|
-
pointerId to the press target, the
|
|
405
|
+
pointerId to the press target, the view included (a drag from empty
|
|
266
406
|
space keeps delivering to the root as it crosses sprites; a drag from a
|
|
267
407
|
sprite keeps naming it with live coordinates). Enter/leave fire on the
|
|
268
408
|
sprite alone - a group never receives them - while the root sees every
|
|
@@ -290,7 +430,12 @@ hover, wheel and tap rules headless.
|
|
|
290
430
|
live app-long. Dispose atlases yourself (or let the reactive owner do it -
|
|
291
431
|
createAtlas registers with the owning scope like every core texture).
|
|
292
432
|
- `capacity` is a reservation, not a limit, on both layer kinds; reserve
|
|
293
|
-
realistically to skip the growth copies.
|
|
433
|
+
realistically to skip the growth copies. The pool idiom that fits a
|
|
434
|
+
game: reserve `capacity`, add every sprite at mount with `visible:
|
|
435
|
+
false`, then show/hide - a hidden sprite keeps its slot, so draw order
|
|
436
|
+
stays stable and a spawn is one setSprite, never an add. Keep a free
|
|
437
|
+
list of hidden handles; scanning the pool for one is the cost that
|
|
438
|
+
shows up first at a few hundred entities. On the records layer, do not
|
|
294
439
|
cache `layer.records` across addSprite - growth replaces the array and
|
|
295
440
|
a hoisted reference becomes a dead copy whose writes publish nothing.
|
|
296
441
|
`layer.withRecords(fn)` is the hoist-proof read; a bare `layer.records`
|
|
@@ -307,10 +452,18 @@ hover, wheel and tap rules headless.
|
|
|
307
452
|
mid-flight pose (what is on screen). Clearing the transition (null) keeps the
|
|
308
453
|
mid-flight pose on the node while the mirror still holds the old
|
|
309
454
|
target: the next setSprite write snaps to whatever it says.
|
|
310
|
-
- Node layer picking reads the index as of the last core flush;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
455
|
+
- Node layer picking reads the index as of the last core flush; every
|
|
456
|
+
query runs the layer's pending batch first, so write-then-query in one
|
|
457
|
+
tick is coherent. Producers moving nodes between flushes are one frame
|
|
458
|
+
stale to picking, like every query.
|
|
459
|
+
- The queries are per-body answers - a hit test, a marquee, a blast
|
|
460
|
+
radius, one mover call per character - not a collision broadphase. A
|
|
461
|
+
bullets-vs-crowd test at frame rate is hundreds of core queries per
|
|
462
|
+
frame, and it is simulation logic anyway, which lives in plain
|
|
463
|
+
TypeScript with no layer in sight (core's "keep the simulation out of
|
|
464
|
+
the renderer"): the app's own arrays, a uniform grid over them,
|
|
465
|
+
runnable headless. Tile-layer collision (tiles are baked records, not
|
|
466
|
+
nodes) is a solid-cell grid query of its own, not the sprite index.
|
|
314
467
|
- Records layer: record order is draw order: `removeSprite` shifts every
|
|
315
468
|
later sprite down one slot (copyWithin + index fixup, O(later
|
|
316
469
|
sprites)). Its flush publishes the WHOLE live prefix, not a dirty
|
|
@@ -340,14 +493,24 @@ hover, wheel and tap rules headless.
|
|
|
340
493
|
slot recycles. Do not read style truth from the buffer; getSprite reads
|
|
341
494
|
the JS mirror.
|
|
342
495
|
- `createImage` is the wrong loader for pixel-art atlases: it never forwards
|
|
343
|
-
sampler options, so it is always `filter: "linear"`. `createAtlas`
|
|
344
|
-
bytes
|
|
345
|
-
`
|
|
496
|
+
sampler options, so it is always `filter: "linear"`. `createAtlas` takes
|
|
497
|
+
decoded pixels (`createAtlas(decodeImage(bytes), { filter: "nearest" })`,
|
|
498
|
+
or pixels built in code) and passes the full sampler through (`filter`,
|
|
499
|
+
`wrap`, `mipmap`, `anisotropy`); the record it returns is what `grid` and
|
|
500
|
+
`namedFrames` slice.
|
|
501
|
+
- Frames that share an edge do not bleed: the fragment stage clamps every
|
|
502
|
+
sample into its frame, so a sprite at a fractional position (a scrolling
|
|
503
|
+
backdrop, an easing camera) never paints a line of the neighbouring cell,
|
|
504
|
+
and an edge-to-edge sheet needs no gutter and no shaved rect table. The
|
|
505
|
+
one case left is a `mipmap` atlas, whose mip texels average across cell
|
|
506
|
+
edges before sampling: pass `inset` to `grid`/`namedFrames` (2^k texels
|
|
507
|
+
keeps mip level k clean, losing that border) or pack a gutter as
|
|
508
|
+
`spacing`, until extrusion lands (okf/backlog/2d-atlas-extrude.md).
|
|
346
509
|
- Tint multiplies the sampled texel (`texture * tint`) and the pipeline
|
|
347
510
|
blends with `blend: "alpha"` in record order, the premultiplied composite.
|
|
348
511
|
The atlas is premultiplied because `decodeImage` premultiplies by default;
|
|
349
512
|
an atlas uploaded from straight-alpha pixels (`decodeImage(bytes, { alpha:
|
|
350
|
-
"straight" })`
|
|
513
|
+
"straight" })` into `createAtlas`) draws color under transparent texels
|
|
351
514
|
as opaque - the classic "keyed-out backdrop becomes a wash" symptom.
|
|
352
515
|
- Every rotation must agree on direction (clockwise, y-down):
|
|
353
516
|
`pointInSprite` in pick.ts with the vertex stage's `iRot`, and
|
package/README.md
CHANGED
|
@@ -7,12 +7,12 @@ texture element.
|
|
|
7
7
|
*Status: experimental. Expect API churn.*
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
|
-
import { render } from "@solidrt/core"
|
|
10
|
+
import { decodeImage, render } from "@solidrt/core"
|
|
11
11
|
import { createAtlas, grid, Sprite, SpriteLayer } from "@solidrt/2d"
|
|
12
12
|
import sheet from "./sheet.png" with { type: "binary" }
|
|
13
13
|
|
|
14
|
-
let atlas = createAtlas(sheet, { filter: "nearest" })
|
|
15
|
-
let frames = grid(
|
|
14
|
+
let atlas = createAtlas(decodeImage(sheet), { filter: "nearest" })
|
|
15
|
+
let frames = grid(atlas, 4, 4)
|
|
16
16
|
|
|
17
17
|
render(() => (
|
|
18
18
|
<window>
|
package/examples/README.md
CHANGED
|
@@ -12,27 +12,52 @@ copy one and adapt it.
|
|
|
12
12
|
one texture may be) baked into lazily-allocated chunks, flown over by a
|
|
13
13
|
ship-style camera (fixed screen pivot, the world panning and ROTATING
|
|
14
14
|
under it via the `<TileLayer>` camera prop - transform writes, never a
|
|
15
|
-
re-bake),
|
|
16
|
-
|
|
15
|
+
re-bake), seeded by ONE `setTiles` rect write over a Uint16Array of
|
|
16
|
+
frame indices (the layer's `frames` table; -1 where the world is
|
|
17
|
+
empty, and those chunks never allocate), and a timer editing tiles to
|
|
18
|
+
show that a `setTile` batch re-bakes only the chunks it touches.
|
|
17
19
|
- `anim.tsx` - frame animation with `createAnimation`: three sprites share
|
|
18
20
|
one looping clip (one clock, stepped by wall time independent of display
|
|
19
21
|
rate) and a one-shot clip holds its last frame and fires `onEnd`.
|
|
20
22
|
Self-asserting - watch the logs for ANIM-OK.
|
|
21
23
|
- `camera.tsx` - the 2d camera (`createCamera2d`) over a world larger than
|
|
22
|
-
the window,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`
|
|
24
|
+
the window, driving the layer's one view from an input map the app
|
|
25
|
+
binds (`camera2dBindings` over the view's pointer feed, bridged from
|
|
26
|
+
the view's root with `feedPointer`, and any pad): drag empty space to
|
|
27
|
+
pan with inertia, wheel/pinch to zoom about the pointer, tap empty
|
|
28
|
+
space to glide there (the view's `onTap` with `e.sprite` null), tap a
|
|
29
|
+
sprite to select it, drag a sprite to move it (the sprite stops its
|
|
30
|
+
down, so the camera never pans under it), F to follow a roaming sprite
|
|
31
|
+
through a dead zone, R to spin the view, Space to fit the world. The
|
|
32
|
+
VIEW's `handlers` spread onto its leaf; `update(dt)` from `onFrame` is
|
|
33
|
+
the only per-frame call. Debug commands `camera`, `mode`, `selected`
|
|
34
|
+
and `first` drive it headless.
|
|
31
35
|
- `pick.tsx` - the event model through the component face: exact
|
|
32
36
|
rotated-rect hit testing topmost-first, `onTap` (the dispatch's own
|
|
33
37
|
click, no slop bookkeeping), a claimed press dragging a sprite under a
|
|
34
38
|
`<Camera2d>` that pans empty space and zooms on wheel, the layer's own
|
|
35
39
|
`onTap` seeing misses with `e.sprite` null (and `tapCount` for double
|
|
36
40
|
taps), and removal through a signal so `<For>` unmounts the `<Sprite>`
|
|
37
|
-
and the layer recycles the slot.
|
|
38
|
-
|
|
41
|
+
and the layer recycles the slot. A corner `<View2d>` inset shows the
|
|
42
|
+
same sprites again under a `<Camera2d>` of its own (drag or wheel on
|
|
43
|
+
the inset moves only the inset's camera; a sprite tapped or dragged
|
|
44
|
+
there responds as in the main view). The `state` debug command reads
|
|
45
|
+
both cameras and the sprite positions back.
|
|
46
|
+
- `views.tsx` - layer views (`layer.createView`, the only way a layer
|
|
47
|
+
shows): the world rendered twice from ONE set of sprites - the
|
|
48
|
+
window-filling main view under a `createCamera2d` and a corner minimap
|
|
49
|
+
showing the whole world at the fit zoom, a stroked rect outlining the
|
|
50
|
+
main view rect through `map.project`. A tap on the minimap glides the main camera there (the
|
|
51
|
+
view's root listener, `e.x`/`e.y` in world pixels), and a tap on a
|
|
52
|
+
sprite in the minimap selects it as in the main view (the same sprite
|
|
53
|
+
handlers, the walk ending at the view). Debug commands `state`,
|
|
54
|
+
`camera`, `selected` and `first` drive it headless.
|
|
55
|
+
- `split-screen.tsx` - one `<SpriteLayer output={false}>` (sprites, no
|
|
56
|
+
leaf of its own) shown through two `<View2d>` panes filling the window
|
|
57
|
+
side by side, each with a `<Camera2d>` of its own following one of two
|
|
58
|
+
roaming sprites through a dead zone, so the two poses differ every
|
|
59
|
+
frame; wheel zooms a pane alone while its follow keeps tracking, a tap
|
|
60
|
+
on a sprite in either pane tints it in both (one sprite, two views).
|
|
61
|
+
Each pane's map binds `gamepad.next()`: the first pad to press any
|
|
62
|
+
button drives pane 0, the next pane 1. The `cameras` debug command
|
|
63
|
+
reads both poses back.
|
package/examples/anim.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The atlas is the core logo sliced 2x2 by grid(): a 4-frame "clip". Real
|
|
8
8
|
// art would slice a sheet the same way and pass a consecutive slice of the
|
|
9
9
|
// frames array per animation.
|
|
10
|
-
import { render } from "@solidrt/core"
|
|
10
|
+
import { decodeImage, render } from "@solidrt/core"
|
|
11
11
|
import { addSprite, createAnimation, createAtlas, createSpriteLayer, getSprite, grid } from "@solidrt/2d"
|
|
12
12
|
import type { Frame, SpriteHandle } from "@solidrt/2d"
|
|
13
13
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
@@ -21,12 +21,10 @@ const LOOP_FPS = 2
|
|
|
21
21
|
const SHOT_FPS = 5
|
|
22
22
|
|
|
23
23
|
function App() {
|
|
24
|
-
let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
|
|
25
|
-
let frames = grid(
|
|
26
|
-
let layer = createSpriteLayer(
|
|
27
|
-
|
|
28
|
-
label: "anim",
|
|
29
|
-
})
|
|
24
|
+
let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
|
|
25
|
+
let frames = grid(atlas, 2, 2)
|
|
26
|
+
let layer = createSpriteLayer(atlas.texture, { label: "anim" })
|
|
27
|
+
let view = layer.createView({ width: W, height: H, clearColor: [0.05, 0.05, 0.09, 1] })
|
|
30
28
|
|
|
31
29
|
// Three sprites share the looping clip - one clock steps all of them.
|
|
32
30
|
let looping = createAnimation(frames, LOOP_FPS)
|
|
@@ -78,7 +76,7 @@ function App() {
|
|
|
78
76
|
|
|
79
77
|
return (
|
|
80
78
|
<window alignItems="center" justifyContent="center">
|
|
81
|
-
<texture src={
|
|
79
|
+
<texture src={view.texture} width={W} height={H} />
|
|
82
80
|
</window>
|
|
83
81
|
)
|
|
84
82
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// 3. Pointer dispatch round-trips: a pointer at the projected screen
|
|
17
17
|
// point hits the sprite and reports its world coordinates.
|
|
18
18
|
// 4. Tile chunks bake with the pinned identity camera rotation.
|
|
19
|
-
import { onFrame, render } from "@solidrt/core"
|
|
19
|
+
import { decodeImage, onFrame, render } from "@solidrt/core"
|
|
20
20
|
import { readTexture } from "@solidrt/core/gpu"
|
|
21
21
|
import {
|
|
22
22
|
addSprite,
|
|
@@ -53,12 +53,14 @@ function rng(seed: number): () => number {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function App() {
|
|
56
|
-
let atlas = createAtlas(logoBytes, { label: "logo-atlas" })
|
|
57
|
-
let frames = grid(
|
|
58
|
-
let nodes = createSpriteLayer(
|
|
59
|
-
let records = createRecordLayer(
|
|
60
|
-
nodes.
|
|
61
|
-
records.
|
|
56
|
+
let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" })
|
|
57
|
+
let frames = grid(atlas, 2, 2)
|
|
58
|
+
let nodes = createSpriteLayer(atlas.texture, { label: "cam-nodes" })
|
|
59
|
+
let records = createRecordLayer(atlas.texture, { label: "cam-records" })
|
|
60
|
+
let nodeView = nodes.createView({ width: W, height: H, label: "cam-nodes" })
|
|
61
|
+
let recordView = records.createView({ width: W, height: H, label: "cam-records" })
|
|
62
|
+
nodeView.setCamera(CAM)
|
|
63
|
+
recordView.setCamera(CAM)
|
|
62
64
|
|
|
63
65
|
// The same deterministic population on both layers, kept inside world
|
|
64
66
|
// 20..100 so its reach (~145 with rotation) stays clear of both sample
|
|
@@ -103,8 +105,8 @@ function App() {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
let shaderCheck = () => {
|
|
106
|
-
let img = readTexture(
|
|
107
|
-
let n =
|
|
108
|
+
let img = readTexture(nodeView.texture)
|
|
109
|
+
let n = nodeView.oversample
|
|
108
110
|
let [sx, sy] = projectCamera(CAM, 320, 320)
|
|
109
111
|
check(sx > 8 && sx < W - 8 && sy > 8 && sy < H - 8, `projected probe on screen (${sx.toFixed(1)}, ${sy.toFixed(1)})`)
|
|
110
112
|
check(alphaAt(img, sx, sy, n) > 0, "probe center opaque at its PROJECTED pixel")
|
|
@@ -118,8 +120,8 @@ function App() {
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
let pixelParity = () => {
|
|
121
|
-
let a = readTexture(
|
|
122
|
-
let b = readTexture(
|
|
123
|
+
let a = readTexture(nodeView.texture)
|
|
124
|
+
let b = readTexture(recordView.texture)
|
|
123
125
|
if (a.width !== b.width || a.height !== b.height) {
|
|
124
126
|
check(false, `sizes differ: ${a.width}x${a.height} vs ${b.width}x${b.height}`)
|
|
125
127
|
return
|
|
@@ -146,7 +148,7 @@ function App() {
|
|
|
146
148
|
let [sx, sy] = projectCamera(CAM, 320, 320)
|
|
147
149
|
// Synthesize the element event the built-in leaf would deliver (layout
|
|
148
150
|
// null: localX/localY are layer pixels already).
|
|
149
|
-
|
|
151
|
+
nodeView.handlers.onPointerDown({
|
|
150
152
|
localX: sx,
|
|
151
153
|
localY: sy,
|
|
152
154
|
pointerId: 1,
|
|
@@ -191,8 +193,8 @@ function App() {
|
|
|
191
193
|
|
|
192
194
|
return (
|
|
193
195
|
<window flexDirection="row" alignItems="center" justifyContent="center" gap={8}>
|
|
194
|
-
<texture src={
|
|
195
|
-
<texture src={
|
|
196
|
+
<texture src={nodeView.texture} width={W} height={H} />
|
|
197
|
+
<texture src={recordView.texture} width={W} height={H} />
|
|
196
198
|
</window>
|
|
197
199
|
)
|
|
198
200
|
}
|