@solidrt/2d 0.0.58 → 0.0.60
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 +55 -23
- package/examples/anim.tsx +3 -3
- package/examples/parity.tsx +4 -4
- package/examples/springs.tsx +29 -7
- package/package.json +3 -3
- package/src/animation.ts +5 -9
- package/src/camera2d.ts +20 -3
- package/src/components/group.tsx +6 -6
- package/src/components/sprite-layer.tsx +8 -0
- package/src/components/sprite.tsx +6 -6
- package/src/components/view2d.tsx +8 -0
- package/src/dispatch.ts +2 -2
- package/src/index.ts +6 -3
- package/src/layer.ts +231 -52
- package/src/records.ts +6 -6
- package/src/shaders.ts +22 -18
- package/src/tiles.ts +4 -4
- package/src/views.ts +13 -13
package/src/layer.ts
CHANGED
|
@@ -30,13 +30,21 @@ import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEve
|
|
|
30
30
|
import { beginBufferWrite, createBuffer, destroyBuffer, endBufferWrite } from "@solidrt/core/gpu"
|
|
31
31
|
import type { BufferId, TextureId } from "@solidrt/core/gpu"
|
|
32
32
|
import * as spatial from "flux:spatial"
|
|
33
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
Impact as CoreImpact,
|
|
35
|
+
MoveOptions as CoreMoveOptions,
|
|
36
|
+
NodeEndpoint,
|
|
37
|
+
NodeId,
|
|
38
|
+
NodeTransition,
|
|
39
|
+
NodeTransitionSpec,
|
|
40
|
+
QueryFilter,
|
|
41
|
+
} from "flux:spatial"
|
|
34
42
|
import { on } from "srt:events"
|
|
35
43
|
import type { CameraState, CameraUpdate } from "./camera.ts"
|
|
36
44
|
import type { Frame } from "./frames.ts"
|
|
37
45
|
import { FULL_FRAME, writeFrame } from "./frames.ts"
|
|
38
46
|
import type { RecordLayer } from "./records.ts"
|
|
39
|
-
import { createSpritePipeline,
|
|
47
|
+
import { createSpritePipeline, INSTANCE_LAYOUTS_SPLIT, VERTEX_SPLIT } from "./shaders.ts"
|
|
40
48
|
import { createViews } from "./views.ts"
|
|
41
49
|
import type { ViewHandle, ViewOptions } from "./views.ts"
|
|
42
50
|
|
|
@@ -93,6 +101,26 @@ const WORLD = new Float32Array(16)
|
|
|
93
101
|
let declared = new Map<NodeId, Sprite | SpriteGroup>()
|
|
94
102
|
let subscribed = false
|
|
95
103
|
|
|
104
|
+
// Free routing: a destroyed sprite or group whose declaration carries an
|
|
105
|
+
// `exit` stays in the core as a LEAVING node (drawn, picked by nothing)
|
|
106
|
+
// until its exits settle, and the core's "spatialNodeFreed" event says
|
|
107
|
+
// when it is gone - the cue to recycle what the layer kept for it (a
|
|
108
|
+
// sprite's pose slot). One lazy subscription, keyed by node.
|
|
109
|
+
let freeing = new Map<NodeId, () => void>()
|
|
110
|
+
let freeingSubscribed = false
|
|
111
|
+
|
|
112
|
+
function onFreed(node: NodeId, done: () => void): void {
|
|
113
|
+
freeing.set(node, done)
|
|
114
|
+
if (freeingSubscribed) return
|
|
115
|
+
freeingSubscribed = true
|
|
116
|
+
on("spatialNodeFreed", (event: { node: NodeId }) => {
|
|
117
|
+
let done = freeing.get(event.node)
|
|
118
|
+
if (!done) return
|
|
119
|
+
freeing.delete(event.node)
|
|
120
|
+
done()
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
function declareTransition(node: NodeId, handle: Sprite | SpriteGroup, transition: NodeTransition | string | null): void {
|
|
97
125
|
if (transition === null) {
|
|
98
126
|
declared.delete(node)
|
|
@@ -118,12 +146,12 @@ function declareTransition(node: NodeId, handle: Sprite | SpriteGroup, transitio
|
|
|
118
146
|
* fields - they touch no GPU state (the scene-graph handler rule).
|
|
119
147
|
*/
|
|
120
148
|
export type Sprite = {
|
|
121
|
-
/** The owning layer, null after
|
|
149
|
+
/** The owning layer, null after destroySprite (which owns the write). */
|
|
122
150
|
readonly layer: SpriteLayer | RecordLayer | null
|
|
123
151
|
/**
|
|
124
152
|
* The sprite's SPATIAL ARENA node - the citizenship handle: bind core
|
|
125
153
|
* producers to it or reach it through flux:spatial directly (the layer
|
|
126
|
-
* still owns the node's life; destroy it only via
|
|
154
|
+
* still owns the node's life; destroy it only via destroySprite). Null
|
|
127
155
|
* on a record layer's sprites.
|
|
128
156
|
*/
|
|
129
157
|
readonly node: NodeId | null
|
|
@@ -313,7 +341,9 @@ export type LayerPointerListener = {
|
|
|
313
341
|
onTap?: (event: LayerTapEvent) => void
|
|
314
342
|
}
|
|
315
343
|
|
|
316
|
-
|
|
344
|
+
/** The element handlers for the leaf that shows a view (`ViewHandle.handlers`);
|
|
345
|
+
* spread onto the host element. */
|
|
346
|
+
export type LayerHandlers = {
|
|
317
347
|
onPointerDown: (event: ElementPointerEvent) => void
|
|
318
348
|
onPointerMove: (event: ElementPointerEvent) => void
|
|
319
349
|
onPointerUp: (event: ElementPointerEvent) => void
|
|
@@ -343,6 +373,12 @@ export type SpriteLayerOptions = {
|
|
|
343
373
|
/** Names the GPU resources (buffers, pipeline; views default to
|
|
344
374
|
* `<label>-view`); default "sprites". */
|
|
345
375
|
label?: string
|
|
376
|
+
/** Stagger (ms) on the layer's root: every sprite or group added
|
|
377
|
+
* straight under the layer (no group) that enters or leaves in one
|
|
378
|
+
* frame is spaced by `index * stagger` - the whole-layer form of a
|
|
379
|
+
* group's `stagger` (a group declaring its own wins for what is under
|
|
380
|
+
* it). See setStagger. */
|
|
381
|
+
stagger?: number
|
|
346
382
|
/** Skip the owner-scoped auto-dispose (see createSpriteLayer). */
|
|
347
383
|
autoFree?: boolean
|
|
348
384
|
/**
|
|
@@ -381,7 +417,7 @@ export type SpriteLayerOptions = {
|
|
|
381
417
|
* on the sprite alone), so one group handler covers a whole assembly.
|
|
382
418
|
*/
|
|
383
419
|
export type SpriteGroup = {
|
|
384
|
-
/** The owning layer, null after
|
|
420
|
+
/** The owning layer, null after destroyGroup (which owns the write). */
|
|
385
421
|
readonly layer: SpriteLayer | null
|
|
386
422
|
/** The group's spatial arena node. */
|
|
387
423
|
readonly node: NodeId
|
|
@@ -395,7 +431,7 @@ export type SpriteGroup = {
|
|
|
395
431
|
readonly _visible: boolean
|
|
396
432
|
/** See Sprite._parent. */
|
|
397
433
|
readonly _parent: SpriteGroup | null
|
|
398
|
-
/** The handles parented here (
|
|
434
|
+
/** The handles parented here (destroyGroup removes them with it);
|
|
399
435
|
* internal -
|
|
400
436
|
* membership writes go through addSprite/addGroup/setSpriteParent/
|
|
401
437
|
* setGroup. */
|
|
@@ -476,7 +512,7 @@ export type LayerBase = {
|
|
|
476
512
|
_add(opts?: AddSpriteOptions): Sprite
|
|
477
513
|
_write(sprite: SpriteState, opts: SpriteOptions): void
|
|
478
514
|
_read(sprite: Sprite): Required<SpriteOptions>
|
|
479
|
-
|
|
515
|
+
_destroy(sprite: SpriteState): void
|
|
480
516
|
_schedule(): void
|
|
481
517
|
}
|
|
482
518
|
|
|
@@ -499,7 +535,7 @@ export type QueryOptions = {
|
|
|
499
535
|
|
|
500
536
|
/** One sprite a raycast() ray strikes: the distance along the ray, the
|
|
501
537
|
* point on the sprite's edge and the unit edge normal facing the ray. */
|
|
502
|
-
export type
|
|
538
|
+
export type Hit = { sprite: Sprite; distance: number; point: [number, number]; normal: [number, number] }
|
|
503
539
|
|
|
504
540
|
/** One sprite an overlap() volume touches: its deepest contact - the
|
|
505
541
|
* point on the sprite's edge, the unit direction out of the sprite, and
|
|
@@ -564,7 +600,7 @@ export type SpriteLayer = LayerBase & {
|
|
|
564
600
|
* Reads the index as of the last flush, the pending batch run first,
|
|
565
601
|
* like pick.
|
|
566
602
|
*/
|
|
567
|
-
raycast(x: number, y: number, dx: number, dy: number, opts?: QueryOptions):
|
|
603
|
+
raycast(x: number, y: number, dx: number, dy: number, opts?: QueryOptions): Hit[]
|
|
568
604
|
/**
|
|
569
605
|
* Every shown sprite the volume touches, each with its deepest contact
|
|
570
606
|
* (Godot's intersect_shape, Unity's OverlapCircle/Box/Capsule: a blast
|
|
@@ -600,9 +636,15 @@ export type SpriteLayer = LayerBase & {
|
|
|
600
636
|
*/
|
|
601
637
|
moveAndSlide(volume: Volume, dx: number, dy: number, opts?: MoveOptions): MoveResult
|
|
602
638
|
_groups: Set<GroupState>
|
|
639
|
+
/** Set (or with null clear) the stagger on the layer's root: the
|
|
640
|
+
* spacing, in ms, of the enters and exits of the sprites and groups
|
|
641
|
+
* straight under the layer that begin in one frame (SpriteLayerOptions
|
|
642
|
+
* `stagger`). */
|
|
643
|
+
setStagger(ms: number | null): void
|
|
603
644
|
/** The core node every sprite and group of the layer sits under: what
|
|
604
645
|
* scopes the layer's queries in the arena shared with 3d scenes. */
|
|
605
646
|
_root: NodeId
|
|
647
|
+
_destroyGroup(group: GroupState): void
|
|
606
648
|
}
|
|
607
649
|
|
|
608
650
|
/** The stored UVs at `at` un-mirrored by the sprite's flags: the frame as
|
|
@@ -698,7 +740,7 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
698
740
|
checkTint("createSpriteLayer", tint)
|
|
699
741
|
let pose: BufferId = createBuffer(capacity * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
700
742
|
let style: BufferId = createBuffer(capacity * STYLE_FLOATS * 4, { label: `${label}-style`, autoFree: false })
|
|
701
|
-
let gpu = createSpritePipeline(label, VERTEX_SPLIT,
|
|
743
|
+
let gpu = createSpritePipeline(label, VERTEX_SPLIT, INSTANCE_LAYOUTS_SPLIT)
|
|
702
744
|
|
|
703
745
|
let disposed = false
|
|
704
746
|
let scheduled = false
|
|
@@ -710,6 +752,9 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
710
752
|
// collapses the instance - so holes draw nothing).
|
|
711
753
|
let highWater = 0
|
|
712
754
|
let freeSlots: number[] = []
|
|
755
|
+
// Destroyed handles still leaving in the core (see onFreed): a sprite's
|
|
756
|
+
// slot stays taken until the core says it is gone; dispose frees them.
|
|
757
|
+
let leaving = new Set<NodeId>()
|
|
713
758
|
let gpuCapacity = capacity
|
|
714
759
|
let styleData = new Float32Array(capacity * STYLE_FLOATS)
|
|
715
760
|
let byNode = new Map<NodeId, SpriteState>()
|
|
@@ -718,6 +763,7 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
718
763
|
fillTransform(0, 0, 0, 1, 1)
|
|
719
764
|
let root = spatial.createNode(TRANSFORM, true)
|
|
720
765
|
let filter: QueryFilter = { root }
|
|
766
|
+
if (opts?.stagger !== undefined) spatial.setTransition(root, { stagger: opts.stagger })
|
|
721
767
|
// Refill the layer's one filter for a query: the root always, the
|
|
722
768
|
// include-list as the sprites' nodes when given.
|
|
723
769
|
let queryFilter = (opts: QueryOptions | undefined, site: string): void => {
|
|
@@ -768,7 +814,7 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
768
814
|
let grownStyle = new Float32Array(next * STYLE_FLOATS)
|
|
769
815
|
grownStyle.set(styleData)
|
|
770
816
|
styleData = grownStyle
|
|
771
|
-
views.setBuffers(
|
|
817
|
+
views.setBuffers([newPose, newStyle])
|
|
772
818
|
destroyBuffer(pose)
|
|
773
819
|
destroyBuffer(style)
|
|
774
820
|
pose = newPose
|
|
@@ -810,7 +856,7 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
810
856
|
pipeline: gpu.pipeline,
|
|
811
857
|
quad: gpu.quad,
|
|
812
858
|
atlas,
|
|
813
|
-
buffers: () =>
|
|
859
|
+
buffers: () => [pose, style],
|
|
814
860
|
count: () => published,
|
|
815
861
|
tint: () => tint,
|
|
816
862
|
pick: (x, y) => layer.pick(x, y),
|
|
@@ -823,7 +869,7 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
823
869
|
opts?.orderBy === "y"
|
|
824
870
|
? { field: POSE_Y_FIELD }
|
|
825
871
|
: opts?.orderBy === "renderOrder"
|
|
826
|
-
? { field: STYLE_KEY_FIELD,
|
|
872
|
+
? { field: STYLE_KEY_FIELD, buffer: 2 }
|
|
827
873
|
: undefined,
|
|
828
874
|
})
|
|
829
875
|
|
|
@@ -874,7 +920,7 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
874
920
|
RAY_DIR[1] = dy
|
|
875
921
|
RAY_DIR[2] = 0
|
|
876
922
|
queryFilter(opts, "raycast")
|
|
877
|
-
let out:
|
|
923
|
+
let out: Hit[] = []
|
|
878
924
|
for (let h of spatial.raycast(RAY_ORIGIN, RAY_DIR, filter)) {
|
|
879
925
|
let sprite = byNode.get(h.node)
|
|
880
926
|
if (sprite) out.push({ sprite, distance: h.distance, point: [h.point[0], h.point[1]], normal: [h.normal[0], h.normal[1]] })
|
|
@@ -949,6 +995,12 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
949
995
|
spatial.destroyNode(group.node)
|
|
950
996
|
}
|
|
951
997
|
layer._groups.clear()
|
|
998
|
+
// Corpses mid-exit go now: their slots must not outlive the buffer.
|
|
999
|
+
for (let node of leaving) {
|
|
1000
|
+
freeing.delete(node)
|
|
1001
|
+
spatial.destroyNode(node)
|
|
1002
|
+
}
|
|
1003
|
+
leaving.clear()
|
|
952
1004
|
spatial.destroyNode(root)
|
|
953
1005
|
// Let the core emit its final slot-zeroing writes while the pose
|
|
954
1006
|
// buffer still exists, then free everything.
|
|
@@ -1035,20 +1087,45 @@ export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions):
|
|
|
1035
1087
|
visible: sprite._visible,
|
|
1036
1088
|
}
|
|
1037
1089
|
},
|
|
1038
|
-
|
|
1090
|
+
_destroy(sprite) {
|
|
1039
1091
|
sprite.layer = null
|
|
1040
1092
|
if (sprite._parent) {
|
|
1041
1093
|
sprite._parent._children.delete(sprite)
|
|
1042
1094
|
sprite._parent = null
|
|
1043
1095
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
declared
|
|
1048
|
-
|
|
1049
|
-
|
|
1096
|
+
let node = sprite.node!
|
|
1097
|
+
byNode.delete(node)
|
|
1098
|
+
declared.delete(node)
|
|
1099
|
+
// A declared exit keeps the node leaving: its slot keeps drawing the
|
|
1100
|
+
// exit until the core frees it (the freed event lands after the
|
|
1101
|
+
// flush that zeroed the slot), then recycles. Otherwise the free is
|
|
1102
|
+
// immediate: the slot zeroes at the next core flush (zero scale =
|
|
1103
|
+
// nothing drawn) and recycles now.
|
|
1104
|
+
let slot = sprite._slot
|
|
1105
|
+
if (spatial.exitNode(node)) {
|
|
1106
|
+
leaving.add(node)
|
|
1107
|
+
onFreed(node, () => {
|
|
1108
|
+
leaving.delete(node)
|
|
1109
|
+
freeSlots.push(slot)
|
|
1110
|
+
})
|
|
1111
|
+
} else {
|
|
1112
|
+
freeSlots.push(slot)
|
|
1113
|
+
}
|
|
1050
1114
|
layer._schedule()
|
|
1051
1115
|
},
|
|
1116
|
+
_destroyGroup(group) {
|
|
1117
|
+
let node = group.node
|
|
1118
|
+
declared.delete(node)
|
|
1119
|
+
if (spatial.exitNode(node)) {
|
|
1120
|
+
leaving.add(node)
|
|
1121
|
+
onFreed(node, () => leaving.delete(node))
|
|
1122
|
+
}
|
|
1123
|
+
layer._schedule()
|
|
1124
|
+
},
|
|
1125
|
+
setStagger(ms) {
|
|
1126
|
+
if (disposed) return
|
|
1127
|
+
spatial.setTransition(root, ms === null ? null : { stagger: ms })
|
|
1128
|
+
},
|
|
1052
1129
|
_schedule() {
|
|
1053
1130
|
if (disposed || scheduled) return
|
|
1054
1131
|
scheduled = true
|
|
@@ -1086,11 +1163,19 @@ export function getSprite(sprite: Sprite): Required<SpriteOptions> | null {
|
|
|
1086
1163
|
}
|
|
1087
1164
|
|
|
1088
1165
|
/**
|
|
1089
|
-
*
|
|
1090
|
-
* calls are no-ops
|
|
1166
|
+
* Destroy a sprite - PixiJS's `sprite.destroy()`, Unity's Destroy: the
|
|
1167
|
+
* handle goes inert (layer null; further setSprite calls are no-ops) and
|
|
1168
|
+
* the sprite is gone for good. With an `exit` in its transition
|
|
1169
|
+
* declaration it animates out first: each component with one plays to
|
|
1170
|
+
* its exit value and the sprite stays drawn meanwhile, a ghost that no
|
|
1171
|
+
* pick, raycast, overlap or sweep sees, then frees when the last exit
|
|
1172
|
+
* settles (no onTransitionEnd fires for an exit: the app already let go).
|
|
1173
|
+
* The one removal verb on a sprite: it cannot exist outside its layer, so
|
|
1174
|
+
* there is no detach; a sprite that should come back is hidden
|
|
1175
|
+
* (`visible: false`), Unity's SetActive.
|
|
1091
1176
|
*/
|
|
1092
|
-
export function
|
|
1093
|
-
sprite.layer?.
|
|
1177
|
+
export function destroySprite(sprite: Sprite): void {
|
|
1178
|
+
sprite.layer?._destroy(sprite)
|
|
1094
1179
|
}
|
|
1095
1180
|
|
|
1096
1181
|
/**
|
|
@@ -1106,7 +1191,7 @@ export function removeSprite(sprite: Sprite): void {
|
|
|
1106
1191
|
export function worldPosition(target: Sprite | SpriteGroup): [number, number] | null {
|
|
1107
1192
|
if (target.layer === null) return null
|
|
1108
1193
|
// The set only groups carry tells the two handle kinds apart (as in
|
|
1109
|
-
//
|
|
1194
|
+
// destroyGroup); a node-less sprite is a record layer's.
|
|
1110
1195
|
if (!("_children" in target) && target.node === null) {
|
|
1111
1196
|
let s = target.layer._read(target)
|
|
1112
1197
|
return [s.x, s.y]
|
|
@@ -1136,6 +1221,92 @@ export function setSpriteParent(sprite: Sprite, parent: SpriteGroup | null): voi
|
|
|
1136
1221
|
layer._schedule()
|
|
1137
1222
|
}
|
|
1138
1223
|
|
|
1224
|
+
/** A lifecycle endpoint in the object form: the value (2d units, as the
|
|
1225
|
+
* bare form) plus the motion that direction plays - a field left out is
|
|
1226
|
+
* the entry's; naming a `curve` or a `bounce` decides the kind outright,
|
|
1227
|
+
* so an ease-out enter pairs with an ease-in exit. */
|
|
1228
|
+
export type SpriteEndpoint<Value> = {
|
|
1229
|
+
value: Value
|
|
1230
|
+
duration?: number
|
|
1231
|
+
curve?: Extract<NodeTransitionSpec, { curve: unknown }>["curve"]
|
|
1232
|
+
bounce?: number
|
|
1233
|
+
delay?: number
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** One spec of a sprite or group transition: the node vocabulary
|
|
1237
|
+
* (`{ duration, bounce? }` a spring, `{ duration, curve }` a tween, or a
|
|
1238
|
+
* shorthand string like "300ms ease-out 100ms"; `delay` in ms holds every
|
|
1239
|
+
* write that long first) with the lifecycle endpoints in 2d units, each a
|
|
1240
|
+
* bare value or a SpriteEndpoint: `from` is the component's enter value,
|
|
1241
|
+
* where a freshly added sprite starts before animating to its mount pose;
|
|
1242
|
+
* `exit` where it animates to when destroySprite/destroyGroup lets go of
|
|
1243
|
+
* it. */
|
|
1244
|
+
export type SpriteTransitionSpec<Value> =
|
|
1245
|
+
| { duration: number; bounce?: number; delay?: number; from?: Value | SpriteEndpoint<Value>; exit?: Value | SpriteEndpoint<Value> }
|
|
1246
|
+
| {
|
|
1247
|
+
duration: number
|
|
1248
|
+
curve: Extract<NodeTransitionSpec, { curve: unknown }>["curve"]
|
|
1249
|
+
delay?: number
|
|
1250
|
+
from?: Value | SpriteEndpoint<Value>
|
|
1251
|
+
exit?: Value | SpriteEndpoint<Value>
|
|
1252
|
+
}
|
|
1253
|
+
| string
|
|
1254
|
+
|
|
1255
|
+
/** A sprite or group transition declaration: a spec per pose component
|
|
1256
|
+
* plus `all` as a catch-all. `position` is x/y (`from: [x, y]`),
|
|
1257
|
+
* `rotation` the rotation in radians (`from: angle`), `scale` a sprite's
|
|
1258
|
+
* w/h (`from: [w, h]`) or a group's uniform scale (`from: s`); `exit`
|
|
1259
|
+
* takes the same units. `stagger` (ms) goes on a GROUP's declaration and
|
|
1260
|
+
* spaces the enters and exits of the sprites and groups under it that
|
|
1261
|
+
* begin in one frame by `index * stagger` (add order for enters, tree
|
|
1262
|
+
* order for exits; the nearest declaring group wins); a sprite has no
|
|
1263
|
+
* children, so on a sprite it does nothing. */
|
|
1264
|
+
export type SpriteTransition = {
|
|
1265
|
+
position?: SpriteTransitionSpec<[number, number]>
|
|
1266
|
+
rotation?: SpriteTransitionSpec<number>
|
|
1267
|
+
scale?: SpriteTransitionSpec<[number, number] | number>
|
|
1268
|
+
all?: NodeTransitionSpec
|
|
1269
|
+
stagger?: number
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/** The 2d declaration in the arena's own lanes: endpoint values lifted
|
|
1273
|
+
* into the plane (z = 0, a unit z scale, the rotation a quaternion about
|
|
1274
|
+
* z), bare or inside the endpoint object; everything else passes
|
|
1275
|
+
* through. */
|
|
1276
|
+
function toNodeTransition(transition: SpriteTransition | string | null): NodeTransition | string | null {
|
|
1277
|
+
if (transition === null || typeof transition === "string") return transition
|
|
1278
|
+
let out: NodeTransition = {}
|
|
1279
|
+
if (transition.all !== undefined) out.all = transition.all
|
|
1280
|
+
if (transition.stagger !== undefined) out.stagger = transition.stagger
|
|
1281
|
+
if (transition.position !== undefined) out.position = liftSpec(transition.position, ([x, y]) => [x, y, 0])
|
|
1282
|
+
if (transition.rotation !== undefined) {
|
|
1283
|
+
out.rotation = liftSpec(transition.rotation, angle => [0, 0, Math.sin(angle / 2), Math.cos(angle / 2)])
|
|
1284
|
+
}
|
|
1285
|
+
if (transition.scale !== undefined) {
|
|
1286
|
+
out.scale = liftSpec(transition.scale, s => (typeof s === "number" ? [s, s, 1] : [s[0], s[1], 1]))
|
|
1287
|
+
}
|
|
1288
|
+
return out
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function liftSpec<Value>(spec: SpriteTransitionSpec<Value>, lift: (value: Value) => number[]): NodeTransitionSpec {
|
|
1292
|
+
if (typeof spec === "string") return spec
|
|
1293
|
+
let { from, exit, ...rest } = spec
|
|
1294
|
+
let out: NodeTransitionSpec = rest
|
|
1295
|
+
if (from !== undefined) out.from = liftEndpoint(from, lift)
|
|
1296
|
+
if (exit !== undefined) out.exit = liftEndpoint(exit, lift)
|
|
1297
|
+
return out
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function liftEndpoint<Value>(endpoint: Value | SpriteEndpoint<Value>, lift: (value: Value) => number[]): number[] | NodeEndpoint {
|
|
1301
|
+
// The object form is the one shape here with a `value` key; a bare
|
|
1302
|
+
// value is a number or a pair.
|
|
1303
|
+
if (typeof endpoint === "object" && endpoint !== null && !Array.isArray(endpoint)) {
|
|
1304
|
+
let { value, ...motion } = endpoint as SpriteEndpoint<Value>
|
|
1305
|
+
return { ...motion, value: lift(value) }
|
|
1306
|
+
}
|
|
1307
|
+
return lift(endpoint as Value)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1139
1310
|
/**
|
|
1140
1311
|
* Declare (or with null clear) how the sprite's pose writes animate: once
|
|
1141
1312
|
* set, setSprite writes are TARGETS the core animates toward - JS writes
|
|
@@ -1145,24 +1316,32 @@ export function setSpriteParent(sprite: Sprite, parent: SpriteGroup | null): voi
|
|
|
1145
1316
|
* rotation (always the short arc) and `scale` its w/h; each spec is
|
|
1146
1317
|
* `{ duration, bounce? }` (a spring, the retargeting-safe default) /
|
|
1147
1318
|
* `{ duration, curve }` (a tween) / a shorthand string like
|
|
1148
|
-
* "300ms ease-out".
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1319
|
+
* "300ms ease-out". A `from` on a component is its enter value: a sprite
|
|
1320
|
+
* declared in the tick that added it starts there and animates to its
|
|
1321
|
+
* mount pose (once, at add; a later declaration animates writes only).
|
|
1322
|
+
* An `exit` is where it animates to when destroySprite lets go of it
|
|
1323
|
+
* (see there); either endpoint takes a SpriteEndpoint to own its
|
|
1324
|
+
* direction's motion, and `delay` holds writes. Clearing cancels running
|
|
1325
|
+
* tracks in place (the sprite keeps its mid-flight pose) and later writes
|
|
1326
|
+
* snap. Each natural settle calls the sprite's `onTransitionEnd` with the
|
|
1327
|
+
* component. Node layer only.
|
|
1152
1328
|
*/
|
|
1153
|
-
export function setSpriteTransition(sprite: Sprite, transition:
|
|
1329
|
+
export function setSpriteTransition(sprite: Sprite, transition: SpriteTransition | string | null): void {
|
|
1154
1330
|
if (sprite.layer === null) return
|
|
1155
1331
|
if (sprite.node === null) throw new Error("setSpriteTransition: record sprites have no node transitions")
|
|
1156
|
-
|
|
1157
|
-
|
|
1332
|
+
let node = toNodeTransition(transition)
|
|
1333
|
+
spatial.setTransition(sprite.node, node)
|
|
1334
|
+
declareTransition(sprite.node, sprite, node)
|
|
1158
1335
|
}
|
|
1159
1336
|
|
|
1160
1337
|
/** The group counterpart of setSpriteTransition (`scale` is the group's
|
|
1161
|
-
* uniform scale)
|
|
1162
|
-
|
|
1338
|
+
* uniform scale), and the home of `stagger`: a group declaring one spaces
|
|
1339
|
+
* the enters and exits of everything under it (see SpriteTransition). */
|
|
1340
|
+
export function setGroupTransition(group: SpriteGroup, transition: SpriteTransition | string | null): void {
|
|
1163
1341
|
if (group.layer === null) return
|
|
1164
|
-
|
|
1165
|
-
|
|
1342
|
+
let node = toNodeTransition(transition)
|
|
1343
|
+
spatial.setTransition(group.node, node)
|
|
1344
|
+
declareTransition(group.node, group, node)
|
|
1166
1345
|
}
|
|
1167
1346
|
|
|
1168
1347
|
/** Add a transform group (see SpriteGroup). */
|
|
@@ -1226,24 +1405,26 @@ export function setGroup(group: SpriteGroup, opts: GroupOptions): void {
|
|
|
1226
1405
|
}
|
|
1227
1406
|
|
|
1228
1407
|
/**
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1408
|
+
* Destroy a group AND everything under it: child sprites and groups die
|
|
1409
|
+
* with it - Unity's Destroy, Godot's queue_free, the subtree form of
|
|
1410
|
+
* destroySprite (@solidrt/3d's `destroy` is the same verb one dimension
|
|
1411
|
+
* up). To keep a child, re-parent it out first (setSpriteParent /
|
|
1412
|
+
* setGroup's `parent`). Every destroyed handle goes inert. Exits play
|
|
1413
|
+
* through the whole subtree: children are let go of first, each
|
|
1414
|
+
* animating its own `exit`, and the group - its own exit or not - stays
|
|
1415
|
+
* in the core as their frame until the last of them has settled, then
|
|
1416
|
+
* frees. A component tree unmounts children first and never sees the
|
|
1417
|
+
* recursion.
|
|
1237
1418
|
*/
|
|
1238
|
-
export function
|
|
1419
|
+
export function destroyGroup(group: SpriteGroup): void {
|
|
1239
1420
|
let layer = group.layer
|
|
1240
1421
|
if (!layer) return
|
|
1241
1422
|
let g: GroupState = group
|
|
1242
1423
|
// Children first, over a snapshot (each removal edits _children); the
|
|
1243
1424
|
// set only groups carry tells the two handle kinds apart.
|
|
1244
1425
|
for (let child of [...g._children]) {
|
|
1245
|
-
if ("_children" in child)
|
|
1246
|
-
else
|
|
1426
|
+
if ("_children" in child) destroyGroup(child)
|
|
1427
|
+
else destroySprite(child)
|
|
1247
1428
|
}
|
|
1248
1429
|
g.layer = null
|
|
1249
1430
|
if (g._parent) {
|
|
@@ -1251,7 +1432,5 @@ export function removeGroup(group: SpriteGroup): void {
|
|
|
1251
1432
|
g._parent = null
|
|
1252
1433
|
}
|
|
1253
1434
|
layer._groups.delete(group)
|
|
1254
|
-
|
|
1255
|
-
spatial.destroyNode(group.node)
|
|
1256
|
-
layer._schedule()
|
|
1435
|
+
layer._destroyGroup(g)
|
|
1257
1436
|
}
|
package/src/records.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type RecordLayerOptions = Omit<SpriteLayerOptions, "orderBy"> & {
|
|
|
47
47
|
* further up the screen = drawn first); or an explicit `{ field,
|
|
48
48
|
* descending? }` float offset into the record for a custom sort key.
|
|
49
49
|
* Record slots stay stable - record i keeps meaning sprite i, and
|
|
50
|
-
*
|
|
50
|
+
* destroySprite still shifts - only the draw order changes. Ties keep
|
|
51
51
|
* record order, so an unset key draws exactly as before. Known
|
|
52
52
|
* limitation: pick() resolves overlapping sprites by record order, not
|
|
53
53
|
* visual order, when a key is set.
|
|
@@ -63,7 +63,7 @@ export type RecordLayer = LayerBase & {
|
|
|
63
63
|
* is draw order - unless the layer was created with `orderBy`, which
|
|
64
64
|
* draws in key order while record i keeps meaning sprite i. Write fields
|
|
65
65
|
* directly for large per-frame populations, then call touch() once. Do
|
|
66
|
-
* not cache indices across
|
|
66
|
+
* not cache indices across destroySprite - records shift - and do not
|
|
67
67
|
* cache the array across addSprite - growth replaces it.
|
|
68
68
|
*/
|
|
69
69
|
records: Float32Array
|
|
@@ -104,7 +104,7 @@ export function createRecordLayer(atlas: TextureId, opts?: RecordLayerOptions):
|
|
|
104
104
|
: orderBy === "y"
|
|
105
105
|
? { field: Y_FIELD_OFFSET }
|
|
106
106
|
: { field: orderBy.field, descending: orderBy.descending }
|
|
107
|
-
let gpu = createSpritePipeline(label, VERTEX, INSTANCE_ATTRIBUTES)
|
|
107
|
+
let gpu = createSpritePipeline(label, VERTEX, [INSTANCE_ATTRIBUTES])
|
|
108
108
|
|
|
109
109
|
let disposed = false
|
|
110
110
|
let dirty = false
|
|
@@ -132,7 +132,7 @@ export function createRecordLayer(atlas: TextureId, opts?: RecordLayerOptions):
|
|
|
132
132
|
out.set(layer.records.subarray(0, count * FLOATS_PER_SPRITE))
|
|
133
133
|
endBufferWrite(target, count * FLOATS_PER_SPRITE * 4)
|
|
134
134
|
if (grown !== null) {
|
|
135
|
-
views.setBuffers(
|
|
135
|
+
views.setBuffers([grown])
|
|
136
136
|
views.setCount(count)
|
|
137
137
|
if (instanceOrder !== undefined) {
|
|
138
138
|
// The growth publish above landed BEFORE the swap (the entry must
|
|
@@ -192,7 +192,7 @@ export function createRecordLayer(atlas: TextureId, opts?: RecordLayerOptions):
|
|
|
192
192
|
pipeline: gpu.pipeline,
|
|
193
193
|
quad: gpu.quad,
|
|
194
194
|
atlas,
|
|
195
|
-
buffers: () =>
|
|
195
|
+
buffers: () => [records],
|
|
196
196
|
count: () => published,
|
|
197
197
|
tint: () => tint,
|
|
198
198
|
pick: (x, y) => layer.pick(x, y),
|
|
@@ -282,7 +282,7 @@ export function createRecordLayer(atlas: TextureId, opts?: RecordLayerOptions):
|
|
|
282
282
|
visible: true,
|
|
283
283
|
}
|
|
284
284
|
},
|
|
285
|
-
|
|
285
|
+
_destroy(sprite) {
|
|
286
286
|
// Later sprites shift down one draw slot (order preserved).
|
|
287
287
|
sprite.layer = null
|
|
288
288
|
let index = sprite._slot
|
package/src/shaders.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// against oracles but NOT against these shaders - if you touch one
|
|
22
22
|
// rotation, touch all.
|
|
23
23
|
import { compileShader, createBuffer, createRenderPipeline, destroyBuffer, destroyProgram, destroyRenderPipeline, destroyShader, glsl, linkProgram } from "@solidrt/core/gpu"
|
|
24
|
-
import type { BufferId,
|
|
24
|
+
import type { BufferId, RenderPipelineId, VertexAttribute } from "@solidrt/core/gpu"
|
|
25
25
|
|
|
26
26
|
export let VERTEX = glsl`
|
|
27
27
|
in vec2 aPos;
|
|
@@ -84,11 +84,11 @@ export let FRAGMENT = glsl`
|
|
|
84
84
|
|
|
85
85
|
/** The instance attribute list matching the 13-float record layout. */
|
|
86
86
|
export const INSTANCE_ATTRIBUTES: VertexAttribute[] = [
|
|
87
|
-
{ name: "iCenter", format: "
|
|
88
|
-
{ name: "iSize", format: "
|
|
89
|
-
{ name: "iUv", format: "
|
|
90
|
-
{ name: "iRot", format: "
|
|
91
|
-
{ name: "iTint", format: "
|
|
87
|
+
{ name: "iCenter", format: "float32x2" },
|
|
88
|
+
{ name: "iSize", format: "float32x2" },
|
|
89
|
+
{ name: "iUv", format: "float32x4" },
|
|
90
|
+
{ name: "iRot", format: "float32" },
|
|
91
|
+
{ name: "iTint", format: "float32x4" },
|
|
92
92
|
]
|
|
93
93
|
|
|
94
94
|
export let VERTEX_SPLIT = glsl`
|
|
@@ -119,15 +119,20 @@ export let VERTEX_SPLIT = glsl`
|
|
|
119
119
|
}
|
|
120
120
|
`
|
|
121
121
|
|
|
122
|
-
/** The split layout:
|
|
123
|
-
* (iRenderOrder is layout-only - see the
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
/** The split layout: one instance buffer holding the Pose2D record, a
|
|
123
|
+
* second holding the style record (iRenderOrder is layout-only - see the
|
|
124
|
+
* header note). */
|
|
125
|
+
export const INSTANCE_LAYOUTS_SPLIT: VertexAttribute[][] = [
|
|
126
|
+
[
|
|
127
|
+
{ name: "iPos", format: "float32x2" },
|
|
128
|
+
{ name: "iRot", format: "float32" },
|
|
129
|
+
{ name: "iScale", format: "float32x2" },
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
{ name: "iUv", format: "float32x4" },
|
|
133
|
+
{ name: "iTint", format: "float32x4" },
|
|
134
|
+
{ name: "iRenderOrder", format: "float32" },
|
|
135
|
+
],
|
|
131
136
|
]
|
|
132
137
|
|
|
133
138
|
/** What a layer's targets draw with: the unit quad every instance reuses
|
|
@@ -142,7 +147,7 @@ export type SpritePipeline = { quad: BufferId; pipeline: RenderPipelineId; dispo
|
|
|
142
147
|
* view - add entries over the one pipeline. The program lives as long as
|
|
143
148
|
* the pipeline; `dispose` frees both and the quad.
|
|
144
149
|
*/
|
|
145
|
-
export function createSpritePipeline(label: string, vertex: string,
|
|
150
|
+
export function createSpritePipeline(label: string, vertex: string, instanceLayouts: VertexAttribute[][]): SpritePipeline {
|
|
146
151
|
// One unit quad (triangle strip), reused by every instance.
|
|
147
152
|
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), {
|
|
148
153
|
label: `${label}-quad`,
|
|
@@ -156,8 +161,7 @@ export function createSpritePipeline(label: string, vertex: string, instanceAttr
|
|
|
156
161
|
let pipeline = createRenderPipeline(program, {
|
|
157
162
|
label,
|
|
158
163
|
topology: "triangle-strip",
|
|
159
|
-
attributes: [{ name: "aPos", format: "
|
|
160
|
-
instanceAttributes,
|
|
164
|
+
buffers: [{ attributes: [{ name: "aPos", format: "float32x2" }] }, ...instanceLayouts.map(attributes => ({ stepMode: "instance" as const, attributes }))],
|
|
161
165
|
blend: "alpha",
|
|
162
166
|
})
|
|
163
167
|
return {
|
package/src/tiles.ts
CHANGED
|
@@ -304,10 +304,10 @@ export function createTileLayer(
|
|
|
304
304
|
label: `${label}-chunk`,
|
|
305
305
|
topology: "triangle-strip",
|
|
306
306
|
vertexCount: 4,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
buffers: [
|
|
308
|
+
{ attributes: [{ name: "aPos", format: "float32x2" }], buffer: quad },
|
|
309
|
+
{ stepMode: "instance", attributes: INSTANCE_ATTRIBUTES, buffer },
|
|
310
|
+
],
|
|
311
311
|
instanceCount: perChunk,
|
|
312
312
|
blend: "alpha",
|
|
313
313
|
textures: { uAtlas: atlas },
|