@driftengine/ui2d 3.61.0

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.
Files changed (52) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +228 -0
  4. package/dist/camera2d.d.ts +47 -0
  5. package/dist/camera2d.js +46 -0
  6. package/dist/index.d.ts +30 -0
  7. package/dist/index.js +21 -0
  8. package/dist/shaders/generated/sprite.wgsl.d.ts +54 -0
  9. package/dist/shaders/generated/sprite.wgsl.js +60 -0
  10. package/dist/shaders/sprite.d.ts +3 -0
  11. package/dist/shaders/sprite.js +98 -0
  12. package/dist/spriteBatch.d.ts +78 -0
  13. package/dist/spriteBatch.js +93 -0
  14. package/dist/spriteGl.d.ts +26 -0
  15. package/dist/spriteGl.js +207 -0
  16. package/dist/spriteGpu.d.ts +31 -0
  17. package/dist/spriteGpu.js +201 -0
  18. package/dist/spritePass.d.ts +54 -0
  19. package/dist/spritePass.js +142 -0
  20. package/dist/spriteSheet.d.ts +64 -0
  21. package/dist/spriteSheet.js +84 -0
  22. package/dist/spriteTexture.d.ts +36 -0
  23. package/dist/spriteTexture.js +5 -0
  24. package/dist/tilemap.d.ts +47 -0
  25. package/dist/tilemap.js +70 -0
  26. package/dist/uiDraw.d.ts +28 -0
  27. package/dist/uiDraw.js +41 -0
  28. package/dist/uiFocus.d.ts +36 -0
  29. package/dist/uiFocus.js +78 -0
  30. package/dist/uiInput.d.ts +51 -0
  31. package/dist/uiInput.js +85 -0
  32. package/dist/uiLayout.d.ts +23 -0
  33. package/dist/uiLayout.js +144 -0
  34. package/dist/uiNode.d.ts +105 -0
  35. package/dist/uiNode.js +79 -0
  36. package/package.json +57 -0
  37. package/src/camera2d.ts +88 -0
  38. package/src/index.ts +55 -0
  39. package/src/shaders/generated/sprite.wgsl.ts +63 -0
  40. package/src/shaders/sprite.ts +102 -0
  41. package/src/spriteBatch.ts +169 -0
  42. package/src/spriteGl.ts +270 -0
  43. package/src/spriteGpu.ts +271 -0
  44. package/src/spritePass.ts +231 -0
  45. package/src/spriteSheet.ts +147 -0
  46. package/src/spriteTexture.ts +42 -0
  47. package/src/tilemap.ts +114 -0
  48. package/src/uiDraw.ts +63 -0
  49. package/src/uiFocus.ts +80 -0
  50. package/src/uiInput.ts +115 -0
  51. package/src/uiLayout.ts +157 -0
  52. package/src/uiNode.ts +186 -0
