@driftengine/ui2d 4.6.1 → 4.7.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 +19 -1
- package/dist/shaders/generated/sprite.wgsl.d.ts +1 -1
- package/dist/shaders/generated/sprite.wgsl.js +1 -1
- package/dist/shaders/sprite.d.ts +1 -1
- package/dist/shaders/sprite.js +12 -1
- package/dist/uiDraw.d.ts +15 -2
- package/dist/uiDraw.js +75 -17
- package/dist/uiFocus.js +4 -0
- package/package.json +2 -2
- package/src/shaders/generated/sprite.wgsl.ts +1 -1
- package/src/shaders/sprite.ts +12 -1
- package/src/uiDraw.ts +99 -17
- package/src/uiFocus.ts +3 -0
package/README.md
CHANGED
|
@@ -48,6 +48,12 @@ the time, so they are named separately and each says which it is.
|
|
|
48
48
|
A `Camera2D` is where it is, how far in (`zoom`, in pixels per world unit) and which way up
|
|
49
49
|
(`rotation`, radians, anticlockwise). Turning the camera turns the world the other way.
|
|
50
50
|
|
|
51
|
+
**A picture keeps its top up in both.** A frame's top row is drawn at the top of the sprite whichever
|
|
52
|
+
way y counts, because the vertex stage reads the affine's orientation and turns the frame's rows to
|
|
53
|
+
match. It did not until 2026-10-02: every sprite and tile drawn through `worldToNdc` came out upside
|
|
54
|
+
down, and `scripts/sprite-check.mjs`, which checked screen space only, passed. It draws the same
|
|
55
|
+
rectangles in a world now and asserts the frame is byte for byte the one screen space draws.
|
|
56
|
+
|
|
51
57
|
## Order is the layering
|
|
52
58
|
|
|
53
59
|
There is no depth here, on purpose: a sprite writes no depth and tests none, so **the order sprites
|
|
@@ -178,6 +184,18 @@ interface tree is tens of nodes built once and mutated, not thousands rebuilt pe
|
|
|
178
184
|
The box you give it is what is _available_ — the root resolves its own size against it, so the same
|
|
179
185
|
call lays out a full-screen HUD and a tooltip.
|
|
180
186
|
|
|
187
|
+
## Clipping and scrolling
|
|
188
|
+
|
|
189
|
+
A node made with `clip: true` bounds what its descendants draw and where they can be pointed at.
|
|
190
|
+
`drawUiTree` cuts every quad under it to the rectangle its clipping ancestors leave, an image's frame
|
|
191
|
+
in proportion, and hands the sink that visible rectangle so a caller can cut its text the same way;
|
|
192
|
+
`uiHitTest` finds nothing outside it. A clipping node's `scrollX` and `scrollY` move its children
|
|
193
|
+
under it at layout, and `routeScrollWheel`, `scrollBy` and `clampScroll` move them within
|
|
194
|
+
`scrollExtent`. **Until 2026-10-02 the clip was laid out and scrolled and nothing drew it**: a list's
|
|
195
|
+
rows drew wherever they had scrolled to, and a row scrolled below the list still took the pointer
|
|
196
|
+
there. It is cut on the CPU rather than by a scissor, which is state a contributed pass would have to
|
|
197
|
+
set and restore on both backends.
|
|
198
|
+
|
|
181
199
|
## Hit testing, focus and routing
|
|
182
200
|
|
|
183
201
|
`uiHitTest` searches **last-drawn first**, so it agrees with the picture. **A node that is not
|
|
@@ -195,7 +213,7 @@ node hidden since it was focused does not take the keyboard with it.
|
|
|
195
213
|
|
|
196
214
|
**Text is the caller's.** This package draws quads; core already draws two kinds of text. Pass a
|
|
197
215
|
`UiContentSink` to `drawUiTree` and it hands you every node with text at its resolved rect, in the
|
|
198
|
-
order it was drawn.
|
|
216
|
+
order it was drawn, with the part of it a clip leaves visible.
|
|
199
217
|
|
|
200
218
|
## From DriftScript
|
|
201
219
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare 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((¶m));\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((¶m_1));\n (*c_1) = _e63;\n }\n let _e64 = (*c_1);\n param_2 = _e64;\n let _e65 = linearToSrgb_u0028_vf3_u003b((¶m_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((¶m_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";
|
|
2
|
-
export declare 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
|
|
2
|
+
export declare 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 var orientation: f32;\n var along: vec2<f32>;\n var local: f32;\n\n let _e31 = 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 _e33 = indexable[_e31];\n corner = _e33;\n let _e34 = aOrigin_1;\n let _e35 = aEdges_1;\n let _e38 = corner[0u];\n let _e41 = aEdges_1;\n let _e44 = corner[1u];\n p = ((_e34 + (_e35.xy * _e38)) + (_e41.zw * _e44));\n let _e49 = unnamed.uToNdc0_[0u];\n let _e51 = p[0u];\n let _e55 = unnamed.uToNdc0_[2u];\n let _e57 = p[1u];\n let _e62 = unnamed.uToNdc1_[0u];\n let _e66 = unnamed.uToNdc0_[1u];\n let _e68 = p[0u];\n let _e72 = unnamed.uToNdc0_[3u];\n let _e74 = p[1u];\n let _e79 = unnamed.uToNdc1_[1u];\n ndc = vec2<f32>((((_e49 * _e51) + (_e55 * _e57)) + _e62), (((_e66 * _e68) + (_e72 * _e74)) + _e79));\n let _e84 = unnamed.uToNdc0_[0u];\n let _e87 = unnamed.uToNdc0_[3u];\n let _e91 = unnamed.uToNdc0_[1u];\n let _e94 = unnamed.uToNdc0_[2u];\n orientation = ((_e84 * _e87) - (_e91 * _e94));\n let _e98 = corner[0u];\n let _e99 = orientation;\n if (_e99 > 0f) {\n let _e102 = corner[1u];\n local = (1f - _e102);\n } else {\n let _e105 = corner[1u];\n local = _e105;\n }\n let _e106 = local;\n along = vec2<f32>(_e98, _e106);\n let _e108 = aUv_1;\n let _e110 = aUv_1;\n let _e112 = along;\n vUv = mix(_e108.xy, _e110.zw, _e112);\n let _e114 = aTint_1;\n vTint = _e114;\n let _e116 = unnamed.uClipCorrection;\n let _e117 = ndc;\n unnamed_1.gl_Position = (_e116 * vec4<f32>(_e117.x, _e117.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";
|
|
3
3
|
/**
|
|
4
4
|
* What the transform assigned, so the renderer binds the same numbers.
|
|
5
5
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* the next generation, and `npm run wgsl:check` fails the build when this is stale.
|
|
6
6
|
*/
|
|
7
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((¶m));\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((¶m_1));\n (*c_1) = _e63;\n }\n let _e64 = (*c_1);\n param_2 = _e64;\n let _e65 = linearToSrgb_u0028_vf3_u003b((¶m_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((¶m_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
|
|
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 var orientation: f32;\n var along: vec2<f32>;\n var local: f32;\n\n let _e31 = 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 _e33 = indexable[_e31];\n corner = _e33;\n let _e34 = aOrigin_1;\n let _e35 = aEdges_1;\n let _e38 = corner[0u];\n let _e41 = aEdges_1;\n let _e44 = corner[1u];\n p = ((_e34 + (_e35.xy * _e38)) + (_e41.zw * _e44));\n let _e49 = unnamed.uToNdc0_[0u];\n let _e51 = p[0u];\n let _e55 = unnamed.uToNdc0_[2u];\n let _e57 = p[1u];\n let _e62 = unnamed.uToNdc1_[0u];\n let _e66 = unnamed.uToNdc0_[1u];\n let _e68 = p[0u];\n let _e72 = unnamed.uToNdc0_[3u];\n let _e74 = p[1u];\n let _e79 = unnamed.uToNdc1_[1u];\n ndc = vec2<f32>((((_e49 * _e51) + (_e55 * _e57)) + _e62), (((_e66 * _e68) + (_e72 * _e74)) + _e79));\n let _e84 = unnamed.uToNdc0_[0u];\n let _e87 = unnamed.uToNdc0_[3u];\n let _e91 = unnamed.uToNdc0_[1u];\n let _e94 = unnamed.uToNdc0_[2u];\n orientation = ((_e84 * _e87) - (_e91 * _e94));\n let _e98 = corner[0u];\n let _e99 = orientation;\n if (_e99 > 0f) {\n let _e102 = corner[1u];\n local = (1f - _e102);\n } else {\n let _e105 = corner[1u];\n local = _e105;\n }\n let _e106 = local;\n along = vec2<f32>(_e98, _e106);\n let _e108 = aUv_1;\n let _e110 = aUv_1;\n let _e112 = along;\n vUv = mix(_e108.xy, _e110.zw, _e112);\n let _e114 = aTint_1;\n vTint = _e114;\n let _e116 = unnamed.uClipCorrection;\n let _e117 = ndc;\n unnamed_1.gl_Position = (_e116 * vec4<f32>(_e117.x, _e117.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
9
|
/**
|
|
10
10
|
* What the transform assigned, so the renderer binds the same numbers.
|
|
11
11
|
*
|
package/dist/shaders/sprite.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
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,
|
|
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 /*\n * **A picture keeps its top up in either space.** A frame's v runs down the image, and a sprite's\n * second edge runs toward larger y: down the screen in screen space, up the world in a 2D world.\n * So where the affine keeps orientation, which a world camera's does and the screen's does not,\n * the frame's rows are read from the other end. Read from the affine rather than passed in,\n * because a flag beside it could disagree with it. It was missing until 2026-10-02, and every\n * sprite and tile drawn in a world came out upside down while every check, in screen space,\n * passed.\n */\n float orientation = uToNdc0.x * uToNdc0.w - uToNdc0.y * uToNdc0.z;\n vec2 along = vec2(corner.x, orientation > 0.0 ? 1.0 - corner.y : corner.y);\n vUv = mix(aUv.xy, aUv.zw, along);\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
3
|
export declare const SPRITE_FRAG: string;
|
package/dist/shaders/sprite.js
CHANGED
|
@@ -65,7 +65,18 @@ void main() {
|
|
|
65
65
|
uToNdc0.x * p.x + uToNdc0.z * p.y + uToNdc1.x,
|
|
66
66
|
uToNdc0.y * p.x + uToNdc0.w * p.y + uToNdc1.y
|
|
67
67
|
);
|
|
68
|
-
|
|
68
|
+
/*
|
|
69
|
+
* **A picture keeps its top up in either space.** A frame's v runs down the image, and a sprite's
|
|
70
|
+
* second edge runs toward larger y: down the screen in screen space, up the world in a 2D world.
|
|
71
|
+
* So where the affine keeps orientation, which a world camera's does and the screen's does not,
|
|
72
|
+
* the frame's rows are read from the other end. Read from the affine rather than passed in,
|
|
73
|
+
* because a flag beside it could disagree with it. It was missing until 2026-10-02, and every
|
|
74
|
+
* sprite and tile drawn in a world came out upside down while every check, in screen space,
|
|
75
|
+
* passed.
|
|
76
|
+
*/
|
|
77
|
+
float orientation = uToNdc0.x * uToNdc0.w - uToNdc0.y * uToNdc0.z;
|
|
78
|
+
vec2 along = vec2(corner.x, orientation > 0.0 ? 1.0 - corner.y : corner.y);
|
|
79
|
+
vUv = mix(aUv.xy, aUv.zw, along);
|
|
69
80
|
vTint = aTint;
|
|
70
81
|
/* z is 0 and nothing depth-tests here: the order sprites were submitted in is the layering. */
|
|
71
82
|
gl_Position = uClipCorrection * vec4(ndc, 0.0, 1.0);
|
package/dist/uiDraw.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Turn a laid-out interface tree into quads, parents first so children land on top. */
|
|
2
2
|
import type { SpriteBatch } from './spriteBatch.ts';
|
|
3
|
-
import type { UiNode } from './uiNode.ts';
|
|
3
|
+
import type { UiNode, UiRect } from './uiNode.ts';
|
|
4
4
|
/**
|
|
5
5
|
* Where a caller draws what a quad cannot be.
|
|
6
6
|
*
|
|
@@ -13,7 +13,12 @@ import type { UiNode } from './uiNode.ts';
|
|
|
13
13
|
* label lands over its own background and under whatever is drawn after it.
|
|
14
14
|
*/
|
|
15
15
|
export interface UiContentSink {
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `visible` is the part of the node its clipping ancestors leave on screen, which outside any clip
|
|
18
|
+
* is the whole of it. Text has to be cut where the quads are, and only the caller drawing it can
|
|
19
|
+
* cut it: a label in a row half scrolled out of a list is skipped, or drawn cut, here.
|
|
20
|
+
*/
|
|
21
|
+
content(node: UiNode, visible: UiRect): void;
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
19
24
|
* Draw a tree. Returns how many quads it came to.
|
|
@@ -22,6 +27,14 @@ export interface UiContentSink {
|
|
|
22
27
|
* there is no depth here, so what is drawn later is what is on top. A node draws its background
|
|
23
28
|
* first and its image second, so an icon lands on its own plate.
|
|
24
29
|
*
|
|
30
|
+
* **A clipping node bounds what its descendants draw.** Each quad is cut to the rectangle every
|
|
31
|
+
* clipping ancestor leaves, on the CPU, and an image's frame is cut in proportion, so a scrolled list
|
|
32
|
+
* shows the rows inside it and nothing past its edges. Cut rather than scissored, because a scissor
|
|
33
|
+
* is state a contributed pass would have to set and restore on both backends, and a cut is four
|
|
34
|
+
* numbers per quad in the batch it already writes. Until 2026-10-02 `clip` was laid out, scrolled
|
|
35
|
+
* and documented, and drawn by nothing: a list's rows drew wherever they had scrolled to. The tree is
|
|
36
|
+
* in screen space, CSS pixels from the top-left, which is the space the cut assumes.
|
|
37
|
+
*
|
|
25
38
|
* Allocates nothing. `layoutUiTree` must have run over this root, or every rect is whatever it was
|
|
26
39
|
* left at.
|
|
27
40
|
*/
|
package/dist/uiDraw.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/** Turn a laid-out interface tree into quads, parents first so children land on top. */
|
|
2
2
|
import { drawSprite } from './spriteBatch.js';
|
|
3
|
-
|
|
3
|
+
import { createSpriteFrame } from './spriteSheet.js';
|
|
4
|
+
/* One placement, refilled per node. This is a per-frame path and these are the only objects in it. */
|
|
4
5
|
const PLACEMENT = {
|
|
5
6
|
x: 0,
|
|
6
7
|
y: 0,
|
|
7
8
|
w: 0,
|
|
8
9
|
h: 0,
|
|
9
10
|
};
|
|
11
|
+
/* The visible part of a node, handed to the sink, and the part of its frame that part shows. */
|
|
12
|
+
const VISIBLE = { x: 0, y: 0, w: 0, h: 0 };
|
|
13
|
+
const CUT = createSpriteFrame();
|
|
10
14
|
/**
|
|
11
15
|
* Draw a tree. Returns how many quads it came to.
|
|
12
16
|
*
|
|
@@ -14,28 +18,82 @@ const PLACEMENT = {
|
|
|
14
18
|
* there is no depth here, so what is drawn later is what is on top. A node draws its background
|
|
15
19
|
* first and its image second, so an icon lands on its own plate.
|
|
16
20
|
*
|
|
21
|
+
* **A clipping node bounds what its descendants draw.** Each quad is cut to the rectangle every
|
|
22
|
+
* clipping ancestor leaves, on the CPU, and an image's frame is cut in proportion, so a scrolled list
|
|
23
|
+
* shows the rows inside it and nothing past its edges. Cut rather than scissored, because a scissor
|
|
24
|
+
* is state a contributed pass would have to set and restore on both backends, and a cut is four
|
|
25
|
+
* numbers per quad in the batch it already writes. Until 2026-10-02 `clip` was laid out, scrolled
|
|
26
|
+
* and documented, and drawn by nothing: a list's rows drew wherever they had scrolled to. The tree is
|
|
27
|
+
* in screen space, CSS pixels from the top-left, which is the space the cut assumes.
|
|
28
|
+
*
|
|
17
29
|
* Allocates nothing. `layoutUiTree` must have run over this root, or every rect is whatever it was
|
|
18
30
|
* left at.
|
|
19
31
|
*/
|
|
20
32
|
export function drawUiTree(batch, root, white, sink) {
|
|
21
|
-
|
|
33
|
+
return drawWithin(batch, root, white, sink, -Infinity, -Infinity, Infinity, Infinity);
|
|
34
|
+
}
|
|
35
|
+
function drawWithin(batch, node, white, sink, left, top, right, bottom) {
|
|
36
|
+
if (node.hidden)
|
|
22
37
|
return 0;
|
|
38
|
+
const rect = node.rect;
|
|
39
|
+
const clipped = left !== -Infinity;
|
|
40
|
+
const x0 = Math.max(rect.x, left);
|
|
41
|
+
const y0 = Math.max(rect.y, top);
|
|
42
|
+
const x1 = Math.min(rect.x + rect.w, right);
|
|
43
|
+
const y1 = Math.min(rect.y + rect.h, bottom);
|
|
23
44
|
let drawn = 0;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
/*
|
|
46
|
+
* Inside a clip, a node cut away entirely draws nothing of its own. Outside one, nothing is cut and
|
|
47
|
+
* a node is drawn as it always was, whatever its size. Its children are cut one by one either way,
|
|
48
|
+
* since a child can lie outside its parent's box and still inside the clip.
|
|
49
|
+
*/
|
|
50
|
+
if (!clipped || (x1 > x0 && y1 > y0)) {
|
|
51
|
+
PLACEMENT.x = clipped ? x0 : rect.x;
|
|
52
|
+
PLACEMENT.y = clipped ? y0 : rect.y;
|
|
53
|
+
PLACEMENT.w = clipped ? x1 - x0 : rect.w;
|
|
54
|
+
PLACEMENT.h = clipped ? y1 - y0 : rect.h;
|
|
55
|
+
if (node.background !== null) {
|
|
56
|
+
drawSprite(batch, white, PLACEMENT, null, node.background);
|
|
57
|
+
drawn += 1;
|
|
58
|
+
}
|
|
59
|
+
if (node.texture >= 0) {
|
|
60
|
+
const frame = clipped ? cutFrame(node, x0, y0, x1, y1) : node.frame;
|
|
61
|
+
drawSprite(batch, node.texture, PLACEMENT, frame, node.tint);
|
|
62
|
+
drawn += 1;
|
|
63
|
+
}
|
|
64
|
+
if (sink !== null && node.text !== '') {
|
|
65
|
+
VISIBLE.x = PLACEMENT.x;
|
|
66
|
+
VISIBLE.y = PLACEMENT.y;
|
|
67
|
+
VISIBLE.w = PLACEMENT.w;
|
|
68
|
+
VISIBLE.h = PLACEMENT.h;
|
|
69
|
+
sink.content(node, VISIBLE);
|
|
70
|
+
}
|
|
35
71
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
/* What a clipping node leaves its children: its own box, inside whatever it was left itself. */
|
|
73
|
+
const keep = node.clip;
|
|
74
|
+
const cl = keep ? Math.max(left, rect.x) : left;
|
|
75
|
+
const ct = keep ? Math.max(top, rect.y) : top;
|
|
76
|
+
const cr = keep ? Math.min(right, rect.x + rect.w) : right;
|
|
77
|
+
const cb = keep ? Math.min(bottom, rect.y + rect.h) : bottom;
|
|
78
|
+
for (const child of node.children)
|
|
79
|
+
drawn += drawWithin(batch, child, white, sink, cl, ct, cr, cb);
|
|
40
80
|
return drawn;
|
|
41
81
|
}
|
|
82
|
+
/** The part of a node's frame its visible box shows, or the whole frame when nothing was cut. */
|
|
83
|
+
function cutFrame(node, x0, y0, x1, y1) {
|
|
84
|
+
const rect = node.rect;
|
|
85
|
+
if (x0 === rect.x && y0 === rect.y && x1 === rect.x + rect.w && y1 === rect.y + rect.h) {
|
|
86
|
+
return node.frame;
|
|
87
|
+
}
|
|
88
|
+
const u0 = node.frame?.u0 ?? 0;
|
|
89
|
+
const v0 = node.frame?.v0 ?? 0;
|
|
90
|
+
const u1 = node.frame?.u1 ?? 1;
|
|
91
|
+
const v1 = node.frame?.v1 ?? 1;
|
|
92
|
+
const perX = rect.w > 0 ? (u1 - u0) / rect.w : 0;
|
|
93
|
+
const perY = rect.h > 0 ? (v1 - v0) / rect.h : 0;
|
|
94
|
+
CUT.u0 = u0 + (x0 - rect.x) * perX;
|
|
95
|
+
CUT.u1 = u0 + (x1 - rect.x) * perX;
|
|
96
|
+
CUT.v0 = v0 + (y0 - rect.y) * perY;
|
|
97
|
+
CUT.v1 = v0 + (y1 - rect.y) * perY;
|
|
98
|
+
return CUT;
|
|
99
|
+
}
|
package/dist/uiFocus.js
CHANGED
|
@@ -16,6 +16,10 @@ import { uiRectHolds } from './uiNode.js';
|
|
|
16
16
|
export function uiHitTest(root, x, y) {
|
|
17
17
|
if (root.hidden)
|
|
18
18
|
return null;
|
|
19
|
+
/* A clipping node bounds its descendants here as it does in the picture: a row scrolled out of a
|
|
20
|
+
list is not under the pointer, whatever is drawn where it would have been. */
|
|
21
|
+
if (root.clip && !uiRectHolds(root, x, y))
|
|
22
|
+
return null;
|
|
19
23
|
for (let i = root.children.length - 1; i >= 0; i -= 1) {
|
|
20
24
|
const hit = uiHitTest(root.children[i], x, y);
|
|
21
25
|
if (hit !== null)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/ui2d",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.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.7.1"
|
|
31
31
|
},
|
|
32
32
|
"author": "Drift Technologies",
|
|
33
33
|
"repository": {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
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((¶m));\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((¶m_1));\n (*c_1) = _e63;\n }\n let _e64 = (*c_1);\n param_2 = _e64;\n let _e65 = linearToSrgb_u0028_vf3_u003b((¶m_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((¶m_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";
|
|
9
9
|
|
|
10
|
-
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
|
|
10
|
+
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 var orientation: f32;\n var along: vec2<f32>;\n var local: f32;\n\n let _e31 = 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 _e33 = indexable[_e31];\n corner = _e33;\n let _e34 = aOrigin_1;\n let _e35 = aEdges_1;\n let _e38 = corner[0u];\n let _e41 = aEdges_1;\n let _e44 = corner[1u];\n p = ((_e34 + (_e35.xy * _e38)) + (_e41.zw * _e44));\n let _e49 = unnamed.uToNdc0_[0u];\n let _e51 = p[0u];\n let _e55 = unnamed.uToNdc0_[2u];\n let _e57 = p[1u];\n let _e62 = unnamed.uToNdc1_[0u];\n let _e66 = unnamed.uToNdc0_[1u];\n let _e68 = p[0u];\n let _e72 = unnamed.uToNdc0_[3u];\n let _e74 = p[1u];\n let _e79 = unnamed.uToNdc1_[1u];\n ndc = vec2<f32>((((_e49 * _e51) + (_e55 * _e57)) + _e62), (((_e66 * _e68) + (_e72 * _e74)) + _e79));\n let _e84 = unnamed.uToNdc0_[0u];\n let _e87 = unnamed.uToNdc0_[3u];\n let _e91 = unnamed.uToNdc0_[1u];\n let _e94 = unnamed.uToNdc0_[2u];\n orientation = ((_e84 * _e87) - (_e91 * _e94));\n let _e98 = corner[0u];\n let _e99 = orientation;\n if (_e99 > 0f) {\n let _e102 = corner[1u];\n local = (1f - _e102);\n } else {\n let _e105 = corner[1u];\n local = _e105;\n }\n let _e106 = local;\n along = vec2<f32>(_e98, _e106);\n let _e108 = aUv_1;\n let _e110 = aUv_1;\n let _e112 = along;\n vUv = mix(_e108.xy, _e110.zw, _e112);\n let _e114 = aTint_1;\n vTint = _e114;\n let _e116 = unnamed.uClipCorrection;\n let _e117 = ndc;\n unnamed_1.gl_Position = (_e116 * vec4<f32>(_e117.x, _e117.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";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* What the transform assigned, so the renderer binds the same numbers.
|
package/src/shaders/sprite.ts
CHANGED
|
@@ -68,7 +68,18 @@ void main() {
|
|
|
68
68
|
uToNdc0.x * p.x + uToNdc0.z * p.y + uToNdc1.x,
|
|
69
69
|
uToNdc0.y * p.x + uToNdc0.w * p.y + uToNdc1.y
|
|
70
70
|
);
|
|
71
|
-
|
|
71
|
+
/*
|
|
72
|
+
* **A picture keeps its top up in either space.** A frame's v runs down the image, and a sprite's
|
|
73
|
+
* second edge runs toward larger y: down the screen in screen space, up the world in a 2D world.
|
|
74
|
+
* So where the affine keeps orientation, which a world camera's does and the screen's does not,
|
|
75
|
+
* the frame's rows are read from the other end. Read from the affine rather than passed in,
|
|
76
|
+
* because a flag beside it could disagree with it. It was missing until 2026-10-02, and every
|
|
77
|
+
* sprite and tile drawn in a world came out upside down while every check, in screen space,
|
|
78
|
+
* passed.
|
|
79
|
+
*/
|
|
80
|
+
float orientation = uToNdc0.x * uToNdc0.w - uToNdc0.y * uToNdc0.z;
|
|
81
|
+
vec2 along = vec2(corner.x, orientation > 0.0 ? 1.0 - corner.y : corner.y);
|
|
82
|
+
vUv = mix(aUv.xy, aUv.zw, along);
|
|
72
83
|
vTint = aTint;
|
|
73
84
|
/* z is 0 and nothing depth-tests here: the order sprites were submitted in is the layering. */
|
|
74
85
|
gl_Position = uClipCorrection * vec4(ndc, 0.0, 1.0);
|
package/src/uiDraw.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { drawSprite } from './spriteBatch.ts';
|
|
4
4
|
import type { SpriteBatch, SpritePlacement } from './spriteBatch.ts';
|
|
5
|
-
import
|
|
5
|
+
import { createSpriteFrame } from './spriteSheet.ts';
|
|
6
|
+
import type { SpriteFrame } from './spriteSheet.ts';
|
|
7
|
+
import type { UiNode, UiRect } from './uiNode.ts';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Where a caller draws what a quad cannot be.
|
|
@@ -16,16 +18,24 @@ import type { UiNode } from './uiNode.ts';
|
|
|
16
18
|
* label lands over its own background and under whatever is drawn after it.
|
|
17
19
|
*/
|
|
18
20
|
export interface UiContentSink {
|
|
19
|
-
|
|
21
|
+
/**
|
|
22
|
+
* `visible` is the part of the node its clipping ancestors leave on screen, which outside any clip
|
|
23
|
+
* is the whole of it. Text has to be cut where the quads are, and only the caller drawing it can
|
|
24
|
+
* cut it: a label in a row half scrolled out of a list is skipped, or drawn cut, here.
|
|
25
|
+
*/
|
|
26
|
+
content(node: UiNode, visible: UiRect): void;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
/* One placement, refilled per node. This is a per-frame path and
|
|
29
|
+
/* One placement, refilled per node. This is a per-frame path and these are the only objects in it. */
|
|
23
30
|
const PLACEMENT: SpritePlacement & { x: number; y: number; w: number; h: number } = {
|
|
24
31
|
x: 0,
|
|
25
32
|
y: 0,
|
|
26
33
|
w: 0,
|
|
27
34
|
h: 0,
|
|
28
35
|
};
|
|
36
|
+
/* The visible part of a node, handed to the sink, and the part of its frame that part shows. */
|
|
37
|
+
const VISIBLE: UiRect = { x: 0, y: 0, w: 0, h: 0 };
|
|
38
|
+
const CUT = createSpriteFrame();
|
|
29
39
|
|
|
30
40
|
/**
|
|
31
41
|
* Draw a tree. Returns how many quads it came to.
|
|
@@ -34,6 +44,14 @@ const PLACEMENT: SpritePlacement & { x: number; y: number; w: number; h: number
|
|
|
34
44
|
* there is no depth here, so what is drawn later is what is on top. A node draws its background
|
|
35
45
|
* first and its image second, so an icon lands on its own plate.
|
|
36
46
|
*
|
|
47
|
+
* **A clipping node bounds what its descendants draw.** Each quad is cut to the rectangle every
|
|
48
|
+
* clipping ancestor leaves, on the CPU, and an image's frame is cut in proportion, so a scrolled list
|
|
49
|
+
* shows the rows inside it and nothing past its edges. Cut rather than scissored, because a scissor
|
|
50
|
+
* is state a contributed pass would have to set and restore on both backends, and a cut is four
|
|
51
|
+
* numbers per quad in the batch it already writes. Until 2026-10-02 `clip` was laid out, scrolled
|
|
52
|
+
* and documented, and drawn by nothing: a list's rows drew wherever they had scrolled to. The tree is
|
|
53
|
+
* in screen space, CSS pixels from the top-left, which is the space the cut assumes.
|
|
54
|
+
*
|
|
37
55
|
* Allocates nothing. `layoutUiTree` must have run over this root, or every rect is whatever it was
|
|
38
56
|
* left at.
|
|
39
57
|
*/
|
|
@@ -43,21 +61,85 @@ export function drawUiTree(
|
|
|
43
61
|
white: number,
|
|
44
62
|
sink: UiContentSink | null,
|
|
45
63
|
): number {
|
|
46
|
-
|
|
64
|
+
return drawWithin(batch, root, white, sink, -Infinity, -Infinity, Infinity, Infinity);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function drawWithin(
|
|
68
|
+
batch: SpriteBatch,
|
|
69
|
+
node: UiNode,
|
|
70
|
+
white: number,
|
|
71
|
+
sink: UiContentSink | null,
|
|
72
|
+
left: number,
|
|
73
|
+
top: number,
|
|
74
|
+
right: number,
|
|
75
|
+
bottom: number,
|
|
76
|
+
): number {
|
|
77
|
+
if (node.hidden) return 0;
|
|
78
|
+
const rect = node.rect;
|
|
79
|
+
const clipped = left !== -Infinity;
|
|
80
|
+
const x0 = Math.max(rect.x, left);
|
|
81
|
+
const y0 = Math.max(rect.y, top);
|
|
82
|
+
const x1 = Math.min(rect.x + rect.w, right);
|
|
83
|
+
const y1 = Math.min(rect.y + rect.h, bottom);
|
|
47
84
|
let drawn = 0;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
/*
|
|
86
|
+
* Inside a clip, a node cut away entirely draws nothing of its own. Outside one, nothing is cut and
|
|
87
|
+
* a node is drawn as it always was, whatever its size. Its children are cut one by one either way,
|
|
88
|
+
* since a child can lie outside its parent's box and still inside the clip.
|
|
89
|
+
*/
|
|
90
|
+
if (!clipped || (x1 > x0 && y1 > y0)) {
|
|
91
|
+
PLACEMENT.x = clipped ? x0 : rect.x;
|
|
92
|
+
PLACEMENT.y = clipped ? y0 : rect.y;
|
|
93
|
+
PLACEMENT.w = clipped ? x1 - x0 : rect.w;
|
|
94
|
+
PLACEMENT.h = clipped ? y1 - y0 : rect.h;
|
|
95
|
+
if (node.background !== null) {
|
|
96
|
+
drawSprite(batch, white, PLACEMENT, null, node.background);
|
|
97
|
+
drawn += 1;
|
|
98
|
+
}
|
|
99
|
+
if (node.texture >= 0) {
|
|
100
|
+
const frame = clipped ? cutFrame(node, x0, y0, x1, y1) : node.frame;
|
|
101
|
+
drawSprite(batch, node.texture, PLACEMENT, frame, node.tint);
|
|
102
|
+
drawn += 1;
|
|
103
|
+
}
|
|
104
|
+
if (sink !== null && node.text !== '') {
|
|
105
|
+
VISIBLE.x = PLACEMENT.x;
|
|
106
|
+
VISIBLE.y = PLACEMENT.y;
|
|
107
|
+
VISIBLE.w = PLACEMENT.w;
|
|
108
|
+
VISIBLE.h = PLACEMENT.h;
|
|
109
|
+
sink.content(node, VISIBLE);
|
|
110
|
+
}
|
|
59
111
|
}
|
|
60
|
-
|
|
61
|
-
|
|
112
|
+
/* What a clipping node leaves its children: its own box, inside whatever it was left itself. */
|
|
113
|
+
const keep = node.clip;
|
|
114
|
+
const cl = keep ? Math.max(left, rect.x) : left;
|
|
115
|
+
const ct = keep ? Math.max(top, rect.y) : top;
|
|
116
|
+
const cr = keep ? Math.min(right, rect.x + rect.w) : right;
|
|
117
|
+
const cb = keep ? Math.min(bottom, rect.y + rect.h) : bottom;
|
|
118
|
+
for (const child of node.children) drawn += drawWithin(batch, child, white, sink, cl, ct, cr, cb);
|
|
62
119
|
return drawn;
|
|
63
120
|
}
|
|
121
|
+
|
|
122
|
+
/** The part of a node's frame its visible box shows, or the whole frame when nothing was cut. */
|
|
123
|
+
function cutFrame(
|
|
124
|
+
node: UiNode,
|
|
125
|
+
x0: number,
|
|
126
|
+
y0: number,
|
|
127
|
+
x1: number,
|
|
128
|
+
y1: number,
|
|
129
|
+
): SpriteFrame | null {
|
|
130
|
+
const rect = node.rect;
|
|
131
|
+
if (x0 === rect.x && y0 === rect.y && x1 === rect.x + rect.w && y1 === rect.y + rect.h) {
|
|
132
|
+
return node.frame;
|
|
133
|
+
}
|
|
134
|
+
const u0 = node.frame?.u0 ?? 0;
|
|
135
|
+
const v0 = node.frame?.v0 ?? 0;
|
|
136
|
+
const u1 = node.frame?.u1 ?? 1;
|
|
137
|
+
const v1 = node.frame?.v1 ?? 1;
|
|
138
|
+
const perX = rect.w > 0 ? (u1 - u0) / rect.w : 0;
|
|
139
|
+
const perY = rect.h > 0 ? (v1 - v0) / rect.h : 0;
|
|
140
|
+
CUT.u0 = u0 + (x0 - rect.x) * perX;
|
|
141
|
+
CUT.u1 = u0 + (x1 - rect.x) * perX;
|
|
142
|
+
CUT.v0 = v0 + (y0 - rect.y) * perY;
|
|
143
|
+
CUT.v1 = v0 + (y1 - rect.y) * perY;
|
|
144
|
+
return CUT;
|
|
145
|
+
}
|
package/src/uiFocus.ts
CHANGED
|
@@ -18,6 +18,9 @@ import type { UiNode } from './uiNode.ts';
|
|
|
18
18
|
*/
|
|
19
19
|
export function uiHitTest(root: UiNode, x: number, y: number): UiNode | null {
|
|
20
20
|
if (root.hidden) return null;
|
|
21
|
+
/* A clipping node bounds its descendants here as it does in the picture: a row scrolled out of a
|
|
22
|
+
list is not under the pointer, whatever is drawn where it would have been. */
|
|
23
|
+
if (root.clip && !uiRectHolds(root, x, y)) return null;
|
|
21
24
|
for (let i = root.children.length - 1; i >= 0; i -= 1) {
|
|
22
25
|
const hit = uiHitTest(root.children[i] as UiNode, x, y);
|
|
23
26
|
if (hit !== null) return hit;
|