packages/text/src/lib/bounds.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import type { TextGeometryResult } from '../index.js';
2 import type { PathGeometry } from "@pibbl/core";
3 /**
4 * Axis-aligned bounds of visible glyph outlines in logical output coordinates.
5 *
6 * @see {@link TextGeometryResult}
7 */
8 export interface InkBounds {
9 /**
10 * Horizontal coordinate or displacement in the containing coordinate system. See
11 * {@link InkBounds}.
12 */
13 readonly x: number;
14 /**
15 * Vertical coordinate or displacement in the containing coordinate system. See {@link InkBounds}
16 * .
17 */
18 readonly y: number;
19 /**
20 * Horizontal extent in the units of the containing geometry or surface. See {@link InkBounds}.
21 */
22 readonly width: number;
23 /** Vertical extent in the units of the containing geometry or surface. See {@link InkBounds}. */
24 readonly height: number;
25 }
26 /** Curve extrema, not control-point bounds. One linear traversal; no flattening. */
27 export function inkBounds(path: PathGeometry): Readonly<InkBounds> | null {
28 let minX = Infinity,
29 minY = Infinity,
30 maxX = -Infinity,
31 maxY = -Infinity;
32 let x = 0,
33 y = 0,
34 startX = 0,
35 startY = 0;
36 const include = (a: number, b: number) => {
37 minX = Math.min(minX, a);
38 maxX = Math.max(maxX, a);
39 minY = Math.min(minY, b);
40 maxY = Math.max(maxY, b);
41 };
42 const cubicRoots = (p0: number, p1: number, p2: number, p3: number) => {
43 // Normalize first to keep coefficient arithmetic finite for large coordinates.
44 const scale = Math.max(
45 Math.abs(p0),
46 Math.abs(p1),
47 Math.abs(p2),
48 Math.abs(p3),
49 1,
50 );
51 p0 /= scale;
52 p1 /= scale;
53 p2 /= scale;
54 p3 /= scale;
55 const a = -p0 + 3 * p1 - 3 * p2 + p3;
56 const b = 2 * (p0 - 2 * p1 + p2),
57 c = p1 - p0;
58 if (a === 0) return b === 0 ? [] : [-c / b];
59 const discriminant = b * b - 4 * a * c;
60 if (discriminant < 0) return [];
61 const q = -0.5 * (b + (b < 0 ? -1 : 1) * Math.sqrt(discriminant));
62 return q === 0 ? [-b / (2 * a)] : [q / a, c / q];
63 };
64 const lerp = (a: number, b: number, t: number) => a * (1 - t) + b * t;
65 const quadratic = (a: number, b: number, c: number, t: number) =>
66 lerp(lerp(a, b, t), lerp(b, c, t), t);
67 const cubic = (a: number, b: number, c: number, d: number, t: number) =>
68 lerp(quadratic(a, b, c, t), quadratic(b, c, d, t), t);
69 for (const segment of path) {
70 if (segment.type === "move") {
71 x = startX = segment.x;
72 y = startY = segment.y;
73 continue;
74 }
75 if (segment.type === "close") {
76 if (x !== startX || y !== startY) {
77 include(x, y);
78 include(startX, startY);
79 }
80 x = startX;
81 y = startY;
82 continue;
83 }
84 if (segment.type === "arc")
85 throw new Error("Font outlines cannot contain analytic arc commands.");
86 include(x, y);
87 include(segment.x, segment.y);
88 if (segment.type === "quadratic") {
89 const root = (a: number, b: number, c: number) => {
90 const scale = Math.max(Math.abs(a), Math.abs(b), Math.abs(c), 1);
91 a /= scale;
92 b /= scale;
93 c /= scale;
94 return (a - b) / (a - 2 * b + c);
95 };
96 for (const t of [
97 root(x, segment.cpx, segment.x),
98 root(y, segment.cpy, segment.y),
99 ]) {
100 if (t > 0 && t < 1)
101 include(
102 quadratic(x, segment.cpx, segment.x, t),
103 quadratic(y, segment.cpy, segment.y, t),
104 );
105 }
106 } else if (segment.type === "cubic") {
107 for (const t of [
108 ...cubicRoots(x, segment.cp1x, segment.cp2x, segment.x),
109 ...cubicRoots(y, segment.cp1y, segment.cp2y, segment.y),
110 ]) {
111 if (t > 0 && t < 1)
112 include(
113 cubic(x, segment.cp1x, segment.cp2x, segment.x, t),
114 cubic(y, segment.cp1y, segment.cp2y, segment.y, t),
115 );
116 }
117 }
118 x = segment.x;
119 y = segment.y;
120 }
121 if (minX === Infinity) return null;
122 const result = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
123 if (!Object.values(result).every(Number.isFinite))
124 throw new RangeError("Outline bounds overflow.");
125 return Object.freeze(result);
126 }
127
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.