@driftengine/ui2d 4.12.0 → 4.13.1
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/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/spriteGl.d.ts +15 -0
- package/dist/spriteGl.js +30 -0
- package/dist/spriteGpu.d.ts +18 -0
- package/dist/spriteGpu.js +41 -2
- package/dist/spritePass.d.ts +23 -1
- package/dist/spritePass.js +45 -16
- package/dist/spriteTexture.d.ts +10 -0
- package/dist/theme.d.ts +9 -8
- package/dist/theme.js +19 -4
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/spriteGl.ts +39 -0
- package/src/spriteGpu.ts +58 -2
- package/src/spritePass.ts +84 -16
- package/src/spriteTexture.ts +11 -0
- package/src/theme.ts +14 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The 2D layer: quads with a texture on them, batched, in the order you drew them.
|
|
4
4
|
|
|
5
|
-
**Cost: 10.
|
|
5
|
+
**Cost: 10.6 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it
|
|
6
6
|
drifts more than 3% — the number is derived from the same floors that gate asserts, so a README
|
|
7
7
|
quoting a stale one is a red suite rather than a thing somebody notices.
|
|
8
8
|
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type { SpriteBatch, SpritePlacement, SpriteRun, UvRect } from './spriteBa
|
|
|
15
15
|
export { DEFAULT_SPRITE_CAPACITY, DEFAULT_SPRITE_SLOTS, createSpritePass } from './spritePass.ts';
|
|
16
16
|
export type { SpritePass, SpritePassOptions } from './spritePass.ts';
|
|
17
17
|
export { DEFAULT_SPRITE_TEXTURE_OPTIONS } from './spriteTexture.ts';
|
|
18
|
-
export type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
|
|
18
|
+
export type { SpriteImage, SpriteRegion, SpriteTextureOptions } from './spriteTexture.ts';
|
|
19
19
|
export { createSpriteFrame, frameOf, gridSheet, namedSheet, sheetFrame, sheetFrameHeight, sheetFrameWidth, } from './spriteSheet.ts';
|
|
20
20
|
export type { SheetEntry, SpriteFrame, SpriteSheet } from './spriteSheet.ts';
|
|
21
21
|
export { TILE_EMPTY, createTilemap, drawTilemap, setTile, tileAt } from './tilemap.ts';
|
package/dist/spriteGl.d.ts
CHANGED
|
@@ -12,6 +12,21 @@ export interface Webgl2Sprites {
|
|
|
12
12
|
export declare function createWebgl2Sprites(gl: WebGL2RenderingContext, capacity: number, slots: number, label: string): Webgl2Sprites;
|
|
13
13
|
/** Replace a texture slot. The previous texture in that slot is deleted. */
|
|
14
14
|
export declare function setWebgl2SpriteTexture(gl: WebGL2RenderingContext, sprites: Webgl2Sprites, slot: number, source: TexImageSource, options: SpriteTextureOptions): void;
|
|
15
|
+
/**
|
|
16
|
+
* Copy one rectangle of `source` into the slot's texture at the same place: the WebGL2 half of
|
|
17
|
+
* `SpritePass.updateTexture`, which checks that the slot holds a texture of `source`'s size and has
|
|
18
|
+
* clipped the rectangle to it. See `updateGpuSpriteTexture` for why it exists.
|
|
19
|
+
*
|
|
20
|
+
* **The rectangle is chosen by the unpack state, and the state is put back to zero.** WebGL2 reads a
|
|
21
|
+
* sub-rectangle of a canvas or a bitmap through `UNPACK_SKIP_PIXELS`, `UNPACK_SKIP_ROWS` and
|
|
22
|
+
* `UNPACK_ROW_LENGTH`, set to the image's own width so a row is read as a row of the image. That is
|
|
23
|
+
* context state every other upload on this context reads too, core's included, so leaving it set
|
|
24
|
+
* would have the next texture anywhere in the engine start at this rectangle's corner. A chain,
|
|
25
|
+
* where the slot has one, is generated again, since its lower levels mixed the texels just replaced.
|
|
26
|
+
*/
|
|
27
|
+
export declare function updateWebgl2SpriteTexture(gl: WebGL2RenderingContext, sprites: Webgl2Sprites, slot: number, source: TexImageSource & {
|
|
28
|
+
readonly width: number;
|
|
29
|
+
}, x: number, y: number, width: number, height: number, mipmap: boolean): void;
|
|
15
30
|
/**
|
|
16
31
|
* Fill a slot with one opaque white texel, from an array rather than an image.
|
|
17
32
|
*
|
package/dist/spriteGl.js
CHANGED
|
@@ -132,6 +132,36 @@ export function setWebgl2SpriteTexture(gl, sprites, slot, source, options) {
|
|
|
132
132
|
gl.activeTexture(gl.TEXTURE0);
|
|
133
133
|
sprites.textures[slot] = texture;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Copy one rectangle of `source` into the slot's texture at the same place: the WebGL2 half of
|
|
137
|
+
* `SpritePass.updateTexture`, which checks that the slot holds a texture of `source`'s size and has
|
|
138
|
+
* clipped the rectangle to it. See `updateGpuSpriteTexture` for why it exists.
|
|
139
|
+
*
|
|
140
|
+
* **The rectangle is chosen by the unpack state, and the state is put back to zero.** WebGL2 reads a
|
|
141
|
+
* sub-rectangle of a canvas or a bitmap through `UNPACK_SKIP_PIXELS`, `UNPACK_SKIP_ROWS` and
|
|
142
|
+
* `UNPACK_ROW_LENGTH`, set to the image's own width so a row is read as a row of the image. That is
|
|
143
|
+
* context state every other upload on this context reads too, core's included, so leaving it set
|
|
144
|
+
* would have the next texture anywhere in the engine start at this rectangle's corner. A chain,
|
|
145
|
+
* where the slot has one, is generated again, since its lower levels mixed the texels just replaced.
|
|
146
|
+
*/
|
|
147
|
+
export function updateWebgl2SpriteTexture(gl, sprites, slot, source, x, y, width, height, mipmap) {
|
|
148
|
+
const texture = sprites.textures[slot];
|
|
149
|
+
if (texture === null || texture === undefined)
|
|
150
|
+
return;
|
|
151
|
+
gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
|
|
152
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
153
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, source.width);
|
|
154
|
+
gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, x);
|
|
155
|
+
gl.pixelStorei(gl.UNPACK_SKIP_ROWS, y);
|
|
156
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
157
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
|
|
158
|
+
gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0);
|
|
159
|
+
gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0);
|
|
160
|
+
if (mipmap)
|
|
161
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
162
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
163
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
164
|
+
}
|
|
135
165
|
/**
|
|
136
166
|
* Fill a slot with one opaque white texel, from an array rather than an image.
|
|
137
167
|
*
|
package/dist/spriteGpu.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ import type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
|
|
|
5
5
|
export interface GpuSpriteSlot {
|
|
6
6
|
readonly texture: GPUTexture;
|
|
7
7
|
readonly bindGroup: GPUBindGroup;
|
|
8
|
+
/** Kept so a partial update can rebuild the chain it overwrote part of. */
|
|
9
|
+
readonly format: GPUTextureFormat;
|
|
10
|
+
readonly levels: number;
|
|
8
11
|
}
|
|
9
12
|
/** Pipelines by colour format, then depth format, then sample count: a lookup that allocates nothing. */
|
|
10
13
|
type PipelinesByTarget = Map<GPUTextureFormat, Map<GPUTextureFormat, Map<number, GPURenderPipeline>>>;
|
|
@@ -49,6 +52,21 @@ export declare function createGpuSprites(device: GPUDevice, format: GPUTextureFo
|
|
|
49
52
|
*/
|
|
50
53
|
export declare function spritePipelineFor(device: GPUDevice, sprites: GpuSprites, format: GPUTextureFormat, depthFormat: GPUTextureFormat, samples: number): GPURenderPipeline;
|
|
51
54
|
export declare function setGpuSpriteTexture(device: GPUDevice, sprites: GpuSprites, slot: number, source: SpriteImage, options: SpriteTextureOptions): void;
|
|
55
|
+
/**
|
|
56
|
+
* Copy one rectangle of `source` into the slot's texture at the same place, keeping the texture and
|
|
57
|
+
* its bind group: the WebGPU half of `SpritePass.updateTexture`, which checks that the slot holds a
|
|
58
|
+
* texture of `source`'s size before it calls this, and has clipped the rectangle to it.
|
|
59
|
+
*
|
|
60
|
+
* **Why this exists.** `setGpuSpriteTexture` destroys the slot's texture, makes it again at full size
|
|
61
|
+
* and copies the whole image in, so an atlas painted one picture at a time sent every texel of the
|
|
62
|
+
* atlas for each picture: measured by a consumer at about 100 ms for a 5120-texel side. This sends
|
|
63
|
+
* the rectangle. A chain, where the slot has one, is rendered again from level zero, because a lower
|
|
64
|
+
* level mixes the texels just replaced with their neighbours; that is GPU work rather than an upload.
|
|
65
|
+
*
|
|
66
|
+
* **Ordered like `setTexture`, on the queue**: a copy issued after a draw that reads the slot was
|
|
67
|
+
* recorded, and before that frame is submitted, is what the draw sees. Paint before `drawPass`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function updateGpuSpriteTexture(device: GPUDevice, sprites: GpuSprites, slot: number, source: SpriteImage, x: number, y: number, width: number, height: number): void;
|
|
52
70
|
/** The same one white texel, written straight into a texture. See the WebGL2 half for why. */
|
|
53
71
|
export declare function setGpuWhiteTexture(device: GPUDevice, sprites: GpuSprites, slot: number): void;
|
|
54
72
|
export declare function uploadGpuInstances(device: GPUDevice, sprites: GpuSprites, batch: SpriteBatch): void;
|
package/dist/spriteGpu.js
CHANGED
|
@@ -243,8 +243,47 @@ export function setGpuSpriteTexture(device, sprites, slot, source, options) {
|
|
|
243
243
|
{ binding: FRAG.textures.uSpriteTexture.sampler, resource: sampler },
|
|
244
244
|
],
|
|
245
245
|
});
|
|
246
|
-
sprites.slots[slot] = { texture, bindGroup };
|
|
246
|
+
sprites.slots[slot] = { texture, bindGroup, format, levels };
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Copy one rectangle of `source` into the slot's texture at the same place, keeping the texture and
|
|
250
|
+
* its bind group: the WebGPU half of `SpritePass.updateTexture`, which checks that the slot holds a
|
|
251
|
+
* texture of `source`'s size before it calls this, and has clipped the rectangle to it.
|
|
252
|
+
*
|
|
253
|
+
* **Why this exists.** `setGpuSpriteTexture` destroys the slot's texture, makes it again at full size
|
|
254
|
+
* and copies the whole image in, so an atlas painted one picture at a time sent every texel of the
|
|
255
|
+
* atlas for each picture: measured by a consumer at about 100 ms for a 5120-texel side. This sends
|
|
256
|
+
* the rectangle. A chain, where the slot has one, is rendered again from level zero, because a lower
|
|
257
|
+
* level mixes the texels just replaced with their neighbours; that is GPU work rather than an upload.
|
|
258
|
+
*
|
|
259
|
+
* **Ordered like `setTexture`, on the queue**: a copy issued after a draw that reads the slot was
|
|
260
|
+
* recorded, and before that frame is submitted, is what the draw sees. Paint before `drawPass`.
|
|
261
|
+
*/
|
|
262
|
+
export function updateGpuSpriteTexture(device, sprites, slot, source, x, y, width, height) {
|
|
263
|
+
const held = sprites.slots[slot];
|
|
264
|
+
if (held === null || held === undefined)
|
|
265
|
+
return;
|
|
266
|
+
/* Refilled rather than built: a timer's digits are an update every frame. */
|
|
267
|
+
COPY_SOURCE.source = source;
|
|
268
|
+
COPY_ORIGIN.x = x;
|
|
269
|
+
COPY_ORIGIN.y = y;
|
|
270
|
+
COPY_TARGET.texture = held.texture;
|
|
271
|
+
COPY_SIZE.width = width;
|
|
272
|
+
COPY_SIZE.height = height;
|
|
273
|
+
device.queue.copyExternalImageToTexture(COPY_SOURCE, COPY_TARGET, COPY_SIZE);
|
|
274
|
+
/* Let go of the caller's canvas until the next copy, rather than holding it alive here. */
|
|
275
|
+
COPY_SOURCE.source = EMPTY_SOURCE;
|
|
276
|
+
generateMipChain(device, sprites.mipPipelines, held.texture, held.format, held.levels);
|
|
277
|
+
}
|
|
278
|
+
/* The one origin both ends of the copy share: a rectangle lands where it was in the image. */
|
|
279
|
+
const COPY_ORIGIN = { x: 0, y: 0 };
|
|
280
|
+
const EMPTY_SOURCE = null;
|
|
281
|
+
const COPY_SOURCE = { source: EMPTY_SOURCE, origin: COPY_ORIGIN, flipY: false };
|
|
282
|
+
const COPY_TARGET = {
|
|
283
|
+
texture: null,
|
|
284
|
+
origin: COPY_ORIGIN,
|
|
285
|
+
};
|
|
286
|
+
const COPY_SIZE = { width: 0, height: 0 };
|
|
248
287
|
/** The same one white texel, written straight into a texture. See the WebGL2 half for why. */
|
|
249
288
|
export function setGpuWhiteTexture(device, sprites, slot) {
|
|
250
289
|
const texture = device.createTexture({
|
|
@@ -265,7 +304,7 @@ export function setGpuWhiteTexture(device, sprites, slot) {
|
|
|
265
304
|
{ binding: FRAG.textures.uSpriteTexture.sampler, resource: sprites.samplers.nearest },
|
|
266
305
|
],
|
|
267
306
|
});
|
|
268
|
-
sprites.slots[slot] = { texture, bindGroup };
|
|
307
|
+
sprites.slots[slot] = { texture, bindGroup, format: 'rgba8unorm', levels: 1 };
|
|
269
308
|
}
|
|
270
309
|
export function uploadGpuInstances(device, sprites, batch) {
|
|
271
310
|
if (batch.count === 0)
|
package/dist/spritePass.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { PassDefinition } from '@driftengine/core';
|
|
3
3
|
import type { Affine2D } from './camera2d.ts';
|
|
4
4
|
import type { SpriteBatch } from './spriteBatch.ts';
|
|
5
|
-
import type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
|
|
5
|
+
import type { SpriteImage, SpriteRegion, SpriteTextureOptions } from './spriteTexture.ts';
|
|
6
6
|
/** Sprites a frame, if the caller does not say. Four thousand quads is 224 KB of instance data. */
|
|
7
7
|
export declare const DEFAULT_SPRITE_CAPACITY = 4096;
|
|
8
8
|
/**
|
|
@@ -50,5 +50,27 @@ export interface SpritePass extends PassDefinition {
|
|
|
50
50
|
setTransform(affine: Affine2D): void;
|
|
51
51
|
/** Fill a texture slot. Safe before the pass is registered; applied when it gets a device. */
|
|
52
52
|
setTexture(slot: number, source: SpriteImage, options?: SpriteTextureOptions): void;
|
|
53
|
+
/**
|
|
54
|
+
* Copy `region` of `source` into the slot's texture at the same place, keeping the rest: what an
|
|
55
|
+
* atlas painted one picture at a time sends, instead of all of itself.
|
|
56
|
+
*
|
|
57
|
+
* The slot must hold a texture made from an image of `source`'s size, through `setTexture`; it
|
|
58
|
+
* keeps the options it was made with. Otherwise, or before the pass has a device, this does what
|
|
59
|
+
* `setTexture` does with the slot's options (the default ones for an empty slot), so a caller
|
|
60
|
+
* never has to ask which. The region is clipped to the image, and an empty one copies nothing. A
|
|
61
|
+
* slot with a mip chain has the chain rebuilt.
|
|
62
|
+
*
|
|
63
|
+
* **The saving, measured by a consumer painting a 2D layer into atlases**: `setTexture` sends the
|
|
64
|
+
* whole image and, on WebGPU, makes the texture again, about 100 ms for a 5120-texel side, while
|
|
65
|
+
* the pictures new on that frame covered about 6 of its 26 million texels and a typical later
|
|
66
|
+
* frame changes a timer's digits. Call it before `drawPass` in a frame, as `setTexture` is.
|
|
67
|
+
*
|
|
68
|
+
* **Paint such an atlas into a 2D context made with `willReadFrequently: true`.** On WebGPU a
|
|
69
|
+
* canvas drawn on the GPU is transferred whole before any part of it is copied, measured at 66 ms
|
|
70
|
+
* for one 256 by 64 rectangle of a 5120-texel atlas against 0.4 ms from a canvas kept in memory;
|
|
71
|
+
* WebGL2 copies a rectangle quickly from either. What the option gives up is painting on the GPU,
|
|
72
|
+
* which matters for a canvas redrawn whole every frame and not for an atlas painted in pieces.
|
|
73
|
+
*/
|
|
74
|
+
updateTexture(slot: number, source: SpriteImage, region: SpriteRegion): void;
|
|
53
75
|
}
|
|
54
76
|
export declare function createSpritePass(options?: SpritePassOptions): SpritePass;
|
package/dist/spritePass.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** The 2D layer as a pass a consumer registers: one batch of quads, drawn where the caller says. */
|
|
2
|
+
import { interfaceGrade } from '@driftengine/core';
|
|
2
3
|
import { SPRITE_BINDINGS } from './shaders/generated/sprite.wgsl.js';
|
|
3
4
|
import { createAffine2D } from './camera2d.js';
|
|
4
5
|
import { createSpriteBatch, resetSpriteBatch } from './spriteBatch.js';
|
|
5
|
-
import { createWebgl2Sprites, disposeWebgl2Sprites, drawWebgl2Sprites, setWebgl2SpriteTexture, setWebgl2WhiteTexture, uploadWebgl2Instances, } from './spriteGl.js';
|
|
6
|
-
import { createGpuSprites, disposeGpuSprites, drawGpuSprites, spritePipelineFor, setGpuSpriteTexture, setGpuWhiteTexture, uploadGpuInstances, } from './spriteGpu.js';
|
|
6
|
+
import { createWebgl2Sprites, disposeWebgl2Sprites, drawWebgl2Sprites, setWebgl2SpriteTexture, updateWebgl2SpriteTexture, setWebgl2WhiteTexture, uploadWebgl2Instances, } from './spriteGl.js';
|
|
7
|
+
import { createGpuSprites, disposeGpuSprites, drawGpuSprites, spritePipelineFor, setGpuSpriteTexture, updateGpuSpriteTexture, setGpuWhiteTexture, uploadGpuInstances, } from './spriteGpu.js';
|
|
7
8
|
import { DEFAULT_SPRITE_TEXTURE_OPTIONS } from './spriteTexture.js';
|
|
8
9
|
const VERT = SPRITE_BINDINGS.SPRITE_VERT;
|
|
9
10
|
const FRAG = SPRITE_BINDINGS.SPRITE_FRAG;
|
|
@@ -36,6 +37,21 @@ export function createSpritePass(options = {}) {
|
|
|
36
37
|
let device = null;
|
|
37
38
|
let clipCorrection = IDENTITY_CLIP;
|
|
38
39
|
const pending = [];
|
|
40
|
+
/* What each slot was made from, so an update knows whether a partial copy fits it. */
|
|
41
|
+
const held = Array.from({ length: slots }, () => null);
|
|
42
|
+
function setTexture(slot, source, textureOptions) {
|
|
43
|
+
if (slot < 0 || slot >= slots) {
|
|
44
|
+
throw new RangeError(`${label}: slot ${slot} is outside the ${slots} this pass has`);
|
|
45
|
+
}
|
|
46
|
+
const options = textureOptions ?? DEFAULT_SPRITE_TEXTURE_OPTIONS;
|
|
47
|
+
held[slot] = { width: source.width, height: source.height, options };
|
|
48
|
+
const entry = { slot, source, options };
|
|
49
|
+
if (gl === null && gpu === null) {
|
|
50
|
+
pending.push(entry);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
applyTexture(entry);
|
|
54
|
+
}
|
|
39
55
|
const applyTexture = (entry) => {
|
|
40
56
|
if (gl !== null && context !== null) {
|
|
41
57
|
setWebgl2SpriteTexture(context, gl, entry.slot, entry.source, entry.options);
|
|
@@ -55,20 +71,32 @@ export function createSpritePass(options = {}) {
|
|
|
55
71
|
setTransform(affine) {
|
|
56
72
|
toNdc.set(affine);
|
|
57
73
|
},
|
|
58
|
-
setTexture
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
setTexture,
|
|
75
|
+
updateTexture(slot, source, region) {
|
|
76
|
+
const was = slot >= 0 && slot < slots ? held[slot] : null;
|
|
77
|
+
if (was === null ||
|
|
78
|
+
was === undefined ||
|
|
79
|
+
was.width !== source.width ||
|
|
80
|
+
was.height !== source.height ||
|
|
81
|
+
(gl === null && gpu === null)) {
|
|
82
|
+
setTexture(slot, source, was?.options);
|
|
83
|
+
return;
|
|
61
84
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
85
|
+
/* Whole texels inside the image: a rectangle reaching past an edge copies what is inside it. */
|
|
86
|
+
const x0 = Math.max(0, Math.floor(region.x));
|
|
87
|
+
const y0 = Math.max(0, Math.floor(region.y));
|
|
88
|
+
const x1 = Math.min(source.width, Math.ceil(region.x + region.width));
|
|
89
|
+
const y1 = Math.min(source.height, Math.ceil(region.y + region.height));
|
|
90
|
+
if (x1 <= x0 || y1 <= y0)
|
|
91
|
+
return;
|
|
92
|
+
if (gl !== null && context !== null) {
|
|
93
|
+
const mipmap = was.options.mipmap === true;
|
|
94
|
+
updateWebgl2SpriteTexture(context, gl, slot, source, x0, y0, x1 - x0, y1 - y0, mipmap);
|
|
69
95
|
return;
|
|
70
96
|
}
|
|
71
|
-
|
|
97
|
+
if (gpu !== null && device !== null) {
|
|
98
|
+
updateGpuSpriteTexture(device, gpu, slot, source, x0, y0, x1 - x0, y1 - y0);
|
|
99
|
+
}
|
|
72
100
|
},
|
|
73
101
|
init(passDevice) {
|
|
74
102
|
/*
|
|
@@ -101,7 +129,7 @@ export function createSpritePass(options = {}) {
|
|
|
101
129
|
if (gl === null)
|
|
102
130
|
return;
|
|
103
131
|
uploadWebgl2Instances(ctx.gl, gl, batch);
|
|
104
|
-
drawWebgl2Sprites(ctx.gl, gl, batch, toNdc, clipCorrection, ctx.outputTransform,
|
|
132
|
+
drawWebgl2Sprites(ctx.gl, gl, batch, toNdc, clipCorrection, interfaceGrade(ctx.outputTransform), 1);
|
|
105
133
|
return;
|
|
106
134
|
}
|
|
107
135
|
if (gpu === null || device === null)
|
|
@@ -114,8 +142,9 @@ export function createSpritePass(options = {}) {
|
|
|
114
142
|
f[VERT.fields.uToNdc1.offset / 4] = toNdc[4];
|
|
115
143
|
f[VERT.fields.uToNdc1.offset / 4 + 1] = toNdc[5];
|
|
116
144
|
f.set(clipCorrection, VERT.fields.uClipCorrection.offset / 4);
|
|
117
|
-
|
|
118
|
-
gpu.
|
|
145
|
+
/* A 2D layer is painted for the screen: encoded, never put through the scene's curve. */
|
|
146
|
+
gpu.fragmentInts[FRAG.fields.uOutputTransform.offset / 4] = interfaceGrade(ctx.outputTransform);
|
|
147
|
+
gpu.fragmentFloats[FRAG.fields.uOutputExposure.offset / 4] = 1;
|
|
119
148
|
/*
|
|
120
149
|
* Written from inside an open render pass, which is allowed and ordered: a queue write
|
|
121
150
|
* issued now lands before the command buffer this pass is being recorded into is submitted.
|
package/dist/spriteTexture.d.ts
CHANGED
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
* ask the caller how big the image it just handed over is.
|
|
13
13
|
*/
|
|
14
14
|
export type SpriteImage = ImageBitmap | HTMLCanvasElement | OffscreenCanvas;
|
|
15
|
+
/**
|
|
16
|
+
* A rectangle of a sprite image, in its texels from the top left: what `updateTexture` copies, from
|
|
17
|
+
* the image into the slot's texture at the same place.
|
|
18
|
+
*/
|
|
19
|
+
export interface SpriteRegion {
|
|
20
|
+
readonly x: number;
|
|
21
|
+
readonly y: number;
|
|
22
|
+
readonly width: number;
|
|
23
|
+
readonly height: number;
|
|
24
|
+
}
|
|
15
25
|
export interface SpriteTextureOptions {
|
|
16
26
|
/**
|
|
17
27
|
* `srgb` is right for anything painted to be looked at, which is every sprite sheet — the
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Named colours and sizes, so an interface stops repeating its own constants.
|
|
3
|
-
*
|
|
4
|
-
* **Flattened at derivation rather than chained at lookup.** A token is read per node per frame, so
|
|
5
|
-
* a chain of parent themes would be a walk on every one of those reads. Spreading once when a
|
|
6
|
-
* theme is derived costs a record and makes every lookup a single property access.
|
|
7
|
-
*/
|
|
1
|
+
/** Named colours, packed as `0xRRGGBBAA`, and sizes, flattened when the theme is derived. */
|
|
8
2
|
export interface Theme {
|
|
9
3
|
readonly values: Readonly<Record<string, number>>;
|
|
10
4
|
}
|
|
@@ -14,7 +8,14 @@ export declare function deriveTheme(base: Theme, overrides: Record<string, numbe
|
|
|
14
8
|
export declare function themeColour(theme: Theme, name: string, fallback: number): number;
|
|
15
9
|
export declare function themeSize(theme: Theme, name: string, fallback: number): number;
|
|
16
10
|
/**
|
|
17
|
-
* A packed `0xRRGGBBAA` token as the four floats a node's `background` and `tint` are
|
|
11
|
+
* A packed `0xRRGGBBAA` token as the four floats a node's `background` and `tint` are: the colour
|
|
12
|
+
* decoded from sRGB to the linear light the renderer draws in, the coverage as it is.
|
|
13
|
+
*
|
|
14
|
+
* **Decoded, because a hex colour is a display colour** wherever one is written, CSS included, and
|
|
15
|
+
* the renderer grades everything it draws: read as linear, `0x336699` came out as `0x7caacb` under
|
|
16
|
+
* the default `outputTransform`, every panel a washed-out version of the one picked. What it gives
|
|
17
|
+
* up is a token meant as an amount rather than a look, which a colour token is not. A renderer
|
|
18
|
+
* asked for `outputTransform: 'none'` draws these darker than picked, as it does every colour.
|
|
18
19
|
*
|
|
19
20
|
* **Two of this package's own APIs did not meet**: a theme token is one number and a `UiNode`
|
|
20
21
|
* colour is a `Float32Array`, so every consumer wrote the same four shifts. The editor found it by
|
package/dist/theme.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named colours and sizes, so an interface stops repeating its own constants.
|
|
3
|
+
*
|
|
4
|
+
* **Flattened at derivation rather than chained at lookup.** A token is read per node per frame, so
|
|
5
|
+
* a chain of parent themes would be a walk on every one of those reads. Spreading once when a
|
|
6
|
+
* theme is derived costs a record and makes every lookup a single property access.
|
|
7
|
+
*/
|
|
8
|
+
import { srgbToLinear } from '@driftengine/core';
|
|
1
9
|
export function createTheme(values) {
|
|
2
10
|
return { values: { ...values } };
|
|
3
11
|
}
|
|
@@ -12,7 +20,14 @@ export function themeSize(theme, name, fallback) {
|
|
|
12
20
|
return theme.values[name] ?? fallback;
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
|
-
* A packed `0xRRGGBBAA` token as the four floats a node's `background` and `tint` are
|
|
23
|
+
* A packed `0xRRGGBBAA` token as the four floats a node's `background` and `tint` are: the colour
|
|
24
|
+
* decoded from sRGB to the linear light the renderer draws in, the coverage as it is.
|
|
25
|
+
*
|
|
26
|
+
* **Decoded, because a hex colour is a display colour** wherever one is written, CSS included, and
|
|
27
|
+
* the renderer grades everything it draws: read as linear, `0x336699` came out as `0x7caacb` under
|
|
28
|
+
* the default `outputTransform`, every panel a washed-out version of the one picked. What it gives
|
|
29
|
+
* up is a token meant as an amount rather than a look, which a colour token is not. A renderer
|
|
30
|
+
* asked for `outputTransform: 'none'` draws these darker than picked, as it does every colour.
|
|
16
31
|
*
|
|
17
32
|
* **Two of this package's own APIs did not meet**: a theme token is one number and a `UiNode`
|
|
18
33
|
* colour is a `Float32Array`, so every consumer wrote the same four shifts. The editor found it by
|
|
@@ -24,9 +39,9 @@ export function themeSize(theme, name, fallback) {
|
|
|
24
39
|
* number, which is exactly the shape of bug that survives review.
|
|
25
40
|
*/
|
|
26
41
|
export function unpackRgba(packed, out) {
|
|
27
|
-
out[0] = ((packed >>> 24) & 0xff) / 255;
|
|
28
|
-
out[1] = ((packed >>> 16) & 0xff) / 255;
|
|
29
|
-
out[2] = ((packed >>> 8) & 0xff) / 255;
|
|
42
|
+
out[0] = srgbToLinear(((packed >>> 24) & 0xff) / 255);
|
|
43
|
+
out[1] = srgbToLinear(((packed >>> 16) & 0xff) / 255);
|
|
44
|
+
out[2] = srgbToLinear(((packed >>> 8) & 0xff) / 255);
|
|
30
45
|
out[3] = (packed & 0xff) / 255;
|
|
31
46
|
return out;
|
|
32
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/ui2d",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.1",
|
|
4
4
|
"description": "The 2D layer: batched sprites, sheets, tilemaps, and a retained interface tree over them",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@driftengine/core": "4.
|
|
30
|
+
"@driftengine/core": "4.13.1"
|
|
31
31
|
},
|
|
32
32
|
"author": "Drift Technologies",
|
|
33
33
|
"repository": {
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { DEFAULT_SPRITE_CAPACITY, DEFAULT_SPRITE_SLOTS, createSpritePass } from
|
|
|
25
25
|
export type { SpritePass, SpritePassOptions } from './spritePass.ts';
|
|
26
26
|
|
|
27
27
|
export { DEFAULT_SPRITE_TEXTURE_OPTIONS } from './spriteTexture.ts';
|
|
28
|
-
export type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
|
|
28
|
+
export type { SpriteImage, SpriteRegion, SpriteTextureOptions } from './spriteTexture.ts';
|
|
29
29
|
|
|
30
30
|
export {
|
|
31
31
|
createSpriteFrame,
|
package/src/spriteGl.ts
CHANGED
|
@@ -169,6 +169,45 @@ export function setWebgl2SpriteTexture(
|
|
|
169
169
|
sprites.textures[slot] = texture;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Copy one rectangle of `source` into the slot's texture at the same place: the WebGL2 half of
|
|
174
|
+
* `SpritePass.updateTexture`, which checks that the slot holds a texture of `source`'s size and has
|
|
175
|
+
* clipped the rectangle to it. See `updateGpuSpriteTexture` for why it exists.
|
|
176
|
+
*
|
|
177
|
+
* **The rectangle is chosen by the unpack state, and the state is put back to zero.** WebGL2 reads a
|
|
178
|
+
* sub-rectangle of a canvas or a bitmap through `UNPACK_SKIP_PIXELS`, `UNPACK_SKIP_ROWS` and
|
|
179
|
+
* `UNPACK_ROW_LENGTH`, set to the image's own width so a row is read as a row of the image. That is
|
|
180
|
+
* context state every other upload on this context reads too, core's included, so leaving it set
|
|
181
|
+
* would have the next texture anywhere in the engine start at this rectangle's corner. A chain,
|
|
182
|
+
* where the slot has one, is generated again, since its lower levels mixed the texels just replaced.
|
|
183
|
+
*/
|
|
184
|
+
export function updateWebgl2SpriteTexture(
|
|
185
|
+
gl: WebGL2RenderingContext,
|
|
186
|
+
sprites: Webgl2Sprites,
|
|
187
|
+
slot: number,
|
|
188
|
+
source: TexImageSource & { readonly width: number },
|
|
189
|
+
x: number,
|
|
190
|
+
y: number,
|
|
191
|
+
width: number,
|
|
192
|
+
height: number,
|
|
193
|
+
mipmap: boolean,
|
|
194
|
+
): void {
|
|
195
|
+
const texture = sprites.textures[slot];
|
|
196
|
+
if (texture === null || texture === undefined) return;
|
|
197
|
+
gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
|
|
198
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
199
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, source.width);
|
|
200
|
+
gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, x);
|
|
201
|
+
gl.pixelStorei(gl.UNPACK_SKIP_ROWS, y);
|
|
202
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
203
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
|
|
204
|
+
gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0);
|
|
205
|
+
gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0);
|
|
206
|
+
if (mipmap) gl.generateMipmap(gl.TEXTURE_2D);
|
|
207
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
208
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
209
|
+
}
|
|
210
|
+
|
|
172
211
|
/**
|
|
173
212
|
* Fill a slot with one opaque white texel, from an array rather than an image.
|
|
174
213
|
*
|
package/src/spriteGpu.ts
CHANGED
|
@@ -37,6 +37,9 @@ const TEXTURE_USAGE = 4 | 2 | 16; // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHM
|
|
|
37
37
|
export interface GpuSpriteSlot {
|
|
38
38
|
readonly texture: GPUTexture;
|
|
39
39
|
readonly bindGroup: GPUBindGroup;
|
|
40
|
+
/** Kept so a partial update can rebuild the chain it overwrote part of. */
|
|
41
|
+
readonly format: GPUTextureFormat;
|
|
42
|
+
readonly levels: number;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/** Pipelines by colour format, then depth format, then sample count: a lookup that allocates nothing. */
|
|
@@ -331,9 +334,62 @@ export function setGpuSpriteTexture(
|
|
|
331
334
|
{ binding: FRAG.textures.uSpriteTexture.sampler, resource: sampler },
|
|
332
335
|
],
|
|
333
336
|
});
|
|
334
|
-
sprites.slots[slot] = { texture, bindGroup };
|
|
337
|
+
sprites.slots[slot] = { texture, bindGroup, format, levels };
|
|
335
338
|
}
|
|
336
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Copy one rectangle of `source` into the slot's texture at the same place, keeping the texture and
|
|
342
|
+
* its bind group: the WebGPU half of `SpritePass.updateTexture`, which checks that the slot holds a
|
|
343
|
+
* texture of `source`'s size before it calls this, and has clipped the rectangle to it.
|
|
344
|
+
*
|
|
345
|
+
* **Why this exists.** `setGpuSpriteTexture` destroys the slot's texture, makes it again at full size
|
|
346
|
+
* and copies the whole image in, so an atlas painted one picture at a time sent every texel of the
|
|
347
|
+
* atlas for each picture: measured by a consumer at about 100 ms for a 5120-texel side. This sends
|
|
348
|
+
* the rectangle. A chain, where the slot has one, is rendered again from level zero, because a lower
|
|
349
|
+
* level mixes the texels just replaced with their neighbours; that is GPU work rather than an upload.
|
|
350
|
+
*
|
|
351
|
+
* **Ordered like `setTexture`, on the queue**: a copy issued after a draw that reads the slot was
|
|
352
|
+
* recorded, and before that frame is submitted, is what the draw sees. Paint before `drawPass`.
|
|
353
|
+
*/
|
|
354
|
+
export function updateGpuSpriteTexture(
|
|
355
|
+
device: GPUDevice,
|
|
356
|
+
sprites: GpuSprites,
|
|
357
|
+
slot: number,
|
|
358
|
+
source: SpriteImage,
|
|
359
|
+
x: number,
|
|
360
|
+
y: number,
|
|
361
|
+
width: number,
|
|
362
|
+
height: number,
|
|
363
|
+
): void {
|
|
364
|
+
const held = sprites.slots[slot];
|
|
365
|
+
if (held === null || held === undefined) return;
|
|
366
|
+
/* Refilled rather than built: a timer's digits are an update every frame. */
|
|
367
|
+
COPY_SOURCE.source = source as GPUCopyExternalImageSource;
|
|
368
|
+
COPY_ORIGIN.x = x;
|
|
369
|
+
COPY_ORIGIN.y = y;
|
|
370
|
+
COPY_TARGET.texture = held.texture;
|
|
371
|
+
COPY_SIZE.width = width;
|
|
372
|
+
COPY_SIZE.height = height;
|
|
373
|
+
device.queue.copyExternalImageToTexture(COPY_SOURCE, COPY_TARGET, COPY_SIZE);
|
|
374
|
+
/* Let go of the caller's canvas until the next copy, rather than holding it alive here. */
|
|
375
|
+
COPY_SOURCE.source = EMPTY_SOURCE;
|
|
376
|
+
generateMipChain(device, sprites.mipPipelines, held.texture, held.format, held.levels);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* The one origin both ends of the copy share: a rectangle lands where it was in the image. */
|
|
380
|
+
const COPY_ORIGIN = { x: 0, y: 0 };
|
|
381
|
+
const EMPTY_SOURCE = null as unknown as GPUCopyExternalImageSource;
|
|
382
|
+
const COPY_SOURCE: {
|
|
383
|
+
source: GPUCopyExternalImageSource;
|
|
384
|
+
origin: typeof COPY_ORIGIN;
|
|
385
|
+
flipY: boolean;
|
|
386
|
+
} = { source: EMPTY_SOURCE, origin: COPY_ORIGIN, flipY: false };
|
|
387
|
+
const COPY_TARGET: { texture: GPUTexture; origin: typeof COPY_ORIGIN } = {
|
|
388
|
+
texture: null as unknown as GPUTexture,
|
|
389
|
+
origin: COPY_ORIGIN,
|
|
390
|
+
};
|
|
391
|
+
const COPY_SIZE = { width: 0, height: 0 };
|
|
392
|
+
|
|
337
393
|
/** The same one white texel, written straight into a texture. See the WebGL2 half for why. */
|
|
338
394
|
export function setGpuWhiteTexture(device: GPUDevice, sprites: GpuSprites, slot: number): void {
|
|
339
395
|
const texture = device.createTexture({
|
|
@@ -359,7 +415,7 @@ export function setGpuWhiteTexture(device: GPUDevice, sprites: GpuSprites, slot:
|
|
|
359
415
|
{ binding: FRAG.textures.uSpriteTexture.sampler, resource: sprites.samplers.nearest },
|
|
360
416
|
],
|
|
361
417
|
});
|
|
362
|
-
sprites.slots[slot] = { texture, bindGroup };
|
|
418
|
+
sprites.slots[slot] = { texture, bindGroup, format: 'rgba8unorm', levels: 1 };
|
|
363
419
|
}
|
|
364
420
|
|
|
365
421
|
export function uploadGpuInstances(
|
package/src/spritePass.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The 2D layer as a pass a consumer registers: one batch of quads, drawn where the caller says. */
|
|
2
2
|
|
|
3
|
+
import { interfaceGrade } from '@driftengine/core';
|
|
3
4
|
import type { PassContext, PassDefinition, PassDevice } from '@driftengine/core';
|
|
4
5
|
|
|
5
6
|
import { SPRITE_BINDINGS } from './shaders/generated/sprite.wgsl.ts';
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
disposeWebgl2Sprites,
|
|
13
14
|
drawWebgl2Sprites,
|
|
14
15
|
setWebgl2SpriteTexture,
|
|
16
|
+
updateWebgl2SpriteTexture,
|
|
15
17
|
setWebgl2WhiteTexture,
|
|
16
18
|
uploadWebgl2Instances,
|
|
17
19
|
} from './spriteGl.ts';
|
|
@@ -22,12 +24,13 @@ import {
|
|
|
22
24
|
drawGpuSprites,
|
|
23
25
|
spritePipelineFor,
|
|
24
26
|
setGpuSpriteTexture,
|
|
27
|
+
updateGpuSpriteTexture,
|
|
25
28
|
setGpuWhiteTexture,
|
|
26
29
|
uploadGpuInstances,
|
|
27
30
|
} from './spriteGpu.ts';
|
|
28
31
|
import type { GpuSprites } from './spriteGpu.ts';
|
|
29
32
|
import { DEFAULT_SPRITE_TEXTURE_OPTIONS } from './spriteTexture.ts';
|
|
30
|
-
import type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
|
|
33
|
+
import type { SpriteImage, SpriteRegion, SpriteTextureOptions } from './spriteTexture.ts';
|
|
31
34
|
|
|
32
35
|
const VERT = SPRITE_BINDINGS.SPRITE_VERT;
|
|
33
36
|
const FRAG = SPRITE_BINDINGS.SPRITE_FRAG;
|
|
@@ -85,6 +88,34 @@ export interface SpritePass extends PassDefinition {
|
|
|
85
88
|
setTransform(affine: Affine2D): void;
|
|
86
89
|
/** Fill a texture slot. Safe before the pass is registered; applied when it gets a device. */
|
|
87
90
|
setTexture(slot: number, source: SpriteImage, options?: SpriteTextureOptions): void;
|
|
91
|
+
/**
|
|
92
|
+
* Copy `region` of `source` into the slot's texture at the same place, keeping the rest: what an
|
|
93
|
+
* atlas painted one picture at a time sends, instead of all of itself.
|
|
94
|
+
*
|
|
95
|
+
* The slot must hold a texture made from an image of `source`'s size, through `setTexture`; it
|
|
96
|
+
* keeps the options it was made with. Otherwise, or before the pass has a device, this does what
|
|
97
|
+
* `setTexture` does with the slot's options (the default ones for an empty slot), so a caller
|
|
98
|
+
* never has to ask which. The region is clipped to the image, and an empty one copies nothing. A
|
|
99
|
+
* slot with a mip chain has the chain rebuilt.
|
|
100
|
+
*
|
|
101
|
+
* **The saving, measured by a consumer painting a 2D layer into atlases**: `setTexture` sends the
|
|
102
|
+
* whole image and, on WebGPU, makes the texture again, about 100 ms for a 5120-texel side, while
|
|
103
|
+
* the pictures new on that frame covered about 6 of its 26 million texels and a typical later
|
|
104
|
+
* frame changes a timer's digits. Call it before `drawPass` in a frame, as `setTexture` is.
|
|
105
|
+
*
|
|
106
|
+
* **Paint such an atlas into a 2D context made with `willReadFrequently: true`.** On WebGPU a
|
|
107
|
+
* canvas drawn on the GPU is transferred whole before any part of it is copied, measured at 66 ms
|
|
108
|
+
* for one 256 by 64 rectangle of a 5120-texel atlas against 0.4 ms from a canvas kept in memory;
|
|
109
|
+
* WebGL2 copies a rectangle quickly from either. What the option gives up is painting on the GPU,
|
|
110
|
+
* which matters for a canvas redrawn whole every frame and not for an atlas painted in pieces.
|
|
111
|
+
*/
|
|
112
|
+
updateTexture(slot: number, source: SpriteImage, region: SpriteRegion): void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface HeldTexture {
|
|
116
|
+
width: number;
|
|
117
|
+
height: number;
|
|
118
|
+
options: SpriteTextureOptions;
|
|
88
119
|
}
|
|
89
120
|
|
|
90
121
|
interface PendingTexture {
|
|
@@ -111,6 +142,26 @@ export function createSpritePass(options: SpritePassOptions = {}): SpritePass {
|
|
|
111
142
|
let device: GPUDevice | null = null;
|
|
112
143
|
let clipCorrection: Float32Array = IDENTITY_CLIP;
|
|
113
144
|
const pending: PendingTexture[] = [];
|
|
145
|
+
/* What each slot was made from, so an update knows whether a partial copy fits it. */
|
|
146
|
+
const held: (HeldTexture | null)[] = Array.from({ length: slots }, () => null);
|
|
147
|
+
|
|
148
|
+
function setTexture(
|
|
149
|
+
slot: number,
|
|
150
|
+
source: SpriteImage,
|
|
151
|
+
textureOptions?: SpriteTextureOptions,
|
|
152
|
+
): void {
|
|
153
|
+
if (slot < 0 || slot >= slots) {
|
|
154
|
+
throw new RangeError(`${label}: slot ${slot} is outside the ${slots} this pass has`);
|
|
155
|
+
}
|
|
156
|
+
const options = textureOptions ?? DEFAULT_SPRITE_TEXTURE_OPTIONS;
|
|
157
|
+
held[slot] = { width: source.width, height: source.height, options };
|
|
158
|
+
const entry: PendingTexture = { slot, source, options };
|
|
159
|
+
if (gl === null && gpu === null) {
|
|
160
|
+
pending.push(entry);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
applyTexture(entry);
|
|
164
|
+
}
|
|
114
165
|
|
|
115
166
|
const applyTexture = (entry: PendingTexture): void => {
|
|
116
167
|
if (gl !== null && context !== null) {
|
|
@@ -135,20 +186,34 @@ export function createSpritePass(options: SpritePassOptions = {}): SpritePass {
|
|
|
135
186
|
toNdc.set(affine);
|
|
136
187
|
},
|
|
137
188
|
|
|
138
|
-
setTexture
|
|
139
|
-
|
|
140
|
-
|
|
189
|
+
setTexture,
|
|
190
|
+
|
|
191
|
+
updateTexture(slot: number, source: SpriteImage, region: SpriteRegion): void {
|
|
192
|
+
const was = slot >= 0 && slot < slots ? held[slot] : null;
|
|
193
|
+
if (
|
|
194
|
+
was === null ||
|
|
195
|
+
was === undefined ||
|
|
196
|
+
was.width !== source.width ||
|
|
197
|
+
was.height !== source.height ||
|
|
198
|
+
(gl === null && gpu === null)
|
|
199
|
+
) {
|
|
200
|
+
setTexture(slot, source, was?.options);
|
|
201
|
+
return;
|
|
141
202
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
203
|
+
/* Whole texels inside the image: a rectangle reaching past an edge copies what is inside it. */
|
|
204
|
+
const x0 = Math.max(0, Math.floor(region.x));
|
|
205
|
+
const y0 = Math.max(0, Math.floor(region.y));
|
|
206
|
+
const x1 = Math.min(source.width, Math.ceil(region.x + region.width));
|
|
207
|
+
const y1 = Math.min(source.height, Math.ceil(region.y + region.height));
|
|
208
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
209
|
+
if (gl !== null && context !== null) {
|
|
210
|
+
const mipmap = was.options.mipmap === true;
|
|
211
|
+
updateWebgl2SpriteTexture(context, gl, slot, source, x0, y0, x1 - x0, y1 - y0, mipmap);
|
|
149
212
|
return;
|
|
150
213
|
}
|
|
151
|
-
|
|
214
|
+
if (gpu !== null && device !== null) {
|
|
215
|
+
updateGpuSpriteTexture(device, gpu, slot, source, x0, y0, x1 - x0, y1 - y0);
|
|
216
|
+
}
|
|
152
217
|
},
|
|
153
218
|
|
|
154
219
|
init(passDevice: PassDevice): void {
|
|
@@ -191,8 +256,8 @@ export function createSpritePass(options: SpritePassOptions = {}): SpritePass {
|
|
|
191
256
|
batch,
|
|
192
257
|
toNdc,
|
|
193
258
|
clipCorrection,
|
|
194
|
-
ctx.outputTransform,
|
|
195
|
-
|
|
259
|
+
interfaceGrade(ctx.outputTransform),
|
|
260
|
+
1,
|
|
196
261
|
);
|
|
197
262
|
return;
|
|
198
263
|
}
|
|
@@ -205,8 +270,11 @@ export function createSpritePass(options: SpritePassOptions = {}): SpritePass {
|
|
|
205
270
|
f[VERT.fields.uToNdc1.offset / 4] = toNdc[4] as number;
|
|
206
271
|
f[VERT.fields.uToNdc1.offset / 4 + 1] = toNdc[5] as number;
|
|
207
272
|
f.set(clipCorrection, VERT.fields.uClipCorrection.offset / 4);
|
|
208
|
-
|
|
209
|
-
gpu.
|
|
273
|
+
/* A 2D layer is painted for the screen: encoded, never put through the scene's curve. */
|
|
274
|
+
gpu.fragmentInts[FRAG.fields.uOutputTransform.offset / 4] = interfaceGrade(
|
|
275
|
+
ctx.outputTransform,
|
|
276
|
+
);
|
|
277
|
+
gpu.fragmentFloats[FRAG.fields.uOutputExposure.offset / 4] = 1;
|
|
210
278
|
/*
|
|
211
279
|
* Written from inside an open render pass, which is allowed and ordered: a queue write
|
|
212
280
|
* issued now lands before the command buffer this pass is being recorded into is submitted.
|
package/src/spriteTexture.ts
CHANGED
|
@@ -14,6 +14,17 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export type SpriteImage = ImageBitmap | HTMLCanvasElement | OffscreenCanvas;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A rectangle of a sprite image, in its texels from the top left: what `updateTexture` copies, from
|
|
19
|
+
* the image into the slot's texture at the same place.
|
|
20
|
+
*/
|
|
21
|
+
export interface SpriteRegion {
|
|
22
|
+
readonly x: number;
|
|
23
|
+
readonly y: number;
|
|
24
|
+
readonly width: number;
|
|
25
|
+
readonly height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export interface SpriteTextureOptions {
|
|
18
29
|
/**
|
|
19
30
|
* `srgb` is right for anything painted to be looked at, which is every sprite sheet — the
|
package/src/theme.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* a chain of parent themes would be a walk on every one of those reads. Spreading once when a
|
|
6
6
|
* theme is derived costs a record and makes every lookup a single property access.
|
|
7
7
|
*/
|
|
8
|
+
import { srgbToLinear } from '@driftengine/core';
|
|
9
|
+
|
|
10
|
+
/** Named colours, packed as `0xRRGGBBAA`, and sizes, flattened when the theme is derived. */
|
|
8
11
|
export interface Theme {
|
|
9
12
|
readonly values: Readonly<Record<string, number>>;
|
|
10
13
|
}
|
|
@@ -27,7 +30,14 @@ export function themeSize(theme: Theme, name: string, fallback: number): number
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
|
-
* A packed `0xRRGGBBAA` token as the four floats a node's `background` and `tint` are
|
|
33
|
+
* A packed `0xRRGGBBAA` token as the four floats a node's `background` and `tint` are: the colour
|
|
34
|
+
* decoded from sRGB to the linear light the renderer draws in, the coverage as it is.
|
|
35
|
+
*
|
|
36
|
+
* **Decoded, because a hex colour is a display colour** wherever one is written, CSS included, and
|
|
37
|
+
* the renderer grades everything it draws: read as linear, `0x336699` came out as `0x7caacb` under
|
|
38
|
+
* the default `outputTransform`, every panel a washed-out version of the one picked. What it gives
|
|
39
|
+
* up is a token meant as an amount rather than a look, which a colour token is not. A renderer
|
|
40
|
+
* asked for `outputTransform: 'none'` draws these darker than picked, as it does every colour.
|
|
31
41
|
*
|
|
32
42
|
* **Two of this package's own APIs did not meet**: a theme token is one number and a `UiNode`
|
|
33
43
|
* colour is a `Float32Array`, so every consumer wrote the same four shifts. The editor found it by
|
|
@@ -39,9 +49,9 @@ export function themeSize(theme: Theme, name: string, fallback: number): number
|
|
|
39
49
|
* number, which is exactly the shape of bug that survives review.
|
|
40
50
|
*/
|
|
41
51
|
export function unpackRgba(packed: number, out: Float32Array): Float32Array {
|
|
42
|
-
out[0] = ((packed >>> 24) & 0xff) / 255;
|
|
43
|
-
out[1] = ((packed >>> 16) & 0xff) / 255;
|
|
44
|
-
out[2] = ((packed >>> 8) & 0xff) / 255;
|
|
52
|
+
out[0] = srgbToLinear(((packed >>> 24) & 0xff) / 255);
|
|
53
|
+
out[1] = srgbToLinear(((packed >>> 16) & 0xff) / 255);
|
|
54
|
+
out[2] = srgbToLinear(((packed >>> 8) & 0xff) / 255);
|
|
45
55
|
out[3] = (packed & 0xff) / 255;
|
|
46
56
|
return out;
|
|
47
57
|
}
|