@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/text.ts CHANGED
@@ -9,6 +9,9 @@
9
9
  //
10
10
  // The node layer only: a run is a group. Three verbs like the sprites':
11
11
  // addText, setText (re-laid out, the sprite pool reused), destroyText.
12
+ // A setText that changes only the paint (tint, outline, renderOrder)
13
+ // re-writes the glyph sprites without shaping: a fade is sprite writes,
14
+ // not an engine call per frame.
12
15
  import { addGroup, addSprite, destroyGroup, destroySprite, setGroup, setSprite } from "./layer.ts"
13
16
  import type { Sprite, SpriteGroup, SpriteLayer } from "./layer.ts"
14
17
  import type { SpriteFont } from "./font.ts"
@@ -119,7 +122,9 @@ export function addText(layer: SpriteLayer, opts: AddTextOptions): TextRun {
119
122
  }
120
123
 
121
124
  /** Change a run: the style fields re-lay it out (the sprite pool is
122
- * reused), the pose fields move its group. Absent keys keep values. */
125
+ * reused), the pose fields move its group. Absent keys keep values, and so
126
+ * does a key passed with its current value; a change to only tint,
127
+ * outline or renderOrder re-writes the glyph sprites without shaping. */
123
128
  export function setText(run: TextRun, opts: Partial<TextStyle> & TextPose): void {
124
129
  let state = run as RunState
125
130
  if (state.group.layer === null) return
@@ -127,12 +132,42 @@ export function setText(run: TextRun, opts: Partial<TextStyle> & TextPose): void
127
132
  if (x !== undefined || y !== undefined || rotation !== undefined || visible !== undefined) {
128
133
  setGroup(state.group, { x, y, rotation, visible })
129
134
  }
130
- let keys = Object.keys(style) as (keyof TextStyle)[]
131
- if (keys.some(k => style[k] !== undefined)) {
132
- let next: TextStyle = { ...state._style }
133
- for (let k of keys) if (style[k] !== undefined) (next as Record<string, unknown>)[k] = style[k]
134
- relayout(state, next)
135
+ // Compare by value, not by presence: a component effect passes every
136
+ // style key on any change, and only a key that differs costs anything.
137
+ let current = state._style
138
+ let next: TextStyle = { ...current }
139
+ let reshape = false
140
+ let repaint = false
141
+ for (let k of Object.keys(style) as (keyof TextStyle)[]) {
142
+ let value = style[k]
143
+ if (value === undefined || sameStyleValue(k, value, current[k])) continue
144
+ ;(next as Record<string, unknown>)[k] = value
145
+ if (PAINT_KEYS.has(k)) repaint = true
146
+ else reshape = true
135
147
  }
148
+ if (reshape) relayout(state, next)
149
+ else if (repaint) {
150
+ checkStyle(next)
151
+ state._style = next
152
+ paint(state)
153
+ }
154
+ }
155
+
156
+ // The style keys that change only how a glyph sprite is drawn, never which
157
+ // glyphs or where: every other key re-shapes.
158
+ const PAINT_KEYS: ReadonlySet<keyof TextStyle> = new Set<keyof TextStyle>(["tint", "outline", "renderOrder"])
159
+
160
+ // Equal as a style value: tint by channel, outline by colour and width,
161
+ // the rest by identity.
162
+ function sameStyleValue(key: keyof TextStyle, a: unknown, b: unknown): boolean {
163
+ if (a === b) return true
164
+ if (key === "tint" && Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((c, i) => c === b[i])
165
+ if (key === "outline" && a != null && b != null) {
166
+ let { color: ca, width: wa } = a as NonNullable<TextStyle["outline"]>
167
+ let { color: cb, width: wb } = b as NonNullable<TextStyle["outline"]>
168
+ return wa === wb && ca[0] === cb[0] && ca[1] === cb[1] && ca[2] === cb[2]
169
+ }
170
+ return false
136
171
  }
137
172
 
138
173
  /** Remove the run: its group and every glyph sprite. */
@@ -164,6 +199,27 @@ function relayout(run: RunState, style: TextStyle): void {
164
199
  place(run)
165
200
  }
166
201
 
202
+ // The paint fields of the run's style, as every glyph sprite takes them.
203
+ function paintOf(style: TextStyle) {
204
+ return { tint: style.tint ?? [1, 1, 1, 1], outline: style.outline ?? null, renderOrder: style.renderOrder ?? 0 }
205
+ }
206
+
207
+ // Write a sprite's outline from the style (none clears it).
208
+ function writeOutline(layer: SpriteLayer, sprite: Sprite, outline: TextStyle["outline"]): void {
209
+ if (outline != null) layer._outline(sprite, outline.color[0], outline.color[1], outline.color[2], outline.width)
210
+ else layer._outline(sprite, 0, 0, 0, 0)
211
+ }
212
+
213
+ // Re-paint the glyph sprites in place: the layout, the pool and the
214
+ // pending cells all stand, only tint, outline and order are written.
215
+ function paint(run: RunState): void {
216
+ let { tint, outline, renderOrder } = paintOf(run._style)
217
+ for (let sprite of run.sprites) {
218
+ setSprite(sprite, { tint, renderOrder })
219
+ writeOutline(run.layer, sprite, outline)
220
+ }
221
+ }
222
+
167
223
  // Write every glyph sprite from the layout and the font's cells: a cell
168
224
  // the atlas lacks leaves its sprite hidden and asks the font for it.
169
225
  function place(run: RunState): void {
@@ -172,9 +228,7 @@ function place(run: RunState): void {
172
228
  let fontSize = style.fontSize ?? font.fontSize
173
229
  // Cells are made at `font.size` texels per em; the run draws at fontSize.
174
230
  let scale = fontSize / font.size
175
- let tint = style.tint ?? [1, 1, 1, 1]
176
- let outline = style.outline ?? null
177
- let renderOrder = style.renderOrder ?? 0
231
+ let { tint, outline, renderOrder } = paintOf(style)
178
232
  let placements = run._layout.glyphs
179
233
  run._pending.clear()
180
234
  let missing: number[] = []
@@ -210,8 +264,7 @@ function place(run: RunState): void {
210
264
  } else {
211
265
  setSprite(sprite, fields)
212
266
  }
213
- if (outline !== null) layer._outline(sprite, outline.color[0], outline.color[1], outline.color[2], outline.width)
214
- else layer._outline(sprite, 0, 0, 0, 0)
267
+ writeOutline(layer, sprite, outline)
215
268
  used++
216
269
  }
217
270
  for (let i = used; i < sprites.length; i++) destroySprite(sprites[i]!)
package/src/tiles.ts CHANGED
@@ -5,15 +5,18 @@
5
5
  // chunk textures, and scrolling is a transform on the composited world
6
6
  // (see <TileLayer> in components/tile-layer.tsx), never a repaint.
7
7
  //
8
- // Each chunk is a manual target over the layer's ONE sprite pipeline
9
- // (shaders.ts, compiled once per layer for its atlases) with fixed record
8
+ // Each chunk is a manual target over the layer's ONE pipeline (its
9
+ // material's, resolved once per layer for its atlases) with fixed record
10
10
  // slots - record localRow * chunkTiles + localCol IS that tile, an empty
11
11
  // tile is a zero-size quad, instance count is constant per chunk. Records
12
12
  // hold WORLD pixel coordinates; the chunk target's uCamera is its pixel
13
13
  // origin, so the shared vertex stage does the chunk-local mapping (the
14
14
  // same mechanism a camera pass uses, pointed at a chunk rect). A cell's
15
15
  // frame names its atlas like a sprite's (the record's atlas index), so a
16
- // tile world draws from several tilesets in one bake.
16
+ // tile world draws from several tilesets in one bake. The material's
17
+ // style streams are per chunk too (a cell's `style`), plain arrays
18
+ // uploaded whole with the chunk's records; its params reach every
19
+ // resident chunk, each write a re-bake like the layer tint.
17
20
  //
18
21
  // Chunks allocate lazily on the first setTile that gives them content: an
19
22
  // empty chunk costs nothing - no records, no buffer, no texture - so a
@@ -32,20 +35,27 @@ import {
32
35
  destroyBuffer,
33
36
  destroyTexture,
34
37
  endBufferWrite,
38
+ isFloatLayout,
39
+ layoutFields,
40
+ layoutStride,
35
41
  limits,
36
42
  renderTarget,
37
43
  setTargetParams,
38
44
  setTargetSize,
45
+ writeBuffer,
46
+ writeRecord,
39
47
  } from "@solidrt/core/gpu"
40
- import type { BlendMode, BufferId, FilterMode, TextureId } from "@solidrt/core/gpu"
48
+ import type { AttributeAccess, BufferId, FilterMode, ShaderParams, TextureId, VertexAttribute } from "@solidrt/core/gpu"
41
49
  import { checkAtlases, frameIndex } from "./atlas.ts"
42
50
  import type { Atlas } from "./atlas.ts"
43
51
  import { isFrame } from "./frames.ts"
44
52
  import type { Frame } from "./frames.ts"
45
- import { checkTint } from "./layer.ts"
53
+ import { atlasBindings, RECORD_ATTRIBUTES } from "./glsl.ts"
54
+ import { checkParams, checkTint } from "./layer.ts"
55
+ import { materialBlank, materialSamplers, unlit } from "./material.ts"
56
+ import type { Material } from "./material.ts"
46
57
  import { checkOversample, thrashSentinel } from "./oversample.ts"
47
58
  import { INSTANCE_FLOATS } from "./records.ts"
48
- import { atlasBindings, createSpritePipeline, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
49
59
  import { checkCell, checkRect, chunkOf, eachChunkSlice, slotOf } from "./tiles-math.ts"
50
60
 
51
61
  const RESOLVED = Promise.resolve()
@@ -77,12 +87,19 @@ export type TileLayerOptions = {
77
87
  */
78
88
  filter?: FilterMode
79
89
  /**
80
- * How the cells blend into their chunk bakes, core gpu's BlendMode;
81
- * default "alpha". "add" for a glow or light map baked over the
82
- * transparent chunk clear, "multiply" to darken. Pipeline state, fixed
83
- * at creation.
90
+ * What the cells look like: the layer's material (see
91
+ * SpriteLayerOptions.material), fixed at creation; default `unlit()`.
92
+ * Its blend is how the cells bake into their chunks ("add" for a glow
93
+ * or light map baked over the transparent chunk clear), its params and
94
+ * textures reach every chunk target, its instance buffers are a style
95
+ * record per cell (`setTile`'s `style`). A tile layer BAKES, so a
96
+ * param change re-renders every resident chunk, like the tint:
97
+ * animated uniforms belong on a sprite layer.
84
98
  */
85
- blend?: BlendMode
99
+ material?: Material
100
+ /** Uniform values seeded on every chunk over the material's own; see
101
+ * TileLayer.setParams. */
102
+ params?: ShaderParams
86
103
  /**
87
104
  * Target texels per world pixel in the baked chunks (positive integer,
88
105
  * default 1); see TileLayer.setOversample. `<TileLayer>` picks it from
@@ -120,10 +137,26 @@ export type TileChunk = {
120
137
  height: number
121
138
  }
122
139
 
140
+ /** A cell write's options: the cell's tint and, under a material with
141
+ * instance buffers, its style record. */
142
+ export type TileCellOptions = {
143
+ /** The cell's tint, multiplied over its texels like a sprite's; absent
144
+ * keeps the cell's current tint. */
145
+ tint?: Tint
146
+ /** The cell's style record (the material's FIRST instance buffer, one
147
+ * value per component in attribute order), absent keeps it; a cell set
148
+ * from empty starts at the material's instanceStyle. Throws on a
149
+ * material without an instance buffer. */
150
+ style?: ArrayLike<number>
151
+ }
152
+
123
153
  export type TileLayer = {
124
154
  /** The atlases the layer bakes from, as declared at creation: every
125
155
  * frame set names one of their textures (see LayerBase.atlases). */
126
156
  readonly atlases: readonly Atlas[]
157
+ /** The material the layer bakes with, as declared at creation (the
158
+ * stock `unlit()` when none was). */
159
+ readonly material: Material
127
160
  /** Grid shape, fixed at creation. */
128
161
  cols: number
129
162
  rows: number
@@ -164,7 +197,7 @@ export type TileLayer = {
164
197
  * a frame) publishes and re-bakes ONLY the chunks that changed, however
165
198
  * many tiles did.
166
199
  */
167
- setTile(col: number, row: number, frame: Frame | null, opts?: { tint?: Tint }): void
200
+ setTile(col: number, row: number, frame: Frame | null, opts?: TileCellOptions): void
168
201
  /**
169
202
  * Set a rect of cells at once: `cols` x `rows` cells from `col`, `row`,
170
203
  * `cells` in row-major order (`cols * rows` long) - frames (null clears)
@@ -174,10 +207,10 @@ export type TileLayer = {
174
207
  * touches, the per-cell loop inside the layer over its own records, the
175
208
  * same batching and flush. A chunk the rect only clears never allocates,
176
209
  * so a whole-world write of a sparse world stays sparse. `tint` applies
177
- * to every cell set, as setTile's; absent, each cell keeps its own. A
178
- * bad entry throws before any cell changes.
210
+ * to every cell set, as setTile's, and so does `style`; absent, each
211
+ * cell keeps its own. A bad entry throws before any cell changes.
179
212
  */
180
- setTiles(col: number, row: number, cols: number, rows: number, cells: ArrayLike<Frame | null> | ArrayLike<number>, opts?: { tint?: Tint }): void
213
+ setTiles(col: number, row: number, cols: number, rows: number, cells: ArrayLike<Frame | null> | ArrayLike<number>, opts?: TileCellOptions): void
181
214
  /** The frame at a cell, or null when empty. */
182
215
  getTile(col: number, row: number): Frame | null
183
216
  /** The frames table given at creation (what setTiles' indices name), or
@@ -191,18 +224,35 @@ export type TileLayer = {
191
224
  * animate it coarsely, not per frame.
192
225
  */
193
226
  setTint(tint: [number, number, number, number]): void
227
+ /**
228
+ * Write uniform values to every resident chunk (the material's prelude
229
+ * uniforms, over its own `params`): LayerBase.setParams for a baked
230
+ * layer, so each write re-renders every resident chunk, like setTint -
231
+ * drive it from slow state, not per frame. Absent names keep their
232
+ * values; a name the program does not declare is skipped.
233
+ */
234
+ setParams(params: ShaderParams): void
194
235
  dispose(): void
195
236
  }
196
237
 
197
238
  type Chunk = TileChunk & {
198
239
  records: Float32Array
199
240
  buffer: BufferId
241
+ /** The material's style records of the chunk, one array and buffer
242
+ * per instance buffer, uploaded whole with the records. */
243
+ styles: ChunkStyle[]
200
244
  dirty: boolean
201
245
  /** Records changed since the last flush (a tint-only or resize re-bake
202
246
  * renders without re-uploading them). */
203
247
  wrote: boolean
204
248
  }
205
249
 
250
+ type ChunkStyle = { bytes: Uint8Array; buffer: BufferId; fields: AttributeAccess[]; floats: Float32Array | null }
251
+
252
+ // One style stream of the material as the tile layer keeps it: the
253
+ // layout, the blank record, and the stride.
254
+ type StyleShape = { layout: VertexAttribute[]; blank: Uint8Array; stride: number }
255
+
206
256
  /**
207
257
  * Create a baked tile layer: `cols` x `rows` cells of `tileW` x `tileH`
208
258
  * pixels, every tile drawing one atlas frame, baked into lazily-allocated
@@ -243,9 +293,12 @@ export function createTileLayer(
243
293
  )
244
294
  }
245
295
  let label = opts?.label ?? "tiles"
246
- let blend: BlendMode = opts?.blend ?? "alpha"
247
296
  let tint: Tint = opts?.tint ?? [1, 1, 1, 1]
248
297
  checkTint("createTileLayer", tint)
298
+ let material = opts?.material ?? unlit()
299
+ checkAtlases("createTileLayer", atlases, materialSamplers(material))
300
+ let params: ShaderParams = { ...material.params, ...opts?.params }
301
+ let shapes: StyleShape[] = (material.instanceBuffers ?? []).map((b, i) => ({ layout: b.attributes, blank: materialBlank(material, i), stride: layoutStride(b.attributes) }))
249
302
  // The frames table, copied (the caller's array may move on) and checked
250
303
  // once, plus its UVs and atlas index as five floats per entry: an index
251
304
  // write copies by offset instead of reading a frame object per cell.
@@ -273,9 +326,10 @@ export function createTileLayer(
273
326
  let chunkCols = Math.ceil(cols / chunkTiles)
274
327
  let perChunk = chunkTiles * chunkTiles
275
328
  // The one pipeline (and unit quad) every chunk target draws with:
276
- // compiled once per layer, however many chunks allocate.
277
- let gpu = createSpritePipeline(label, VERTEX, [INSTANCE_ATTRIBUTES], blend, atlases)
278
- let textures = atlasBindings(atlases.map(a => a.texture))
329
+ // the material's, resolved once per layer, however many chunks
330
+ // allocate.
331
+ let gpu = material.pipeline(atlases, [RECORD_ATTRIBUTES], label)
332
+ let textures = { ...atlasBindings(atlases.map(a => a.texture)), ...material.textures }
279
333
 
280
334
  let disposed = false
281
335
  let scheduled = false
@@ -309,6 +363,7 @@ export function createTileLayer(
309
363
  let out = beginBufferWrite(chunk.buffer)
310
364
  out.set(chunk.records)
311
365
  endBufferWrite(chunk.buffer, chunk.records.byteLength)
366
+ for (let style of chunk.styles) writeBuffer(style.buffer, style.bytes)
312
367
  }
313
368
  renderTarget(chunk.texture)
314
369
  }
@@ -328,16 +383,29 @@ export function createTileLayer(
328
383
  let y = Math.floor(index / chunkCols) * chunkH
329
384
  let records = new Float32Array(perChunk * INSTANCE_FLOATS)
330
385
  let buffer = createBuffer(records.byteLength, { label: `${label}-chunk-records`, autoFree: false })
386
+ // Every cell's style slot starts blank (the material's
387
+ // instanceStyle), so an unwritten cell shows the material's default.
388
+ let styles: ChunkStyle[] = shapes.map((shape, i) => {
389
+ let bytes = new Uint8Array(perChunk * shape.stride)
390
+ for (let slot = 0; slot < perChunk; slot++) bytes.set(shape.blank, slot * shape.stride)
391
+ let view = new DataView(bytes.buffer)
392
+ return {
393
+ bytes,
394
+ buffer: createBuffer(bytes.byteLength, { label: `${label}-chunk-style${i}`, autoFree: false }),
395
+ fields: layoutFields(shape.layout, () => view),
396
+ floats: isFloatLayout(shape.layout) ? new Float32Array(bytes.buffer) : null,
397
+ }
398
+ })
331
399
  let texture = createShaderTarget(
332
400
  gpu.pipeline,
333
401
  chunkW * oversample,
334
402
  chunkH * oversample,
335
403
  // Bake targets pin the camera rotation to identity: the tile camera
336
404
  // rotates the COMPOSITED world (<TileLayer>'s view), never the bake.
337
- { uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
405
+ { uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint, ...params },
338
406
  {
339
407
  label: `${label}-chunk`,
340
- buffers: [gpu.quad, buffer],
408
+ buffers: [gpu.quad, buffer, ...styles.map(s => s.buffer)],
341
409
  vertexCount: 4,
342
410
  instanceCount: perChunk,
343
411
  textures,
@@ -347,7 +415,7 @@ export function createTileLayer(
347
415
  autoFree: false,
348
416
  },
349
417
  )
350
- let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, dirty: false, wrote: false }
418
+ let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, styles, dirty: false, wrote: false }
351
419
  resident.set(index, chunk)
352
420
  layer.chunks.push(chunk)
353
421
  layer.onChunk?.(chunk)
@@ -386,14 +454,27 @@ export function createTileLayer(
386
454
  }
387
455
  }
388
456
  // Clear one cell: a zero-size quad draws nothing (the tint stays, and
389
- // a later set from empty resets it).
457
+ // a later set from empty resets it; the style stays the same way).
390
458
  let clearCell = (r: Float32Array, at: number): void => {
391
459
  r[at + 2] = 0
392
460
  r[at + 3] = 0
393
461
  }
462
+ // A cell write's style, checked before any cell changes: the material
463
+ // must declare an instance buffer, and the record must be its length
464
+ // (the encode throws on a wrong count).
465
+ let checkStyle = (verb: string, style: ArrayLike<number> | undefined): void => {
466
+ if (style === undefined) return
467
+ if (shapes.length === 0) throw new Error(`${verb}: the layer's material declares no instance buffer (no style record)`)
468
+ }
469
+ // Write a cell's style record into its chunk's first style stream.
470
+ let writeStyle = (chunk: Chunk, slot: number, style: ArrayLike<number>, verb: string): void => {
471
+ let s = chunk.styles[0]!
472
+ writeRecord(shapes[0]!.layout, s.fields, s.floats, slot, style, verb)
473
+ }
394
474
 
395
475
  let layer: TileLayer = {
396
476
  atlases,
477
+ material,
397
478
  cols,
398
479
  rows,
399
480
  tileW,
@@ -432,9 +513,11 @@ export function createTileLayer(
432
513
  } else {
433
514
  if (!isFrame(frame)) throw new Error(`setTile: not a frame, got ${JSON.stringify(frame)}`)
434
515
  if (opts?.tint !== undefined) checkTint("setTile", opts.tint)
516
+ checkStyle("setTile", opts?.style)
435
517
  let atlas = frameIndex("setTile", atlasIndex, frame)
436
518
  chunk ??= allocate(index)
437
519
  writeCell(chunk.records, at, col, row, frame.u0, frame.v0, frame.u1, frame.v1, atlas, opts?.tint)
520
+ if (opts?.style !== undefined) writeStyle(chunk, at / INSTANCE_FLOATS, opts.style, "setTile")
438
521
  }
439
522
  touch(chunk)
440
523
  },
@@ -443,7 +526,9 @@ export function createTileLayer(
443
526
  checkRect("setTiles", col, row, w, h, cols, rows)
444
527
  if (cells.length !== w * h) throw new Error(`setTiles: a ${w} x ${h} rect takes ${w * h} cells, got ${cells.length}`)
445
528
  if (opts?.tint !== undefined) checkTint("setTiles", opts.tint)
529
+ checkStyle("setTiles", opts?.style)
446
530
  let cellTint = opts?.tint
531
+ let cellStyle = opts?.style
447
532
  // The two forms: indices into the table, or frames. Every entry is
448
533
  // checked (and a frame resolved to its atlas) before the first write,
449
534
  // so a bad one changes nothing.
@@ -512,6 +597,7 @@ export function createTileLayer(
512
597
  r = chunk.records
513
598
  }
514
599
  writeCell(r, at, x, y, u0, v0, u1, v1, atlas, cellTint)
600
+ if (cellStyle !== undefined) writeStyle(chunk!, at / INSTANCE_FLOATS, cellStyle, "setTiles")
515
601
  }
516
602
  }
517
603
  if (chunk) touch(chunk)
@@ -536,6 +622,15 @@ export function createTileLayer(
536
622
  touch(chunk, false)
537
623
  }
538
624
  },
625
+ setParams(next) {
626
+ if (disposed) return
627
+ checkParams("setParams", next)
628
+ Object.assign(params, next)
629
+ for (let chunk of resident.values()) {
630
+ setTargetParams(chunk.texture, next)
631
+ touch(chunk, false)
632
+ }
633
+ },
539
634
  dispose() {
540
635
  if (disposed) return
541
636
  disposed = true
@@ -543,10 +638,10 @@ export function createTileLayer(
543
638
  for (let chunk of resident.values()) {
544
639
  destroyTexture(chunk.texture)
545
640
  destroyBuffer(chunk.buffer)
641
+ for (let style of chunk.styles) destroyBuffer(style.buffer)
546
642
  }
547
643
  resident.clear()
548
644
  layer.chunks.length = 0
549
- gpu.dispose()
550
645
  },
551
646
  }
552
647
 
package/src/views.ts CHANGED
@@ -18,14 +18,13 @@
18
18
  // a sprite under a minimap gets its ordinary handlers, and the view's
19
19
  // listeners are the last stop.
20
20
  import { addDraw, createDrawTarget, destroyTexture, removeDraw, setDrawBuffers, setDrawRange, setTargetParams, setTargetSize } from "@solidrt/core/gpu"
21
- import type { BufferId, DrawId, InstanceOrder, RenderPipelineId, TextureId } from "@solidrt/core/gpu"
21
+ import type { BufferId, DrawId, InstanceOrder, RenderPipelineId, ShaderParams, TextureBindings, TextureId } from "@solidrt/core/gpu"
22
22
  import { applyCamera, cameraParams, checkCamera, defaultCamera, projectCamera, unprojectCamera } from "./camera.ts"
23
23
  import type { CameraState, CameraUpdate } from "./camera.ts"
24
24
  import { spriteDispatch } from "./dispatch.ts"
25
25
  import type { PointerFeed } from "@solidrt/core"
26
26
  import type { LayerPointerListener, Sprite, LayerHandlers } from "./layer.ts"
27
27
  import { checkOversample, thrashSentinel } from "./oversample.ts"
28
- import { atlasBindings } from "./shaders.ts"
29
28
 
30
29
  export type ViewOptions = {
31
30
  /** View pixels: the viewport its camera maps the world onto. */
@@ -111,16 +110,20 @@ export type ViewDeps = {
111
110
  label: string
112
111
  pipeline: RenderPipelineId
113
112
  quad: BufferId
114
- /** The layer's atlas textures in sampler order (the pipeline was
115
- * compiled for exactly this many). */
116
- atlases: readonly TextureId[]
113
+ /** The sampler bindings every view binds: the layer's atlases by
114
+ * sampler index (the pipeline was compiled for exactly those) and the
115
+ * material's own textures. */
116
+ textures: TextureBindings
117
117
  /** The layer's current instance buffers, in the pipeline's layout order
118
- * after the quad (the pose and style pair on the node layer, the one
119
- * record buffer on records). */
118
+ * after the quad (the pose and sprite pair on the node layer, the one
119
+ * record buffer on records, the material's style buffers after them). */
120
120
  buffers: () => BufferId[]
121
121
  /** The instance count as last published. */
122
122
  count: () => number
123
123
  tint: () => [number, number, number, number]
124
+ /** The params every view seeds beyond the viewport, the camera and
125
+ * the tint: the material's, with the layer's own over them. */
126
+ params: () => ShaderParams
124
127
  /** The layer's pick, `zoom` the camera zoom its floors are measured at. */
125
128
  pick: (x: number, y: number, zoom: number) => Sprite[]
126
129
  /** The layer's key order (`orderBy`), declared by ONE live entry. */
@@ -135,6 +138,8 @@ export type Views = {
135
138
  setBuffers(instanceBuffers: BufferId[]): void
136
139
  setCount(count: number): void
137
140
  setTint(tint: [number, number, number, number]): void
141
+ /** The layer's params changed: every view's target takes the write. */
142
+ setParams(params: ShaderParams): void
138
143
  dispose(): void
139
144
  }
140
145
 
@@ -180,8 +185,8 @@ export function createViews(deps: ViewDeps): Views {
180
185
  let texture = createDrawTarget(
181
186
  width * oversample,
182
187
  height * oversample,
183
- { uViewport: [width, height], ...cameraParams(cam), uTint: deps.tint() },
184
- { textures: atlasBindings(deps.atlases), clearColor: opts.clearColor ?? [0, 0, 0, 0], label, autoFree: false },
188
+ { uViewport: [width, height], ...cameraParams(cam), uTint: deps.tint(), ...deps.params() },
189
+ { textures: deps.textures, clearColor: opts.clearColor ?? [0, 0, 0, 0], label, autoFree: false },
185
190
  )
186
191
  let order = ordered === null ? deps.order : undefined
187
192
  let entry = entryFor(texture, order)
@@ -277,6 +282,9 @@ export function createViews(deps: ViewDeps): Views {
277
282
  setTint(tint) {
278
283
  for (let v of views) setTargetParams(v.texture, { uTint: tint })
279
284
  },
285
+ setParams(params) {
286
+ for (let v of views) setTargetParams(v.texture, params)
287
+ },
280
288
  dispose() {
281
289
  for (let v of [...views]) v.dispose()
282
290
  },