@driftengine/core 4.1.5 → 4.2.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.
- package/CHANGELOG.json +16 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -1
- package/dist/render/backend/webgpu/renderer.js +4 -4
- package/dist/render/textRenderer.js +23 -4
- package/package.json +6 -6
- package/src/index.ts +11 -1
- package/src/render/backend/webgpu/renderer.ts +4 -8
- package/src/render/textRenderer.ts +23 -14
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"date": "2026-09-21",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "fixed",
|
|
8
|
+
"short": "Text is drawn at the cell size it was asked for, so a centred line is centred again. 4.1.4 snapped the cell down without telling the caller that measured it.",
|
|
9
|
+
"text": "**Reported as text that \"is not centered anymore and maybe tinier\", in every consumer at once**, correct in a full-size landscape browser and wrong in mobile portrait.\n\n4.1.4 made the draw path snap the cell down to whole device pixels, so a 5x7 face could not draw strokes of two different widths. That defect is real and the snap is the right cure for it, but imposing it was wrong. `textWidthPx` is the only measurement a consumer has, it takes no viewport and therefore cannot know the device ratio, so every consumer went on laying out against the cell it asked for while the engine drew a smaller one.\n\n**Everything done with a measured width broke together**: a centred line, a right-aligned column, a line fitted to a box. Measured on a 23-character string, a cell of 4.68 at ratio 1 draws 46.6 px narrower than measured; a whole cell of 3 on a 1.25 display draws 41.1 px narrower and 20% smaller than asked. The release note that shipped it called this \"at most half a device pixel per cell off centre\", which was wrong by two orders of magnitude.\n\n**It looked fine on a desktop because the snap is a no-op there.** A whole cell at a whole device ratio is returned untouched, which is exactly the case a full-size landscape browser produces. A consumer that divides an available width by a cell count, which is what a responsive layout does, gets a fraction and pays the whole gap. That is why it was reported as a mobile-portrait fault rather than a text fault.\n\nBoth backends now draw the cell they are handed. The origin snap stays: it rounds to the nearest device pixel, shifts a line by at most half of one and breaks no layout.\n\n**What this gives up** is that a fractional cell draws strokes of two widths again for a caller that does not opt in, which is the defect 4.1.4 was written to fix. It is available rather than imposed now, and the entry below is how."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"kind": "added",
|
|
13
|
+
"short": "deviceSnappedCellSize is exported, so a caller can ask for whole-pixel glyph strokes and measure the size it will actually get.",
|
|
14
|
+
"text": "The cell size to draw at so that every cell of a bitmap glyph covers whole device pixels, which the engine used to apply on the caller's behalf and no longer does.\n\n**It was not exported at all before**, which is why no consumer could have compensated for the snap being imposed: the decision was reachable only from inside the renderer. Snap once, at the point the cell is computed, then use that one number to measure, to lay out and to draw. The measurement and the picture then agree by construction, which is the property the imposed version could not have.\n\n```js\nconst cell = deviceSnappedCellSize(available / cells, viewportWidth, canvas.width);\nconst width = textWidthPx(label, cell);\nrenderer.drawText(text, viewportWidth, viewportHeight, (boxWidth - width) / 2, y, { ...style, cellSize: cell }, now);\n```\n\nThe cell drawn is the largest whole number of device pixels not larger than the one asked for. Down rather than to nearest, because a caller that fitted a line to a box measured it first and rounding up would draw a line wider than the box; rounding down can only leave a gap. A cell already whole is returned untouched, so nothing moves where there was nothing to fix."
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
2
18
|
{
|
|
3
19
|
"version": "4.1.5",
|
|
4
20
|
"date": "2026-09-21",
|
package/dist/index.d.ts
CHANGED
|
@@ -331,7 +331,7 @@ export { BoltBatch } from './render/boltBatch.ts';
|
|
|
331
331
|
export { createLineSegments, setPolyline } from './render/linePoints.ts';
|
|
332
332
|
export type { LineSegments } from './render/linePoints.ts';
|
|
333
333
|
export { FlockRenderer } from './render/flockRenderer.ts';
|
|
334
|
-
export { DEFAULT_TEXT_STYLE, textHeightPx, textWidthPx } from './render/textLayout.ts';
|
|
334
|
+
export { DEFAULT_TEXT_STYLE, deviceSnappedCellSize, textHeightPx, textWidthPx, } from './render/textLayout.ts';
|
|
335
335
|
export type { TextStyle } from './render/textLayout.ts';
|
|
336
336
|
export { GLYPH_HEIGHT, GLYPH_SPACING, GLYPH_WIDTH, countCells, forEachCell, forEachRun, glyphRows, hasGlyph, measureText, } from './geometry/pixelFont.ts';
|
|
337
337
|
export { parseSdfFont } from './render/sdfFont.ts';
|
package/dist/index.js
CHANGED
|
@@ -433,7 +433,13 @@ export { FlockRenderer } from './render/flockRenderer.js';
|
|
|
433
433
|
* the same shape scatter, plumes, particles, flocks and bolts already have. The measuring
|
|
434
434
|
* helpers were statics on the class and are free functions now, because neither needs a device.
|
|
435
435
|
*/
|
|
436
|
-
export { DEFAULT_TEXT_STYLE,
|
|
436
|
+
export { DEFAULT_TEXT_STYLE,
|
|
437
|
+
/*
|
|
438
|
+
* Exported so a caller can ask for whole-pixel strokes, which the engine used to impose. See
|
|
439
|
+
* the note at the cell upload in `render/textRenderer.ts`: snap once, then measure, lay out
|
|
440
|
+
* and draw with that one number.
|
|
441
|
+
*/
|
|
442
|
+
deviceSnappedCellSize, textHeightPx, textWidthPx, } from './render/textLayout.js';
|
|
437
443
|
export { GLYPH_HEIGHT, GLYPH_SPACING, GLYPH_WIDTH, countCells, forEachCell, forEachRun, glyphRows, hasGlyph, measureText, } from './geometry/pixelFont.js';
|
|
438
444
|
/*
|
|
439
445
|
* SDF text, reached the same way pixel text is: the verbs are on the renderer and the
|
|
@@ -38,7 +38,7 @@ import { POINT_SHADOW_SAMPLER, PointShadowSystem, } from '../../pointShadowSyste
|
|
|
38
38
|
import { GpuPointShadowArray, GpuPointShadowMap } from './pointShadowPass.js';
|
|
39
39
|
import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, panelPipeline, PANEL_FRAG_FIELDS, PANEL_FRAG_SIZE, PANEL_VERTEX_COUNT, PANEL_VERT_FIELDS, PANEL_VERT_SIZE, } from './panelPass.js';
|
|
40
40
|
import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
|
|
41
|
-
import {
|
|
41
|
+
import { deviceSnappedOrigin } from '../../textLayout.js';
|
|
42
42
|
import { GpuSurfaceTexture } from './surfaceTexturePass.js';
|
|
43
43
|
import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
|
|
44
44
|
import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
|
|
@@ -2492,9 +2492,9 @@ export class WebGPURenderer {
|
|
|
2492
2492
|
deviceSnappedOrigin(originX, viewportWidth, this.surface.canvas.width),
|
|
2493
2493
|
deviceSnappedOrigin(originY, viewportHeight, this.surface.canvas.height),
|
|
2494
2494
|
]);
|
|
2495
|
-
/*
|
|
2496
|
-
|
|
2497
|
-
v.writeFloat(vertexSlot, at('uCellSize'),
|
|
2495
|
+
/* The cell the caller asked for, drawn faithfully. See `textRenderer.ts` for why the snap
|
|
2496
|
+
this used to impose is the caller's to ask for. */
|
|
2497
|
+
v.writeFloat(vertexSlot, at('uCellSize'), style.cellSize);
|
|
2498
2498
|
/* Far enough that a whole line barely converges, near enough that a turn reads as a turn. */
|
|
2499
2499
|
v.writeFloat(vertexSlot, at('uDepth'), Math.max(viewportWidth, 600) * 1.4);
|
|
2500
2500
|
v.writeFloat(vertexSlot, at('uReveal'), style.reveal);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_CELLS, TEXT_CUBE, TextLayout,
|
|
1
|
+
import { MAX_CELLS, TEXT_CUBE, TextLayout, deviceSnappedOrigin } from './textLayout.js';
|
|
2
2
|
import { compileProgram, uniformLocations } from './shader.js';
|
|
3
3
|
import { TEXT_FRAG, TEXT_VERT } from './shaders/text.js';
|
|
4
4
|
/**
|
|
@@ -103,9 +103,28 @@ export class TextRenderer {
|
|
|
103
103
|
/* Snapped with the cell below: a whole pitch on a fractional origin still splits a stroke.
|
|
104
104
|
See `deviceSnappedOrigin`. */
|
|
105
105
|
setVec2(gl, this.uniforms['uOrigin'], deviceSnappedOrigin(originX, viewportWidth, gl.drawingBufferWidth), deviceSnappedOrigin(originY, viewportHeight, gl.drawingBufferHeight));
|
|
106
|
-
/*
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
/*
|
|
107
|
+
* **The cell the caller asked for, drawn faithfully.**
|
|
108
|
+
*
|
|
109
|
+
* 4.1.4 snapped this down to whole device pixels here, so a 5x7 face could not draw strokes
|
|
110
|
+
* of two different widths. That defect is real and the snap is the right cure for it, but
|
|
111
|
+
* imposing it was wrong: `textWidthPx` is the only measurement a consumer has, it takes no
|
|
112
|
+
* viewport and so cannot know the device ratio, and every consumer went on laying out against
|
|
113
|
+
* the cell it asked for while this drew a smaller one. A centred line, a right-aligned column
|
|
114
|
+
* and a line fitted to a box all broke at once, silently, and only where the snap was not a
|
|
115
|
+
* no-op: a whole cell at a whole ratio is untouched, which is why it looked right in a
|
|
116
|
+
* full-size landscape browser and wrong in mobile portrait, where a consumer divides an
|
|
117
|
+
* available width by a cell count and gets a fraction.
|
|
118
|
+
*
|
|
119
|
+
* So the snap is the caller's to ask for. `deviceSnappedCellSize` is exported; a caller that
|
|
120
|
+
* wants whole-pixel strokes snaps once and uses that one number to measure, to lay out and to
|
|
121
|
+
* draw, where the measurement and the picture agree by construction.
|
|
122
|
+
*
|
|
123
|
+
* **What this gives up** is that a fractional cell draws strokes of two widths again for a
|
|
124
|
+
* caller that does not opt in. **What would make it wrong** is a measurement that could snap
|
|
125
|
+
* on the caller's behalf, which would need the viewport `textWidthPx` is not handed.
|
|
126
|
+
*/
|
|
127
|
+
setFloat(gl, this.uniforms['uCellSize'], style.cellSize);
|
|
109
128
|
// Far enough that a whole line barely converges, near enough that a rotating character
|
|
110
129
|
// reads as turning rather than shearing.
|
|
111
130
|
setFloat(gl, this.uniforms['uDepth'], Math.max(viewportWidth, 600) * 1.4);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "DriftEngine reusable browser 3D runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.
|
|
33
|
-
"@driftengine/physics": "4.
|
|
32
|
+
"@driftengine/drft": "4.2.0",
|
|
33
|
+
"@driftengine/physics": "4.2.0",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@driftengine/nav": "4.
|
|
67
|
-
"@driftengine/network": "4.
|
|
68
|
-
"@driftengine/texture": "4.
|
|
66
|
+
"@driftengine/nav": "4.2.0",
|
|
67
|
+
"@driftengine/network": "4.2.0",
|
|
68
|
+
"@driftengine/texture": "4.2.0"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/index.ts
CHANGED
|
@@ -933,7 +933,17 @@ export { FlockRenderer } from './render/flockRenderer.ts';
|
|
|
933
933
|
* the same shape scatter, plumes, particles, flocks and bolts already have. The measuring
|
|
934
934
|
* helpers were statics on the class and are free functions now, because neither needs a device.
|
|
935
935
|
*/
|
|
936
|
-
export {
|
|
936
|
+
export {
|
|
937
|
+
DEFAULT_TEXT_STYLE,
|
|
938
|
+
/*
|
|
939
|
+
* Exported so a caller can ask for whole-pixel strokes, which the engine used to impose. See
|
|
940
|
+
* the note at the cell upload in `render/textRenderer.ts`: snap once, then measure, lay out
|
|
941
|
+
* and draw with that one number.
|
|
942
|
+
*/
|
|
943
|
+
deviceSnappedCellSize,
|
|
944
|
+
textHeightPx,
|
|
945
|
+
textWidthPx,
|
|
946
|
+
} from './render/textLayout.ts';
|
|
937
947
|
export type { TextStyle } from './render/textLayout.ts';
|
|
938
948
|
export {
|
|
939
949
|
GLYPH_HEIGHT,
|
|
@@ -150,7 +150,7 @@ import {
|
|
|
150
150
|
type GpuTextCube,
|
|
151
151
|
} from './textPass.ts';
|
|
152
152
|
import type { TextStyle } from '../../textLayout.ts';
|
|
153
|
-
import {
|
|
153
|
+
import { deviceSnappedOrigin } from '../../textLayout.ts';
|
|
154
154
|
import { GpuSurfaceTexture } from './surfaceTexturePass.ts';
|
|
155
155
|
import {
|
|
156
156
|
createSdfTextBindGroup,
|
|
@@ -3185,13 +3185,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3185
3185
|
deviceSnappedOrigin(originX, viewportWidth, this.surface.canvas.width),
|
|
3186
3186
|
deviceSnappedOrigin(originY, viewportHeight, this.surface.canvas.height),
|
|
3187
3187
|
]);
|
|
3188
|
-
/*
|
|
3189
|
-
|
|
3190
|
-
v.writeFloat(
|
|
3191
|
-
vertexSlot,
|
|
3192
|
-
at('uCellSize'),
|
|
3193
|
-
deviceSnappedCellSize(style.cellSize, viewportWidth, this.surface.canvas.width),
|
|
3194
|
-
);
|
|
3188
|
+
/* The cell the caller asked for, drawn faithfully. See `textRenderer.ts` for why the snap
|
|
3189
|
+
this used to impose is the caller's to ask for. */
|
|
3190
|
+
v.writeFloat(vertexSlot, at('uCellSize'), style.cellSize);
|
|
3195
3191
|
/* Far enough that a whole line barely converges, near enough that a turn reads as a turn. */
|
|
3196
3192
|
v.writeFloat(vertexSlot, at('uDepth'), Math.max(viewportWidth, 600) * 1.4);
|
|
3197
3193
|
v.writeFloat(vertexSlot, at('uReveal'), style.reveal);
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { TextStyle } from './textLayout.ts';
|
|
2
|
-
import {
|
|
3
|
-
MAX_CELLS,
|
|
4
|
-
TEXT_CUBE,
|
|
5
|
-
TextLayout,
|
|
6
|
-
deviceSnappedCellSize,
|
|
7
|
-
deviceSnappedOrigin,
|
|
8
|
-
} from './textLayout.ts';
|
|
2
|
+
import { MAX_CELLS, TEXT_CUBE, TextLayout, deviceSnappedOrigin } from './textLayout.ts';
|
|
9
3
|
import { compileProgram, uniformLocations } from './shader.ts';
|
|
10
4
|
import { TEXT_FRAG, TEXT_VERT } from './shaders/text.ts';
|
|
11
5
|
|
|
@@ -129,13 +123,28 @@ export class TextRenderer {
|
|
|
129
123
|
deviceSnappedOrigin(originX, viewportWidth, gl.drawingBufferWidth),
|
|
130
124
|
deviceSnappedOrigin(originY, viewportHeight, gl.drawingBufferHeight),
|
|
131
125
|
);
|
|
132
|
-
/*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
/*
|
|
127
|
+
* **The cell the caller asked for, drawn faithfully.**
|
|
128
|
+
*
|
|
129
|
+
* 4.1.4 snapped this down to whole device pixels here, so a 5x7 face could not draw strokes
|
|
130
|
+
* of two different widths. That defect is real and the snap is the right cure for it, but
|
|
131
|
+
* imposing it was wrong: `textWidthPx` is the only measurement a consumer has, it takes no
|
|
132
|
+
* viewport and so cannot know the device ratio, and every consumer went on laying out against
|
|
133
|
+
* the cell it asked for while this drew a smaller one. A centred line, a right-aligned column
|
|
134
|
+
* and a line fitted to a box all broke at once, silently, and only where the snap was not a
|
|
135
|
+
* no-op: a whole cell at a whole ratio is untouched, which is why it looked right in a
|
|
136
|
+
* full-size landscape browser and wrong in mobile portrait, where a consumer divides an
|
|
137
|
+
* available width by a cell count and gets a fraction.
|
|
138
|
+
*
|
|
139
|
+
* So the snap is the caller's to ask for. `deviceSnappedCellSize` is exported; a caller that
|
|
140
|
+
* wants whole-pixel strokes snaps once and uses that one number to measure, to lay out and to
|
|
141
|
+
* draw, where the measurement and the picture agree by construction.
|
|
142
|
+
*
|
|
143
|
+
* **What this gives up** is that a fractional cell draws strokes of two widths again for a
|
|
144
|
+
* caller that does not opt in. **What would make it wrong** is a measurement that could snap
|
|
145
|
+
* on the caller's behalf, which would need the viewport `textWidthPx` is not handed.
|
|
146
|
+
*/
|
|
147
|
+
setFloat(gl, this.uniforms['uCellSize'], style.cellSize);
|
|
139
148
|
// Far enough that a whole line barely converges, near enough that a rotating character
|
|
140
149
|
// reads as turning rather than shearing.
|
|
141
150
|
setFloat(gl, this.uniforms['uDepth'], Math.max(viewportWidth, 600) * 1.4);
|