@solidrt/2d 0.0.66 → 0.0.68
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 +246 -69
- package/README.md +33 -18
- package/examples/README.md +29 -0
- package/examples/anim.tsx +1 -1
- package/examples/atlases.tsx +151 -0
- package/examples/camera-probe.tsx +7 -7
- package/examples/camera.tsx +2 -3
- package/examples/markers.tsx +185 -0
- package/examples/parity.tsx +2 -2
- package/examples/pick.tsx +1 -1
- package/examples/split-screen.tsx +1 -1
- package/examples/springs.tsx +1 -1
- package/examples/sprites.tsx +1 -1
- package/examples/text.tsx +143 -0
- package/examples/tiles.tsx +1 -1
- package/examples/views.tsx +3 -3
- package/package.json +3 -3
- package/src/atlas.ts +75 -12
- package/src/camera2d.ts +47 -34
- package/src/collision.ts +20 -0
- package/src/components/camera2d.tsx +6 -13
- package/src/components/group.tsx +8 -11
- package/src/components/index.ts +3 -1
- package/src/components/sprite-layer.tsx +51 -14
- package/src/components/sprite.tsx +19 -9
- package/src/components/text2d.tsx +77 -0
- package/src/components/tile-layer.tsx +11 -5
- package/src/components/view2d.tsx +2 -0
- package/src/dispatch.ts +16 -4
- package/src/extrude.ts +156 -0
- package/src/font.ts +207 -0
- package/src/frames.ts +65 -55
- package/src/index.ts +37 -18
- package/src/layer.ts +273 -45
- package/src/pack.ts +70 -0
- package/src/pick.ts +25 -1
- package/src/records.ts +286 -121
- package/src/shaders.ts +224 -77
- package/src/shots.ts +23 -9
- package/src/text-layout.ts +163 -0
- package/src/text.ts +240 -0
- package/src/tiles.ts +111 -71
- package/src/views.ts +21 -13
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @solidrt/2d - agent notes
|
|
2
2
|
|
|
3
|
-
An instanced sprite layer above `@solidrt/core/gpu`:
|
|
4
|
-
quads in ONE draw call, composited into
|
|
3
|
+
An instanced sprite layer above `@solidrt/core/gpu`: a layer's atlases
|
|
4
|
+
bound together, N quads from any of them in ONE draw call, composited into
|
|
5
|
+
the app as an ordinary `<texture>`
|
|
5
6
|
leaf. The live layer backs every sprite with a SPATIAL ARENA node (never a
|
|
6
7
|
rendertree element - `d-texture` sprites are the right tool up to the low
|
|
7
8
|
thousands; measured ~0.65us paint and ~15KB memory per NODE, and every
|
|
@@ -15,6 +16,7 @@ Contents:
|
|
|
15
16
|
- [The model](#the-model)
|
|
16
17
|
- [Three faces](#three-faces)
|
|
17
18
|
- [A layer renders through its views](#a-layer-renders-through-its-views)
|
|
19
|
+
- [Atlases and frames](#atlases-and-frames)
|
|
18
20
|
- [Pose and style slots](#pose-and-style-slots)
|
|
19
21
|
- [Fixed instance slots](#fixed-instance-slots)
|
|
20
22
|
- [Growth](#growth)
|
|
@@ -23,7 +25,7 @@ Contents:
|
|
|
23
25
|
- [Groups](#groups)
|
|
24
26
|
- [Visibility](#visibility)
|
|
25
27
|
- [Flips](#flips)
|
|
26
|
-
- [Mutations batch to
|
|
28
|
+
- [Mutations batch to the frame](#mutations-batch-to-the-frame)
|
|
27
29
|
- [The records layer](#the-records-layer)
|
|
28
30
|
- [Layer space](#layer-space)
|
|
29
31
|
- [The camera](#the-camera)
|
|
@@ -33,6 +35,7 @@ Contents:
|
|
|
33
35
|
- [Retargeted motion](#retargeted-motion)
|
|
34
36
|
- [Frame-rate motion](#frame-rate-motion)
|
|
35
37
|
- [Frame animation](#frame-animation)
|
|
38
|
+
- [Text runs](#text-runs)
|
|
36
39
|
- [Pure pieces](#pure-pieces)
|
|
37
40
|
- [The baked tile layer (tiles.ts)](#the-baked-tile-layer-tilests)
|
|
38
41
|
- [Chunks](#chunks)
|
|
@@ -48,6 +51,7 @@ Contents:
|
|
|
48
51
|
- [Slots, capacity and order](#slots-capacity-and-order)
|
|
49
52
|
- [Nodes, transitions and queries](#nodes-transitions-and-queries)
|
|
50
53
|
- [Cameras and rotation](#cameras-and-rotation)
|
|
54
|
+
- [Text runs](#text-runs-1)
|
|
51
55
|
- [Tile layer](#tile-layer)
|
|
52
56
|
|
|
53
57
|
## The model
|
|
@@ -73,15 +77,49 @@ Godot World2D only through Viewports, and there is no privileged first
|
|
|
73
77
|
one. The window view, the minimap, two split-screen panes are the same
|
|
74
78
|
`ViewHandle`. Details under Views below.
|
|
75
79
|
|
|
80
|
+
### Atlases and frames
|
|
81
|
+
|
|
82
|
+
A layer draws from the ATLASES it declares at creation
|
|
83
|
+
(`createSpriteLayer([a, b], ...)`, `<SpriteLayer atlases={[a, b]}>`, the
|
|
84
|
+
record and tile layers alike): `createAtlas` records, or
|
|
85
|
+
`{ texture, width, height }` literals for a render target or a camera
|
|
86
|
+
frame, up to `limits.maxTextureUnits` of them (16 on every GLES 3.0
|
|
87
|
+
device), of any sizes and sampler states. They bind together as ONE
|
|
88
|
+
draw per view - the multi-texture batch of PixiJS and Phaser, where
|
|
89
|
+
Unity and Godot break the batch on every texture change - so sprites
|
|
90
|
+
from several sheets interleave freely in draw order, key order
|
|
91
|
+
included, and a tile world bakes from several tilesets in one pass. A
|
|
92
|
+
`Frame` carries its texture (`{ texture, u0, v0, u1, v1 }`, stamped by
|
|
93
|
+
`grid`/`namedFrames`/`fullFrame`; PixiJS's Texture, Phaser's Frame,
|
|
94
|
+
Unity's Sprite and Godot's AtlasTexture pair the texture with the rect
|
|
95
|
+
the same way), the record stores that texture's index in the layer's
|
|
96
|
+
list (`layer.atlases`, what a raw record writer stores), and the
|
|
97
|
+
fragment stage is GENERATED per layer with one sampler per atlas, the
|
|
98
|
+
flat per-instance index picking it: derivatives are taken from the
|
|
99
|
+
unclamped uv before the branch and the tap is `textureGrad`, so mip
|
|
100
|
+
selection is the quad's own and well defined inside the branch. A frame
|
|
101
|
+
whose texture the layer did not declare THROWS at the write (the
|
|
102
|
+
wrong-sheet bug, caught instead of drawn); the frame left out of
|
|
103
|
+
`addSprite` is the whole FIRST atlas. An atlas declared `sdf: { range }`
|
|
104
|
+
(a sprite font's msdf atlas, or any distance field you upload) is
|
|
105
|
+
decoded in its branch instead of sampled as colour: the median of rgb is
|
|
106
|
+
the edge, anti-aliased over one screen pixel at whatever zoom, the true
|
|
107
|
+
field in alpha grows a per-sprite outline (text runs set it; a colour
|
|
108
|
+
atlas ignores the field). Fixed at creation like the list itself.
|
|
109
|
+
|
|
76
110
|
### Pose and style slots
|
|
77
111
|
|
|
78
|
-
Node layer ownership split, two instance-buffer slots on one pipeline
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
Node layer ownership split, two instance-buffer slots on one pipeline
|
|
113
|
+
(the layer's, blended with its `blend` option, "alpha" unless it says
|
|
114
|
+
"add", "multiply" or "none" at creation - two blend modes are two
|
|
115
|
+
layers): slot 0 is the POSE buffer `[x, y, angle, sx, sy]` written ONLY
|
|
116
|
+
by the core (each sprite node's Pose2D record sink; one coalesced buffer
|
|
117
|
+
write per flush however many nodes moved), slot 1 the STYLE buffer
|
|
118
|
+
`[u0, v0, u1, v1, tint rgba, renderOrder, minScreenPx, maxScreenPx,
|
|
119
|
+
atlas, outline rgb, outlineWidth]` (16 floats), JS-owned, published
|
|
120
|
+
through the zero-copy write lease. NEVER
|
|
121
|
+
write the pose buffer from JS - the core's staging mirror owns it and
|
|
122
|
+
will overwrite.
|
|
85
123
|
|
|
86
124
|
### Fixed instance slots
|
|
87
125
|
|
|
@@ -108,6 +146,30 @@ unordered. Every query passes the core a filter with the layer's root
|
|
|
108
146
|
node (sprites and groups without a parent hang off it), so the arena
|
|
109
147
|
being shared with e.g. a 3d scene costs nothing in JS.
|
|
110
148
|
|
|
149
|
+
The screen-size clamp bends this one way. A sprite with `minScreenPx`
|
|
150
|
+
and/or `maxScreenPx` draws its smaller axis within those VIEW pixels
|
|
151
|
+
(scaled uniformly, in the vertex stage over the core-written pose, so
|
|
152
|
+
after group scale and mid-transition; core's `screenSizeScale`, the
|
|
153
|
+
same function @solidrt/3d's sprite material uses), which differs from
|
|
154
|
+
its index column once the zoom carries it past a bound - larger under
|
|
155
|
+
a floor, smaller under a ceiling; equal bounds hold a constant screen
|
|
156
|
+
size. So `pick(x, y, zoom)` takes the camera zoom the clamps are
|
|
157
|
+
measured at (default 1): while no sprite on the layer is clamped it is
|
|
158
|
+
the raycast above; once one is, the candidates are the columns
|
|
159
|
+
containing the point (the ray) plus, when a floor is on, those within
|
|
160
|
+
the widest floor's reach of it (`floorReach` in pick.ts over the
|
|
161
|
+
clamped sprites, recomputed on the next pick after one leaves or
|
|
162
|
+
shrinks; the box overlap is a SURFACE contact in the core, so a box
|
|
163
|
+
inside a big column finds nothing, which is why the ray stays in the
|
|
164
|
+
union), and each is tested exactly against its drawn rect from the
|
|
165
|
+
node's world matrix (`screenSizeScale` + `pointInSprite`). The pointer
|
|
166
|
+
walk and `view.pick(x, y)` pass the view camera's zoom; a bare
|
|
167
|
+
`layer.pick(x, y)` is the unzoomed world. `pickRect`, overlap, sweep and
|
|
168
|
+
the mover stay world-space body queries and never see a clamp. The
|
|
169
|
+
record layer's JS walk applies the same scale per record. The pair is
|
|
170
|
+
validated against the STORED bounds before any write (a ceiling set
|
|
171
|
+
alone may not drop below the floor already there).
|
|
172
|
+
|
|
111
173
|
### Spatial queries
|
|
112
174
|
|
|
113
175
|
Spatial queries (`tests/collision.test.tsx` pins the contract): the
|
|
@@ -119,7 +181,8 @@ every sprite a volume touches with its deepest contact `{ sprite,
|
|
|
119
181
|
point, normal, depth }`, unordered; `layer.sweep(volume, dx, dy,
|
|
120
182
|
opts?)` every sprite the moving volume first touches `{ sprite, time,
|
|
121
183
|
point, normal }`, earliest first; `layer.moveAndSlide(volume, dx, dy,
|
|
122
|
-
opts?)` the
|
|
184
|
+
opts?)` (and the free `moveAndSlide(layer, ...)` spelling of it, as
|
|
185
|
+
@solidrt/3d's) the character mover over them, one core call per body per
|
|
123
186
|
frame (the depenetration, slide and floor-snap loop runs in the spatial
|
|
124
187
|
core, the same one @solidrt/3d's mover uses), returning `{ motion,
|
|
125
188
|
floor, wall, ceiling, hits }` with `up` defaulting to [0, -1] (y-down).
|
|
@@ -170,19 +233,41 @@ flip). The flags live on the Sprite and re-apply to every later frame
|
|
|
170
233
|
write; `getSprite` returns the frame un-mirrored plus the flags. Raw
|
|
171
234
|
`records` writers swap u0/u1 themselves.
|
|
172
235
|
|
|
173
|
-
### Mutations batch to
|
|
236
|
+
### Mutations batch to the frame
|
|
174
237
|
|
|
175
|
-
Mutations batch
|
|
176
|
-
|
|
177
|
-
|
|
238
|
+
Mutations batch: style lease publish + count setDraw + `spatial.flush()`
|
|
239
|
+
go out once, in the publish pass of core's `onBeforeRender` ahead of the
|
|
240
|
+
frame's paint, so a write made anywhere in the frame's JS (onFrame, an
|
|
241
|
+
effect, an onLayout handler) is in that frame's picture, never the next;
|
|
242
|
+
a write made outside a frame (an event handler, a timer) publishes at
|
|
243
|
+
the microtask. No mutation, no publish, no frame: a static layer costs
|
|
244
|
+
zero, the same demand-gate story as the rest of the platform.
|
|
178
245
|
|
|
179
246
|
### The records layer
|
|
180
247
|
|
|
181
|
-
The records layer (`createRecordLayer`) keeps the old model whole:
|
|
248
|
+
The records layer (`createRecordLayer`) keeps the old model whole: 20
|
|
182
249
|
JS-owned floats per sprite `[cx, cy, w, h, u0, v0, u1, v1, rot, tint
|
|
183
|
-
rgba
|
|
184
|
-
|
|
185
|
-
|
|
250
|
+
rgba, minScreenPx, maxScreenPx, atlas, outline rgb, outlineWidth]`
|
|
251
|
+
(`INSTANCE_FLOATS`; `atlas` is the frame's texture as its index in
|
|
252
|
+
`layer.atlases`; the outline is read by a distance-field atlas only), draw order =
|
|
253
|
+
insertion order (or key order with `orderBy` - see below), remove
|
|
254
|
+
shifts, raw writes, JS pick walk. The raw path speaks @solidrt/3d's
|
|
255
|
+
record-mesh verbs, one dimension down: `records(layer)` is the mirror
|
|
256
|
+
(read it at use time - growth replaces it), `updateRecords(layer, {
|
|
257
|
+
first?, count? })` publishes a RANGE of it at the flush (the whole
|
|
258
|
+
mirror by default; ten moved records of ten thousand cost ten - one
|
|
259
|
+
`writeBuffer` at the range's byte offset, clipped to the live sprites;
|
|
260
|
+
under `orderBy` the drawn prefix whole through the lease instead, since
|
|
261
|
+
the core gathers it into key order during the copy), and
|
|
262
|
+
`setRecordCount(layer, n)` dials the drawn prefix - the first n live
|
|
263
|
+
sprites draw and pick, the rest stay live but undrawn and unpicked,
|
|
264
|
+
and the dial stays in force as sprites come and go (write ahead, dial
|
|
265
|
+
after; `layer.count` is the live total regardless). The sprite verbs
|
|
266
|
+
mark their own record, so a `setSprite` here publishes one record, not
|
|
267
|
+
the prefix. The POPULATION verbs differ by kind and stay so: a record
|
|
268
|
+
mesh's records are opaque data (`setRecords` copies in and grows), a
|
|
269
|
+
record layer's are sprites (`addSprite`/`destroySprite`), so the copy-in
|
|
270
|
+
here is `records(layer).set(src)` then `updateRecords(layer)`. It is the
|
|
186
271
|
escape hatch for motion only JS can compute at scale (measured 30k
|
|
187
272
|
sprites: 12.9ms raw records vs 30.8ms via setSprite; both figures are
|
|
188
273
|
the WRITE path only - whatever computes the motion is excluded and is
|
|
@@ -190,7 +275,9 @@ usually the dominant cost, e.g. a 24k-particle sim measured ~25ms with
|
|
|
190
275
|
a near-free publish) - the axis is
|
|
191
276
|
WHERE MOTION IS COMPUTED, not retained-vs-dynamic. The sprite functions
|
|
192
277
|
(addSprite/setSprite/getSprite/destroySprite) work on both layer kinds;
|
|
193
|
-
record sprites have `node: null` and no groups.
|
|
278
|
+
record sprites have `node: null` and no groups. `tests/records.test.tsx`
|
|
279
|
+
pins the range, the dial, growth and the ordered publish on painted
|
|
280
|
+
frames.
|
|
194
281
|
|
|
195
282
|
### Layer space
|
|
196
283
|
|
|
@@ -224,8 +311,8 @@ range, the contain clamp) apply to pose plus lanes and move the pose,
|
|
|
224
311
|
and the PUSH writes the final camera. `pose()` is the pose alone;
|
|
225
312
|
`camera()` the final camera as the layers received it.
|
|
226
313
|
|
|
227
|
-
Camera control: `createCamera2d(view | views, {
|
|
228
|
-
|
|
314
|
+
Camera control: `createCamera2d(view | views, { world?: { width,
|
|
315
|
+
height, damping? }, min/maxZoom?,
|
|
229
316
|
pivot?, follow?: { damping?, deadZone?, hardLimits?, lookahead? },
|
|
230
317
|
offset?, panSpeed?, zoomSpeed?, rollSpeed?, damping?, inertia?, x?, y?,
|
|
231
318
|
zoom?, rotation? })` - Godot's Camera2D and Three's MapControls in one
|
|
@@ -241,9 +328,13 @@ at the pivot", the pivot a viewport fraction defaulting to the center,
|
|
|
241
328
|
so `camera().x/y` is the view center and glideTo/follow land there
|
|
242
329
|
(without a `world`, the default pose puts world 0,0 at the pivot: a
|
|
243
330
|
fill layer that wants world = screen at rest takes `pivot: { x: 0, y:
|
|
244
|
-
0 }`). The
|
|
245
|
-
|
|
246
|
-
|
|
331
|
+
0 }`). The pivot is the point the control zooms and rolls about - the
|
|
332
|
+
3d orbit control's target, which that control holds at the view centre
|
|
333
|
+
and re-seats in world space (`setPivot`); this one places it on screen.
|
|
334
|
+
The first argument is anything with a view's `setCamera` and `size()`
|
|
335
|
+
(a view of a sprite layer, of a record layer, several at once - one
|
|
336
|
+
camera over a scene's layers, the FIRST one's size the viewport - or a
|
|
337
|
+
`{ setCamera, size }` pair over a signal feeding `<TileLayer camera>`).
|
|
247
338
|
|
|
248
339
|
**Follow.** `follow(x, y)` chases a world point through the framing in
|
|
249
340
|
`follow` (core's camera-control, shared with the 3d orbit camera):
|
|
@@ -277,7 +368,7 @@ view - a wide camera and a close one, each with its own control - and
|
|
|
277
368
|
`activate(name, { blend? })` makes the highest-priority enabled shot
|
|
278
369
|
live, blending the view from the current output over `blend` seconds
|
|
279
370
|
(default 0.5; 0 cuts; x/y/rotation/pivot linear, the zoom in log
|
|
280
|
-
space, `
|
|
371
|
+
space, `mixCamera`); `deactivate` falls back by priority. A live
|
|
281
372
|
shot's pushes reach the view at once; a blend needs `update(dt)` while
|
|
282
373
|
`active()`, the controls' rule. `live()` and `camera()` read the state.
|
|
283
374
|
A switch mid-blend starts from the output of that moment, so a quick
|
|
@@ -309,7 +400,8 @@ gesture measured (viewport heights per second from the pointer feed;
|
|
|
309
400
|
the control has no estimator of its own). The
|
|
310
401
|
verbs, each pushing the pose at once: `panBy(dx, dy)` screen pixels,
|
|
311
402
|
`zoomAt(sx, sy, factor, { glide? })`, `rollBy(angle)`, `set(pose)`,
|
|
312
|
-
`glideTo(x
|
|
403
|
+
`glideTo({ x?, y?, zoom?, rotation? })` (eased, the fields given, a
|
|
404
|
+
rotation glide included - the 3d controls' glideTo(pose)),
|
|
313
405
|
`fit`, `follow`/`unfollow`, `shake`, `interrupt`, `release([vx, vy])`
|
|
314
406
|
(screen pixels per second of content travel). An input
|
|
315
407
|
map (core AGENTS.md) drives the axes by name and the APP binds devices
|
|
@@ -345,7 +437,7 @@ root with `e.sprite` null is "tap on empty space"). `<Camera2d
|
|
|
345
437
|
input={input}>` inside `<SpriteLayer>` is all of that wired through
|
|
346
438
|
context, the 3d `<OrbitCamera>` shape: options read at mount, driving
|
|
347
439
|
the nearest view (the `<SpriteLayer>`'s own or the enclosing
|
|
348
|
-
`<View2d>`),
|
|
440
|
+
`<View2d>`), that view's `size()` the viewport, the map's
|
|
349
441
|
`pan`/`zoom`/`roll` actions (or the names in `actions`) driving the
|
|
350
442
|
axes, frames only while `active()`. A view's feed normalizes a drag by
|
|
351
443
|
the leaf's own box, so a leaf under a designSize fit pans and zooms
|
|
@@ -392,9 +484,10 @@ entry ordered), every other entry reads them sorted. Growth, the
|
|
|
392
484
|
instance count and the layer tint fan out to every view; the camera
|
|
393
485
|
and the viewport are the view's own params, so a view costs no
|
|
394
486
|
per-frame JS beyond its camera writes. The `ViewHandle` is the
|
|
395
|
-
viewport contract (texture,
|
|
396
|
-
oversample/setOversample, setCamera/camera/project/unproject,
|
|
397
|
-
listen/handlers/handlersFor, dispose -
|
|
487
|
+
viewport contract (texture, size/setSize,
|
|
488
|
+
oversample/setOversample, setCamera/camera/project/unproject, pick (the
|
|
489
|
+
layer's, through this camera), listen/handlers/handlersFor, dispose -
|
|
490
|
+
views also die with the layer);
|
|
398
491
|
the sprites stay the layer's (`addSprite`, `pick`, `setTint`,
|
|
399
492
|
`createView`, `dispose`). Pointer events on a view leaf run the
|
|
400
493
|
dispatch with the VIEW's camera undone over the layer's pick, the view
|
|
@@ -484,12 +577,46 @@ attach via `ref` and leave the `frame` prop off - the clip owns that
|
|
|
484
577
|
field (the prop effect passes absent props as undefined, which setSprite
|
|
485
578
|
keeps, so other props stay reactive).
|
|
486
579
|
|
|
580
|
+
### Text runs
|
|
581
|
+
|
|
582
|
+
Text IN the layer's world - a label under a sprite, a name on a map, a
|
|
583
|
+
damage number - is a text run: a group of glyph sprites drawn from a
|
|
584
|
+
SPRITE FONT's atlas (`createSpriteFont`, `addText`/`setText`/
|
|
585
|
+
`destroyText`, `<Text2d>`; `examples/text.tsx`). The font is the
|
|
586
|
+
runtime's own glyph engine (`flux:font`): it shapes the text (kerning is
|
|
587
|
+
the engine's, carets are one pass), makes glyph cells on a worker thread
|
|
588
|
+
and packs them into one atlas texture that grows as glyphs arrive. The
|
|
589
|
+
atlas is one more entry in the layer's list - `createSpriteLayer([art,
|
|
590
|
+
font.atlas])` - so labels draw in the one draw with the sprites they
|
|
591
|
+
belong to and pan, zoom, sort (`orderBy`), pick and transition as
|
|
592
|
+
sprites; a run's pose is its group's (`run.group`), where glyph pointer
|
|
593
|
+
events bubble. Two cell kinds: "msdf" (the default: one atlas sharp at
|
|
594
|
+
every zoom, the fragment stage decoding `Atlas.sdf`; the per-run
|
|
595
|
+
`outline` draws only here) and "mask" (coverage at the face's exact size,
|
|
596
|
+
for 1:1 drawing). Text
|
|
597
|
+
as elements is still the right tool up to hundreds of labels: `d-text`
|
|
598
|
+
under a `<view>` carrying the camera transform (what `<TileLayer>` does
|
|
599
|
+
for its chunks) costs no JS per label on a camera move and is rasterized
|
|
600
|
+
at the real scale; runs are for thousands, for in-layer draw order, and
|
|
601
|
+
for a zoom animation that would re-rasterize every label per frame.
|
|
602
|
+
|
|
603
|
+
Layout is `layoutText` (text-layout.ts, pure): `\n` breaks, `maxWidth`
|
|
604
|
+
wraps with core's greedy rule, `align` per line, `anchor`
|
|
605
|
+
(start/middle/end, as d-text) and `anchorY` (top/middle/baseline/bottom)
|
|
606
|
+
place the run's point, `letterSpacing` and `lineHeight` as named. The
|
|
607
|
+
run reports `width`, `height`, `ascent` and `lines`. A run's `fontSize`
|
|
608
|
+
defaults to the face's; glyphs scale their cells by `fontSize /
|
|
609
|
+
font.size`.
|
|
610
|
+
|
|
487
611
|
### Pure pieces
|
|
488
612
|
|
|
489
|
-
frames.ts,
|
|
490
|
-
|
|
491
|
-
math,
|
|
492
|
-
|
|
613
|
+
frames.ts, extrude.ts (the mip-gutter repack, tests/extrude.test.ts),
|
|
614
|
+
pack.ts (the shelf packer, tests/pack.test.ts), pick.ts, camera.ts,
|
|
615
|
+
camera-motion.ts, dispatch.ts, oversample-math.ts, tiles-math.ts (the
|
|
616
|
+
tile grid's chunk and slot math, tests/tiles.test.ts) and text-layout.ts
|
|
617
|
+
(glyph placement over prepared units, tests/text-layout.test.ts) are pure
|
|
618
|
+
(no GPU imports) BY DESIGN so they can be checked headless; keep them
|
|
619
|
+
that way.
|
|
493
620
|
|
|
494
621
|
## The baked tile layer (tiles.ts)
|
|
495
622
|
|
|
@@ -497,17 +624,19 @@ can be checked headless; keep them that way.
|
|
|
497
624
|
|
|
498
625
|
Static 2D bulk as a few quads: on tiled GPUs the budget is primitive count
|
|
499
626
|
(core agents/performance.md), so a 100x100 tile world must not be 10,000
|
|
500
|
-
quads per frame. `createTileLayer(cols, rows, tileW, tileH,
|
|
501
|
-
the world into CHUNKED `render: "manual"` targets (default ~512px
|
|
502
|
-
per chunk, `chunkTiles` to tune), each chunk a
|
|
503
|
-
pipeline (shaders.ts
|
|
627
|
+
quads per frame. `createTileLayer(cols, rows, tileW, tileH, atlases)`
|
|
628
|
+
bakes the world into CHUNKED `render: "manual"` targets (default ~512px
|
|
629
|
+
of tiles per chunk, `chunkTiles` to tune), each chunk a manual target
|
|
630
|
+
over the layer's ONE sprite pipeline (shaders.ts, compiled once per
|
|
631
|
+
layer for its atlases) with FIXED record slots - an empty tile is a
|
|
504
632
|
zero-size quad, instance count is constant per chunk. Records hold WORLD
|
|
505
633
|
pixel coordinates; each chunk target's `uCamera` is its pixel origin, so
|
|
506
634
|
the shared vertex stage does the chunk-local mapping. Chunks allocate on
|
|
507
635
|
the first `setTile` that reaches them - an empty chunk costs nothing, a
|
|
508
636
|
sparse world is bounded by its content, and world size is bounded by
|
|
509
|
-
memory, not `maxTextureSize`. `setTile` batches to
|
|
510
|
-
|
|
637
|
+
memory, not `maxTextureSize`. `setTile` batches to the frame's publish
|
|
638
|
+
pass (the microtask, outside a frame), whose flush publishes and re-bakes
|
|
639
|
+
ONLY dirty chunks. After that the layer is static
|
|
511
640
|
textures: zero per-frame cost however many tiles exist.
|
|
512
641
|
|
|
513
642
|
### Scrolling and the camera
|
|
@@ -567,16 +696,17 @@ on approach, evict) - okf/backlog/2d-baked-layers.md.
|
|
|
567
696
|
|
|
568
697
|
| Component | Props |
|
|
569
698
|
|---|---|
|
|
570
|
-
| `SpriteLayer` |
|
|
571
|
-
| `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? |
|
|
699
|
+
| `SpriteLayer` | atlases (Atlas[], the sheets bound as one draw; mount-fixed) or layer (an existing createSpriteLayer handle to adopt: the component shows and populates it, keeps only the view and the leaf as its own and leaves the layer's lifetime to its maker, so `<Sprite>`/`<Text2d>` sit over an imperative layer; the creation props atlases/capacity/blend/orderBy then throw), capacity?, blend? (core gpu BlendMode, "alpha" default, "add" for glows; mount-fixed), tint? ([r,g,b,a] 0..1, over the whole layer in every view), orderBy?, stagger? (ms, the layer root's enter/exit spacing), label?, ref?(layer) - the layer; plus its OWN VIEW, unless `output={false}`: width?, height? (view pixels - both, or neither = FILL: the leaf lays out at 100% of its sized parent and the view follows its box, so view pixels are the leaf's own coordinates; mount-fixed, a function `output` requires explicit sizes, matching `<Scene>` in @solidrt/3d), clearColor?, camera?, oversample?, maxOversample?, viewRef?(view), output? (a function composes the leaf from the texture id; `false` = no own view, the layer shows only through `<View2d>` children and the view props throw), events?, pointer? (a createPointerFeed fed from the view's root, what a map over this view binds; `useSpriteLayer().pointer` inside), onPointer{Down,Move,Up}?, onWheel?, onTap? (the view's root: `event.sprite` is the hit sprite or null over empty space) |
|
|
700
|
+
| `Sprite` | x, y (center; local to the enclosing `<Group>`), w, h, frame?, rotation? (radians, clockwise), tint? ([r,g,b,a] 0..1), minScreenPx?, maxScreenPx? (the screen-size clamp on the smaller axis, view pixels; 0 = off; equal = constant size), visible?, transition?, onPointer{Down,Move,Up,Enter,Leave}?, onWheel?, onTap?, ref? |
|
|
572
701
|
| `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? |
|
|
573
|
-
| `Camera2d` | createCamera2d's options
|
|
702
|
+
| `Camera2d` | createCamera2d's options (world?, min/maxZoom?, pivot?, follow?, offset?, panSpeed?, zoomSpeed?, rollSpeed?, damping?, inertia?, x?, y?, zoom?, rotation?), 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; its `size()` the viewport) from the map, nothing else; read at mount; throws under `output={false}` outside a `<View2d>` |
|
|
574
703
|
| `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 |
|
|
575
|
-
| `TileLayer` | cols, rows, tileW, tileH,
|
|
704
|
+
| `TileLayer` | cols, rows, tileW, tileH, atlases (Atlas[], the tilesets bound as one bake; mount-fixed), frames? (the tileset `setTiles` indices name), chunkClearColor?, blend? (the bake's blend mode, "alpha" default; mount-fixed), 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? |
|
|
576
705
|
|
|
577
706
|
### SpriteLayer and useSpriteLayer
|
|
578
707
|
|
|
579
|
-
`SpriteLayer` owns the layer
|
|
708
|
+
`SpriteLayer` owns the layer (or adopts an existing one through `layer`,
|
|
709
|
+
owning only what follows) and its own view, rendered as the built-in
|
|
580
710
|
`<texture>` leaf carrying the view's pointer handlers (opt out with
|
|
581
711
|
`events={false}`, which throws at mount together with a `pointer` feed -
|
|
582
712
|
the feed would listen at a root no event reaches; the same on `<View2d>`;
|
|
@@ -591,7 +721,7 @@ inside a `<View2d>` that view), what a `<Camera2d>` drives, and
|
|
|
591
721
|
`pointer` that view's feed (the owner's `pointer` prop, null without
|
|
592
722
|
one); read under `output={false}` outside a `<View2d>` `viewport` throws. A FILL-mode
|
|
593
723
|
view is 1x1 until the first layout: `ref` fires at mount, before it, so
|
|
594
|
-
`viewport.
|
|
724
|
+
`viewport.size()` reads 1x1 there. Sprites added and positioned from
|
|
595
725
|
`ref` (the pool, the opening scene) are fine - the first layout resizes
|
|
596
726
|
the view before the first paint - but anything that needs the real size
|
|
597
727
|
(centering on the view) waits for `onLayout`, or works in a `designSize`
|
|
@@ -645,23 +775,45 @@ hover, wheel and tap rules headless.
|
|
|
645
775
|
|
|
646
776
|
### Atlases and sampling
|
|
647
777
|
|
|
648
|
-
- The
|
|
649
|
-
live app-long. Dispose atlases yourself (or let the reactive
|
|
650
|
-
createAtlas registers with the owning scope like every
|
|
778
|
+
- The atlases are NOT owned by the layer: layers come and go, atlases
|
|
779
|
+
usually live app-long. Dispose atlases yourself (or let the reactive
|
|
780
|
+
owner do it - createAtlas registers with the owning scope like every
|
|
781
|
+
core texture).
|
|
651
782
|
- `createImage` is the wrong loader for pixel-art atlases: it never forwards
|
|
652
783
|
sampler options, so it is always `filter: "linear"`. `createAtlas` takes
|
|
653
784
|
decoded pixels (`createAtlas(decodeImage(bytes), { filter: "nearest" })`,
|
|
654
785
|
or pixels built in code) and passes the full sampler through (`filter`,
|
|
655
|
-
`wrap`, `mipmap`, `anisotropy`); the record it returns is what `grid
|
|
656
|
-
`namedFrames` slice
|
|
786
|
+
`wrap`, `mipmap`, `anisotropy`); the record it returns is what `grid`,
|
|
787
|
+
`namedFrames` and `fullFrame` slice, and what a layer's `atlases` list
|
|
788
|
+
holds.
|
|
789
|
+
- A frame from a sheet the layer did not declare THROWS at the write
|
|
790
|
+
(addSprite, setSprite, setTile, setTiles, a tile layer's `frames`
|
|
791
|
+
table), naming the layer's atlases: a sprite never silently draws from
|
|
792
|
+
the wrong sheet. The same frames serve every layer that declares their
|
|
793
|
+
atlas (a scene layer and its additive glow layer over the same art).
|
|
794
|
+
Raw record writers store the texture's index in `layer.atlases`.
|
|
795
|
+
- The sampler cap: a layer binds at most `limits.maxTextureUnits` atlases
|
|
796
|
+
(16 on every GLES 3.0 device, the whole pass's budget - a layer's
|
|
797
|
+
targets bind nothing else). Past it, pack sheets together; runtime
|
|
798
|
+
images need the packer (okf/backlog/2d-atlas-runtime-packing.md).
|
|
657
799
|
- Frames that share an edge do not bleed: the fragment stage clamps every
|
|
658
800
|
sample into its frame, so a sprite at a fractional position (a scrolling
|
|
659
801
|
backdrop, an easing camera) never paints a line of the neighbouring cell,
|
|
660
|
-
and an edge-to-edge sheet needs no gutter and no shaved rect table.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
`
|
|
802
|
+
and an edge-to-edge sheet needs no gutter and no shaved rect table. A
|
|
803
|
+
`mipmap` atlas is different: its mip texels average across cell edges
|
|
804
|
+
BEFORE sampling, and no UV-side trick undoes that. Extrude the sheet
|
|
805
|
+
before `createAtlas` - `extrudeGrid(image, cols, rows, gutter)` for a
|
|
806
|
+
uniform sheet, `extrudeRects(image, rects, gutter)` for a hand-packed
|
|
807
|
+
one: every cell is copied into a gutter of its own replicated edge
|
|
808
|
+
pixels, the result slices with the returned `options`/`rects`, and mip
|
|
809
|
+
levels 0 through log2(gutter) sample clean (TexturePacker's "Extrude",
|
|
810
|
+
Unity's "Extrude Edges"). The rules: `gutter` a power of two and every
|
|
811
|
+
cell size a multiple of it, so cell edges land on mip texel edges (the
|
|
812
|
+
calls throw otherwise, naming the fix); the sheet grows by 2 x gutter
|
|
813
|
+
per cell per axis; deeper levels mix again, so size the gutter to the
|
|
814
|
+
smallest scale the sheet is drawn at (gutter 4 covers down to a
|
|
815
|
+
quarter). Load-time CPU work, a few milliseconds for a 1024-square
|
|
816
|
+
sheet.
|
|
665
817
|
- The colour contract: tint multiplies the atlas texel as stored -
|
|
666
818
|
premultiplied sRGB, no decode - and the layer tint multiplies over
|
|
667
819
|
that, so a tint of `[0.5, 0.5, 0.5, 1]` halves the ENCODED value (about
|
|
@@ -671,7 +823,11 @@ hover, wheel and tap rules headless.
|
|
|
671
823
|
shading: the same tint triple is a different brightness in the two
|
|
672
824
|
packages, so a palette shared across them needs its own conversion.
|
|
673
825
|
- Tint multiplies the sampled texel (`texture * tint`) and the pipeline
|
|
674
|
-
blends with `blend
|
|
826
|
+
blends with the layer's `blend` in record order - "alpha" by default,
|
|
827
|
+
the premultiplied composite; "add" accumulates (glows, sparks, light
|
|
828
|
+
cones: two half-alpha whites make opaque white), "multiply" darkens,
|
|
829
|
+
"none" overwrites. Pipeline state, so it is per layer and fixed at
|
|
830
|
+
creation; an additive effect layer draws over the scene layer.
|
|
675
831
|
The atlas is premultiplied because `decodeImage` premultiplies by default;
|
|
676
832
|
an atlas uploaded from straight-alpha pixels (`decodeImage(bytes, { alpha:
|
|
677
833
|
"straight" })` into `createAtlas`) draws color under transparent texels
|
|
@@ -716,23 +872,24 @@ hover, wheel and tap rules headless.
|
|
|
716
872
|
stays stable and a spawn is one setSprite, never an add. Keep a free
|
|
717
873
|
list of hidden handles; scanning the pool for one is the cost that
|
|
718
874
|
shows up first at a few hundred entities. On the records layer, do not
|
|
719
|
-
cache `layer
|
|
720
|
-
a hoisted reference becomes a dead copy whose writes publish nothing
|
|
721
|
-
|
|
722
|
-
at use time is equally live.
|
|
875
|
+
cache `records(layer)` across addSprite - growth replaces the array and
|
|
876
|
+
a hoisted reference becomes a dead copy whose writes publish nothing;
|
|
877
|
+
read it at use time (the top of the frame's step is the place).
|
|
723
878
|
- Records layer: record order is draw order: `destroySprite` shifts every
|
|
724
879
|
later sprite down one slot (copyWithin + index fixup, O(later
|
|
725
|
-
sprites))
|
|
726
|
-
range
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
880
|
+
sprites)) and marks the shifted records dirty. The flush publishes the
|
|
881
|
+
DIRTY record range (one buffer write at its byte offset, clipped to
|
|
882
|
+
the live sprites), or under `orderBy` the drawn prefix whole through
|
|
883
|
+
the lease - so a moved sprite costs its 80 bytes on a plain layer and
|
|
884
|
+
count x 80 bytes on an ordered one. The node layer's style publish is
|
|
885
|
+
still the whole-prefix shape (a boolean dirty flag over the high-water
|
|
886
|
+
mark), the one place a range is not yet tracked.
|
|
730
887
|
- `orderBy` on BOTH layers: the core gathers publishes into key order
|
|
731
888
|
(gpu `instanceOrder`, radix sort + one extra memcpy, no per-record JS),
|
|
732
889
|
so a y-sorted crowd costs the same flush as an unsorted one. Records
|
|
733
890
|
and slots stay stably addressed; ties keep slot order; `pick()` still
|
|
734
891
|
resolves overlap by slot/record order. Records layer keys: "y" or a raw
|
|
735
|
-
`{ field, descending? }` offset into the
|
|
892
|
+
`{ field, descending? }` offset into the 16-float record. Node layer
|
|
736
893
|
keys: `"y"` - WORLD y from the core-written pose buffer, so sprites
|
|
737
894
|
moved by native transitions (or any core producer) re-sort with zero JS
|
|
738
895
|
per frame - or `"renderOrder"` - the app-owned per-sprite `renderOrder`
|
|
@@ -743,7 +900,7 @@ hover, wheel and tap rules headless.
|
|
|
743
900
|
pose AND style under ONE permutation and republishes the sibling buffer
|
|
744
901
|
itself when the key buffer re-orders (the multi-buffer stage of
|
|
745
902
|
okf/backlog/gpu-instance-order.md). `renderOrder` on a record-layer
|
|
746
|
-
sprite throws - its
|
|
903
|
+
sprite throws - its 16-float record has no key field.
|
|
747
904
|
- The node layer's STYLE slots are not compacted: a removed sprite leaves
|
|
748
905
|
its style floats in place (invisible - the pose is zeroed) until the
|
|
749
906
|
slot recycles. Do not read style truth from the buffer; getSprite reads
|
|
@@ -804,6 +961,26 @@ hover, wheel and tap rules headless.
|
|
|
804
961
|
away with the transform-on-the-leaf camera only because its composited
|
|
805
962
|
view is the WORLD, not a viewport of it.
|
|
806
963
|
|
|
964
|
+
### Text runs
|
|
965
|
+
|
|
966
|
+
- A run draws nothing for a glyph whose cell is not in the atlas yet:
|
|
967
|
+
the sprite is there, hidden, at its place, and the font re-frames the
|
|
968
|
+
run when the cell lands a frame or two later (`font.ready()` awaits
|
|
969
|
+
that; `settle` does in a test). Printable ASCII is made at font
|
|
970
|
+
creation so a run of it never waits; `chars: false` skips that.
|
|
971
|
+
- The atlas grows by doubling and EVERY cell moves when it does:
|
|
972
|
+
`font.atlas.width`/`height` change in place and every run over the
|
|
973
|
+
font is re-framed. Never cache a glyph's frame outside the font.
|
|
974
|
+
- A sprite font over "msdf" cells (the default) is sharp at every zoom;
|
|
975
|
+
one over "mask" cells is exact at the face's size, resamples under zoom
|
|
976
|
+
like any texture, and silently ignores `outline`.
|
|
977
|
+
- A run is node-layer only (it is a group); `addText` on a layer that
|
|
978
|
+
does not declare `font.atlas` throws. Dispose runs before the font
|
|
979
|
+
(`font.dispose`, or the owner's cleanup).
|
|
980
|
+
- The style record is 16 floats and the raw record 20: a raw record
|
|
981
|
+
writer (`records(layer)`) that assumed 16 is off by the outline field
|
|
982
|
+
per sprite.
|
|
983
|
+
|
|
807
984
|
### Tile layer
|
|
808
985
|
|
|
809
986
|
- `<TileLayer>`'s world view is WORLD sized (cols * tileW) and takes that
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @solidrt/2d
|
|
2
2
|
|
|
3
|
-
An instanced sprite layer for SolidRT:
|
|
4
|
-
of sprites in a single draw call, composited into your
|
|
5
|
-
texture element.
|
|
3
|
+
An instanced sprite layer for SolidRT: your atlases bound together, one GPU
|
|
4
|
+
buffer, thousands of sprites in a single draw call, composited into your
|
|
5
|
+
app as an ordinary texture element.
|
|
6
6
|
|
|
7
7
|
*Status: experimental. Expect API churn.*
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ let frames = grid(atlas, 4, 4)
|
|
|
16
16
|
|
|
17
17
|
render(() => (
|
|
18
18
|
<window>
|
|
19
|
-
<SpriteLayer width={720} height={480}
|
|
19
|
+
<SpriteLayer width={720} height={480} atlases={[atlas]}>
|
|
20
20
|
<Sprite x={100} y={120} w={32} h={32} frame={frames[0]} />
|
|
21
21
|
<Sprite x={200} y={160} w={32} h={32} frame={frames[5]} rotation={0.4} />
|
|
22
22
|
</SpriteLayer>
|
|
@@ -24,28 +24,43 @@ render(() => (
|
|
|
24
24
|
))
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
A frame knows the sheet it is cut from, so a layer declaring several
|
|
28
|
+
atlases draws sprites from any of them in the same draw (the multi-texture
|
|
29
|
+
batch), and a mipmapped sheet goes through `extrudeGrid`/`extrudeRects`
|
|
30
|
+
first so its mip chain does not bleed across cells.
|
|
31
|
+
|
|
27
32
|
The rendertree already handles 2D well: a `d-texture` with atlas sub-rects is
|
|
28
33
|
a sprite, with native transitions, gesture-arena pointer events, and
|
|
29
34
|
per-element inspectability, and it carries populations into the low
|
|
30
|
-
thousands. This package is for what lies beyond that: dense,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
thousands. This package is for what lies beyond that: dense, animated
|
|
36
|
+
populations - entities, particles, bullets - drawn as one instanced draw
|
|
37
|
+
per view. Every sprite is a node of the spatial core, so native
|
|
38
|
+
transitions, groups, picking and the collision queries reach it with no
|
|
39
|
+
per-frame JS: a sprite write is a target the core animates toward, and
|
|
40
|
+
the core writes the pose buffer itself. For motion only JS can compute at
|
|
41
|
+
scale (a flock, a particle sim), the records layer is the escape hatch:
|
|
42
|
+
sixteen JS-owned floats per sprite in one `Float32Array`, published by
|
|
43
|
+
dirty range with the same `records`/`updateRecords`/`setRecordCount`
|
|
44
|
+
verbs as `@solidrt/3d`'s record mesh - ten thousand float stores and one
|
|
45
|
+
buffer write, not twenty thousand FFI calls.
|
|
36
46
|
|
|
37
47
|
Two layers, like `@solidrt/3d`: an imperative core
|
|
38
48
|
(`createSpriteLayer` / `addSprite` / `setSprite` - per-frame motion calls
|
|
39
49
|
these from `onFrame`) and the component face (`SpriteLayer` / `Sprite`) for
|
|
40
50
|
structure and slow state. A static layer publishes nothing and costs nothing;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
the
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
draw order is slot order unless the layer names a key (`orderBy`: world y
|
|
52
|
+
for a perspective crowd, or a per-sprite `renderOrder`); pointer events
|
|
53
|
+
hit-test exact rotated rects, topmost first, with pointer capture.
|
|
54
|
+
|
|
55
|
+
In the package: atlas creation, slicing and extrusion (`createAtlas`,
|
|
56
|
+
`grid`, `namedFrames`, `fullFrame`, `extrudeGrid`, `extrudeRects`), the
|
|
57
|
+
sprite layer with its views and cameras, sprite pointer events, the baked
|
|
58
|
+
tile layer, frame animation, key ordering, world-space text (a sprite
|
|
59
|
+
font over the runtime's glyph engine is one more atlas, a run of text a
|
|
60
|
+
group of glyph sprites: `createSpriteFont`, `addText`, `<Text2d>`), and
|
|
61
|
+
the component face. Staged
|
|
62
|
+
next: streaming tile worlds and the retro presets (pixel canvas, palette
|
|
63
|
+
and scanline passes) - see `okf/backlog/`.
|
|
49
64
|
|
|
50
65
|
The full model and the sharp edges are in [AGENTS.md](AGENTS.md); runnable
|
|
51
66
|
patterns in [examples/](examples/).
|