@driftengine/core 4.1.2 → 4.1.4
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/CHANGELOG.json +52 -0
- package/README.md +1 -1
- package/dist/index.js +12 -2
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +62 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.js +192 -45
- package/dist/render/backend/webgpu/renderer.js +10 -2
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +91 -5
- package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
- package/dist/render/shaders/flat/pointShadow.js +42 -16
- package/dist/render/shaders/flat/preamble.js +44 -7
- package/dist/render/shaders/generated/flat.wgsl.js +16 -16
- package/dist/render/textLayout.d.ts +58 -0
- package/dist/render/textLayout.js +83 -0
- package/dist/render/textRenderer.js +7 -3
- package/package.json +6 -6
- package/src/index.ts +12 -2
- package/src/render/backend/webgpu/gpuDrivenPass.ts +195 -47
- package/src/render/backend/webgpu/renderer.ts +14 -2
- package/src/render/shaders/flat/main.ts +91 -5
- package/src/render/shaders/flat/pointShadow.ts +42 -16
- package/src/render/shaders/flat/preamble.ts +44 -7
- package/src/render/shaders/generated/flat.wgsl.ts +16 -16
- package/src/render/textLayout.ts +88 -0
- package/src/render/textRenderer.ts +22 -3
package/src/render/textLayout.ts
CHANGED
|
@@ -70,6 +70,94 @@ export function textHeightPx(cellSize: number): number {
|
|
|
70
70
|
return GLYPH_HEIGHT * cellSize;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The cell size to draw at, so every cell of a bitmap glyph covers whole device pixels.
|
|
75
|
+
*
|
|
76
|
+
* **A 5x7 face draws every stroke exactly one cell wide, so a fractional cell is not a slightly
|
|
77
|
+
* softer glyph — it is a glyph whose strokes are different widths.** The rasteriser gives one
|
|
78
|
+
* column of cells four pixels and the next three, and a face carrying its legibility entirely in
|
|
79
|
+
* uniform strokes comes apart. Reported three times as text "eaten away" in a checkerboard while
|
|
80
|
+
* larger text in the same frame stayed solid, which is the size dependence this explains: a whole
|
|
81
|
+
* cell is uniform and a fractional one is not, and nothing about it is a dropped pixel.
|
|
82
|
+
*
|
|
83
|
+
* Two doors reach a fractional cell, which is why it was reported as unrelated faults. A caller
|
|
84
|
+
* asks for one outright, sizing a second line at a fraction of its headline; or asks for a whole
|
|
85
|
+
* one that the device ratio makes fractional, a 3-pixel cell at a 1.25 ratio being 3.75 on the
|
|
86
|
+
* grid. `viewportWidth` is whatever units the caller lays out in — CSS pixels for an overlay
|
|
87
|
+
* sized from `cssWidth`, device pixels for one sized from the canvas — and `bufferWidth` is the
|
|
88
|
+
* drawing buffer, so the ratio between them is exactly what turns one into the other.
|
|
89
|
+
*
|
|
90
|
+
* **What it gives up is the asked size.** The cell drawn is the largest whole number of device
|
|
91
|
+
* pixels that is not larger than the one asked for, so a string is drawn up to one device pixel
|
|
92
|
+
* per cell narrower than `textWidthPx` measures it. Down rather than to nearest, because a caller
|
|
93
|
+
* that fitted a line to a box measured it first: rounding up would draw a line wider than the box
|
|
94
|
+
* it was fitted to, and rounding down can only leave a gap.
|
|
95
|
+
*
|
|
96
|
+
* **`textWidthPx` and `RendererApi.textWidth` are deliberately left unsnapped**, and that is the
|
|
97
|
+
* cost rather than an oversight. Neither is handed a viewport, so neither can know whether the
|
|
98
|
+
* caller lays out in CSS pixels or device ones — and this ratio is exactly that question. A width
|
|
99
|
+
* is therefore an upper bound on what is drawn: a centred line sits up to half the shortfall off
|
|
100
|
+
* centre, at most half a device pixel per cell. Handing the measurement a viewport would fix it
|
|
101
|
+
* and is a change to the public surface rather than to a patch.
|
|
102
|
+
*
|
|
103
|
+
* **What would make it wrong** is a face whose strokes are not cell-aligned — an SDF atlas, where
|
|
104
|
+
* the glyph is a distance field and a fractional size is exactly what it is built to serve.
|
|
105
|
+
* `sdfTextLayout.ts` is that path and does not come through here.
|
|
106
|
+
*/
|
|
107
|
+
export function deviceSnappedCellSize(
|
|
108
|
+
cellSize: number,
|
|
109
|
+
viewportWidth: number,
|
|
110
|
+
bufferWidth: number,
|
|
111
|
+
): number {
|
|
112
|
+
const ratio = bufferWidth / viewportWidth;
|
|
113
|
+
if (!Number.isFinite(ratio) || ratio <= 0) return cellSize;
|
|
114
|
+
const device = cellSize * ratio;
|
|
115
|
+
if (!Number.isFinite(device)) return cellSize;
|
|
116
|
+
/*
|
|
117
|
+
* The tolerance is against float error rather than a fudge: a ratio of 1 turns an asked 4 into
|
|
118
|
+
* 3.9999999999999996 often enough, and flooring that would shrink every whole cell on every
|
|
119
|
+
* ordinary display by a pixel — the change doing most harm where there was nothing to fix.
|
|
120
|
+
*/
|
|
121
|
+
const whole = Math.floor(device + 1e-6);
|
|
122
|
+
/* Below one device pixel there is no whole size to snap to, and flooring would draw nothing. */
|
|
123
|
+
if (whole < 1) return cellSize;
|
|
124
|
+
return whole / ratio;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The same grid's **phase**, which is the other half of the same defect.
|
|
129
|
+
*
|
|
130
|
+
* **A whole pitch on a fractional origin still draws strokes of two widths.** Snapping the cell to
|
|
131
|
+
* four device pixels puts the boundaries four apart, and starting them at 10.4 puts every boundary
|
|
132
|
+
* at `.4` — so each stroke covers three whole pixels and two halves, and the rasteriser resolves
|
|
133
|
+
* that the same ragged way it resolved a fractional cell. The pitch decides how far apart the
|
|
134
|
+
* boundaries are; this decides where the first one is, and a face of uniform strokes needs both.
|
|
135
|
+
*
|
|
136
|
+
* It was found the way the first half was: the cell snap shipped, and the reporter said the text was
|
|
137
|
+
* still wrong.
|
|
138
|
+
*
|
|
139
|
+
* **To nearest rather than down**, which is the opposite of the cell above and for the opposite
|
|
140
|
+
* reason: this is a position, not a size. Moving a label up to half a device pixel is the smallest
|
|
141
|
+
* change that puts its grid on the display's, while flooring would shift every label the same way
|
|
142
|
+
* and bias a centred one off centre.
|
|
143
|
+
*
|
|
144
|
+
* **What would make it wrong** is a caller animating an origin sub-pixel on purpose — a label that
|
|
145
|
+
* slides smoothly now steps by a device pixel. For a face whose strokes are one pixel wide that is
|
|
146
|
+
* the trade this whole function exists to make, and `sdfTextLayout.ts`, which does not come through
|
|
147
|
+
* here, is the path for anything that wants the other answer.
|
|
148
|
+
*/
|
|
149
|
+
export function deviceSnappedOrigin(
|
|
150
|
+
value: number,
|
|
151
|
+
viewportWidth: number,
|
|
152
|
+
bufferWidth: number,
|
|
153
|
+
): number {
|
|
154
|
+
const ratio = bufferWidth / viewportWidth;
|
|
155
|
+
if (!Number.isFinite(ratio) || ratio <= 0) return value;
|
|
156
|
+
const device = value * ratio;
|
|
157
|
+
if (!Number.isFinite(device)) return value;
|
|
158
|
+
return Math.round(device) / ratio;
|
|
159
|
+
}
|
|
160
|
+
|
|
73
161
|
export class TextLayout {
|
|
74
162
|
/** Reused every upload; text changes far too often to allocate per change. */
|
|
75
163
|
readonly cells = new Float32Array(MAX_CELLS * 2);
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { TextStyle } from './textLayout.ts';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
MAX_CELLS,
|
|
4
|
+
TEXT_CUBE,
|
|
5
|
+
TextLayout,
|
|
6
|
+
deviceSnappedCellSize,
|
|
7
|
+
deviceSnappedOrigin,
|
|
8
|
+
} from './textLayout.ts';
|
|
3
9
|
import { compileProgram, uniformLocations } from './shader.ts';
|
|
4
10
|
import { TEXT_FRAG, TEXT_VERT } from './shaders/text.ts';
|
|
5
11
|
|
|
@@ -115,8 +121,21 @@ export class TextRenderer {
|
|
|
115
121
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
116
122
|
|
|
117
123
|
setVec2(gl, this.uniforms['uViewport'], viewportWidth, viewportHeight);
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
/* Snapped with the cell below: a whole pitch on a fractional origin still splits a stroke.
|
|
125
|
+
See `deviceSnappedOrigin`. */
|
|
126
|
+
setVec2(
|
|
127
|
+
gl,
|
|
128
|
+
this.uniforms['uOrigin'],
|
|
129
|
+
deviceSnappedOrigin(originX, viewportWidth, gl.drawingBufferWidth),
|
|
130
|
+
deviceSnappedOrigin(originY, viewportHeight, gl.drawingBufferHeight),
|
|
131
|
+
);
|
|
132
|
+
/* Whole device pixels per cell, or a 5x7 face draws strokes of two different widths. The
|
|
133
|
+
decision is shared with WebGPU; only this binding is per-backend. See `textLayout.ts`. */
|
|
134
|
+
setFloat(
|
|
135
|
+
gl,
|
|
136
|
+
this.uniforms['uCellSize'],
|
|
137
|
+
deviceSnappedCellSize(style.cellSize, viewportWidth, gl.drawingBufferWidth),
|
|
138
|
+
);
|
|
120
139
|
// Far enough that a whole line barely converges, near enough that a rotating character
|
|
121
140
|
// reads as turning rather than shearing.
|
|
122
141
|
setFloat(gl, this.uniforms['uDepth'], Math.max(viewportWidth, 600) * 1.4);
|