@solidrt/2d 0.0.71 → 0.0.72
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 +211 -42
- package/README.md +7 -3
- package/examples/README.md +1 -1
- package/examples/camera-probe.tsx +4 -4
- package/examples/markers.tsx +3 -3
- package/package.json +5 -4
- package/src/animation.ts +26 -5
- package/src/atlas.ts +8 -4
- package/src/camera.ts +2 -2
- package/src/components/camera2d.tsx +2 -2
- package/src/components/group.tsx +15 -3
- package/src/components/shots.tsx +2 -2
- package/src/components/sprite-layer.tsx +40 -12
- package/src/components/sprite.tsx +31 -3
- package/src/components/text2d.tsx +2 -2
- package/src/components/tile-layer.tsx +21 -7
- package/src/components/view2d.tsx +2 -2
- package/src/dispatch.ts +10 -6
- package/src/frames.ts +1 -1
- package/src/glsl.ts +399 -0
- package/src/index.ts +14 -6
- package/src/layer.ts +240 -101
- package/src/material.ts +368 -0
- package/src/records.ts +135 -32
- package/src/style.ts +98 -0
- package/src/tiles.ts +119 -24
- package/src/views.ts +17 -9
- package/src/shaders.ts +0 -323
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,8 @@ Contents:
|
|
|
17
17
|
- [Three faces](#three-faces)
|
|
18
18
|
- [A layer renders through its views](#a-layer-renders-through-its-views)
|
|
19
19
|
- [Atlases and frames](#atlases-and-frames)
|
|
20
|
-
- [
|
|
20
|
+
- [Materials](#materials)
|
|
21
|
+
- [Pose, sprite and style slots](#pose-sprite-and-style-slots)
|
|
21
22
|
- [Fixed instance slots](#fixed-instance-slots)
|
|
22
23
|
- [Growth](#growth)
|
|
23
24
|
- [Picking](#picking)
|
|
@@ -48,6 +49,7 @@ Contents:
|
|
|
48
49
|
- [Pointer events](#pointer-events)
|
|
49
50
|
- [Traps](#traps)
|
|
50
51
|
- [Atlases and sampling](#atlases-and-sampling)
|
|
52
|
+
- [Materials](#materials-1)
|
|
51
53
|
- [Slots, capacity and order](#slots-capacity-and-order)
|
|
52
54
|
- [Nodes, transitions and queries](#nodes-transitions-and-queries)
|
|
53
55
|
- [Cameras and rotation](#cameras-and-rotation)
|
|
@@ -94,10 +96,12 @@ included, and a tile world bakes from several tilesets in one pass. A
|
|
|
94
96
|
Unity's Sprite and Godot's AtlasTexture pair the texture with the rect
|
|
95
97
|
the same way), the record stores that texture's index in the layer's
|
|
96
98
|
list (`layer.atlases`, what a raw record writer stores), and the
|
|
97
|
-
fragment
|
|
98
|
-
flat per-instance index picking it
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
fragment SET is GENERATED per layer with one sampler per atlas, the
|
|
100
|
+
flat per-instance index picking it (`spriteSource` in `@solidrt/2d/glsl`,
|
|
101
|
+
prepended to whatever fragment the layer's material carries):
|
|
102
|
+
derivatives are taken from the unclamped uv before the branch and the
|
|
103
|
+
tap is `textureGrad`, so mip selection is the quad's own and well
|
|
104
|
+
defined inside the branch. A frame
|
|
101
105
|
whose texture the layer did not declare THROWS at the write (the
|
|
102
106
|
wrong-sheet bug, caught instead of drawn); the frame left out of
|
|
103
107
|
`addSprite` is the whole FIRST atlas. An atlas declared `sdf: { range }`
|
|
@@ -107,19 +111,114 @@ the edge, anti-aliased over one screen pixel at whatever zoom, the true
|
|
|
107
111
|
field in alpha grows a per-sprite outline (text runs set it; a colour
|
|
108
112
|
atlas ignores the field). Fixed at creation like the list itself.
|
|
109
113
|
|
|
110
|
-
###
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
### Materials
|
|
115
|
+
|
|
116
|
+
What a layer's pixels look like is its MATERIAL (material.ts), one per
|
|
117
|
+
layer, fixed at creation (`createSpriteLayer(atlases, { material })`,
|
|
118
|
+
the records and tile layers alike, `<SpriteLayer material>` /
|
|
119
|
+
`<TileLayer material>`): the material sits on the thing that is one
|
|
120
|
+
draw entry - the layer is the batch - as @solidrt/3d's sits on the
|
|
121
|
+
mesh, so two looks are two layers, which is also how you order them
|
|
122
|
+
(the additive glow layer draws over the scene layer). The same model
|
|
123
|
+
and the same words as 3d, deliberately: `unlit()` is the stock
|
|
124
|
+
material, `prelude`/`surface` its tier-2 slot, `shaderMaterialClass`
|
|
125
|
+
/ `shaderMaterial` the class form over the layer's GLSL set,
|
|
126
|
+
`instanceBuffers` / `instanceStyle` / `setInstanceStyle` /
|
|
127
|
+
`instanceAttribute` the per-sprite style record (the comparison and
|
|
128
|
+
the decisions: okf/plans/materials-one-model.md; Godot's
|
|
129
|
+
`CanvasItem.material` with a `canvas_item` shader is the nearest
|
|
130
|
+
engine shape, PixiJS and Phaser the batch-per-look precedent). The one
|
|
131
|
+
deliberate asymmetry: a 3d `blend` implies `transparent` and the
|
|
132
|
+
scene's back-to-front sort; a layer has no depth and draws in record
|
|
133
|
+
order, so nothing sorts.
|
|
134
|
+
|
|
135
|
+
- `unlit({ blend?, prelude?, surface?, params?, textures?,
|
|
136
|
+
instanceBuffers?, instanceStyle?, label? })` - the atlas texel times
|
|
137
|
+
the sprite's tint (a distance-field atlas decoded to its fill and
|
|
138
|
+
outline), times the layer tint, blended with `blend` ("alpha" by
|
|
139
|
+
default, "add" for glows and additive particles, "multiply" to
|
|
140
|
+
darken, "none" to overwrite; pipeline state, so per layer). Every
|
|
141
|
+
layer draws with `unlit()` unless told otherwise. The TIER-2 slot:
|
|
142
|
+
`surface` declares `void surface(inout Sprite s)`, called with the
|
|
143
|
+
struct filled and before the layer tint - `color` (the stock result,
|
|
144
|
+
premultiplied: texel times tint, or the decoded field), `uv`,
|
|
145
|
+
`frame`, `tint`, `atlas` - rewrite `s.color` or `discard`;
|
|
146
|
+
`prelude` is file scope after the set (uniforms, each a LAYER PARAM;
|
|
147
|
+
helpers); `spriteSample(uv)` resamples the fragment's own atlas at
|
|
148
|
+
another uv (the pick, the frame clamp, the field decode, the tint),
|
|
149
|
+
so a scroll is `spriteSample(mix(s.frame.xy, s.frame.zw,
|
|
150
|
+
fract(t)))` - the clamp into the frame still applies, so a wrap is
|
|
151
|
+
`fract` in FRAME space. One program per distinct source, cached for
|
|
152
|
+
the app's lifetime like 3d's.
|
|
153
|
+
- PER-SPRITE DATA: `instanceBuffers: [{ attributes: [{ name, format }]
|
|
154
|
+
}]` on either form declares the material's STYLE record(s), one
|
|
155
|
+
stream per buffer bound after the layer's own records, any vertex
|
|
156
|
+
format (a palette index is a `uint8x4` at four bytes, a dissolve
|
|
157
|
+
amount a `float16x2`), every slot starting as `instanceStyle` (the
|
|
158
|
+
identity of what the look does with it). On `unlit` the stock
|
|
159
|
+
vertex stage forwards each attribute unchanged as a flat varying -
|
|
160
|
+
`iPalette` reaches the prelude and the surface as `vPalette`
|
|
161
|
+
(`forwardedName`; names follow the layer's own `i` + capital
|
|
162
|
+
convention) - so a palette swap is one attribute, one surface line
|
|
163
|
+
and a `setInstanceStyle(layer, sprite, [row])` per sprite. Writes:
|
|
164
|
+
`setInstanceStyle(layer, sprite, values)` (the first buffer, one
|
|
165
|
+
value per component as the shader sees them; marks the slot like
|
|
166
|
+
setSprite), `instanceAttribute(layer, name)` (the accessor: slot in,
|
|
167
|
+
component values out and in through the format's codec; valid
|
|
168
|
+
across growth) plus `updateRecords(layer, { stream, first, count
|
|
169
|
+
})`, or `records(layer, stream)` in bulk (stream 1.. the material's
|
|
170
|
+
buffers in order; a Float32Array over an all-float layout, bytes
|
|
171
|
+
otherwise), `<Sprite style>` in the component face, `setTile(...,
|
|
172
|
+
{ style })` on a tile layer. A recycled slot (and a records layer's
|
|
173
|
+
shifted one) starts blank again.
|
|
174
|
+
- LAYER PARAMS: `layer.setParams({ uFlash: 0.5 })` writes a prelude's
|
|
175
|
+
uniforms to every view (and every resident chunk of a tile layer,
|
|
176
|
+
which RE-BAKES on each write like its tint) over the material's own
|
|
177
|
+
`params`; the `params` option and prop seed them, a view created
|
|
178
|
+
later inherits the merged record. @solidrt/3d's setMeshParams one
|
|
179
|
+
dimension down. Material `textures` (a palette LUT, a noise texture)
|
|
180
|
+
bind beside the atlases and count against the same
|
|
181
|
+
`limits.maxTextureUnits` budget, checked at layer creation.
|
|
182
|
+
- The class form, `shaderMaterialClass({ vertex, fragment,
|
|
183
|
+
instanceBuffers?, instanceStyle?, blend?, label? })` with
|
|
184
|
+
`instance({ params?, textures? })` and `dispose()` on the class
|
|
185
|
+
(`shaderMaterial(opts)` is a class with one instance): TIER 1 pairs
|
|
186
|
+
a vertex stage of your own with `unlitFragment(options)` from
|
|
187
|
+
`@solidrt/2d/glsl` - compose `SPRITE_VARYINGS`, core's
|
|
188
|
+
`SCREEN_SIZE_GLSL`, your placement into `corner`/`center`/`rot`/
|
|
189
|
+
`clampScale`, then `SPRITE_VERTEX_BODY`; `SPRITE_VERTEX` is the
|
|
190
|
+
stock stage, `unlitVertex(forwards)` the stock stage forwarding
|
|
191
|
+
attributes - and TIER 3 writes a `main` of its own over the layer's
|
|
192
|
+
SET, which the layer prepends (the varyings, `uAtlasN`, `uTint`, the
|
|
193
|
+
`Sprite` struct, `spriteOf()`, `spriteSample(uv)`: declare none of
|
|
194
|
+
them, start with `Sprite s = spriteOf();` since the derivatives
|
|
195
|
+
every tap uses are taken there, and write premultiplied). One
|
|
196
|
+
program per ATLAS LIST met (the set is generated per list), one
|
|
197
|
+
pipeline per instance-layout list met on top - the records and tile
|
|
198
|
+
layers bind one record buffer, the node layer a pose and a sprite
|
|
199
|
+
buffer, with the same attribute NAMES on both (iPos, iRot, iScale,
|
|
200
|
+
iUv, iTint, iScreenPx, iAtlas, iOutline), so one vertex stage
|
|
201
|
+
serves every layer kind. A vertex stage must declare and use
|
|
202
|
+
`uCamera`, `uCameraRot` and `uViewport` (checked at creation), an
|
|
203
|
+
instance attribute may not reuse a layer name
|
|
204
|
+
(`LAYER_ATTRIBUTE_NAMES`), a texture may not take an atlas sampler's
|
|
205
|
+
(`uAtlasN`). `tests/materials.test.tsx` pins all three tiers, the
|
|
206
|
+
style streams through growth and shift, the params, the resample,
|
|
207
|
+
the tile bake and the throws.
|
|
208
|
+
|
|
209
|
+
### Pose, sprite and style slots
|
|
210
|
+
|
|
211
|
+
Node layer ownership split, instance-buffer slots on one pipeline (the
|
|
212
|
+
material's; two materials are two layers): slot 0 is the POSE buffer
|
|
213
|
+
`[x, y, angle, sx, sy]` written ONLY by the core (each sprite node's
|
|
214
|
+
Pose2D record sink; one coalesced buffer write per flush however many
|
|
215
|
+
nodes moved), slot 1 the SPRITE record `[u0, v0, u1, v1, tint rgba,
|
|
216
|
+
renderOrder, minScreenPx, maxScreenPx, atlas, outline rgb,
|
|
217
|
+
outlineWidth]` (`SPRITE_FLOATS`, 16), JS-owned through the sprite verbs
|
|
218
|
+
and published through the zero-copy write lease, and after them the
|
|
219
|
+
material's STYLE streams, one per instance buffer it declares (app-owned
|
|
220
|
+
per-sprite data, Materials above). NEVER write the pose buffer from JS -
|
|
221
|
+
the core's staging mirror owns it and will overwrite.
|
|
123
222
|
|
|
124
223
|
### Fixed instance slots
|
|
125
224
|
|
|
@@ -212,7 +311,9 @@ detach here (3d's `remove`), and a sprite that should come back is
|
|
|
212
311
|
hidden (`visible: false`).
|
|
213
312
|
A handle's x/y are local to its group; `worldPosition(sprite | group)`
|
|
214
313
|
reads the layer-pixel position composed through every enclosing group
|
|
215
|
-
from the core's world matrix (@solidrt/3d's worldPosition)
|
|
314
|
+
from the core's world matrix (@solidrt/3d's worldPosition) - under a
|
|
315
|
+
transition the pose of this frame, since native motion steps before the
|
|
316
|
+
frame's JS.
|
|
216
317
|
|
|
217
318
|
### Visibility
|
|
218
319
|
|
|
@@ -273,14 +374,18 @@ publish itself is the core's record stream (`createRecordStream` in
|
|
|
273
374
|
the range-or-whole publish, growth into a replacement), the one model
|
|
274
375
|
the 3d instance streams run on too; this file only decides what to
|
|
275
376
|
mark and when to flush. The node layer speaks the same two verbs over
|
|
276
|
-
its
|
|
277
|
-
(`
|
|
377
|
+
its SPRITE records: `records(spriteLayer)` is the sprite mirror
|
|
378
|
+
(`SPRITE_FLOATS` floats per slot: `[u0, v0, u1, v1, tint rgba,
|
|
278
379
|
renderOrder, minScreenPx, maxScreenPx, atlas, outline rgb,
|
|
279
380
|
outlineWidth]`, slot-indexed, poses stay the core's) and
|
|
280
381
|
`updateRecords(spriteLayer, { first?, count? })` publishes the slot
|
|
281
382
|
range, so a bulk restyle (a palette cycle over thousands of sprites) is
|
|
282
383
|
one loop and one range write instead of a setSprite each - the 3d
|
|
283
|
-
`records(instancedMesh)` pairing, one dimension down.
|
|
384
|
+
`records(instancedMesh)` pairing, one dimension down. Both layer
|
|
385
|
+
kinds take a `stream` beyond 0 on the same verbs for the material's
|
|
386
|
+
style streams (`records(layer, 1)`, `updateRecords(layer, { stream: 1
|
|
387
|
+
})`), which grow, publish and (on a records layer) shift with the
|
|
388
|
+
layer's own record. It is the
|
|
284
389
|
escape hatch for motion only JS can compute at scale (measured 30k
|
|
285
390
|
sprites: 12.9ms raw records vs 30.8ms via setSprite; both figures are
|
|
286
391
|
the WRITE path only - whatever computes the motion is excluded and is
|
|
@@ -478,7 +583,9 @@ kinds): one `uTint` shared-params write multiplied over every sprite's
|
|
|
478
583
|
own tint - day/night, a dimmed parallax plane, a fade-in. Cheap to
|
|
479
584
|
animate (no record touches), unlike TileLayer's, which re-renders
|
|
480
585
|
resident chunks. The same contract as TileLayer.setTint, so one signal
|
|
481
|
-
drives a whole scene across layer kinds.
|
|
586
|
+
drives a whole scene across layer kinds. The general form is
|
|
587
|
+
`setParams` (Materials above): the tint is the one param every material
|
|
588
|
+
takes.
|
|
482
589
|
|
|
483
590
|
### Views
|
|
484
591
|
|
|
@@ -562,6 +669,25 @@ never on a cancel, snap or exit; the raw "spatialTransitionEnd" engine
|
|
|
562
669
|
event (sol:events, node = sprite.node) stays for flux:spatial consumers.
|
|
563
670
|
See examples/springs.tsx (tap a sprite: it leaves through its `exit`).
|
|
564
671
|
|
|
672
|
+
PAUSE and time scale are declarations on the same nodes:
|
|
673
|
+
`layer.setTimeScale(scale)` / `<SpriteLayer timeScale>` sets the rate
|
|
674
|
+
every sprite and group transition in the layer runs at - with their
|
|
675
|
+
delays, enters, exits and stagger slots - 0 a pause that leaves the
|
|
676
|
+
app's UI moving, 1 app time, 0.5 slow motion; `setGroupTimeScale` /
|
|
677
|
+
`<Group timeScale>` and `setSpriteTimeScale` / `<Sprite timeScale>`
|
|
678
|
+
declare it for a subtree or one sprite. The NEAREST declaring node wins,
|
|
679
|
+
never a product (Godot's process_mode shape): a frozen layer with a
|
|
680
|
+
`<Group timeScale={1}>` inside keeps that group running. A frozen layer
|
|
681
|
+
asks for no frames (the write that resumes it brings the frame), writes
|
|
682
|
+
while frozen retarget and wait, a destroyed sprite with an `exit` stays
|
|
683
|
+
a ghost until its time moves again (the layer's `dispose` still cuts it
|
|
684
|
+
short), and a sprite added frozen sits at its `from`. The scale covers
|
|
685
|
+
the CORE's motion: frame animations follow it only when given a `clock`
|
|
686
|
+
(see Frame animation), and your own `onFrame` logic, timers and audio
|
|
687
|
+
stay on app time - multiply your step by `layer.timeRate()` /
|
|
688
|
+
`timeRate(sprite)`, the effective rate read. The dev clock
|
|
689
|
+
(`/clock?scale=`) scales app time underneath all of this.
|
|
690
|
+
|
|
565
691
|
### Frame-rate motion
|
|
566
692
|
|
|
567
693
|
Frame-rate motion only JS can compute (physics, flocking) bypasses the
|
|
@@ -588,7 +714,12 @@ most one animation (add detaches the previous); removed sprites prune
|
|
|
588
714
|
lazily on the next step. Works on both layer kinds; with `<Sprite>`,
|
|
589
715
|
attach via `ref` and leave the `frame` prop off - the clip owns that
|
|
590
716
|
field (the prop effect passes absent props as undefined, which setSprite
|
|
591
|
-
keeps, so other props stay reactive).
|
|
717
|
+
keeps, so other props stay reactive). A clip is a JS clock, so a layer's
|
|
718
|
+
`setTimeScale` does not reach it by itself: `createAnimation(frames, fps,
|
|
719
|
+
{ clock: layer })` (a group or a sprite works too) runs the clip at that
|
|
720
|
+
handle's `timeRate`, read every frame, so the one pause that freezes the
|
|
721
|
+
world's transitions holds its walk cycles as well, and a slow-motion
|
|
722
|
+
group slows the flipbooks in it.
|
|
592
723
|
|
|
593
724
|
### Text runs
|
|
594
725
|
|
|
@@ -640,8 +771,10 @@ Static 2D bulk as a few quads: on tiled GPUs the budget is primitive count
|
|
|
640
771
|
quads per frame. `createTileLayer(cols, rows, tileW, tileH, atlases)`
|
|
641
772
|
bakes the world into CHUNKED `render: "manual"` targets (default ~512px
|
|
642
773
|
of tiles per chunk, `chunkTiles` to tune), each chunk a manual target
|
|
643
|
-
over the layer's ONE
|
|
644
|
-
|
|
774
|
+
over the layer's ONE pipeline (its material's, resolved once per layer
|
|
775
|
+
for its atlases; a cell's style record is `setTile`'s `style`, its
|
|
776
|
+
params re-bake every resident chunk on each write) with FIXED record
|
|
777
|
+
slots - an empty tile is a
|
|
645
778
|
zero-size quad, instance count is constant per chunk. Records hold WORLD
|
|
646
779
|
pixel coordinates; each chunk target's `uCamera` is its pixel origin, so
|
|
647
780
|
the shared vertex stage does the chunk-local mapping. Chunks allocate on
|
|
@@ -700,8 +833,11 @@ the whole layer through the shared `uTint` uniform, multiplied over the
|
|
|
700
833
|
per-cell tints - day/night, a dimmed parallax plane. A layer-tint write
|
|
701
834
|
is not a record write (chunks re-render GPU-side, nothing re-uploads),
|
|
702
835
|
but it does re-render every resident chunk: drive it from slow state,
|
|
703
|
-
not per frame.
|
|
704
|
-
|
|
836
|
+
not per frame. `setParams` (a material prelude's uniforms) and a cell's
|
|
837
|
+
`style` (the material's per-cell record, `setTile`/`setTiles` `{ style
|
|
838
|
+
}`) are the same two levels for a custom look, Materials above. Not
|
|
839
|
+
built yet: camera-driven residency (bake far chunks on approach, evict)
|
|
840
|
+
- okf/backlog/2d-baked-layers.md.
|
|
705
841
|
|
|
706
842
|
## Components
|
|
707
843
|
|
|
@@ -709,12 +845,12 @@ on approach, evict) - okf/backlog/2d-baked-layers.md.
|
|
|
709
845
|
|
|
710
846
|
| Component | Props |
|
|
711
847
|
|---|---|
|
|
712
|
-
| `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/
|
|
713
|
-
| `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? |
|
|
848
|
+
| `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/material/orderBy then throw), capacity?, material? (the layer's material, `unlit()` default, `unlit({ blend: "add" })` for glows; mount-fixed), params? (layer.setParams as a prop, over the material's own; live, merge semantics), 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) |
|
|
849
|
+
| `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), style? (the sprite's style record under the layer's material, one value per component of its first instance buffer; live), visible?, transition?, onPointer{Down,Move,Up,Enter,Leave}?, onWheel?, onTap?, ref? |
|
|
714
850
|
| `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? |
|
|
715
851
|
| `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>` |
|
|
716
852
|
| `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 |
|
|
717
|
-
| `TileLayer` | cols, rows, tileW, tileH, atlases (Atlas[], the tilesets bound as one bake; mount-fixed), frames? (the tileset `setTiles` indices name), chunkClearColor?,
|
|
853
|
+
| `TileLayer` | cols, rows, tileW, tileH, atlases (Atlas[], the tilesets bound as one bake; mount-fixed), frames? (the tileset `setTiles` indices name), chunkClearColor?, material? (the bake's material, `unlit()` default; mount-fixed), params? (layer.setParams as a prop; live, each change re-bakes), 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? |
|
|
718
854
|
|
|
719
855
|
### SpriteLayer and useSpriteLayer
|
|
720
856
|
|
|
@@ -772,12 +908,15 @@ space keeps delivering to the root as it crosses sprites; a drag from a
|
|
|
772
908
|
sprite keeps naming it with live coordinates). Enter/leave fire on the
|
|
773
909
|
sprite alone - a group never receives them - while the root sees every
|
|
774
910
|
move, so "hovering empty space" is a root move with `sprite` null. Taps
|
|
775
|
-
are synthesized by the dispatch
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
911
|
+
are synthesized by the dispatch, as DOM click (Unity's click handler
|
|
912
|
+
fires for every button; ours, like DOM's, for the primary alone): `onTap`
|
|
913
|
+
fires after the up when a PRIMARY-button press (a right click walks its
|
|
914
|
+
down and up with `button` 2 but never taps, like core's press; read
|
|
915
|
+
`button` on the down to act on it) released on the target it pressed
|
|
916
|
+
within the slop (8 window px, core's recognizer slop, so a press is never
|
|
917
|
+
both a tap and a pan), was the only pointer down for its whole press (a
|
|
918
|
+
pinch never taps), with `tapCount` counting repeats within 300 ms and 20
|
|
919
|
+
px on the same target (a double tap is `tapCount === 2`).
|
|
781
920
|
Wheel walks like a move with `deltaX/deltaY`. Every event carries
|
|
782
921
|
`timeStamp` and `predicted` as the element event has them (time input
|
|
783
922
|
with the first; a `predicted` move is one to draw with, not to measure
|
|
@@ -844,11 +983,12 @@ hover, wheel and tap rules headless.
|
|
|
844
983
|
shading: the same tint triple is a different brightness in the two
|
|
845
984
|
packages, so a palette shared across them needs its own conversion.
|
|
846
985
|
- Tint multiplies the sampled texel (`texture * tint`) and the pipeline
|
|
847
|
-
blends with the
|
|
848
|
-
the premultiplied composite; "add" accumulates (glows,
|
|
849
|
-
cones: two half-alpha whites make opaque white),
|
|
850
|
-
"none" overwrites. Pipeline state, so it is per
|
|
851
|
-
|
|
986
|
+
blends with the material's `blend` in record order - "alpha" by
|
|
987
|
+
default, the premultiplied composite; "add" accumulates (glows,
|
|
988
|
+
sparks, light cones: two half-alpha whites make opaque white),
|
|
989
|
+
"multiply" darkens, "none" overwrites. Pipeline state, so it is per
|
|
990
|
+
layer (`material: unlit({ blend: "add" })`) and fixed at creation; an
|
|
991
|
+
additive effect layer draws over the scene layer.
|
|
852
992
|
The atlas is premultiplied because `decodeImage` premultiplies by default;
|
|
853
993
|
an atlas uploaded from straight-alpha pixels (`decodeImage(bytes, { alpha:
|
|
854
994
|
"straight" })` into `createAtlas`) draws color under transparent texels
|
|
@@ -884,6 +1024,35 @@ hover, wheel and tap rules headless.
|
|
|
884
1024
|
snapping the fit to an integer: the scene should fill its box at any
|
|
885
1025
|
ratio.
|
|
886
1026
|
|
|
1027
|
+
### Materials
|
|
1028
|
+
|
|
1029
|
+
- A `surface` reads the struct, never a local of the generated program:
|
|
1030
|
+
`s.color` is premultiplied and BEFORE the layer tint (`uTint`
|
|
1031
|
+
multiplies after the slot), so a rewrite to an opaque colour is
|
|
1032
|
+
`vec4(rgb, 1.0) * s.color.a` to keep the sprite's coverage.
|
|
1033
|
+
- A custom `main` (tier 3) starts with `Sprite s = spriteOf();`: the uv
|
|
1034
|
+
derivatives every tap uses are taken there, in uniform control flow.
|
|
1035
|
+
A `spriteSample` before it reads zero derivatives and picks the
|
|
1036
|
+
sharpest mip everywhere.
|
|
1037
|
+
- `spriteSample(uv)` clamps into the FRAME like the stock tap: a uv
|
|
1038
|
+
past the frame's edge lands on the edge texel, never on the next
|
|
1039
|
+
cell. Wrap in frame space (`fract`) before mixing into the frame rect.
|
|
1040
|
+
- The stock stage forwards a material's instance attributes as FLAT
|
|
1041
|
+
varyings named `v` + the name without its `i` - `iPalette` is
|
|
1042
|
+
`vPalette` in the surface; a name not spelled `i` + capital throws at
|
|
1043
|
+
`unlit()`. A custom vertex stage forwards what it likes.
|
|
1044
|
+
- A material's `textures` and the atlases share one sampler budget
|
|
1045
|
+
(`limits.maxTextureUnits`, 16): a LUT and a noise texture leave room
|
|
1046
|
+
for fourteen sheets, and the layer creation throws past it.
|
|
1047
|
+
- A tile layer BAKES: `setParams` on it re-renders every resident chunk,
|
|
1048
|
+
like `setTint`. Animated uniforms belong on a sprite layer, whose
|
|
1049
|
+
params are one shared write per view.
|
|
1050
|
+
- The stock material classes are cached for the app's lifetime (one per
|
|
1051
|
+
blend x prelude x surface x layouts, like 3d's unlit); a
|
|
1052
|
+
`shaderMaterialClass` is yours to `dispose()`, and its programs are
|
|
1053
|
+
compiled per atlas LIST met - two layers over the same atlases share
|
|
1054
|
+
the program, two over different lists compile twice.
|
|
1055
|
+
|
|
887
1056
|
### Slots, capacity and order
|
|
888
1057
|
|
|
889
1058
|
- `capacity` is a reservation, not a limit, on both layer kinds; reserve
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ transitions, groups, picking and the collision queries reach it with no
|
|
|
39
39
|
per-frame JS: a sprite write is a target the core animates toward, and
|
|
40
40
|
the core writes the pose buffer itself. For motion only JS can compute at
|
|
41
41
|
scale (a flock, a particle sim), the records layer is the escape hatch:
|
|
42
|
-
|
|
42
|
+
twenty JS-owned floats per sprite in one `Float32Array`, published by
|
|
43
43
|
dirty range with the same `records`/`updateRecords`/`setRecordCount`
|
|
44
44
|
verbs as `@solidrt/3d`'s record mesh - ten thousand float stores and one
|
|
45
45
|
buffer write, not twenty thousand FFI calls.
|
|
@@ -54,8 +54,12 @@ hit-test exact rotated rects, topmost first, with pointer capture.
|
|
|
54
54
|
|
|
55
55
|
In the package: atlas creation, slicing and extrusion (`createAtlas`,
|
|
56
56
|
`grid`, `namedFrames`, `fullFrame`, `extrudeGrid`, `extrudeRects`), the
|
|
57
|
-
sprite layer with its views and cameras,
|
|
58
|
-
|
|
57
|
+
sprite layer with its views and cameras, materials (the stock `unlit`
|
|
58
|
+
with its blend and a `surface` slot over the sprite, `shaderMaterial`
|
|
59
|
+
over the layer's GLSL set, per-sprite style records in any vertex format
|
|
60
|
+
- the same three tiers and the same words as `@solidrt/3d`, the material
|
|
61
|
+
on the layer because the layer is the batch), sprite pointer events, the
|
|
62
|
+
baked tile layer, frame animation, key ordering, world-space text (a sprite
|
|
59
63
|
font over the runtime's glyph engine is one more atlas, a run of text a
|
|
60
64
|
group of glyph sprites: `createSpriteFont`, `addText`, `<Text2d>`), and
|
|
61
65
|
the component face. Staged
|
package/examples/README.md
CHANGED
|
@@ -67,7 +67,7 @@ copy one and adapt it.
|
|
|
67
67
|
`maxScreenPx`), a label bar that never grows past 24 px (`maxScreenPx`)
|
|
68
68
|
and a selection ring that never shrinks under 48 px (`minScreenPx`,
|
|
69
69
|
gliding to the tapped sprite through a position transition), the glow
|
|
70
|
-
layer the same atlas on `blend: "add"` with haloes that add up to white
|
|
70
|
+
layer the same atlas on `unlit({ blend: "add" })` with haloes that add up to white
|
|
71
71
|
where they overlap. Picking follows the drawn size. Debug commands
|
|
72
72
|
`zoom`, `select` and `pick` drive it headless.
|
|
73
73
|
- `split-screen.tsx` - one `<SpriteLayer output={false}>` (sprites, no
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Camera-mapping probe: the live guard that the three spellings of the
|
|
2
|
-
// camera mapping agree - projectCamera (camera.ts), the vertex
|
|
3
|
-
// uCameraRot (
|
|
2
|
+
// camera mapping agree - projectCamera (camera.ts), the vertex stage's
|
|
3
|
+
// uCameraRot (glsl.ts), and pointer dispatch's unprojectCamera
|
|
4
4
|
// (layer.ts). The headless check (tests/camera.test.ts) ties
|
|
5
5
|
// projectCamera to the <view>-prop oracle but cannot see the shader; this
|
|
6
6
|
// probe closes that gap on a real client. Self-asserting: watch the logs
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// What it checks, all under one rotated + pivoted camera:
|
|
12
12
|
// 1. The shader implements projectCamera: a probe sprite samples opaque
|
|
13
13
|
// at its PROJECTED pixel and background at the unrotated position.
|
|
14
|
-
// 2.
|
|
15
|
-
// between the two layer kinds.
|
|
14
|
+
// 2. The one vertex stage over the two record layouts (records, nodes)
|
|
15
|
+
// agrees with itself: pixel parity between the two layer kinds.
|
|
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.
|
package/examples/markers.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// px however far in you zoom (maxScreenPx alone), and the selection RING
|
|
8
8
|
// that never shrinks under 48 px however far out (minScreenPx alone), so
|
|
9
9
|
// the selected sprite stays findable at the overview. The glow layer is
|
|
10
|
-
// the same atlas on
|
|
10
|
+
// the same atlas on an additive material: half-lit haloes that
|
|
11
11
|
// accumulate to white where they overlap, over the transparent clear.
|
|
12
12
|
// Wheel or pinch to zoom, drag empty space to pan, tap a sprite to
|
|
13
13
|
// select it (the ring glides along), tap empty space to deselect.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// pixels) and the ring's; `pick` ({ x, y }) returns
|
|
22
22
|
// what the world view hits at those VIEW pixels, as marker kinds.
|
|
23
23
|
import { createEffect, createInputMap, createPointerFeed, decodeImage, displayScale, onFrame, render, windowSize } from "@solidrt/core"
|
|
24
|
-
import { addSprite, camera2dActions, camera2dBindings, createAtlas, createCamera2d, createSpriteLayer, feedPointer, fitOversample, grid, setSprite, setSpriteTransition } from "@solidrt/2d"
|
|
24
|
+
import { addSprite, camera2dActions, camera2dBindings, createAtlas, createCamera2d, createSpriteLayer, feedPointer, fitOversample, grid, setSprite, setSpriteTransition, unlit } from "@solidrt/2d"
|
|
25
25
|
import type { Camera2dHandle, SpriteHandle, ViewHandle } from "@solidrt/2d"
|
|
26
26
|
import { registerDebug } from "sol:dev"
|
|
27
27
|
import logoBytes from "./logo.png" with { type: "binary" }
|
|
@@ -80,7 +80,7 @@ function App() {
|
|
|
80
80
|
let frames = grid(atlas, 2, 2)
|
|
81
81
|
let win = { width: 1, height: 1 }
|
|
82
82
|
let layer = createSpriteLayer([atlas], { capacity: 128, label: "markers" })
|
|
83
|
-
let glow = createSpriteLayer([atlas], { capacity: 64, blend: "add", label: "glow" })
|
|
83
|
+
let glow = createSpriteLayer([atlas], { capacity: 64, material: unlit({ blend: "add" }), label: "glow" })
|
|
84
84
|
world = layer.createView({ width: win.width, height: win.height, clearColor: [0.05, 0.05, 0.09, 1], label: "world" })
|
|
85
85
|
let glowView = glow.createView({ width: win.width, height: win.height, clearColor: [0, 0, 0, 0], label: "glow" })
|
|
86
86
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.72",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "src/index.ts",
|
|
9
9
|
"exports": {
|
|
10
|
-
".": "./src/index.ts"
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./glsl": "./src/glsl.ts"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"src/",
|
|
@@ -15,10 +16,10 @@
|
|
|
15
16
|
"AGENTS.md"
|
|
16
17
|
],
|
|
17
18
|
"devDependencies": {
|
|
18
|
-
"@solidrt/test": "0.0.
|
|
19
|
+
"@solidrt/test": "0.0.72"
|
|
19
20
|
},
|
|
20
21
|
"peerDependencies": {
|
|
21
22
|
"@solidjs/signals": "2.0.0-rc.13",
|
|
22
|
-
"@solidrt/core": "0.0.
|
|
23
|
+
"@solidrt/core": "0.0.72"
|
|
23
24
|
}
|
|
24
25
|
}
|
package/src/animation.ts
CHANGED
|
@@ -6,23 +6,42 @@
|
|
|
6
6
|
// nothing - the demand-gate story unchanged. The clock is the app's frame
|
|
7
7
|
// time: the clip freezes and resumes with the dev tools' clock, plays at
|
|
8
8
|
// its own speed under `sol render`, and a step lands on the frame it is
|
|
9
|
-
// due in
|
|
9
|
+
// due in; with a `clock` it runs at that layer's, group's or sprite's
|
|
10
|
+
// time rate instead, so one setTimeScale pauses the walk cycles with the
|
|
11
|
+
// world. The clip
|
|
10
12
|
// does not own its sprites (destroySprite prunes lazily on the next step),
|
|
11
13
|
// and a sprite belongs to at most one animation. Plain JS over setSprite,
|
|
12
14
|
// so it works on both layer kinds and composes with <Sprite> via ref (leave
|
|
13
15
|
// the frame prop off - the clip owns that field).
|
|
14
16
|
import { getOwner, onCleanup, onFrame, runWithOwner } from "@solidrt/core"
|
|
15
17
|
import type { Frame } from "./frames.ts"
|
|
16
|
-
import type { Sprite } from "./layer.ts"
|
|
17
|
-
import { setSprite } from "./layer.ts"
|
|
18
|
+
import type { Sprite, SpriteGroup, SpriteLayer } from "./layer.ts"
|
|
19
|
+
import { setSprite, timeRate } from "./layer.ts"
|
|
18
20
|
|
|
19
21
|
export type AnimationOptions = {
|
|
20
22
|
/** Wrap around (default) or play once and hold the last frame. */
|
|
21
23
|
loop?: boolean
|
|
24
|
+
/**
|
|
25
|
+
* Run on this layer's, group's or sprite's time (its `timeRate`, read
|
|
26
|
+
* every frame) instead of plain app time: the clip's step is the frame
|
|
27
|
+
* step times the rate, so a layer paused with `setTimeScale(0)` holds
|
|
28
|
+
* the walk cycles too, and a slow-motion group slows the flipbooks in
|
|
29
|
+
* it. One clock per clip, since every attached sprite shows the same
|
|
30
|
+
* frame; a destroyed clock reads as app time.
|
|
31
|
+
*/
|
|
32
|
+
clock?: SpriteLayer | SpriteGroup | Sprite
|
|
22
33
|
/** Skip the owner-scoped auto-dispose (the core resource contract). */
|
|
23
34
|
autoFree?: boolean
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/** The rate a clip's clock runs at now: the handle's own, or 1 without a
|
|
38
|
+
* clock or for one that is gone (a destroyed sprite, a record sprite). */
|
|
39
|
+
function rateOf(clock: SpriteLayer | SpriteGroup | Sprite | undefined): number {
|
|
40
|
+
if (clock === undefined) return 1
|
|
41
|
+
if ("_root" in clock) return clock.timeRate()
|
|
42
|
+
return timeRate(clock) ?? 1
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
export type SpriteAnimation = {
|
|
27
46
|
/** Current frame index into the clip. */
|
|
28
47
|
frame(): number
|
|
@@ -66,6 +85,7 @@ export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOp
|
|
|
66
85
|
throw new Error(`createAnimation: fps must be positive, got ${fps}`)
|
|
67
86
|
}
|
|
68
87
|
let loop = opts?.loop !== false
|
|
88
|
+
let clock = opts?.clock
|
|
69
89
|
let sprites = new Set<Sprite>()
|
|
70
90
|
let current = 0
|
|
71
91
|
let playing = true
|
|
@@ -116,8 +136,9 @@ export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOp
|
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
function tick(now: number): void {
|
|
119
|
-
// Never backward: across a hot reload the frame time restarts.
|
|
120
|
-
|
|
139
|
+
// Never backward: across a hot reload the frame time restarts. The
|
|
140
|
+
// step is the frame step on the clock's time (rateOf: 1 without one).
|
|
141
|
+
if (lastTick !== null) elapsed += Math.max(0, now - lastTick) * rateOf(clock)
|
|
121
142
|
lastTick = now
|
|
122
143
|
let raw = Math.floor((elapsed * fps) / 1000)
|
|
123
144
|
if (!loop && raw >= frames.length) {
|
package/src/atlas.ts
CHANGED
|
@@ -82,16 +82,20 @@ export function isAtlas(a: unknown): a is Atlas {
|
|
|
82
82
|
* Validate a layer's `atlases` list (throws - the dev validation policy):
|
|
83
83
|
* a non-empty array of distinct atlas records, no more than the device
|
|
84
84
|
* binds in one pass (`limits.maxTextureUnits`, 16 on every GLES 3.0
|
|
85
|
-
* device)
|
|
85
|
+
* device) once the material's own `samplers` (its textures: a palette
|
|
86
|
+
* LUT, a noise texture) are counted in - the two lists bind on the same
|
|
87
|
+
* targets. Returns the texture -> sampler index map the layer resolves
|
|
86
88
|
* frames through; the index is the atlas's position in the list.
|
|
87
89
|
* Internal - every layer kind calls it.
|
|
88
90
|
*/
|
|
89
|
-
export function checkAtlases(verb: string, atlases: readonly Atlas[]): Map<TextureId, number> {
|
|
91
|
+
export function checkAtlases(verb: string, atlases: readonly Atlas[], samplers = 0): Map<TextureId, number> {
|
|
90
92
|
if (!Array.isArray(atlases) || atlases.length === 0) {
|
|
91
93
|
throw new Error(`${verb}: atlases must be a non-empty array of atlas records (createAtlas, or { texture, width, height })`)
|
|
92
94
|
}
|
|
93
|
-
if (atlases.length > limits.maxTextureUnits) {
|
|
94
|
-
throw new Error(
|
|
95
|
+
if (atlases.length + samplers > limits.maxTextureUnits) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`${verb}: ${atlases.length} atlases${samplers > 0 ? ` plus the material's ${samplers} textures` : ""} exceed the ${limits.maxTextureUnits} sampler inputs one pass may bind (limits.maxTextureUnits); pack sheets together`,
|
|
98
|
+
)
|
|
95
99
|
}
|
|
96
100
|
let index = new Map<TextureId, number>()
|
|
97
101
|
atlases.forEach((atlas, i) => {
|
package/src/camera.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// imports, so its test (tests/camera.test.ts) exercises this
|
|
4
4
|
// module headless on the flux binary. Three consumers implement or undo
|
|
5
5
|
// exactly this mapping and must agree with it: the vertex stages
|
|
6
|
-
// (
|
|
6
|
+
// (glsl.ts, uCamera + uCameraRot), the tile layer's composite
|
|
7
7
|
// transform (the <view> props in components/tile-layer.tsx) and the pointer inverse
|
|
8
8
|
// (spriteDispatch in layer.ts, which calls unprojectCamera directly). The
|
|
9
9
|
// check keeps the spellings from drifting.
|
|
@@ -101,7 +101,7 @@ export function applyCamera(camera: CameraState, update: CameraUpdate): void {
|
|
|
101
101
|
if (update.pivotY !== undefined) camera.pivotY = update.pivotY
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** The shared params the vertex
|
|
104
|
+
/** The shared params the vertex stage (glsl.ts) reads for a camera:
|
|
105
105
|
* uCamera [x, y, zoom, zoom] and uCameraRot [cos, sin, pivotX, pivotY]. */
|
|
106
106
|
export function cameraParams(camera: CameraState): { uCamera: [number, number, number, number]; uCameraRot: [number, number, number, number] } {
|
|
107
107
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEffect, merge, onFrame, untrack } from "@solidrt/core"
|
|
1
|
+
import { callRef, createEffect, merge, onFrame, untrack } from "@solidrt/core"
|
|
2
2
|
import type { InputMap, VoidComponent } from "@solidrt/core"
|
|
3
3
|
import { createCamera2d } from "../camera2d.ts"
|
|
4
4
|
import type { Camera2d as Camera2dHandle, Camera2dAxes, Camera2dOptions } from "../camera2d.ts"
|
|
@@ -72,6 +72,6 @@ export let Camera2d: VoidComponent<Camera2dProps> = props => {
|
|
|
72
72
|
() => untrack(() => cam.set({})),
|
|
73
73
|
{ defer: true },
|
|
74
74
|
)
|
|
75
|
-
|
|
75
|
+
callRef(() => props.ref, cam)
|
|
76
76
|
return null
|
|
77
77
|
}
|