@@ -0,0 +1,60 @@
1
+ /*
2
+ * Generated from ../sprite.ts by `npm run wgsl`. Do not edit.
3
+ *
4
+ * The GLSL beside this file is the source of truth. A hand edit here is discarded by
5
+ * the next generation, and `npm run wgsl:check` fails the build when this is stale.
6
+ */
7
+ export const SPRITE_FRAG_WGSL = "struct Uniforms {\n uOutputTransform: i32,\n uOutputExposure: f32,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\n@group(0) @binding(32) \nvar uSpriteTexture_t: texture_2d<f32>;\n@group(0) @binding(33) \nvar uSpriteTexture_s: sampler;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vTint_1: vec4<f32>;\nvar<private> fragColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e55 = (*c);\n low = (_e55 * 12.92f);\n let _e57 = (*c);\n high = ((pow(max(_e57, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e63 = high;\n let _e64 = low;\n let _e65 = (*c);\n return mix(_e63, _e64, step(_e65, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e55 = (*v);\n let _e56 = (*v);\n a = ((_e55 * (_e56 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e62 = (*v);\n let _e63 = (*v);\n b = ((_e62 * ((_e63 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e70 = a;\n let _e71 = b;\n return (_e70 / _e71);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e55 = unnamed.uOutputExposure;\n let _e56 = (*x);\n (*x) = (_e56 * _e55);\n let _e58 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e58);\n let _e60 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e60), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n\n let _e56 = unnamed.uOutputTransform;\n if (_e56 == 0i) {\n let _e58 = (*c_1);\n return _e58;\n }\n let _e60 = unnamed.uOutputTransform;\n if (_e60 == 2i) {\n let _e62 = (*c_1);\n param_1 = _e62;\n let _e63 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_1) = _e63;\n }\n let _e64 = (*c_1);\n param_2 = _e64;\n let _e65 = linearToSrgb_u0028_vf3_u003b((&param_2));\n return _e65;\n}\n\nfn main_1() {\n var texel: vec4<f32>;\n var colour: vec4<f32>;\n var param_3: vec3<f32>;\n\n let _e55 = vUv_1;\n let _e56 = textureSample(uSpriteTexture_t, uSpriteTexture_s, _e55);\n texel = _e56;\n let _e57 = texel;\n let _e58 = vTint_1;\n colour = (_e57 * _e58);\n let _e61 = colour[3u];\n if (_e61 < 0.00392157f) {\n discard;\n }\n let _e63 = colour;\n param_3 = _e63.xyz;\n let _e65 = applyOutputTransform_u0028_vf3_u003b((&param_3));\n let _e66 = colour;\n colour = vec4<f32>(_e65.x, _e65.y, _e65.z, _e66.w);\n let _e72 = colour;\n let _e75 = colour[3u];\n let _e76 = (_e72.xyz * _e75);\n let _e78 = colour[3u];\n fragColor = vec4<f32>(_e76.x, _e76.y, _e76.z, _e78);\n return;\n}\n\n@fragment \nfn main(@location(0) vUv: vec2<f32>, @location(1) vTint: vec4<f32>) -> @location(0) vec4<f32> {\n vUv_1 = vUv;\n vTint_1 = vTint;\n main_1();\n let _e5 = fragColor;\n return _e5;\n}\n";
8
+ export const SPRITE_VERT_WGSL = "struct Uniforms {\n uToNdc0_: vec4<f32>,\n uToNdc1_: vec4<f32>,\n uClipCorrection: mat4x4<f32>,\n}\n\nstruct gl_PerVertex {\n @builtin(position) gl_Position: vec4<f32>,\n gl_PointSize: f32,\n}\n\nstruct VertexOutput {\n @location(0) member: vec2<f32>,\n @location(1) member_1: vec4<f32>,\n @builtin(position) gl_Position: vec4<f32>,\n}\n\nvar<private> gl_VertexIndex_1: i32;\nvar<private> aOrigin_1: vec2<f32>;\nvar<private> aEdges_1: vec4<f32>;\n@group(0) @binding(0) \nvar<uniform> unnamed: Uniforms;\nvar<private> vUv: vec2<f32>;\nvar<private> aUv_1: vec4<f32>;\nvar<private> vTint: vec4<f32>;\nvar<private> aTint_1: vec4<f32>;\nvar<private> unnamed_1: gl_PerVertex = gl_PerVertex(vec4<f32>(0f, 0f, 0f, 1f), 1f);\n\nfn main_1() {\n var corner: vec2<f32>;\n var indexable: array<vec2<f32>, 6>;\n var p: vec2<f32>;\n var ndc: vec2<f32>;\n\n let _e28 = gl_VertexIndex_1;\n indexable = array<vec2<f32>, 6>(vec2<f32>(0f, 0f), vec2<f32>(1f, 0f), vec2<f32>(0f, 1f), vec2<f32>(0f, 1f), vec2<f32>(1f, 0f), vec2<f32>(1f, 1f));\n let _e30 = indexable[_e28];\n corner = _e30;\n let _e31 = aOrigin_1;\n let _e32 = aEdges_1;\n let _e35 = corner[0u];\n let _e38 = aEdges_1;\n let _e41 = corner[1u];\n p = ((_e31 + (_e32.xy * _e35)) + (_e38.zw * _e41));\n let _e46 = unnamed.uToNdc0_[0u];\n let _e48 = p[0u];\n let _e52 = unnamed.uToNdc0_[2u];\n let _e54 = p[1u];\n let _e59 = unnamed.uToNdc1_[0u];\n let _e63 = unnamed.uToNdc0_[1u];\n let _e65 = p[0u];\n let _e69 = unnamed.uToNdc0_[3u];\n let _e71 = p[1u];\n let _e76 = unnamed.uToNdc1_[1u];\n ndc = vec2<f32>((((_e46 * _e48) + (_e52 * _e54)) + _e59), (((_e63 * _e65) + (_e69 * _e71)) + _e76));\n let _e79 = aUv_1;\n let _e81 = aUv_1;\n let _e83 = corner;\n vUv = mix(_e79.xy, _e81.zw, _e83);\n let _e85 = aTint_1;\n vTint = _e85;\n let _e87 = unnamed.uClipCorrection;\n let _e88 = ndc;\n unnamed_1.gl_Position = (_e87 * vec4<f32>(_e88.x, _e88.y, 0f, 1f));\n return;\n}\n\n@vertex \nfn main(@builtin(vertex_index) gl_VertexIndex: u32, @location(3) aOrigin: vec2<f32>, @location(0) aEdges: vec4<f32>, @location(1) aUv: vec4<f32>, @location(2) aTint: vec4<f32>) -> VertexOutput {\n gl_VertexIndex_1 = i32(gl_VertexIndex);\n aOrigin_1 = aOrigin;\n aEdges_1 = aEdges;\n aUv_1 = aUv;\n aTint_1 = aTint;\n main_1();\n let _e16 = unnamed_1.gl_Position.y;\n unnamed_1.gl_Position.y = -(_e16);\n let _e18 = vUv;\n let _e19 = vTint;\n let _e20 = unnamed_1.gl_Position;\n return VertexOutput(_e18, _e19, _e20);\n}\n";
9
+ /**
10
+ * What the transform assigned, so the renderer binds the same numbers.
11
+ *
12
+ * A permuted shader has one entry per variant, keyed as `FLAT_FRAG_WGSL` is.
13
+ */
14
+ export const SPRITE_BINDINGS = {
15
+ "SPRITE_FRAG": {
16
+ "uniforms": 1,
17
+ "uniformSize": 16,
18
+ "fields": {
19
+ "uOutputTransform": {
20
+ "offset": 0,
21
+ "size": 4,
22
+ "type": "int"
23
+ },
24
+ "uOutputExposure": {
25
+ "offset": 4,
26
+ "size": 4,
27
+ "type": "float"
28
+ }
29
+ },
30
+ "textures": {
31
+ "uSpriteTexture": {
32
+ "texture": 32,
33
+ "sampler": 33,
34
+ "type": "sampler2D"
35
+ }
36
+ }
37
+ },
38
+ "SPRITE_VERT": {
39
+ "uniforms": 0,
40
+ "uniformSize": 96,
41
+ "fields": {
42
+ "uToNdc0": {
43
+ "offset": 0,
44
+ "size": 16,
45
+ "type": "vec4"
46
+ },
47
+ "uToNdc1": {
48
+ "offset": 16,
49
+ "size": 16,
50
+ "type": "vec4"
51
+ },
52
+ "uClipCorrection": {
53
+ "offset": 32,
54
+ "size": 64,
55
+ "type": "mat4"
56
+ }
57
+ },
58
+ "textures": {}
59
+ }
60
+ };
@@ -0,0 +1,3 @@
1
+ /** The sprite program: one textured, tinted quad per instance, blended in submission order. */
2
+ export declare const SPRITE_VERT = "#version 300 es\n\n/**\n * The two edge vectors of the quad, in the space the affine below maps from.\n *\n * Edge vectors rather than a size and an angle: the rotation is resolved on the CPU once per\n * sprite, where it costs one sine, and the vertex stage does two multiplies and an add. A sprite\n * batch is vertex-bound at four thousand quads and this is the whole of its vertex work.\n */\nlayout(location = 0) in vec4 aEdges;\n/** The frame this sprite reads, as (u0, v0) and (u1, v1). */\nlayout(location = 1) in vec4 aUv;\n/** Straight, not premultiplied. The fragment stage premultiplies after the texture fetch. */\nlayout(location = 2) in vec4 aTint;\n/** The corner the two edges grow from. */\nlayout(location = 3) in vec2 aOrigin;\n\n/**\n * The affine to normalised device coordinates, as (a, b, c, d) and (e, f).\n *\n * Two `vec4`s rather than a `mat3`, because a `mat3` in a uniform block is three\n * sixteen-byte rows for nine useful floats and this is read once per vertex.\n */\nuniform vec4 uToNdc0;\nuniform vec4 uToNdc1;\n/**\n * Clip space, as the backend drawing this defines it. Identity on WebGL2.\n *\n * The same correction `panel.ts` carries and for the same reason: this stage builds its own clip\n * position and never multiplies by a camera, so the generated vertex shader's Y negation would\n * stand uncancelled and the whole 2D layer would land mirrored about the middle of the frame.\n */\nuniform mat4 uClipCorrection;\n\nout vec2 vUv;\nout vec4 vTint;\n\n/**\n * Two triangles, wound so that either winding draws: this pass culls nothing.\n *\n * A sprite is flipped by giving it a negative width, which reverses the winding \u2014 so a cull mode\n * would silently drop every mirrored sprite, which is what a character facing left is.\n */\nconst vec2 CORNERS[6] = vec2[6](\n vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),\n vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0)\n);\n\nvoid main() {\n vec2 corner = CORNERS[gl_VertexID];\n vec2 p = aOrigin + aEdges.xy * corner.x + aEdges.zw * corner.y;\n vec2 ndc = vec2(\n uToNdc0.x * p.x + uToNdc0.z * p.y + uToNdc1.x,\n uToNdc0.y * p.x + uToNdc0.w * p.y + uToNdc1.y\n );\n vUv = mix(aUv.xy, aUv.zw, corner);\n vTint = aTint;\n /* z is 0 and nothing depth-tests here: the order sprites were submitted in is the layering. */\n gl_Position = uClipCorrection * vec4(ndc, 0.0, 1.0);\n}\n";
3
+ export declare const SPRITE_FRAG: string;
@@ -0,0 +1,98 @@
1
+ /** The sprite program: one textured, tinted quad per instance, blended in submission order. */
2
+ import { OUTPUT_TRANSFORM_GLSL } from '@driftengine/core';
3
+ /**
4
+ * Below this the fragment is thrown away rather than blended.
5
+ *
6
+ * Half of one eight-bit step. A sprite sheet's transparent margin is exactly zero and costs
7
+ * nothing to reject; what this buys is the *soft* edge of an antialiased glyph or a feathered
8
+ * particle, whose outermost ring blends a texture fetch and a blend for a contribution no frame
9
+ * can show. It is a fill saving and not a correctness rule, which is why it is this low: a cutout
10
+ * threshold that shaved visible alpha would put a hard edge on every soft one.
11
+ */
12
+ const ALPHA_FLOOR = 1.0 / 255.0;
13
+ export const SPRITE_VERT = `#version 300 es
14
+
15
+ /**
16
+ * The two edge vectors of the quad, in the space the affine below maps from.
17
+ *
18
+ * Edge vectors rather than a size and an angle: the rotation is resolved on the CPU once per
19
+ * sprite, where it costs one sine, and the vertex stage does two multiplies and an add. A sprite
20
+ * batch is vertex-bound at four thousand quads and this is the whole of its vertex work.
21
+ */
22
+ layout(location = 0) in vec4 aEdges;
23
+ /** The frame this sprite reads, as (u0, v0) and (u1, v1). */
24
+ layout(location = 1) in vec4 aUv;
25
+ /** Straight, not premultiplied. The fragment stage premultiplies after the texture fetch. */
26
+ layout(location = 2) in vec4 aTint;
27
+ /** The corner the two edges grow from. */
28
+ layout(location = 3) in vec2 aOrigin;
29
+
30
+ /**
31
+ * The affine to normalised device coordinates, as (a, b, c, d) and (e, f).
32
+ *
33
+ * Two \`vec4\`s rather than a \`mat3\`, because a \`mat3\` in a uniform block is three
34
+ * sixteen-byte rows for nine useful floats and this is read once per vertex.
35
+ */
36
+ uniform vec4 uToNdc0;
37
+ uniform vec4 uToNdc1;
38
+ /**
39
+ * Clip space, as the backend drawing this defines it. Identity on WebGL2.
40
+ *
41
+ * The same correction \`panel.ts\` carries and for the same reason: this stage builds its own clip
42
+ * position and never multiplies by a camera, so the generated vertex shader's Y negation would
43
+ * stand uncancelled and the whole 2D layer would land mirrored about the middle of the frame.
44
+ */
45
+ uniform mat4 uClipCorrection;
46
+
47
+ out vec2 vUv;
48
+ out vec4 vTint;
49
+
50
+ /**
51
+ * Two triangles, wound so that either winding draws: this pass culls nothing.
52
+ *
53
+ * A sprite is flipped by giving it a negative width, which reverses the winding — so a cull mode
54
+ * would silently drop every mirrored sprite, which is what a character facing left is.
55
+ */
56
+ const vec2 CORNERS[6] = vec2[6](
57
+ vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
58
+ vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0)
59
+ );
60
+
61
+ void main() {
62
+ vec2 corner = CORNERS[gl_VertexID];
63
+ vec2 p = aOrigin + aEdges.xy * corner.x + aEdges.zw * corner.y;
64
+ vec2 ndc = vec2(
65
+ uToNdc0.x * p.x + uToNdc0.z * p.y + uToNdc1.x,
66
+ uToNdc0.y * p.x + uToNdc0.w * p.y + uToNdc1.y
67
+ );
68
+ vUv = mix(aUv.xy, aUv.zw, corner);
69
+ vTint = aTint;
70
+ /* z is 0 and nothing depth-tests here: the order sprites were submitted in is the layering. */
71
+ gl_Position = uClipCorrection * vec4(ndc, 0.0, 1.0);
72
+ }
73
+ `;
74
+ export const SPRITE_FRAG = `#version 300 es
75
+ precision highp float;
76
+
77
+ uniform sampler2D uSpriteTexture;
78
+
79
+ in vec2 vUv;
80
+ in vec4 vTint;
81
+ out vec4 fragColor;
82
+
83
+ ${OUTPUT_TRANSFORM_GLSL}
84
+
85
+ void main() {
86
+ /*
87
+ * No branch reaches this fetch, so the implicit derivative is taken in uniform control flow and
88
+ * a sheet may be mipmapped. The 2026-08-07 rule is about a sample under a branch and there is
89
+ * none here; adding one later would mean a \`textureLod\`.
90
+ */
91
+ vec4 texel = texture(uSpriteTexture, vUv);
92
+ vec4 colour = texel * vTint;
93
+ if (colour.a < ${ALPHA_FLOOR.toFixed(8)}) discard;
94
+ colour.rgb = applyOutputTransform(colour.rgb);
95
+ /* Premultiplied out, because the blend is (ONE, ONE_MINUS_SRC_ALPHA). */
96
+ fragColor = vec4(colour.rgb * colour.a, colour.a);
97
+ }
98
+ `;
@@ -0,0 +1,78 @@
1
+ /** The CPU side of a sprite draw: quads packed into one instance buffer, in submission order. */
2
+ /**
3
+ * Floats per instance: two edge vectors, a UV rectangle, a tint, and an origin.
4
+ *
5
+ * Fourteen rather than a rounder sixteen because a vertex buffer's stride only has to be a multiple
6
+ * of four bytes, and two floats a sprite is 8 KB at a four-thousand-sprite batch.
7
+ */
8
+ export declare const SPRITE_FLOATS = 14;
9
+ /** Where a sprite goes, in whatever space the batch is being drawn in. */
10
+ export interface SpritePlacement {
11
+ /**
12
+ * The corner the sprite grows from, and *which* corner depends on the affine.
13
+ *
14
+ * In screen space y counts down, so this is the top-left; in a 2D world y counts up, so it is
15
+ * the bottom-left. The batch does not know which it is in and does not need to: it is the same
16
+ * arithmetic either way, and `camera2d.ts` is where the two conventions are written down.
17
+ */
18
+ readonly x: number;
19
+ readonly y: number;
20
+ readonly w: number;
21
+ readonly h: number;
22
+ /**
23
+ * Radians, anticlockwise in the mathematical sense — which reads as clockwise on screen, where y
24
+ * counts down. Defaults to none.
25
+ */
26
+ readonly rotation?: number;
27
+ /** The point rotation turns about, as a fraction of the sprite. Defaults to its centre. */
28
+ readonly pivotX?: number;
29
+ readonly pivotY?: number;
30
+ }
31
+ /** A rectangle of a texture, in the 0..1 the sampler reads. See `spriteSheet.ts`. */
32
+ export interface UvRect {
33
+ readonly u0: number;
34
+ readonly v0: number;
35
+ readonly u1: number;
36
+ readonly v1: number;
37
+ }
38
+ /**
39
+ * A run of consecutive instances that share a texture.
40
+ *
41
+ * Runs exist because the number to hold down is material changes rather than draws: a tilemap over
42
+ * one sheet is one run however many thousand tiles it is, and a run boundary is the only place the
43
+ * pass has to touch the GPU between them.
44
+ */
45
+ export interface SpriteRun {
46
+ readonly texture: number;
47
+ readonly first: number;
48
+ readonly count: number;
49
+ }
50
+ /**
51
+ * One frame's worth of sprites.
52
+ *
53
+ * Fixed capacity, filled from the front, reset each frame. It grows for nobody: a batch that
54
+ * reallocated mid-frame would allocate in the hot path, and the failure it is protecting against —
55
+ * a caller drawing more than it planned for — is one that wants counting rather than absorbing.
56
+ */
57
+ export interface SpriteBatch {
58
+ /** `capacity * SPRITE_FLOATS`, filled to `count * SPRITE_FLOATS`. */
59
+ readonly instances: Float32Array;
60
+ readonly capacity: number;
61
+ count: number;
62
+ /** Three entries a run: texture, first instance, length. */
63
+ readonly runs: Int32Array;
64
+ runCount: number;
65
+ /** Sprites this frame refused for want of room. Zero is the only good value. */
66
+ dropped: number;
67
+ }
68
+ export declare function createSpriteBatch(capacity: number): SpriteBatch;
69
+ export declare function resetSpriteBatch(batch: SpriteBatch): void;
70
+ /** Read a run back. For tests and diagnostics; the pass reads `runs` directly. */
71
+ export declare function spriteRun(batch: SpriteBatch, index: number): SpriteRun;
72
+ /**
73
+ * Push one quad. Allocates nothing.
74
+ *
75
+ * `source` of `null` is the whole texture and `tint` of `null` is opaque white, because those are
76
+ * what a caller drawing a plain image wants and neither should cost an object per draw.
77
+ */
78
+ export declare function drawSprite(batch: SpriteBatch, texture: number, placement: SpritePlacement, source: UvRect | null, tint: ArrayLike<number> | null): void;
@@ -0,0 +1,93 @@
1
+ /** The CPU side of a sprite draw: quads packed into one instance buffer, in submission order. */
2
+ /**
3
+ * Floats per instance: two edge vectors, a UV rectangle, a tint, and an origin.
4
+ *
5
+ * Fourteen rather than a rounder sixteen because a vertex buffer's stride only has to be a multiple
6
+ * of four bytes, and two floats a sprite is 8 KB at a four-thousand-sprite batch.
7
+ */
8
+ export const SPRITE_FLOATS = 14;
9
+ export function createSpriteBatch(capacity) {
10
+ return {
11
+ instances: new Float32Array(capacity * SPRITE_FLOATS),
12
+ capacity,
13
+ count: 0,
14
+ /*
15
+ * One run per sprite is the worst case — a caller alternating textures every draw — and
16
+ * allocating for it costs twelve bytes a sprite against the fifty-six the sprite itself costs.
17
+ * The alternative is a second capacity to overflow, on a path where overflowing means dropping
18
+ * a draw the caller can see.
19
+ */
20
+ runs: new Int32Array(capacity * 3),
21
+ runCount: 0,
22
+ dropped: 0,
23
+ };
24
+ }
25
+ export function resetSpriteBatch(batch) {
26
+ batch.count = 0;
27
+ batch.runCount = 0;
28
+ batch.dropped = 0;
29
+ }
30
+ /** Read a run back. For tests and diagnostics; the pass reads `runs` directly. */
31
+ export function spriteRun(batch, index) {
32
+ const at = index * 3;
33
+ return {
34
+ texture: batch.runs[at],
35
+ first: batch.runs[at + 1],
36
+ count: batch.runs[at + 2],
37
+ };
38
+ }
39
+ /**
40
+ * Push one quad. Allocates nothing.
41
+ *
42
+ * `source` of `null` is the whole texture and `tint` of `null` is opaque white, because those are
43
+ * what a caller drawing a plain image wants and neither should cost an object per draw.
44
+ */
45
+ export function drawSprite(batch, texture, placement, source, tint) {
46
+ if (batch.count >= batch.capacity) {
47
+ batch.dropped += 1;
48
+ return;
49
+ }
50
+ const rotation = placement.rotation ?? 0;
51
+ const cos = rotation === 0 ? 1 : Math.cos(rotation);
52
+ const sin = rotation === 0 ? 0 : Math.sin(rotation);
53
+ const w = placement.w;
54
+ const h = placement.h;
55
+ // The two edge vectors of the quad, turned.
56
+ const ax = w * cos;
57
+ const ay = w * sin;
58
+ const bx = -h * sin;
59
+ const by = h * cos;
60
+ const pivotX = placement.pivotX ?? 0.5;
61
+ const pivotY = placement.pivotY ?? 0.5;
62
+ // The pivot does not move, so the origin is wherever it has to be for that to hold.
63
+ const originX = placement.x + w * pivotX - (ax * pivotX + bx * pivotY);
64
+ const originY = placement.y + h * pivotY - (ay * pivotX + by * pivotY);
65
+ const at = batch.count * SPRITE_FLOATS;
66
+ const f = batch.instances;
67
+ f[at] = ax;
68
+ f[at + 1] = ay;
69
+ f[at + 2] = bx;
70
+ f[at + 3] = by;
71
+ f[at + 4] = source === null ? 0 : source.u0;
72
+ f[at + 5] = source === null ? 0 : source.v0;
73
+ f[at + 6] = source === null ? 1 : source.u1;
74
+ f[at + 7] = source === null ? 1 : source.v1;
75
+ f[at + 8] = tint === null ? 1 : tint[0];
76
+ f[at + 9] = tint === null ? 1 : tint[1];
77
+ f[at + 10] = tint === null ? 1 : tint[2];
78
+ f[at + 11] = tint === null ? 1 : tint[3];
79
+ f[at + 12] = originX;
80
+ f[at + 13] = originY;
81
+ const lastRun = (batch.runCount - 1) * 3;
82
+ if (batch.runCount > 0 && batch.runs[lastRun] === texture) {
83
+ batch.runs[lastRun + 2] = batch.runs[lastRun + 2] + 1;
84
+ }
85
+ else {
86
+ const run = batch.runCount * 3;
87
+ batch.runs[run] = texture;
88
+ batch.runs[run + 1] = batch.count;
89
+ batch.runs[run + 2] = 1;
90
+ batch.runCount += 1;
91
+ }
92
+ batch.count += 1;
93
+ }
@@ -0,0 +1,26 @@
1
+ /** The WebGL2 half of the sprite pass: one program, one instance buffer, one texture slot table. */
2
+ import type { SpriteBatch } from './spriteBatch.ts';
3
+ import type { SpriteTextureOptions } from './spriteTexture.ts';
4
+ export interface Webgl2Sprites {
5
+ readonly program: WebGLProgram;
6
+ readonly vao: WebGLVertexArrayObject;
7
+ readonly instances: WebGLBuffer;
8
+ readonly uniforms: Readonly<Record<string, WebGLUniformLocation | null>>;
9
+ /** One per slot, `null` until the caller sets it. A run naming an empty slot draws nothing. */
10
+ readonly textures: (WebGLTexture | null)[];
11
+ }
12
+ export declare function createWebgl2Sprites(gl: WebGL2RenderingContext, capacity: number, slots: number, label: string): Webgl2Sprites;
13
+ /** Replace a texture slot. The previous texture in that slot is deleted. */
14
+ export declare function setWebgl2SpriteTexture(gl: WebGL2RenderingContext, sprites: Webgl2Sprites, slot: number, source: TexImageSource, options: SpriteTextureOptions): void;
15
+ /**
16
+ * Fill a slot with one opaque white texel, from an array rather than an image.
17
+ *
18
+ * **White is the identity of the multiply this shader does**, so a quad on this slot draws exactly
19
+ * its tint — which is what a solid background is. Built from four bytes rather than from a canvas
20
+ * because the pass has no DOM to reach for and should not need one: `texImage2D`'s pixel overload
21
+ * takes the texel directly, and `queue.writeTexture` is its WebGPU twin.
22
+ */
23
+ export declare function setWebgl2WhiteTexture(gl: WebGL2RenderingContext, sprites: Webgl2Sprites, slot: number): void;
24
+ export declare function uploadWebgl2Instances(gl: WebGL2RenderingContext, sprites: Webgl2Sprites, batch: SpriteBatch): void;
25
+ export declare function drawWebgl2Sprites(gl: WebGL2RenderingContext, sprites: Webgl2Sprites, batch: SpriteBatch, toNdc: Float32Array, clipCorrection: Float32Array, outputTransform: number, outputExposure: number): void;
26
+ export declare function disposeWebgl2Sprites(gl: WebGL2RenderingContext, sprites: Webgl2Sprites): void;
@@ -0,0 +1,207 @@
1
+ /** The WebGL2 half of the sprite pass: one program, one instance buffer, one texture slot table. */
2
+ import { SPRITE_FRAG, SPRITE_VERT } from './shaders/sprite.js';
3
+ import { SPRITE_FLOATS } from './spriteBatch.js';
4
+ /**
5
+ * The unit this pass borrows for the length of one draw.
6
+ *
7
+ * Fourteen is above every unit `lightBudget.ts` assigns — `COOKIE_ATLAS_TEXTURE_UNIT` is the
8
+ * highest at thirteen — and below the sixteen WebGL2 guarantees. The pass releases it before it
9
+ * returns, because a texture left bound to a unit a later pass attaches is a feedback loop rather
10
+ * than a wrong colour.
11
+ */
12
+ const SPRITE_UNIT = 14;
13
+ /** Bytes per instance. Fourteen floats; see `SPRITE_FLOATS`. */
14
+ const STRIDE = SPRITE_FLOATS * 4;
15
+ function compile(gl, kind, source, label) {
16
+ const shader = gl.createShader(kind);
17
+ if (shader === null)
18
+ throw new Error(`${label}: createShader failed`);
19
+ gl.shaderSource(shader, source);
20
+ gl.compileShader(shader);
21
+ if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) !== true) {
22
+ const log = gl.getShaderInfoLog(shader) ?? '';
23
+ gl.deleteShader(shader);
24
+ throw new Error(`${label}: ${log}`);
25
+ }
26
+ return shader;
27
+ }
28
+ const UNIFORM_NAMES = [
29
+ 'uToNdc0',
30
+ 'uToNdc1',
31
+ 'uClipCorrection',
32
+ 'uSpriteTexture',
33
+ 'uOutputTransform',
34
+ 'uOutputExposure',
35
+ ];
36
+ export function createWebgl2Sprites(gl, capacity, slots, label) {
37
+ const program = gl.createProgram();
38
+ if (program === null)
39
+ throw new Error(`${label}: createProgram failed`);
40
+ const vert = compile(gl, gl.VERTEX_SHADER, SPRITE_VERT, `${label} vertex`);
41
+ const frag = compile(gl, gl.FRAGMENT_SHADER, SPRITE_FRAG, `${label} fragment`);
42
+ gl.attachShader(program, vert);
43
+ gl.attachShader(program, frag);
44
+ gl.linkProgram(program);
45
+ gl.deleteShader(vert);
46
+ gl.deleteShader(frag);
47
+ if (gl.getProgramParameter(program, gl.LINK_STATUS) !== true) {
48
+ const log = gl.getProgramInfoLog(program) ?? '';
49
+ gl.deleteProgram(program);
50
+ throw new Error(`${label}: ${log}`);
51
+ }
52
+ const uniforms = {};
53
+ for (const name of UNIFORM_NAMES)
54
+ uniforms[name] = gl.getUniformLocation(program, name);
55
+ const vao = gl.createVertexArray();
56
+ const instances = gl.createBuffer();
57
+ if (vao === null || instances === null)
58
+ throw new Error(`${label}: buffer allocation failed`);
59
+ gl.bindVertexArray(vao);
60
+ gl.bindBuffer(gl.ARRAY_BUFFER, instances);
61
+ gl.bufferData(gl.ARRAY_BUFFER, capacity * STRIDE, gl.DYNAMIC_DRAW);
62
+ /*
63
+ * Four attributes, every one of them per instance: the quad's own corners come from
64
+ * `gl_VertexID` and cost no buffer at all. Offsets are re-pointed per run, which is how a run
65
+ * starts partway into the buffer without `baseInstance` — WebGL2 has no such call.
66
+ */
67
+ bindAttributes(gl, 0);
68
+ gl.bindVertexArray(null);
69
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
70
+ return { program, vao, instances, uniforms, textures: new Array(slots).fill(null) };
71
+ }
72
+ /** Point the four instance attributes at instance `first`. The buffer must be bound. */
73
+ function bindAttributes(gl, first) {
74
+ const base = first * STRIDE;
75
+ gl.enableVertexAttribArray(0);
76
+ gl.vertexAttribPointer(0, 4, gl.FLOAT, false, STRIDE, base);
77
+ gl.vertexAttribDivisor(0, 1);
78
+ gl.enableVertexAttribArray(1);
79
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, base + 16);
80
+ gl.vertexAttribDivisor(1, 1);
81
+ gl.enableVertexAttribArray(2);
82
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, STRIDE, base + 32);
83
+ gl.vertexAttribDivisor(2, 1);
84
+ gl.enableVertexAttribArray(3);
85
+ gl.vertexAttribPointer(3, 2, gl.FLOAT, false, STRIDE, base + 48);
86
+ gl.vertexAttribDivisor(3, 1);
87
+ }
88
+ /** Replace a texture slot. The previous texture in that slot is deleted. */
89
+ export function setWebgl2SpriteTexture(gl, sprites, slot, source, options) {
90
+ const previous = sprites.textures[slot];
91
+ if (previous !== null && previous !== undefined)
92
+ gl.deleteTexture(previous);
93
+ const texture = gl.createTexture();
94
+ if (texture === null)
95
+ throw new Error('ui2d: createTexture failed');
96
+ gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
97
+ gl.bindTexture(gl.TEXTURE_2D, texture);
98
+ const internal = options.colorSpace === 'linear' ? gl.RGBA : gl.SRGB8_ALPHA8;
99
+ gl.texImage2D(gl.TEXTURE_2D, 0, internal, gl.RGBA, gl.UNSIGNED_BYTE, source);
100
+ const filter = options.filter === 'linear' ? gl.LINEAR : gl.NEAREST;
101
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
102
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
103
+ /*
104
+ * Clamped on both axes, and it is a correctness rule rather than a default. A sheet frame's
105
+ * edge texel is adjacent to the *next* frame's, so a repeating wrap bleeds one sprite into
106
+ * another at exactly the seam a caller cannot see in the atlas.
107
+ */
108
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
109
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
110
+ gl.bindTexture(gl.TEXTURE_2D, null);
111
+ gl.activeTexture(gl.TEXTURE0);
112
+ sprites.textures[slot] = texture;
113
+ }
114
+ /**
115
+ * Fill a slot with one opaque white texel, from an array rather than an image.
116
+ *
117
+ * **White is the identity of the multiply this shader does**, so a quad on this slot draws exactly
118
+ * its tint — which is what a solid background is. Built from four bytes rather than from a canvas
119
+ * because the pass has no DOM to reach for and should not need one: `texImage2D`'s pixel overload
120
+ * takes the texel directly, and `queue.writeTexture` is its WebGPU twin.
121
+ */
122
+ export function setWebgl2WhiteTexture(gl, sprites, slot) {
123
+ const texture = gl.createTexture();
124
+ if (texture === null)
125
+ throw new Error('ui2d: createTexture failed');
126
+ gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
127
+ gl.bindTexture(gl.TEXTURE_2D, texture);
128
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
129
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
130
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
131
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
132
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
133
+ gl.bindTexture(gl.TEXTURE_2D, null);
134
+ gl.activeTexture(gl.TEXTURE0);
135
+ sprites.textures[slot] = texture;
136
+ }
137
+ export function uploadWebgl2Instances(gl, sprites, batch) {
138
+ if (batch.count === 0)
139
+ return;
140
+ gl.bindBuffer(gl.ARRAY_BUFFER, sprites.instances);
141
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, batch.instances, 0, batch.count * SPRITE_FLOATS);
142
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
143
+ }
144
+ export function drawWebgl2Sprites(gl, sprites, batch, toNdc, clipCorrection, outputTransform, outputExposure) {
145
+ if (batch.count === 0)
146
+ return;
147
+ gl.useProgram(sprites.program);
148
+ gl.bindVertexArray(sprites.vao);
149
+ gl.bindBuffer(gl.ARRAY_BUFFER, sprites.instances);
150
+ const u = sprites.uniforms;
151
+ gl.uniform4f(u['uToNdc0'] ?? null, toNdc[0], toNdc[1], toNdc[2], toNdc[3]);
152
+ gl.uniform4f(u['uToNdc1'] ?? null, toNdc[4], toNdc[5], 0, 0);
153
+ gl.uniformMatrix4fv(u['uClipCorrection'] ?? null, false, clipCorrection);
154
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
155
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
156
+ gl.uniform1i(u['uSpriteTexture'] ?? null, SPRITE_UNIT);
157
+ const blendWas = gl.getParameter(gl.BLEND);
158
+ const depthTestWas = gl.getParameter(gl.DEPTH_TEST);
159
+ const depthMaskWas = gl.getParameter(gl.DEPTH_WRITEMASK);
160
+ const cullWas = gl.getParameter(gl.CULL_FACE);
161
+ /*
162
+ * **Culling off, and the WebGPU pipeline says `cullMode: 'none'` for the same reason**: a sprite
163
+ * is mirrored by giving it a negative width, which reverses its winding, so a cull mode drops
164
+ * every flipped sprite — which is what a character facing left is.
165
+ *
166
+ * This line is here because the frame that was missing it drew *nothing at all* on WebGL2 while
167
+ * WebGPU was pixel-perfect, and nothing reported an error. A contributed pass inherits whatever
168
+ * cull state the last scene draw left on, and the screen-space quad's winding is not the scene's;
169
+ * WebGPU has no such inheritance, because a pipeline states its own. Every backend difference
170
+ * this seam has is of that shape: one API carries state between draws and the other does not.
171
+ */
172
+ gl.disable(gl.CULL_FACE);
173
+ gl.enable(gl.BLEND);
174
+ /* Premultiplied `over`: the fragment stage folds alpha in, so this composes onto opaque pixels. */
175
+ gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
176
+ gl.disable(gl.DEPTH_TEST);
177
+ gl.depthMask(false);
178
+ gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
179
+ for (let run = 0; run < batch.runCount; run += 1) {
180
+ const at = run * 3;
181
+ const texture = sprites.textures[batch.runs[at]];
182
+ if (texture === null || texture === undefined)
183
+ continue;
184
+ gl.bindTexture(gl.TEXTURE_2D, texture);
185
+ bindAttributes(gl, batch.runs[at + 1]);
186
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, batch.runs[at + 2]);
187
+ }
188
+ gl.bindTexture(gl.TEXTURE_2D, null);
189
+ gl.activeTexture(gl.TEXTURE0);
190
+ gl.depthMask(depthMaskWas);
191
+ if (cullWas)
192
+ gl.enable(gl.CULL_FACE);
193
+ if (depthTestWas)
194
+ gl.enable(gl.DEPTH_TEST);
195
+ if (!blendWas)
196
+ gl.disable(gl.BLEND);
197
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
198
+ gl.bindVertexArray(null);
199
+ }
200
+ export function disposeWebgl2Sprites(gl, sprites) {
201
+ gl.deleteProgram(sprites.program);
202
+ gl.deleteVertexArray(sprites.vao);
203
+ gl.deleteBuffer(sprites.instances);
204
+ for (const texture of sprites.textures)
205
+ if (texture !== null)
206
+ gl.deleteTexture(texture);
207
+ }
@@ -0,0 +1,31 @@
1
+ /** The WebGPU half of the sprite pass: one pipeline, one instance buffer, a bind group per slot. */
2
+ import type { SpriteBatch } from './spriteBatch.ts';
3
+ import type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
4
+ export interface GpuSpriteSlot {
5
+ readonly texture: GPUTexture;
6
+ readonly bindGroup: GPUBindGroup;
7
+ }
8
+ export interface GpuSprites {
9
+ readonly pipeline: GPURenderPipeline;
10
+ readonly layout: GPUBindGroupLayout;
11
+ readonly vertexUniforms: GPUBuffer;
12
+ readonly fragmentUniforms: GPUBuffer;
13
+ readonly instances: GPUBuffer;
14
+ readonly samplers: {
15
+ readonly nearest: GPUSampler;
16
+ readonly linear: GPUSampler;
17
+ };
18
+ readonly slots: (GpuSpriteSlot | null)[];
19
+ readonly vertexScratch: ArrayBuffer;
20
+ readonly vertexFloats: Float32Array;
21
+ readonly fragmentScratch: ArrayBuffer;
22
+ readonly fragmentFloats: Float32Array;
23
+ readonly fragmentInts: Int32Array;
24
+ }
25
+ export declare function createGpuSprites(device: GPUDevice, format: GPUTextureFormat, depthFormat: GPUTextureFormat, samples: number, capacity: number, slots: number, label: string): GpuSprites;
26
+ export declare function setGpuSpriteTexture(device: GPUDevice, sprites: GpuSprites, slot: number, source: SpriteImage, options: SpriteTextureOptions): void;
27
+ /** The same one white texel, written straight into a texture. See the WebGL2 half for why. */
28
+ export declare function setGpuWhiteTexture(device: GPUDevice, sprites: GpuSprites, slot: number): void;
29
+ export declare function uploadGpuInstances(device: GPUDevice, sprites: GpuSprites, batch: SpriteBatch): void;
30
+ export declare function drawGpuSprites(pass: GPURenderPassEncoder, sprites: GpuSprites, batch: SpriteBatch): void;
31
+ export declare function disposeGpuSprites(sprites: GpuSprites): void;