ntk 8.15.1 → 8.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/index.js +10 -0
- package/lib/path.js +9 -1
- package/lib/renderingcontext_2d.js +210 -0
- package/lib/shadowmath.js +7 -1
- package/lib/shadowtiles.js +326 -0
- package/lib/text/layout.js +54 -16
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -135,6 +135,7 @@ parser, say — can import just that piece, and loads only what it needs:
|
|
|
135
135
|
| `ntk/path` | `Path2D`, `parseSvgPath` | — |
|
|
136
136
|
| `ntk/gl` | `GLError`, `GL_MODES`, `DEFAULT_GL_POLICY`, … | — |
|
|
137
137
|
| `ntk/shadow-math` | `shadowSigma`, `shadowReach`, `blurScale`, … | — |
|
|
138
|
+
| `ntk/shadow-tiles` | `planShadowTiles`, `shadowTileAlpha`, … | — |
|
|
138
139
|
| `ntk/image` | `Image`, `decodeImage`, `loadImage` | `jpeg-js`, `pngjs` |
|
|
139
140
|
| `ntk/svg` | `SvgView` (default export) | `htmlparser2`, `domutils` |
|
|
140
141
|
| `ntk/font` | `Font` (default export) | `fontkit` |
|
package/lib/index.js
CHANGED
|
@@ -56,6 +56,12 @@ import {
|
|
|
56
56
|
shadowReach,
|
|
57
57
|
shadowSigma
|
|
58
58
|
} from './shadow.js';
|
|
59
|
+
import {
|
|
60
|
+
planShadowTiles,
|
|
61
|
+
rasterShadowTile,
|
|
62
|
+
shadowTileAlpha,
|
|
63
|
+
tileScale
|
|
64
|
+
} from './shadowtiles.js';
|
|
59
65
|
import { DEFAULT_SHAPE_POLICY } from './shapeglyphs.js';
|
|
60
66
|
import { DEFAULT_SHARED_GLYPHS_POLICY } from './glyphdirectory.js';
|
|
61
67
|
import { warmSharedGlyphs } from './text/glyphs.js';
|
|
@@ -292,6 +298,10 @@ export {
|
|
|
292
298
|
// would rather draw its shape small in the first place (issue #338)
|
|
293
299
|
DEFAULT_SHADOW_POLICY,
|
|
294
300
|
blurCoverage,
|
|
301
|
+
planShadowTiles,
|
|
302
|
+
rasterShadowTile,
|
|
303
|
+
shadowTileAlpha,
|
|
304
|
+
tileScale,
|
|
295
305
|
blurScale,
|
|
296
306
|
gaussianKernel1d,
|
|
297
307
|
shadowReach,
|
package/lib/path.js
CHANGED
|
@@ -469,7 +469,7 @@ export function parseSvgPath(d) {
|
|
|
469
469
|
// --------------------------------------------------------------------------
|
|
470
470
|
// Path2D
|
|
471
471
|
|
|
472
|
-
function normalizeRadii(w, h, radii) {
|
|
472
|
+
export function normalizeRadii(w, h, radii) {
|
|
473
473
|
if (radii === undefined) radii = 0;
|
|
474
474
|
if (typeof radii === 'number') radii = [radii];
|
|
475
475
|
const take = (v) => (typeof v === 'number' ? { x: v, y: v } : { x: v.x ?? 0, y: v.y ?? 0 });
|
|
@@ -535,6 +535,12 @@ export class Path2D {
|
|
|
535
535
|
// box the fast path can never take (e.g. under rotation).
|
|
536
536
|
this._roundRect = null;
|
|
537
537
|
this._roundRectMiss = null;
|
|
538
|
+
// While the path is nothing but rect()s and roundRect()s, the 2d context
|
|
539
|
+
// keeps them here in device space — `{ x0, y0, x1, y1, corners }` each —
|
|
540
|
+
// so that a shadowed fill of one, or of one inside another filled
|
|
541
|
+
// evenodd, can be drawn from a shadow tile (lib/shadowtiles.js). Null
|
|
542
|
+
// once the path is anything else; any other path verb clears it.
|
|
543
|
+
this._shapes = null;
|
|
538
544
|
if (init instanceof Path2D) {
|
|
539
545
|
this._cmds = init._cmds.map((c) => ({ ...c }));
|
|
540
546
|
this._x = init._x;
|
|
@@ -542,6 +548,7 @@ export class Path2D {
|
|
|
542
548
|
this._sx = init._sx;
|
|
543
549
|
this._sy = init._sy;
|
|
544
550
|
this._roundRect = init._roundRect;
|
|
551
|
+
this._shapes = init._shapes && [...init._shapes];
|
|
545
552
|
} else if (typeof init === 'string') {
|
|
546
553
|
this._append(parseSvgPath(init));
|
|
547
554
|
}
|
|
@@ -567,6 +574,7 @@ export class Path2D {
|
|
|
567
574
|
// one roundRect() loses the tag (roundRect itself re-sets it at the end)
|
|
568
575
|
this._roundRect = null;
|
|
569
576
|
this._roundRectMiss = null;
|
|
577
|
+
this._shapes = null;
|
|
570
578
|
for (const c of cmds) {
|
|
571
579
|
this._cmds.push(c);
|
|
572
580
|
this._track(c);
|
|
@@ -46,6 +46,11 @@ import {
|
|
|
46
46
|
shadowSigma,
|
|
47
47
|
} from "./shadow.js";
|
|
48
48
|
import { Surface } from "./surface.js";
|
|
49
|
+
import {
|
|
50
|
+
normalizeRadii,
|
|
51
|
+
planShadowTiles,
|
|
52
|
+
shadowTileAlpha,
|
|
53
|
+
} from "./shadowtiles.js";
|
|
49
54
|
import {
|
|
50
55
|
BL,
|
|
51
56
|
BR,
|
|
@@ -466,6 +471,23 @@ function parseColor(value) {
|
|
|
466
471
|
* rectangle's size unsigned and its corner in 16 bits, so a negative size
|
|
467
472
|
* that reached the wire threw from inside the paint.
|
|
468
473
|
*/
|
|
474
|
+
const IDENTITY_TRANSFORM = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
475
|
+
|
|
476
|
+
/** whether device box `b` (`{ x0, y0, x1, y1 }`) lies inside box `a` */
|
|
477
|
+
function boxInside(b, a) {
|
|
478
|
+
return b.x0 >= a.x0 && b.y0 >= a.y0 && b.x1 <= a.x1 && b.y1 <= a.y1;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** whether two `{ x, y, width, height }` boxes share any area — either
|
|
482
|
+
* may say `w`/`h` for its size, as a clip rect does */
|
|
483
|
+
function boxesMeet(a, b) {
|
|
484
|
+
const aw = a.width ?? a.w;
|
|
485
|
+
const ah = a.height ?? a.h;
|
|
486
|
+
const bw = b.width ?? b.w;
|
|
487
|
+
const bh = b.height ?? b.h;
|
|
488
|
+
return a.x < b.x + bw && b.x < a.x + aw && a.y < b.y + bh && b.y < a.y + ah;
|
|
489
|
+
}
|
|
490
|
+
|
|
469
491
|
function finiteRect(x, y, w, h) {
|
|
470
492
|
return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(w) && Number.isFinite(h);
|
|
471
493
|
}
|
|
@@ -1586,6 +1608,12 @@ class RenderingContext2d {
|
|
|
1586
1608
|
this._shadowOfPolys(polys, { stroke: true });
|
|
1587
1609
|
return;
|
|
1588
1610
|
}
|
|
1611
|
+
if (
|
|
1612
|
+
!(args[0] instanceof Path2D) &&
|
|
1613
|
+
this._tiledShadow(this._path._shapes, args[0] === "evenodd")
|
|
1614
|
+
) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1589
1617
|
const { polys, rule } = this._polysFor(args);
|
|
1590
1618
|
this._shadowOfPolys(polys, { rule });
|
|
1591
1619
|
}
|
|
@@ -1631,11 +1659,141 @@ class RenderingContext2d {
|
|
|
1631
1659
|
|
|
1632
1660
|
/** the shadow of an axis-aligned rectangle in user space */
|
|
1633
1661
|
_shadowOfRect(x, y, w, h, stroke) {
|
|
1662
|
+
if (!stroke && this._tiledShadow(this._withShape([], x, y, w, h, 0), false)) {
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1634
1665
|
const tmp = new Path2D();
|
|
1635
1666
|
tmp.rect(x, y, w, h);
|
|
1636
1667
|
this._shadowOfPolys(flattenPath(tmp._cmds, this._m), { stroke });
|
|
1637
1668
|
}
|
|
1638
1669
|
|
|
1670
|
+
/**
|
|
1671
|
+
* The shadow of a fill whose path is `shapes` — one rect or rounded rect,
|
|
1672
|
+
* or, filled evenodd, one inside another: the frame an inset box shadow
|
|
1673
|
+
* is cast around — drawn from a tile (lib/shadowtiles.js). A tile has a
|
|
1674
|
+
* short name where a path has none, so it is made once and kept with the
|
|
1675
|
+
* other shadow coverage (`cachedShadow`), and a repaint of any part of
|
|
1676
|
+
* the shadow composites that part of it. False for anything else, and
|
|
1677
|
+
* for a shadow a tile cannot draw; the caller then blurs it as before.
|
|
1678
|
+
*/
|
|
1679
|
+
_tiledShadow(shapes, evenodd) {
|
|
1680
|
+
if (!shapes?.length || !(this._shadowBlur > 0)) return false;
|
|
1681
|
+
if (!shadowPolicyOf(this.window.app).tiles) return false;
|
|
1682
|
+
if (shapes.length === 1) return this._shadowFromTile(shapes[0], null);
|
|
1683
|
+
if (shapes.length !== 2 || !evenodd) return false;
|
|
1684
|
+
const [p, q] = shapes;
|
|
1685
|
+
if (boxInside(q, p)) return this._shadowFromTile(p, q);
|
|
1686
|
+
if (boxInside(p, q)) return this._shadowFromTile(q, p);
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
_shadowFromTile(outer, inner) {
|
|
1691
|
+
const ox = Math.round(this._shadowOffsetX);
|
|
1692
|
+
const oy = Math.round(this._shadowOffsetY);
|
|
1693
|
+
const snap = (r) => ({
|
|
1694
|
+
x0: Math.round(r.x0) + ox,
|
|
1695
|
+
y0: Math.round(r.y0) + oy,
|
|
1696
|
+
x1: Math.round(r.x1) + ox,
|
|
1697
|
+
y1: Math.round(r.y1) + oy,
|
|
1698
|
+
corners: r.corners,
|
|
1699
|
+
});
|
|
1700
|
+
const o = snap(outer);
|
|
1701
|
+
// a shape with nothing in it casts nothing
|
|
1702
|
+
if (!(o.x1 > o.x0 && o.y1 > o.y0)) return true;
|
|
1703
|
+
let hole = inner && snap(inner);
|
|
1704
|
+
if (hole && !(hole.x1 > hole.x0 && hole.y1 > hole.y0)) hole = null;
|
|
1705
|
+
const plan = planShadowTiles(o, hole, this._shadowBlur);
|
|
1706
|
+
if (!plan) return false;
|
|
1707
|
+
const { x, y, width, height } = plan.bounds;
|
|
1708
|
+
// a composite's corner goes over in 16 bits: a shadow that reaches past
|
|
1709
|
+
// them is drawn the old way, whose box is clamped to what can show
|
|
1710
|
+
if (x < -WIRE_REACH || y < -WIRE_REACH) return false;
|
|
1711
|
+
if (x + width > WIRE_REACH || y + height > WIRE_REACH) return false;
|
|
1712
|
+
if (!(x < this.width && y < this.height && x + width > 0 && y + height > 0)) {
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
const clip = this._clipRect();
|
|
1716
|
+
if (clip && !boxesMeet(plan.bounds, clip)) return true;
|
|
1717
|
+
const app = this.window.app;
|
|
1718
|
+
const tile = cachedShadow(app, `tile|${plan.key}`, () =>
|
|
1719
|
+
this._shadowTile(plan),
|
|
1720
|
+
);
|
|
1721
|
+
if (!tile) return false;
|
|
1722
|
+
const picture = tile.picture(app);
|
|
1723
|
+
const style = this._fillStyle;
|
|
1724
|
+
const background = this._backgroundPicture;
|
|
1725
|
+
this.fillStyle = this._shadowColor;
|
|
1726
|
+
try {
|
|
1727
|
+
const op = this._op();
|
|
1728
|
+
for (const [sx, sy, sw, sh, dx, dy, dw, dh] of plan.pieces) {
|
|
1729
|
+
if (clip && !boxesMeet({ x: dx, y: dy, width: dw, height: dh }, clip)) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
// a piece drawn at its own size reads the tile as it is: undo the
|
|
1733
|
+
// transform the last stretched one left on the picture
|
|
1734
|
+
if (sw === dw && sh === dh && picture._tileScaled) {
|
|
1735
|
+
this.Render.SetPictureTransform(picture.id, IDENTITY_TRANSFORM);
|
|
1736
|
+
picture.setFilter("nearest");
|
|
1737
|
+
picture._tileScaled = false;
|
|
1738
|
+
} else if (sw !== dw || sh !== dh) {
|
|
1739
|
+
picture._tileScaled = true;
|
|
1740
|
+
}
|
|
1741
|
+
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
1742
|
+
}
|
|
1743
|
+
} finally {
|
|
1744
|
+
this._fillStyle = style;
|
|
1745
|
+
this._backgroundPicture = background;
|
|
1746
|
+
}
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/** A tile's coverage on an `a8` surface: made here, uploaded once. */
|
|
1751
|
+
_shadowTile(plan) {
|
|
1752
|
+
const app = this.window.app;
|
|
1753
|
+
const { width, height } = plan;
|
|
1754
|
+
const surface = new Surface(app, { width, height, format: "a8" });
|
|
1755
|
+
const alpha = shadowTileAlpha(plan);
|
|
1756
|
+
// PutImage wants each row padded to 4 bytes
|
|
1757
|
+
const stride = (width + 3) & ~3;
|
|
1758
|
+
let data;
|
|
1759
|
+
if (stride === width) {
|
|
1760
|
+
data = Buffer.from(alpha.buffer, alpha.byteOffset, alpha.length);
|
|
1761
|
+
} else {
|
|
1762
|
+
data = Buffer.alloc(stride * height);
|
|
1763
|
+
for (let y = 0; y < height; y++) {
|
|
1764
|
+
Buffer.from(alpha.buffer, alpha.byteOffset + y * width, width).copy(
|
|
1765
|
+
data,
|
|
1766
|
+
y * stride,
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const X = this.X;
|
|
1771
|
+
// one upload GC per app for depth 8, as image.js keeps one for 32
|
|
1772
|
+
let gc = app._coverageUploadGC;
|
|
1773
|
+
if (!gc) {
|
|
1774
|
+
gc = app._coverageUploadGC = X.AllocID();
|
|
1775
|
+
X.CreateGC(gc, surface.pixmap.id);
|
|
1776
|
+
}
|
|
1777
|
+
const maxBytes = ((app.display.max_request_length ?? 65535) - 8) * 4;
|
|
1778
|
+
const rowsPerBand = Math.max(1, Math.floor(maxBytes / stride));
|
|
1779
|
+
for (let y = 0; y < height; y += rowsPerBand) {
|
|
1780
|
+
const rows = Math.min(rowsPerBand, height - y);
|
|
1781
|
+
X.PutImage(
|
|
1782
|
+
2,
|
|
1783
|
+
surface.pixmap.id,
|
|
1784
|
+
gc,
|
|
1785
|
+
width,
|
|
1786
|
+
rows,
|
|
1787
|
+
0,
|
|
1788
|
+
y,
|
|
1789
|
+
0,
|
|
1790
|
+
8,
|
|
1791
|
+
data.subarray(y * stride, (y + rows) * stride),
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
return surface;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1639
1797
|
// ------------------------------------------------------------------
|
|
1640
1798
|
// transform
|
|
1641
1799
|
|
|
@@ -1764,17 +1922,69 @@ class RenderingContext2d {
|
|
|
1764
1922
|
this._path._append(transformCommands(tmp._cmds.slice(1), this._m));
|
|
1765
1923
|
}
|
|
1766
1924
|
|
|
1925
|
+
/** The shapes the default path is so far — none for an empty one, null
|
|
1926
|
+
* once it is anything but rects and rounded rects (Path2D `_shapes`). */
|
|
1927
|
+
_shapesSoFar() {
|
|
1928
|
+
return this._path._cmds.length === 0 ? [] : this._path._shapes;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* `shapes` and one more, in device space: a rect, or a rounded rect with
|
|
1933
|
+
* `radii` as `roundRect` takes them. Null where the list already was, or
|
|
1934
|
+
* where the transform rotates, skews or mirrors — a tile is drawn upright
|
|
1935
|
+
* — or the box is not a finite one.
|
|
1936
|
+
*/
|
|
1937
|
+
_withShape(shapes, x, y, w, h, radii) {
|
|
1938
|
+
if (!shapes || !finiteRect(x, y, w, h)) return null;
|
|
1939
|
+
const [a, b, c, d, e, f] = this._m;
|
|
1940
|
+
if (b !== 0 || c !== 0 || !(a > 0) || !(d > 0)) return null;
|
|
1941
|
+
if (w < 0) {
|
|
1942
|
+
x += w;
|
|
1943
|
+
w = -w;
|
|
1944
|
+
}
|
|
1945
|
+
if (h < 0) {
|
|
1946
|
+
y += h;
|
|
1947
|
+
h = -h;
|
|
1948
|
+
}
|
|
1949
|
+
const corners = normalizeRadii(w, h, radii).map((r) => ({
|
|
1950
|
+
x: r.x * a,
|
|
1951
|
+
y: r.y * d,
|
|
1952
|
+
}));
|
|
1953
|
+
return [
|
|
1954
|
+
...shapes,
|
|
1955
|
+
{
|
|
1956
|
+
x0: a * x + e,
|
|
1957
|
+
y0: d * y + f,
|
|
1958
|
+
x1: a * (x + w) + e,
|
|
1959
|
+
y1: d * (y + h) + f,
|
|
1960
|
+
corners,
|
|
1961
|
+
},
|
|
1962
|
+
];
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1767
1965
|
rect(x, y, w, h) {
|
|
1966
|
+
const shapes = this._shapesSoFar();
|
|
1768
1967
|
const tmp = new Path2D();
|
|
1769
1968
|
tmp.rect(x, y, w, h);
|
|
1770
1969
|
this._path.addPath(tmp, this._m);
|
|
1970
|
+
this._path._shapes = this._withShape(shapes, x, y, w, h, 0);
|
|
1771
1971
|
}
|
|
1772
1972
|
|
|
1773
1973
|
roundRect(x, y, w, h, radii) {
|
|
1974
|
+
const shapes = this._shapesSoFar();
|
|
1774
1975
|
const tmp = new Path2D();
|
|
1775
1976
|
tmp.roundRect(x, y, w, h, radii);
|
|
1776
1977
|
const wasEmpty = this._path._cmds.length === 0;
|
|
1777
1978
|
this._path.addPath(tmp, this._m);
|
|
1979
|
+
// Path2D draws a box of negative extent as a plain rect
|
|
1980
|
+
this._path._shapes = this._withShape(
|
|
1981
|
+
shapes,
|
|
1982
|
+
x,
|
|
1983
|
+
y,
|
|
1984
|
+
w,
|
|
1985
|
+
h,
|
|
1986
|
+
w < 0 || h < 0 ? 0 : radii,
|
|
1987
|
+
);
|
|
1778
1988
|
// Re-derive the recognition tag in device space (addPath cleared it): it
|
|
1779
1989
|
// survives only when this roundRect is the whole path and the CTM is a
|
|
1780
1990
|
// pure translation at record time — the default path bakes the transform
|
package/lib/shadowmath.js
CHANGED
|
@@ -36,13 +36,19 @@
|
|
|
36
36
|
* `maxSigma`-wide blur cannot resample its way down to a thumbnail.
|
|
37
37
|
* `maxScale: 1` blurs everything at full resolution, which is what 8.6
|
|
38
38
|
* did, and is the setting for a caller that needs the exact kernel.
|
|
39
|
+
* - `tiles` — whether a shadowed fill of a rect, a rounded rect, or a rect
|
|
40
|
+
* less a rounded rect filled evenodd is drawn from a shadow tile made once
|
|
41
|
+
* (lib/shadowtiles.js) rather than blurred on every fill. Tiles are not
|
|
42
|
+
* capped at `maxSigma`: what one costs is bounded by the scale it is made
|
|
43
|
+
* at. `false` puts every shadow back on the path above.
|
|
39
44
|
*/
|
|
40
45
|
export const DEFAULT_SHADOW_POLICY = {
|
|
41
46
|
cacheBytes: 4 << 20,
|
|
42
47
|
maxSigma: 32,
|
|
43
48
|
maxPixels: 8 << 20,
|
|
44
49
|
scaleSigma: 4,
|
|
45
|
-
maxScale: 4
|
|
50
|
+
maxScale: 4,
|
|
51
|
+
tiles: true
|
|
46
52
|
};
|
|
47
53
|
|
|
48
54
|
/** the policy for one app, merged over the defaults */
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// The blurred shadow of a rectangle, a rounded rectangle, or a rectangle
|
|
2
|
+
// with a rounded hole in it, drawn from a tile made once instead of blurred
|
|
3
|
+
// on every fill. No X connection in here: the plan and the pixels, which
|
|
4
|
+
// the 2d context composites (renderingcontext_2d.js) and react-x11's
|
|
5
|
+
// CoreGraphics and Direct2D contexts composite the same way. Exported as
|
|
6
|
+
// `ntk/shadow-tiles`.
|
|
7
|
+
//
|
|
8
|
+
// A gaussian over a shape costs its area times its width, and a shadowed
|
|
9
|
+
// fill paid it on every fill — on XQuartz's software RENDER, a header's
|
|
10
|
+
// `box-shadow: inset 0 0 100px` repainted by a scroll's exposed strip was
|
|
11
|
+
// the difference between 51 and 31 frames a second, and CoreGraphics spent
|
|
12
|
+
// 130 to 380 ms on one at 2x. None of it is a fact about where the shape
|
|
13
|
+
// is or how long its sides are: a rounded rectangle's shadow is the same
|
|
14
|
+
// all along its straight edges, so it is enough to make it once for a
|
|
15
|
+
// rectangle only just long enough to have a straight part, and to stretch
|
|
16
|
+
// that part to the length asked for.
|
|
17
|
+
//
|
|
18
|
+
// So a tile holds the shadow of a shape's *corners* — the shape shortened,
|
|
19
|
+
// on each axis that has room, to three pixels of straight edge — and is
|
|
20
|
+
// drawn as up to nine pieces: the corners as they are, the straight pixel
|
|
21
|
+
// between them stretched. Three rather than one because a filtered stretch
|
|
22
|
+
// reads a pixel either side of the one it stretches, and a piece's
|
|
23
|
+
// neighbours in the tile are straight edge too. The tile is named by the
|
|
24
|
+
// corners, the blur and whether there is a hole — never by where the shape
|
|
25
|
+
// is, how big it is past its corners, or what part of it a paint reaches —
|
|
26
|
+
// so every box in a document with the same shadow shares one, and a scroll
|
|
27
|
+
// that exposes a strip of one draws a strip of it.
|
|
28
|
+
//
|
|
29
|
+
// The pixels are made here, not by blurring on a server or a GPU: the
|
|
30
|
+
// shape's coverage, row by row, then a gaussian in two passes, at the
|
|
31
|
+
// canvas σ — half the blur (HTML, "shadows"; CSS Backgrounds 3, 7.1.1) —
|
|
32
|
+
// with no `maxSigma` cap, since a wide blur's cost here is bounded by the
|
|
33
|
+
// scale it is made at. A gaussian carries nothing finer than about σ/2, so
|
|
34
|
+
// past σ 8 the tile is made at a half, a quarter, … of the size and scaled
|
|
35
|
+
// up as it is drawn, as `blurCoverage` does: a 400-pixel blur's tile is a
|
|
36
|
+
// hundred kilobytes rather than twenty megabytes.
|
|
37
|
+
|
|
38
|
+
import { normalizeRadii } from './path.js';
|
|
39
|
+
import { gaussianKernel1d } from './shadowmath.js';
|
|
40
|
+
|
|
41
|
+
export { normalizeRadii };
|
|
42
|
+
|
|
43
|
+
/** The σ, in tile pixels, below which a tile is not made any smaller —
|
|
44
|
+
* the policy's `scaleSigma`: at 4 the scaled blur stays within a few
|
|
45
|
+
* levels of 8-bit alpha of the exact one. */
|
|
46
|
+
const MIN_TILE_SIGMA = 4;
|
|
47
|
+
|
|
48
|
+
/** How far a tile may be scaled down, whatever the blur. */
|
|
49
|
+
const MAX_TILE_SCALE = 16;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The most pixels one tile may have. A shape that is all corner on both
|
|
53
|
+
* axes — an ellipse spelled as a `roundRect` — has nothing to stretch, and
|
|
54
|
+
* its tile is the size of the shape; past this it is not made, and the
|
|
55
|
+
* shadow is drawn as it was before tiles.
|
|
56
|
+
*/
|
|
57
|
+
export const MAX_TILE_PIXELS = 1 << 20;
|
|
58
|
+
|
|
59
|
+
/** Sub-rows each tile row's coverage is sampled on; the blur smooths the
|
|
60
|
+
* rest. Along a row coverage is exact. */
|
|
61
|
+
const SUBROWS = 8;
|
|
62
|
+
|
|
63
|
+
/** How much smaller than the shape its tile is made, for a blur. */
|
|
64
|
+
export function tileScale(blur) {
|
|
65
|
+
const sigma = blur / 2;
|
|
66
|
+
let k = 1;
|
|
67
|
+
while (k * 2 <= MAX_TILE_SCALE && sigma / (k * 2) >= MIN_TILE_SIGMA) k *= 2;
|
|
68
|
+
return k;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One axis of a tile: which of its pixels are drawn where. With room for a
|
|
73
|
+
* straight part, three segments — the features before it, at scale; one
|
|
74
|
+
* tile pixel of it, stretched; the features after it, at scale — and the
|
|
75
|
+
* amount the tile's shape is shorter than the real one. Without, the whole
|
|
76
|
+
* axis at scale.
|
|
77
|
+
*
|
|
78
|
+
* `lo`/`hi` are the outer shape's edges and `nearLo`/`nearHi` how far in
|
|
79
|
+
* from each it stops being straight — the outer corners, and where there is
|
|
80
|
+
* a hole its edge and corners — in whole device pixels; `reach` is how far
|
|
81
|
+
* the blur spreads, a whole number of tile pixels of `k` device pixels.
|
|
82
|
+
*/
|
|
83
|
+
function planAxis(lo, hi, nearLo, nearHi, reach, k) {
|
|
84
|
+
const origin = lo - reach;
|
|
85
|
+
const flatFrom = lo + nearLo + reach;
|
|
86
|
+
const flatTo = hi - nearHi - reach;
|
|
87
|
+
if (flatTo - flatFrom >= 4 * k) {
|
|
88
|
+
// the stretched pixel `at`, with a straight pixel either side of it
|
|
89
|
+
const at = Math.ceil((flatFrom - origin) / k) + 1;
|
|
90
|
+
const cut = flatTo - origin - (at + 2) * k;
|
|
91
|
+
const size = Math.ceil((hi + reach - cut - origin) / k);
|
|
92
|
+
return {
|
|
93
|
+
origin,
|
|
94
|
+
size,
|
|
95
|
+
cut,
|
|
96
|
+
segments: [
|
|
97
|
+
[0, at, origin, origin + at * k],
|
|
98
|
+
[at, at + 1, origin + at * k, origin + (at + 1) * k + cut],
|
|
99
|
+
[at + 1, size, origin + (at + 1) * k + cut, origin + size * k + cut],
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const size = Math.ceil((hi - lo + 2 * reach) / k);
|
|
104
|
+
return {
|
|
105
|
+
origin,
|
|
106
|
+
size,
|
|
107
|
+
cut: 0,
|
|
108
|
+
segments: [[0, size, origin, origin + size * k]],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A number as part of a key, to the 256th of a pixel. */
|
|
113
|
+
const q = (v) => Math.round(v * 256) / 256;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Where a shadow's tile comes from and how it is drawn, or null where it
|
|
117
|
+
* would be too big to keep. `outer` and `inner` are
|
|
118
|
+
* `{ x0, y0, x1, y1, corners }` in whole device pixels, `corners` the four
|
|
119
|
+
* `{ x, y }` radii from top left, clockwise (`normalizeRadii`); `inner`,
|
|
120
|
+
* when there is one, lies inside `outer`, and the shadow is the one the
|
|
121
|
+
* frame between them casts — an inset box shadow's. The shadow's offset is
|
|
122
|
+
* already in both, and `blur` is the canvas `shadowBlur`, in device pixels.
|
|
123
|
+
*
|
|
124
|
+
* `pieces` are `[sx, sy, sw, sh, dx, dy, dw, dh]` — tile pixels in, device
|
|
125
|
+
* pixels out — and cover the shadow's whole reach once, except a frame's
|
|
126
|
+
* middle, which lies inside the hole by more than the blur reaches and has
|
|
127
|
+
* nothing in it. `key` names the tile's pixels: two plans with one key
|
|
128
|
+
* draw from one tile.
|
|
129
|
+
*/
|
|
130
|
+
export function planShadowTiles(outer, inner, blur) {
|
|
131
|
+
const k = tileScale(blur);
|
|
132
|
+
const sigma = blur / 2 / k;
|
|
133
|
+
const reachK = Math.ceil(3 * sigma);
|
|
134
|
+
const reach = reachK * k;
|
|
135
|
+
const oc = outer.corners;
|
|
136
|
+
let nearL = Math.max(oc[0].x, oc[3].x);
|
|
137
|
+
let nearR = Math.max(oc[1].x, oc[2].x);
|
|
138
|
+
let nearT = Math.max(oc[0].y, oc[1].y);
|
|
139
|
+
let nearB = Math.max(oc[3].y, oc[2].y);
|
|
140
|
+
if (inner) {
|
|
141
|
+
const ic = inner.corners;
|
|
142
|
+
nearL = Math.max(nearL, inner.x0 - outer.x0 + Math.max(ic[0].x, ic[3].x));
|
|
143
|
+
nearR = Math.max(nearR, outer.x1 - inner.x1 + Math.max(ic[1].x, ic[2].x));
|
|
144
|
+
nearT = Math.max(nearT, inner.y0 - outer.y0 + Math.max(ic[0].y, ic[1].y));
|
|
145
|
+
nearB = Math.max(nearB, outer.y1 - inner.y1 + Math.max(ic[3].y, ic[2].y));
|
|
146
|
+
}
|
|
147
|
+
const ax = planAxis(outer.x0, outer.x1, nearL, nearR, reach, k);
|
|
148
|
+
const ay = planAxis(outer.y0, outer.y1, nearT, nearB, reach, k);
|
|
149
|
+
if (ax.size * ay.size > MAX_TILE_PIXELS) return null;
|
|
150
|
+
|
|
151
|
+
// the tile's own shape, in tile pixels: the real one shortened by the
|
|
152
|
+
// cut on each stretched axis — every edge past the cut moves back by it
|
|
153
|
+
const toTile = (r) => ({
|
|
154
|
+
x0: (r.x0 - ax.origin) / k,
|
|
155
|
+
y0: (r.y0 - ay.origin) / k,
|
|
156
|
+
x1: (r.x1 - ax.cut - ax.origin) / k,
|
|
157
|
+
y1: (r.y1 - ay.cut - ay.origin) / k,
|
|
158
|
+
corners: r.corners.map((c) => ({ x: c.x / k, y: c.y / k })),
|
|
159
|
+
});
|
|
160
|
+
const shape = { outer: toTile(outer), inner: inner ? toTile(inner) : null };
|
|
161
|
+
|
|
162
|
+
const middle = (axis, i) => axis.segments.length === 3 && i === 1;
|
|
163
|
+
const pieces = [];
|
|
164
|
+
ay.segments.forEach(([ty0, ty1, dy0, dy1], j) => {
|
|
165
|
+
ax.segments.forEach(([tx0, tx1, dx0, dx1], i) => {
|
|
166
|
+
if (inner && middle(ax, i) && middle(ay, j)) return;
|
|
167
|
+
if (!(dx1 > dx0 && dy1 > dy0)) return;
|
|
168
|
+
pieces.push([tx0, ty0, tx1 - tx0, ty1 - ty0, dx0, dy0, dx1 - dx0, dy1 - dy0]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const geometry = (r) =>
|
|
173
|
+
r
|
|
174
|
+
? [r.x0, r.y0, r.x1, r.y1, ...r.corners.flatMap((c) => [c.x, c.y])].map(q).join(',')
|
|
175
|
+
: '';
|
|
176
|
+
return {
|
|
177
|
+
key: `${blur}|${ax.size}x${ay.size}|${geometry(shape.outer)}|${geometry(shape.inner)}`,
|
|
178
|
+
k,
|
|
179
|
+
sigma,
|
|
180
|
+
reach: reachK,
|
|
181
|
+
width: ax.size,
|
|
182
|
+
height: ay.size,
|
|
183
|
+
shape,
|
|
184
|
+
pieces,
|
|
185
|
+
bounds: {
|
|
186
|
+
x: ax.origin,
|
|
187
|
+
y: ay.origin,
|
|
188
|
+
width: ax.segments.at(-1)[3] - ax.origin,
|
|
189
|
+
height: ay.segments.at(-1)[3] - ay.origin,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The x extent of a rounded rectangle along the line `y`, or null where
|
|
195
|
+
* the line misses it. A corner is rounded only with both radii. */
|
|
196
|
+
function spanAt(r, y) {
|
|
197
|
+
if (!(y >= r.y0 && y < r.y1)) return null;
|
|
198
|
+
const [tl, tr, br, bl] = r.corners;
|
|
199
|
+
// how far a corner's ellipse reaches across, `d` above or below its
|
|
200
|
+
// centre, as a fraction of its horizontal radius
|
|
201
|
+
const across = (d, ry) => {
|
|
202
|
+
const t = d / ry;
|
|
203
|
+
return Math.sqrt(Math.max(0, 1 - t * t));
|
|
204
|
+
};
|
|
205
|
+
let left = r.x0;
|
|
206
|
+
let right = r.x1;
|
|
207
|
+
if (tl.x > 0 && tl.y > 0 && y < r.y0 + tl.y) {
|
|
208
|
+
left = r.x0 + tl.x * (1 - across(r.y0 + tl.y - y, tl.y));
|
|
209
|
+
} else if (bl.x > 0 && bl.y > 0 && y > r.y1 - bl.y) {
|
|
210
|
+
left = r.x0 + bl.x * (1 - across(y - (r.y1 - bl.y), bl.y));
|
|
211
|
+
}
|
|
212
|
+
if (tr.x > 0 && tr.y > 0 && y < r.y0 + tr.y) {
|
|
213
|
+
right = r.x1 - tr.x * (1 - across(r.y0 + tr.y - y, tr.y));
|
|
214
|
+
} else if (br.x > 0 && br.y > 0 && y > r.y1 - br.y) {
|
|
215
|
+
right = r.x1 - br.x * (1 - across(y - (r.y1 - br.y), br.y));
|
|
216
|
+
}
|
|
217
|
+
return right > left ? [left, right] : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Add `weight` times the coverage of the span `[a, b)` to a row. */
|
|
221
|
+
function addSpan(row, width, a, b, weight) {
|
|
222
|
+
const from = Math.max(0, a);
|
|
223
|
+
const to = Math.min(width, b);
|
|
224
|
+
if (!(to > from)) return;
|
|
225
|
+
const i0 = Math.floor(from);
|
|
226
|
+
const i1 = Math.ceil(to) - 1;
|
|
227
|
+
if (i0 === i1) {
|
|
228
|
+
row[i0] += (to - from) * weight;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
row[i0] += (i0 + 1 - from) * weight;
|
|
232
|
+
for (let i = i0 + 1; i < i1; i++) row[i] += weight;
|
|
233
|
+
row[i1] += (to - i1) * weight;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The tile's coverage before the blur: how much of each tile pixel the
|
|
238
|
+
* shape covers — the outer shape less the hole — 0..1, row-major.
|
|
239
|
+
*/
|
|
240
|
+
export function shadowCoverage(plan) {
|
|
241
|
+
const { width, height, shape } = plan;
|
|
242
|
+
const out = new Float32Array(width * height);
|
|
243
|
+
const weight = 1 / SUBROWS;
|
|
244
|
+
for (let y = 0; y < height; y++) {
|
|
245
|
+
const row = out.subarray(y * width, (y + 1) * width);
|
|
246
|
+
for (let s = 0; s < SUBROWS; s++) {
|
|
247
|
+
const at = y + (s + 0.5) / SUBROWS;
|
|
248
|
+
const span = spanAt(shape.outer, at);
|
|
249
|
+
if (!span) continue;
|
|
250
|
+
addSpan(row, width, span[0], span[1], weight);
|
|
251
|
+
const hole = shape.inner && spanAt(shape.inner, at);
|
|
252
|
+
if (hole) addSpan(row, width, hole[0], hole[1], -weight);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A gaussian of the tile's σ over `values`, rows then columns, in place.
|
|
259
|
+
* Outside the tile is nothing, which its padding of `reach` makes true. */
|
|
260
|
+
function blurTile(values, width, height, sigma, reach) {
|
|
261
|
+
const kernel = gaussianKernel1d(sigma, reach);
|
|
262
|
+
const line = new Float32Array(Math.max(width, height));
|
|
263
|
+
for (let y = 0; y < height; y++) {
|
|
264
|
+
const base = y * width;
|
|
265
|
+
for (let x = 0; x < width; x++) line[x] = values[base + x];
|
|
266
|
+
for (let x = 0; x < width; x++) {
|
|
267
|
+
let sum = 0;
|
|
268
|
+
const from = Math.max(0, x - reach);
|
|
269
|
+
const to = Math.min(width - 1, x + reach);
|
|
270
|
+
for (let t = from; t <= to; t++) sum += line[t] * kernel[t - x + reach];
|
|
271
|
+
values[base + x] = sum;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (let x = 0; x < width; x++) {
|
|
275
|
+
for (let y = 0; y < height; y++) line[y] = values[y * width + x];
|
|
276
|
+
for (let y = 0; y < height; y++) {
|
|
277
|
+
let sum = 0;
|
|
278
|
+
const from = Math.max(0, y - reach);
|
|
279
|
+
const to = Math.min(height - 1, y + reach);
|
|
280
|
+
for (let t = from; t <= to; t++) sum += line[t] * kernel[t - y + reach];
|
|
281
|
+
values[y * width + x] = sum;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The tile's blurred coverage, 0..1, row-major. */
|
|
287
|
+
export function shadowTileCoverage(plan) {
|
|
288
|
+
const values = shadowCoverage(plan);
|
|
289
|
+
blurTile(values, plan.width, plan.height, plan.sigma, plan.reach);
|
|
290
|
+
return values;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The tile as an `a8` coverage surface stores it: one byte a pixel,
|
|
294
|
+
* rows unpadded. */
|
|
295
|
+
export function shadowTileAlpha(plan) {
|
|
296
|
+
const values = shadowTileCoverage(plan);
|
|
297
|
+
const out = new Uint8Array(values.length);
|
|
298
|
+
for (let i = 0; i < values.length; i++) {
|
|
299
|
+
const v = values[i];
|
|
300
|
+
out[i] = v > 0 ? Math.round(Math.min(1, v) * 255) : 0;
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The tile in a colour, as straight RGBA — for a surface that has no
|
|
307
|
+
* coverage format to composite through a colour. `rgba` is 0..1 each.
|
|
308
|
+
*/
|
|
309
|
+
export function rasterShadowTile(plan, rgba) {
|
|
310
|
+
const values = shadowTileCoverage(plan);
|
|
311
|
+
const [r, g, b, a] = rgba;
|
|
312
|
+
const R = Math.round(r * 255);
|
|
313
|
+
const G = Math.round(g * 255);
|
|
314
|
+
const B = Math.round(b * 255);
|
|
315
|
+
const out = new Uint8Array(values.length * 4);
|
|
316
|
+
for (let i = 0; i < values.length; i++) {
|
|
317
|
+
const v = values[i];
|
|
318
|
+
if (!(v > 0)) continue;
|
|
319
|
+
const o = i * 4;
|
|
320
|
+
out[o] = R;
|
|
321
|
+
out[o + 1] = G;
|
|
322
|
+
out[o + 2] = B;
|
|
323
|
+
out[o + 3] = Math.round(Math.min(1, v) * a * 255);
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
package/lib/text/layout.js
CHANGED
|
@@ -25,8 +25,9 @@ const UNSPACED =
|
|
|
25
25
|
* 12.8px measures 410.0125px in the 410px column it was written for — and
|
|
26
26
|
* a browser takes the line as fitting, comparing in 64ths of a pixel with
|
|
27
27
|
* that much to spare (Blink's line breaker adds a LayoutUnit's epsilon to
|
|
28
|
-
* the width it fits to). What
|
|
29
|
-
* rounded up to a 64th first
|
|
28
|
+
* the width it fits to). What a browser compares is the line's items,
|
|
29
|
+
* each rounded up to a 64th first, which a layout does with `fit: 'items'`
|
|
30
|
+
* (`LineFit`).
|
|
30
31
|
*/
|
|
31
32
|
const FIT_SLACK = 1 / 64;
|
|
32
33
|
|
|
@@ -39,8 +40,10 @@ function snapUp(width) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
* A line's width as
|
|
43
|
-
*
|
|
43
|
+
* A line's width as it is fitted: its spans' parts of it summed, its
|
|
44
|
+
* trailing white space left out — each part rounded up to a 64th of a pixel
|
|
45
|
+
* first where the layout fits as a browser does (`fit: 'items'`), as it is
|
|
46
|
+
* without, a float sum.
|
|
44
47
|
*
|
|
45
48
|
* A browser measures a line in items — the text of one element between its
|
|
46
49
|
* edges, shaped — and rounds each one's width up to a LayoutUnit before it
|
|
@@ -58,12 +61,13 @@ function snapUp(width) {
|
|
|
58
61
|
* width were the next one put on it.
|
|
59
62
|
*/
|
|
60
63
|
class LineFit {
|
|
61
|
-
constructor() {
|
|
64
|
+
constructor(round) {
|
|
65
|
+
this.round = round;
|
|
62
66
|
this.reset();
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
reset() {
|
|
66
|
-
/** the items closed on the line, each rounded
|
|
70
|
+
/** the items closed on the line, each rounded */
|
|
67
71
|
this.closed = 0;
|
|
68
72
|
/** the width of the item the line ends in, as far as it goes */
|
|
69
73
|
this.open = 0;
|
|
@@ -75,10 +79,10 @@ class LineFit {
|
|
|
75
79
|
for (let i = 0; i < frags.length; i++) {
|
|
76
80
|
const frag = frags[i];
|
|
77
81
|
const width = frag.shaped.width + (frag.kern ?? 0) + (i === 0 ? kern : 0);
|
|
78
|
-
if (this.span === null || this.span
|
|
82
|
+
if (this.span === null || sameItem(this.span, frag.span)) {
|
|
79
83
|
this.open += width;
|
|
80
84
|
} else {
|
|
81
|
-
this.closed +=
|
|
85
|
+
this.closed += this.round(this.open);
|
|
82
86
|
this.open = width;
|
|
83
87
|
}
|
|
84
88
|
this.span = frag.span;
|
|
@@ -92,7 +96,7 @@ class LineFit {
|
|
|
92
96
|
with(token, kern) {
|
|
93
97
|
const { closed, open, span } = this;
|
|
94
98
|
this.add(token, kern);
|
|
95
|
-
const width = this.closed +
|
|
99
|
+
const width = this.closed + this.round(this.open - token.wsWidth);
|
|
96
100
|
this.closed = closed;
|
|
97
101
|
this.open = open;
|
|
98
102
|
this.span = span;
|
|
@@ -100,10 +104,25 @@ class LineFit {
|
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
107
|
+
/** Whether a span goes on measuring the item the one before it is in: the
|
|
108
|
+
* same span, or a space spaced out apart (`kernAcross`) either side. */
|
|
109
|
+
function sameItem(before, span) {
|
|
110
|
+
return before === span || span.kernAcross || before.kernAcross;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** How a layout rounds its items before it adds them (`LineFit`). */
|
|
114
|
+
function itemRounding(options) {
|
|
115
|
+
return options.fit === 'items' ? snapUp : unrounded;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function unrounded(width) {
|
|
119
|
+
return width;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A line's tokens' width as it is fitted (`LineFit`). */
|
|
123
|
+
function fittedWidth(toks, round) {
|
|
105
124
|
if (!toks.length) return 0;
|
|
106
|
-
const fit = new LineFit();
|
|
125
|
+
const fit = new LineFit(round);
|
|
107
126
|
for (let i = 0; i < toks.length - 1; i++) fit.add(toks[i], i ? kernBefore(toks[i]) : 0);
|
|
108
127
|
const last = toks.length - 1;
|
|
109
128
|
return fit.with(toks[last], last ? kernBefore(toks[last]) : 0);
|
|
@@ -162,6 +181,10 @@ function isWsGlyph(g) {
|
|
|
162
181
|
* past the line's end, as CSS's `overflow-wrap: normal` does — but for
|
|
163
182
|
* text in a script written without spaces (Thai, Khmer, Javanese…), whose
|
|
164
183
|
* words the breaker cannot find, which is cut as a fallback
|
|
184
|
+
* - `fit` — 'items' fits and measures a line as a browser does: each span's
|
|
185
|
+
* part of it rounded up to a 64th of a pixel before it is added, as Blink
|
|
186
|
+
* rounds an element's text on a line up to a LayoutUnit; a line's `width`
|
|
187
|
+
* is that sum. Default: the advances as they add up (`LineFit`)
|
|
165
188
|
*
|
|
166
189
|
* The result is inspectable before/without drawing: `width`, `height`,
|
|
167
190
|
* `truncated`, and `lines[] = { x, y, height, baseline, width, ascent,
|
|
@@ -207,9 +230,10 @@ export class TextLayout {
|
|
|
207
230
|
const whole = options.overflowWrap === 'normal';
|
|
208
231
|
// text that does not wrap is a line to each forced break, however wide
|
|
209
232
|
const wraps = options.wrap !== false;
|
|
233
|
+
const round = itemRounding(options);
|
|
210
234
|
{
|
|
211
235
|
let cur = [];
|
|
212
|
-
const fit = new LineFit();
|
|
236
|
+
const fit = new LineFit(round);
|
|
213
237
|
const flush = () => {
|
|
214
238
|
lineTokens.push(cur);
|
|
215
239
|
cur = [];
|
|
@@ -272,7 +296,7 @@ export class TextLayout {
|
|
|
272
296
|
// Elision is the last kept line's business, or a line's that does not
|
|
273
297
|
// wrap and runs past the width, and it happens before entries exist,
|
|
274
298
|
// because dropping content means re-shaping the tail.
|
|
275
|
-
const cut = (elide && li === lineTokens.length - 1) || (cutWide && fittedWidth(toks) > maxWidth + FIT_SLACK);
|
|
299
|
+
const cut = (elide && li === lineTokens.length - 1) || (cutWide && fittedWidth(toks, round) > maxWidth + FIT_SLACK);
|
|
276
300
|
const ellipsis = cut ? this._elide(toks, baseSpan) : null;
|
|
277
301
|
if (ellipsis) {
|
|
278
302
|
toks = this._fitBefore(toks, maxWidth - ellipsis.width);
|
|
@@ -348,13 +372,26 @@ export class TextLayout {
|
|
|
348
372
|
let natural = 0;
|
|
349
373
|
const runs = [];
|
|
350
374
|
let x = 0;
|
|
375
|
+
// and how wide it is as it was fitted (`LineFit`): where its spans'
|
|
376
|
+
// parts are each rounded up to a 64th, so is its width, and a layout
|
|
377
|
+
// laid out again at its own width breaks nowhere new
|
|
378
|
+
let fitted = 0;
|
|
379
|
+
let item = 0;
|
|
380
|
+
let itemSpan = null;
|
|
351
381
|
for (const e of entries) {
|
|
382
|
+
const from = x;
|
|
352
383
|
if (e.kern) x += e.kern;
|
|
353
384
|
const positioned = { x, width: e.run.width, run: e.run, span: e.span, start: e.start, end: e.end };
|
|
354
385
|
if (e.ellipsis) positioned.ellipsis = true;
|
|
355
386
|
runs.push(positioned);
|
|
356
387
|
x += e.run.width;
|
|
357
388
|
if (e.kernAfter) x += e.kernAfter;
|
|
389
|
+
if (itemSpan !== null && !sameItem(itemSpan, e.span)) {
|
|
390
|
+
fitted += round(item);
|
|
391
|
+
item = 0;
|
|
392
|
+
}
|
|
393
|
+
item += x - from;
|
|
394
|
+
itemSpan = e.span;
|
|
358
395
|
const m = e.run.font.metrics(e.run.size);
|
|
359
396
|
if (m.ascent > ascent) ascent = m.ascent;
|
|
360
397
|
if (m.descent > descent) descent = m.descent;
|
|
@@ -367,7 +404,8 @@ export class TextLayout {
|
|
|
367
404
|
descent = m.descent;
|
|
368
405
|
natural = m.lineHeight;
|
|
369
406
|
}
|
|
370
|
-
if (
|
|
407
|
+
if (runs.length) fitted += round(item);
|
|
408
|
+
if (fitted > layoutWidth) layoutWidth = fitted;
|
|
371
409
|
// Half-leading (CSS Inline Layout 3): the slack between the glyphs and
|
|
372
410
|
// the line box is split evenly above and below, rather than all of it
|
|
373
411
|
// landing under the text. This is what makes a single line sit
|
|
@@ -385,7 +423,7 @@ export class TextLayout {
|
|
|
385
423
|
y,
|
|
386
424
|
height: box,
|
|
387
425
|
baseline: y + leading + ascent,
|
|
388
|
-
width:
|
|
426
|
+
width: fitted,
|
|
389
427
|
ascent,
|
|
390
428
|
descent,
|
|
391
429
|
runs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.16.1",
|
|
4
4
|
"description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
|
|
5
5
|
"author": "Andrey Sidorov <sidorares@yandex.ru>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"./path": "./lib/path.js",
|
|
32
32
|
"./gl": "./lib/gl.js",
|
|
33
33
|
"./shadow-math": "./lib/shadowmath.js",
|
|
34
|
+
"./shadow-tiles": "./lib/shadowtiles.js",
|
|
34
35
|
"./image": "./lib/image.js",
|
|
35
36
|
"./svg": "./lib/widgets/svgview.js",
|
|
36
37
|
"./font": "./lib/text/font.js"
|