@solidrt/2d 0.0.70 → 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/src/glsl.ts ADDED
@@ -0,0 +1,399 @@
1
+ // The sprite GLSL and the instance layouts: what a layer's material
2
+ // compiles over, and what an app composes its own stages from
3
+ // (`@solidrt/2d/glsl`, the twin of `@solidrt/3d/glsl`).
4
+ //
5
+ // The VERTEX side is one stage for every layer kind: a unit quad
6
+ // instanced over the layer's records, mapped world -> clip through
7
+ // uCamera (offset + zoom), uCameraRot ([cos, sin, pivotX, pivotY]: the
8
+ // camera rotation about the pivot, identity [1, 0, 0, 0]) and uViewport -
9
+ // the projectCamera mapping (camera.ts) spelled in GLSL; the two must
10
+ // agree. World and clip space are both y-down (core gpu.ts pixel
11
+ // contract), so the mapping carries NO flip anywhere - do not add one.
12
+ // The stage reads its attributes BY NAME from whichever instance buffers
13
+ // the layer binds, which is why the two record layouts below share
14
+ // names: the records layer and the tile layer bind the 20-float
15
+ // RECORD_ATTRIBUTES, the node layer binds the core-written
16
+ // POSE_ATTRIBUTES (slot 0, the Pose2D record [x, y, angle, sx, sy]) and
17
+ // the JS-written SPRITE_ATTRIBUTES (slot 1); a material's own instance
18
+ // buffers follow. iRenderOrder is read by no stage - it exists for the
19
+ // core's instanceOrder (orderBy: "renderOrder"); a declared attribute
20
+ // without an active program attribute is skipped, its bytes pad the
21
+ // stride.
22
+ //
23
+ // The FRAGMENT side is GENERATED per layer (spriteSource): one sampler
24
+ // per declared atlas, the record's flat atlas index picking it - the
25
+ // multi-texture batch of PixiJS and Phaser, where Unity and Godot break
26
+ // the batch on every texture change - with a distance-field decode in
27
+ // the branch of every atlas declared `sdf` (Atlas.sdf). The set fills a
28
+ // `Sprite` struct, the contract every look is written against: the
29
+ // stock fragment (unlitFragment) is a surface slot over it, a custom
30
+ // fragment (shaderMaterialClass) a main of its own over the same set.
31
+ // The layer prepends the set to whatever fragment the material carries,
32
+ // so a fragment never names a sampler or branches on an atlas itself.
33
+ //
34
+ // `outline` is rgb plus a width in WORLD pixels; the vertex stage turns
35
+ // the width into screen pixels through the camera zoom and the
36
+ // screen-size clamp, so an outline scales with its glyph. A colour atlas
37
+ // ignores it. The vertex stage applies the screen-size clamp
38
+ // (SpriteOptions.minScreenPx/maxScreenPx) through core's screenSizeScale:
39
+ // the quad scales uniformly so its smaller axis stays within the bounds
40
+ // at the camera's zoom (the pixels per world unit), and picking applies
41
+ // the JS twin to the rect it tests. The rotations here (clockwise,
42
+ // y-down) and their JS partners must agree: iRot with pointInSprite in
43
+ // pick.ts, uCameraRot with projectCamera in camera.ts. The differential
44
+ // checks guard the JS side against oracles but NOT against these
45
+ // shaders - if you touch one rotation, touch all.
46
+ import { glsl, SCREEN_SIZE_GLSL, VERTEX_FORMATS } from "@solidrt/core/gpu"
47
+ import type { TextureBindings, TextureId, VertexAttribute, VertexFormat } from "@solidrt/core/gpu"
48
+
49
+ /** What the fragment set reads of a declared atlas: whether it is a
50
+ * distance field, and its range (see `Atlas.sdf`). An Atlas record is
51
+ * one. */
52
+ export type AtlasShading = { sdf?: { range: number } }
53
+
54
+ /** The core-written pose record of the node layer (slot 0): the Pose2D
55
+ * projection [x, y, angle, sx, sy]. */
56
+ export const POSE_ATTRIBUTES: VertexAttribute[] = [
57
+ { name: "iPos", format: "float32x2" },
58
+ { name: "iRot", format: "float32" },
59
+ { name: "iScale", format: "float32x2" },
60
+ ]
61
+
62
+ /** The JS-written sprite record of the node layer (slot 1, 16 floats):
63
+ * [u0, v0, u1, v1, tintR, tintG, tintB, tintA, renderOrder, minScreenPx,
64
+ * maxScreenPx, atlas, outlineR, outlineG, outlineB, outlineWidth]. */
65
+ export const SPRITE_ATTRIBUTES: VertexAttribute[] = [
66
+ { name: "iUv", format: "float32x4" },
67
+ { name: "iTint", format: "float32x4" },
68
+ { name: "iRenderOrder", format: "float32" },
69
+ { name: "iScreenPx", format: "float32x2" },
70
+ { name: "iAtlas", format: "float32" },
71
+ { name: "iOutline", format: "float32x4" },
72
+ ]
73
+
74
+ /** The records layer's and the tile layer's one record (20 floats):
75
+ * [cx, cy, w, h, u0, v0, u1, v1, rot, tintR, tintG, tintB, tintA,
76
+ * minScreenPx, maxScreenPx, atlas, outlineR, outlineG, outlineB,
77
+ * outlineWidth]. */
78
+ export const RECORD_ATTRIBUTES: VertexAttribute[] = [
79
+ { name: "iPos", format: "float32x2" },
80
+ { name: "iScale", format: "float32x2" },
81
+ { name: "iUv", format: "float32x4" },
82
+ { name: "iRot", format: "float32" },
83
+ { name: "iTint", format: "float32x4" },
84
+ { name: "iScreenPx", format: "float32x2" },
85
+ { name: "iAtlas", format: "float32" },
86
+ { name: "iOutline", format: "float32x4" },
87
+ ]
88
+
89
+ /** The attribute names the layers' own records and the unit quad take:
90
+ * what a material's instance buffers may not reuse. */
91
+ export const LAYER_ATTRIBUTE_NAMES: readonly string[] = ["aPos", ...[...POSE_ATTRIBUTES, ...SPRITE_ATTRIBUTES, ...RECORD_ATTRIBUTES].map(a => a.name)]
92
+
93
+ /** The uniforms every 2d vertex stage must declare and use: the camera
94
+ * mapping is theirs (see SPRITE_VERTEX_BODY), and a stage without them
95
+ * cannot place a sprite. shaderMaterialClass checks for them. */
96
+ export const VERTEX_UNIFORMS: readonly string[] = ["uCamera", "uCameraRot", "uViewport"]
97
+
98
+ /**
99
+ * The varyings the fragment set reads and the uniforms the mapping
100
+ * needs: what a custom vertex stage declares beside its own `in`s.
101
+ * vUv is the sample uv, vFrame the frame's UV rect (flat), vTint the
102
+ * sprite's tint, vAtlas the sampler index (flat), vOutlineColor and
103
+ * vOutlinePx the outline a distance-field atlas draws (flat; write
104
+ * zeros when the stage has no outline).
105
+ */
106
+ export const SPRITE_VARYINGS = glsl`
107
+ out vec2 vUv;
108
+ flat out vec4 vFrame;
109
+ out vec4 vTint;
110
+ flat out int vAtlas;
111
+ flat out vec3 vOutlineColor;
112
+ flat out float vOutlinePx;
113
+ uniform vec2 uViewport;
114
+ uniform vec4 uCamera;
115
+ uniform vec4 uCameraRot;
116
+ `
117
+
118
+ /**
119
+ * The world -> clip mapping and the varying writes, the tail of every
120
+ * vertex stage: expects `corner` (the quad corner in world pixels,
121
+ * after the screen-size clamp), `center` (the sprite's world position),
122
+ * `rot` (its rotation) and `clampScale` (the clamp's scale, 1 without
123
+ * one) as locals, and the record's iUv, iTint, iAtlas and iOutline as
124
+ * `in`s. Compose it after your own placement.
125
+ */
126
+ export const SPRITE_VERTEX_BODY = glsl`
127
+ float c = cos(rot), s = sin(rot);
128
+ vec2 world = center + vec2(corner.x * c - corner.y * s, corner.x * s + corner.y * c);
129
+ vec2 view = (world - uCamera.xy) * uCamera.zw;
130
+ vec2 screen = uCameraRot.zw + vec2(view.x * uCameraRot.x - view.y * uCameraRot.y, view.x * uCameraRot.y + view.y * uCameraRot.x);
131
+ // World and clip are both y-down, so the mapping carries no flip.
132
+ gl_Position = vec4(screen / uViewport * 2.0 - 1.0, 0.0, 1.0);
133
+ vUv = mix(iUv.xy, iUv.zw, aPos + 0.5);
134
+ vFrame = iUv;
135
+ vTint = iTint;
136
+ vAtlas = int(iAtlas);
137
+ vOutlineColor = iOutline.rgb;
138
+ // World pixels to screen pixels: the camera zoom, and the clamp the
139
+ // quad itself scaled by.
140
+ vOutlinePx = iOutline.a * uCamera.z * clampScale;
141
+ `
142
+
143
+ /** The GLSL type a vertex format feeds (its kind and component count):
144
+ * `float`/`vec2`/`vec3`/`vec4` for the float and normalized formats,
145
+ * `uint`/`uvec*` for uint*, `int`/`ivec*` for sint*. */
146
+ export function attributeType(format: VertexFormat): string {
147
+ let codec = VERTEX_FORMATS[format]
148
+ let prefix = codec.kind === "float" ? "" : codec.kind === "uint" ? "u" : "i"
149
+ if (codec.components === 1) return codec.kind === "float" ? "float" : codec.kind === "uint" ? "uint" : "int"
150
+ return `${prefix}vec${codec.components}`
151
+ }
152
+
153
+ /** The varying the stock vertex stage forwards an instance attribute
154
+ * as: `iFoo` becomes `vFoo` (flat, the record's exact value). An
155
+ * attribute not named `i` plus a capital has no forwarded name and
156
+ * throws - the layer's own records follow the same convention. */
157
+ export function forwardedName(name: string): string {
158
+ if (!/^i[A-Z]/.test(name)) throw new Error(`an instance attribute the stock stage forwards is named iSomething (forwarded as vSomething), got '${name}'`)
159
+ return "v" + name.slice(1)
160
+ }
161
+
162
+ /**
163
+ * The stock vertex stage, for every layer kind: the record's pose
164
+ * (iPos, iRot, iScale - the core's pose record or the records layer's
165
+ * fields, by name) placing the unit quad under the screen-size clamp,
166
+ * then SPRITE_VERTEX_BODY. `forwards` are a material's own instance
167
+ * attributes (its instanceBuffers'), each declared as an `in` and
168
+ * forwarded unchanged as a flat varying (forwardedName: `iPalette` ->
169
+ * `vPalette`), which is how the stock material's `surface` reads a
170
+ * per-sprite style record. The tier-1 custom look pairs its own stage
171
+ * with unlitFragment(); a wobble, a wind sway or a billboard-style
172
+ * effect is this stage with its placement changed.
173
+ */
174
+ export function unlitVertex(forwards: readonly VertexAttribute[] = []): string {
175
+ let ins = ""
176
+ let outs = ""
177
+ let writes = ""
178
+ for (let attr of forwards) {
179
+ let type = attributeType(attr.format)
180
+ let varying = forwardedName(attr.name)
181
+ ins += `in ${type} ${attr.name};\n `
182
+ outs += `flat out ${type} ${varying};\n `
183
+ writes += `${varying} = ${attr.name};\n `
184
+ }
185
+ return glsl`
186
+ in vec2 aPos;
187
+ in vec2 iPos;
188
+ in float iRot;
189
+ in vec2 iScale;
190
+ in vec4 iUv;
191
+ in vec4 iTint;
192
+ in vec2 iScreenPx;
193
+ in float iAtlas;
194
+ in vec4 iOutline;
195
+ ${ins}${outs}${SPRITE_VARYINGS}
196
+ ${SCREEN_SIZE_GLSL}
197
+ void main() {
198
+ float clampScale = screenSizeScale(iScale, uCamera.z, iScreenPx);
199
+ vec2 corner = aPos * iScale * clampScale;
200
+ vec2 center = iPos;
201
+ float rot = iRot;
202
+ ${writes}${SPRITE_VERTEX_BODY}
203
+ }
204
+ `
205
+ }
206
+
207
+ /** The stock vertex stage with nothing forwarded: unlitVertex(). */
208
+ export const SPRITE_VERTEX = unlitVertex()
209
+
210
+ /** The sampler uniform of atlas `i` in a layer's program. */
211
+ export function atlasUniform(i: number): string {
212
+ return `uAtlas${i}`
213
+ }
214
+
215
+ /** The texture bindings of a layer's atlases, by sampler uniform: what
216
+ * every target drawing with the layer's pipeline binds (a material's own
217
+ * textures merge over them). */
218
+ export function atlasBindings(atlases: readonly TextureId[]): TextureBindings {
219
+ let out: TextureBindings = {}
220
+ atlases.forEach((texture, i) => (out[atlasUniform(i)] = texture))
221
+ return out
222
+ }
223
+
224
+ /**
225
+ * The fragment SET of a layer over its atlases - what the layer prepends
226
+ * to its material's fragment, so every look is written over it:
227
+ *
228
+ * - the varyings (SPRITE_VARYINGS' `in` side), one `uniform sampler2D
229
+ * uAtlasN` per atlas, and `uniform vec4 uTint`, the LAYER tint
230
+ * (multiplied over the per-instance vTint by the stock fragment;
231
+ * identity [1, 1, 1, 1] - GLSL uniforms default to ZERO, so every
232
+ * target creation pins it explicitly or everything renders
233
+ * transparent black);
234
+ * - `struct Sprite { vec4 color; vec2 uv; vec4 frame; vec4 tint; int
235
+ * atlas; }`: the contract. `color` is the stock result for the
236
+ * fragment, premultiplied, before the layer tint: the atlas texel
237
+ * times the instance tint for a colour atlas, the decoded field (the
238
+ * tint as the fill, the outline under it) for a distance-field one;
239
+ * `uv` the sample uv, `frame` the frame's UV rect, `tint` the
240
+ * instance tint, `atlas` the sampler index;
241
+ * - `Sprite spriteOf()`: fills the struct for this fragment. Takes the
242
+ * uv derivatives first, in main's uniform control flow, so the mip
243
+ * footprint is the quad's own and the lookup inside the per-instance
244
+ * branch is well defined (an implicit-derivative texture() in
245
+ * non-uniform control flow is not); a custom main calls it first;
246
+ * - `vec4 spriteSample(vec2 uv)`: the stock result at another uv of the
247
+ * fragment's own atlas - the pick, the frame clamp, the field decode,
248
+ * the tint - for scrolling and distortion. The clamp into the frame
249
+ * still applies, so a wrap is `fract` in frame space:
250
+ * `spriteSample(mix(s.frame.xy, s.frame.zw, fract(t)))`.
251
+ *
252
+ * The sample is clamped to its frame, pulled in by half a texel of ITS
253
+ * atlas: a quad at a fractional position lands edge samples so close to
254
+ * the frame's edge that a linear tap (and float interpolation under
255
+ * nearest) reaches the cell next door, painting a one-texel line of the
256
+ * neighbour along the sprite's edge. The clamp keeps the quad's texel
257
+ * mapping 1:1 and only moves those edge samples onto the edge texel, so
258
+ * touching cells do not bleed and no gutter is needed at mip level 0.
259
+ * It cannot help a mip chain, whose texels straddle the cell edge before
260
+ * sampling - that is extrude.ts. min/max because a flipped frame stores
261
+ * its UVs swapped, and the inner min/max because a sub-texel frame would
262
+ * otherwise hand clamp() a lower bound above its upper one. The tap is
263
+ * textureGrad over the derivatives spriteOf took, so a clamped uv (a
264
+ * zero derivative along the clamped axis, which would pick a sharper
265
+ * level along every edge) never decides the level.
266
+ *
267
+ * A distance-field atlas yields the instance tint as the fill,
268
+ * anti-aliased over one screen pixel: the field's range mapped through
269
+ * the quad's texel footprint gives the screen pixels per field unit
270
+ * (floored at 1, so a glyph far below its texel size fades instead of
271
+ * vanishing), the median of rgb is the edge, and when the instance
272
+ * carries an outline the true field in alpha, pushed out by the
273
+ * outline's screen pixels, paints the outline colour under the fill.
274
+ */
275
+ export function spriteSource(atlases: readonly AtlasShading[]): string {
276
+ if (!Array.isArray(atlases) || atlases.length === 0) throw new Error(`spriteSource: atlases must be a non-empty list, got ${JSON.stringify(atlases)}`)
277
+ let samplers = ""
278
+ atlases.forEach((_, i) => (samplers += `uniform sampler2D ${atlasUniform(i)};\n `))
279
+ let shade = (i: number) => {
280
+ let sdf = atlases[i]!.sdf
281
+ if (sdf === undefined) return `spriteTap(${atlasUniform(i)}, uv) * vTint`
282
+ if (!(typeof sdf.range === "number" && sdf.range > 0 && Number.isFinite(sdf.range))) {
283
+ throw new Error(`spriteSource: atlases[${i}].sdf.range must be a positive number, got ${sdf.range}`)
284
+ }
285
+ return `spriteField(${atlasUniform(i)}, ${sdf.range.toFixed(2)}, uv)`
286
+ }
287
+ let cases = ""
288
+ atlases.forEach((_, i) => (cases += `case ${i}: return ${shade(i)};\n `))
289
+ let pick =
290
+ atlases.length === 1
291
+ ? `return ${shade(0)};`
292
+ : `switch (vAtlas) {
293
+ ${cases}default: return ${shade(0)};
294
+ }`
295
+ return glsl`
296
+ in vec2 vUv;
297
+ flat in vec4 vFrame;
298
+ in vec4 vTint;
299
+ flat in int vAtlas;
300
+ flat in vec3 vOutlineColor;
301
+ flat in float vOutlinePx;
302
+ ${samplers}uniform vec4 uTint;
303
+
304
+ struct Sprite {
305
+ vec4 color;
306
+ vec2 uv;
307
+ vec4 frame;
308
+ vec4 tint;
309
+ int atlas;
310
+ };
311
+
312
+ // The quad's uv footprint per screen pixel, taken once by spriteOf in
313
+ // uniform control flow; every tap of this fragment uses it.
314
+ vec2 spriteDx;
315
+ vec2 spriteDy;
316
+
317
+ vec4 spriteTap(sampler2D atlas, vec2 uv) {
318
+ vec2 halfTexel = 0.5 / vec2(textureSize(atlas, 0));
319
+ vec2 lo = min(vFrame.xy, vFrame.zw) + halfTexel;
320
+ vec2 hi = max(vFrame.xy, vFrame.zw) - halfTexel;
321
+ return textureGrad(atlas, clamp(uv, min(lo, hi), max(lo, hi)), spriteDx, spriteDy);
322
+ }
323
+
324
+ float spriteMedian3(vec3 v) {
325
+ return max(min(v.r, v.g), min(max(v.r, v.g), v.b));
326
+ }
327
+
328
+ // Screen pixels per field unit (0..1 spans the range): the range in
329
+ // texels over the quad's texel footprint per screen pixel, floored at
330
+ // one so a minified glyph fades rather than aliasing away.
331
+ float spriteScreenPxRange(sampler2D atlas, float range) {
332
+ vec2 unitRange = vec2(range) / vec2(textureSize(atlas, 0));
333
+ vec2 screenTexSize = vec2(1.0) / (abs(spriteDx) + abs(spriteDy));
334
+ return max(0.5 * dot(unitRange, screenTexSize), 1.0);
335
+ }
336
+
337
+ vec4 spriteField(sampler2D atlas, float range, vec2 uv) {
338
+ vec4 t = spriteTap(atlas, uv);
339
+ float px = spriteScreenPxRange(atlas, range);
340
+ float fill = clamp((spriteMedian3(t.rgb) - 0.5) * px + 0.5, 0.0, 1.0);
341
+ vec3 rgb = vTint.rgb;
342
+ float coverage = fill;
343
+ if (vOutlinePx > 0.0) {
344
+ float outline = clamp((t.a - 0.5) * px + vOutlinePx + 0.5, 0.0, 1.0);
345
+ coverage = max(fill, outline);
346
+ rgb = mix(vOutlineColor, vTint.rgb, fill);
347
+ }
348
+ float a = coverage * vTint.a;
349
+ return vec4(rgb * a, a);
350
+ }
351
+
352
+ vec4 spriteSample(vec2 uv) {
353
+ ${pick}
354
+ }
355
+
356
+ Sprite spriteOf() {
357
+ spriteDx = dFdx(vUv);
358
+ spriteDy = dFdy(vUv);
359
+ return Sprite(spriteSample(vUv), vUv, vFrame, vTint, vAtlas);
360
+ }
361
+ `
362
+ }
363
+
364
+ /** The options of the stock fragment: the tier-2 slot. */
365
+ export type UnlitSourceOptions = {
366
+ /** GLSL at file scope, after the layer's set and before main:
367
+ * uniforms (each a layer param - `params` on the layer or
368
+ * `layer.setParams` - or a material texture) and helper functions. */
369
+ prelude?: string
370
+ /** GLSL declaring `void surface(inout Sprite s)`, called with the
371
+ * struct filled (spriteOf) and before the layer tint: rewrite
372
+ * `s.color` or `discard`. */
373
+ surface?: string
374
+ /** The instance attributes the stock vertex stage forwards
375
+ * (unlitVertex's `forwards`): declared here as the matching flat
376
+ * varyings, so the prelude and the surface read `vFoo` for a record's
377
+ * `iFoo`. */
378
+ forwards?: readonly VertexAttribute[]
379
+ }
380
+
381
+ /**
382
+ * The stock fragment over the layer's set (spriteSource, which the layer
383
+ * prepends): the sprite's stock colour, through the surface slot when
384
+ * one is given, times the layer tint. `unlit()` compiles exactly this;
385
+ * the tier-1 custom look pairs it with a vertex stage of its own.
386
+ */
387
+ export function unlitFragment(o: UnlitSourceOptions = {}): string {
388
+ let ins = ""
389
+ for (let attr of o.forwards ?? []) ins += `flat in ${attributeType(attr.format)} ${forwardedName(attr.name)};\n `
390
+ return glsl`
391
+ ${ins}${o.prelude ?? ""}
392
+ ${o.surface ?? ""}
393
+ void main() {
394
+ Sprite s = spriteOf();
395
+ ${o.surface ? "surface(s);" : ""}
396
+ fragColor = s.color * uTint;
397
+ }
398
+ `
399
+ }
package/src/index.ts CHANGED
@@ -9,21 +9,29 @@
9
9
  // node whose Pose2D record sink writes the pose instance buffer at the
10
10
  // core flush, so core producers reach sprites and picking walks the core
11
11
  // BVH; style stays a JS-written second instance buffer. The records layer (createRecordLayer)
12
- // is the raw escape hatch for motion only JS can compute: 16 JS-owned
12
+ // is the raw escape hatch for motion only JS can compute: 20 JS-owned
13
13
  // floats per sprite in a mirror, published by dirty range through the
14
14
  // 3d record mesh's verbs (records/updateRecords/setRecordCount). The baked
15
15
  // tile layer (createTileLayer/TileLayer) is the static sibling: a tile
16
16
  // world rendered once into textures and composited as a few quads,
17
- // re-baked on change. Two faces throughout: the imperative core (usable
18
- // without Solid components) and the components (SpriteLayer/Sprite/Group/
17
+ // re-baked on change. What any layer's pixels look like is its MATERIAL
18
+ // (material.ts): the stock `unlit()` with its blend and its
19
+ // prelude/surface slot, or a shaderMaterialClass over the layer's GLSL
20
+ // set (`@solidrt/2d/glsl`), with per-sprite style records in any vertex
21
+ // format - the same three tiers and the same words as @solidrt/3d, the
22
+ // material sitting on the layer because the layer is the batch. Two
23
+ // faces throughout: the imperative core (usable without Solid
24
+ // components) and the components (SpriteLayer/Sprite/Group/
19
25
  // View2d/TileLayer) on top. Sheets reach the GPU through createAtlas,
20
26
  // sliced by grid/namedFrames/fullFrame, and a mipmapped sheet is extruded
21
27
  // first (extrudeGrid/extrudeRects). See AGENTS.md for the model and the
22
28
  // traps.
23
29
 
24
- export { addGroup, addSprite, createSpriteLayer, getSprite, destroyGroup, destroySprite, setGroup, setGroupTransition, setSprite, setSpriteParent, setSpriteTransition, worldPosition, POSE_FLOATS, STYLE_FLOATS } from "./layer.ts"
25
- export { createRecordLayer, records, setRecordCount, updateRecords, INSTANCE_FLOATS } from "./records.ts"
30
+ export { addGroup, addSprite, createSpriteLayer, getSprite, destroyGroup, destroySprite, setGroup, setGroupTimeScale, setGroupTransition, setSprite, setSpriteParent, setSpriteTimeScale, setSpriteTransition, timeRate, worldPosition, POSE_FLOATS, SPRITE_FLOATS } from "./layer.ts"
31
+ export { createRecordLayer, instanceAttribute, records, setInstanceStyle, setRecordCount, updateRecords, INSTANCE_FLOATS } from "./records.ts"
26
32
  export type { RecordLayer as RecordLayerHandle, RecordLayerOptions, UpdateRecordsOptions } from "./records.ts"
33
+ export { shaderMaterial, shaderMaterialClass, unlit } from "./material.ts"
34
+ export type { Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
27
35
  export { floorReach, pointInSprite } from "./pick.ts"
28
36
  export { projectCamera, unprojectCamera } from "./camera.ts"
29
37
  export type { CameraState, CameraUpdate } from "./camera.ts"
@@ -70,7 +78,7 @@ export { moveAndSlide } from "./collision.ts"
70
78
  export type { MoveLayer } from "./collision.ts"
71
79
  export type { ViewHandle, ViewOptions } from "./views.ts"
72
80
  export { createTileLayer } from "./tiles.ts"
73
- export type { TileChunk, TileLayer as TileLayerHandle, TileLayerOptions } from "./tiles.ts"
81
+ export type { TileCellOptions, TileChunk, TileLayer as TileLayerHandle, TileLayerOptions } from "./tiles.ts"
74
82
 
75
83
  export type { NodeMotionSpec, NodeTransitionSpec } from "flux:spatial"
76
84
  export { fullFrame, grid, isFrame, namedFrames } from "./frames.ts"