pa_font 0.2.7 → 0.3.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/USAGE.md +21 -10
- package/dist/paFont.cjs +339 -442
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +339 -442
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +1 -0
- package/package.json +1 -1
package/dist/paFont.js
CHANGED
|
@@ -3,127 +3,52 @@ import { load, parse } from "opentype.js";
|
|
|
3
3
|
function toScreenPoint(point, scale) {
|
|
4
4
|
return [point.x * scale, -point.y * scale];
|
|
5
5
|
}
|
|
6
|
+
function cloneRawPoint(point) {
|
|
7
|
+
return {
|
|
8
|
+
x: point.x,
|
|
9
|
+
y: point.y
|
|
10
|
+
};
|
|
11
|
+
}
|
|
6
12
|
function pushUniquePoint(points, point) {
|
|
7
13
|
if (points.length === 0 || !pointsEqual2D(points[points.length - 1], point)) points.push(point);
|
|
8
14
|
}
|
|
9
|
-
function flattenQuadratic(p0, p1, p2, tolerance, out) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
stack[sp++] = mx;
|
|
47
|
-
stack[sp++] = my;
|
|
48
|
-
stack[sp++] = d1;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function flattenCubic(p0, p1, p2, p3, tolerance, out) {
|
|
52
|
-
let sp = 0;
|
|
53
|
-
const stack = _cubicStack;
|
|
54
|
-
stack[sp++] = p0[0];
|
|
55
|
-
stack[sp++] = p0[1];
|
|
56
|
-
stack[sp++] = p1[0];
|
|
57
|
-
stack[sp++] = p1[1];
|
|
58
|
-
stack[sp++] = p2[0];
|
|
59
|
-
stack[sp++] = p2[1];
|
|
60
|
-
stack[sp++] = p3[0];
|
|
61
|
-
stack[sp++] = p3[1];
|
|
62
|
-
stack[sp++] = 0;
|
|
63
|
-
while (sp > 0) {
|
|
64
|
-
const depth = stack[--sp];
|
|
65
|
-
const bx3 = stack[--sp];
|
|
66
|
-
const by3 = stack[--sp];
|
|
67
|
-
const bx2 = stack[--sp];
|
|
68
|
-
const by2 = stack[--sp];
|
|
69
|
-
const bx1 = stack[--sp];
|
|
70
|
-
const by1 = stack[--sp];
|
|
71
|
-
const bx0 = stack[--sp];
|
|
72
|
-
const by0 = stack[--sp];
|
|
73
|
-
if (depth >= 12 || _cubicFlatnessInline(bx0, by0, bx1, by1, bx2, by2, bx3, by3) <= tolerance) {
|
|
74
|
-
pushUniquePoint(out, [bx3, by3]);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const m01x = (bx0 + bx1) * .5, m01y = (by0 + by1) * .5;
|
|
78
|
-
const m12x = (bx1 + bx2) * .5, m12y = (by1 + by2) * .5;
|
|
79
|
-
const m23x = (bx2 + bx3) * .5, m23y = (by2 + by3) * .5;
|
|
80
|
-
const m012x = (m01x + m12x) * .5, m012y = (m01y + m12y) * .5;
|
|
81
|
-
const m123x = (m12x + m23x) * .5, m123y = (m12y + m23y) * .5;
|
|
82
|
-
const mx = (m012x + m123x) * .5, my = (m012y + m123y) * .5;
|
|
83
|
-
const d1 = depth + 1;
|
|
84
|
-
stack[sp++] = mx;
|
|
85
|
-
stack[sp++] = my;
|
|
86
|
-
stack[sp++] = m123x;
|
|
87
|
-
stack[sp++] = m123y;
|
|
88
|
-
stack[sp++] = m23x;
|
|
89
|
-
stack[sp++] = m23y;
|
|
90
|
-
stack[sp++] = bx3;
|
|
91
|
-
stack[sp++] = by3;
|
|
92
|
-
stack[sp++] = d1;
|
|
93
|
-
stack[sp++] = bx0;
|
|
94
|
-
stack[sp++] = by0;
|
|
95
|
-
stack[sp++] = m01x;
|
|
96
|
-
stack[sp++] = m01y;
|
|
97
|
-
stack[sp++] = m012x;
|
|
98
|
-
stack[sp++] = m012y;
|
|
99
|
-
stack[sp++] = mx;
|
|
100
|
-
stack[sp++] = my;
|
|
101
|
-
stack[sp++] = d1;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
var _quadStack = new Float64Array(896);
|
|
105
|
-
var _cubicStack = new Float64Array(1152);
|
|
106
|
-
function _quadFlatnessInline(x0, y0, x1, y1, x2, y2) {
|
|
107
|
-
const dx = x2 - x0, dy = y2 - y0;
|
|
108
|
-
const lenSq = dx * dx + dy * dy;
|
|
109
|
-
if (lenSq === 0) {
|
|
110
|
-
const ex = x1 - x0, ey = y1 - y0;
|
|
111
|
-
return Math.sqrt(ex * ex + ey * ey);
|
|
112
|
-
}
|
|
113
|
-
return Math.abs(dx * (y0 - y1) - (x0 - x1) * dy) / Math.sqrt(lenSq);
|
|
114
|
-
}
|
|
115
|
-
function _cubicFlatnessInline(x0, y0, x1, y1, x2, y2, x3, y3) {
|
|
116
|
-
const dx = x3 - x0, dy = y3 - y0;
|
|
117
|
-
const lenSq = dx * dx + dy * dy;
|
|
118
|
-
if (lenSq === 0) {
|
|
119
|
-
const e1x = x1 - x0, e1y = y1 - y0;
|
|
120
|
-
const e2x = x2 - x0, e2y = y2 - y0;
|
|
121
|
-
return Math.max(Math.sqrt(e1x * e1x + e1y * e1y), Math.sqrt(e2x * e2x + e2y * e2y));
|
|
122
|
-
}
|
|
123
|
-
const invLen = 1 / Math.sqrt(lenSq);
|
|
124
|
-
const a1 = Math.abs(dx * (y0 - y1) - (x0 - x1) * dy);
|
|
125
|
-
const a2 = Math.abs(dx * (y0 - y2) - (x0 - x2) * dy);
|
|
126
|
-
return Math.max(a1, a2) * invLen;
|
|
15
|
+
function flattenQuadratic(p0, p1, p2, tolerance, out, depth = 0) {
|
|
16
|
+
if (depth >= 12 || quadFlatness(p0, p1, p2) <= tolerance) {
|
|
17
|
+
pushUniquePoint(out, p2);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const p01 = midpoint(p0, p1);
|
|
21
|
+
const p12 = midpoint(p1, p2);
|
|
22
|
+
const p012 = midpoint(p01, p12);
|
|
23
|
+
flattenQuadratic(p0, p01, p012, tolerance, out, depth + 1);
|
|
24
|
+
flattenQuadratic(p012, p12, p2, tolerance, out, depth + 1);
|
|
25
|
+
}
|
|
26
|
+
function flattenCubic(p0, p1, p2, p3, tolerance, out, depth = 0) {
|
|
27
|
+
if (depth >= 12 || cubicFlatness(p0, p1, p2, p3) <= tolerance) {
|
|
28
|
+
pushUniquePoint(out, p3);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const p01 = midpoint(p0, p1);
|
|
32
|
+
const p12 = midpoint(p1, p2);
|
|
33
|
+
const p23 = midpoint(p2, p3);
|
|
34
|
+
const p012 = midpoint(p01, p12);
|
|
35
|
+
const p123 = midpoint(p12, p23);
|
|
36
|
+
const p0123 = midpoint(p012, p123);
|
|
37
|
+
flattenCubic(p0, p01, p012, p0123, tolerance, out, depth + 1);
|
|
38
|
+
flattenCubic(p0123, p123, p23, p3, tolerance, out, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
function quadFlatness(p0, p1, p2) {
|
|
41
|
+
return pointToLineDistance(p1, p0, p2);
|
|
42
|
+
}
|
|
43
|
+
function cubicFlatness(p0, p1, p2, p3) {
|
|
44
|
+
return Math.max(pointToLineDistance(p1, p0, p3), pointToLineDistance(p2, p0, p3));
|
|
45
|
+
}
|
|
46
|
+
function pointToLineDistance(point, lineStart, lineEnd) {
|
|
47
|
+
const dx = lineEnd[0] - lineStart[0];
|
|
48
|
+
const dy = lineEnd[1] - lineStart[1];
|
|
49
|
+
const lengthSq = dx * dx + dy * dy;
|
|
50
|
+
if (lengthSq === 0) return distance(point, lineStart);
|
|
51
|
+
return Math.abs(dx * (lineStart[1] - point[1]) - (lineStart[0] - point[0]) * dy) / Math.sqrt(lengthSq);
|
|
127
52
|
}
|
|
128
53
|
function midpoint(a, b) {
|
|
129
54
|
return [(a[0] + b[0]) * .5, (a[1] + b[1]) * .5];
|
|
@@ -181,37 +106,28 @@ function distance(a, b) {
|
|
|
181
106
|
return Math.sqrt(dx * dx + dy * dy);
|
|
182
107
|
}
|
|
183
108
|
function sampleRing(ring, step, callback) {
|
|
184
|
-
|
|
185
|
-
for (let index = 0; index < len; index++) {
|
|
109
|
+
for (let index = 0; index < ring.length; index += 1) {
|
|
186
110
|
const a = ring[index];
|
|
187
|
-
const b = ring[(index + 1) %
|
|
188
|
-
const
|
|
189
|
-
const segmentLength = Math.sqrt(dx * dx + dy * dy);
|
|
111
|
+
const b = ring[(index + 1) % ring.length];
|
|
112
|
+
const segmentLength = distance(a, b);
|
|
190
113
|
const divisions = Math.max(1, Math.ceil(segmentLength / step));
|
|
191
|
-
if (index
|
|
192
|
-
callback([a[0], a[1]]);
|
|
193
|
-
for (let offset = 1; offset < divisions; offset++) {
|
|
194
|
-
const t = offset / divisions;
|
|
195
|
-
callback([a[0] + dx * t, a[1] + dy * t]);
|
|
196
|
-
}
|
|
197
|
-
} else for (let offset = 0; offset < divisions; offset++) {
|
|
114
|
+
for (let offset = 0; offset < divisions; offset += 1) if (index > 0 || offset > 0) {
|
|
198
115
|
const t = offset / divisions;
|
|
199
|
-
callback([a[0] +
|
|
200
|
-
}
|
|
116
|
+
callback([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
|
117
|
+
} else callback([a[0], a[1]]);
|
|
201
118
|
}
|
|
202
119
|
}
|
|
203
120
|
function ringBounds(ring) {
|
|
204
|
-
let minX =
|
|
205
|
-
let minY =
|
|
206
|
-
let maxX =
|
|
207
|
-
let maxY =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
121
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
122
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
123
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
124
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
125
|
+
ring.forEach((point) => {
|
|
126
|
+
minX = Math.min(minX, point[0]);
|
|
127
|
+
minY = Math.min(minY, point[1]);
|
|
128
|
+
maxX = Math.max(maxX, point[0]);
|
|
129
|
+
maxY = Math.max(maxY, point[1]);
|
|
130
|
+
});
|
|
215
131
|
return {
|
|
216
132
|
x: minX,
|
|
217
133
|
y: minY,
|
|
@@ -220,10 +136,7 @@ function ringBounds(ring) {
|
|
|
220
136
|
};
|
|
221
137
|
}
|
|
222
138
|
function translateRing(ring, tx, ty) {
|
|
223
|
-
|
|
224
|
-
const result = new Array(len);
|
|
225
|
-
for (let i = 0; i < len; i++) result[i] = [ring[i][0] + tx, ring[i][1] + ty];
|
|
226
|
-
return result;
|
|
139
|
+
return ring.map((point) => [point[0] + tx, point[1] + ty]);
|
|
227
140
|
}
|
|
228
141
|
function translateRect(rect, tx, ty) {
|
|
229
142
|
return {
|
|
@@ -234,28 +147,20 @@ function translateRect(rect, tx, ty) {
|
|
|
234
147
|
};
|
|
235
148
|
}
|
|
236
149
|
function copyRing(ring) {
|
|
237
|
-
|
|
238
|
-
const result = new Array(len);
|
|
239
|
-
for (let i = 0; i < len; i++) result[i] = [ring[i][0], ring[i][1]];
|
|
240
|
-
return result;
|
|
150
|
+
return ring.map((point) => [point[0], point[1]]);
|
|
241
151
|
}
|
|
242
152
|
function combineRects(rects) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
let
|
|
247
|
-
let
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (rx < minX) minX = rx;
|
|
255
|
-
if (ry < minY) minY = ry;
|
|
256
|
-
if (rx2 > maxX) maxX = rx2;
|
|
257
|
-
if (ry2 > maxY) maxY = ry2;
|
|
258
|
-
}
|
|
153
|
+
if (rects.length === 0) return null;
|
|
154
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
155
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
156
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
157
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
158
|
+
rects.forEach((rect) => {
|
|
159
|
+
minX = Math.min(minX, rect.x);
|
|
160
|
+
minY = Math.min(minY, rect.y);
|
|
161
|
+
maxX = Math.max(maxX, rect.x + rect.w);
|
|
162
|
+
maxY = Math.max(maxY, rect.y + rect.h);
|
|
163
|
+
});
|
|
259
164
|
return {
|
|
260
165
|
x: minX,
|
|
261
166
|
y: minY,
|
|
@@ -297,64 +202,43 @@ function ringPerimeter(ring) {
|
|
|
297
202
|
for (let index = 0; index < ring.length; index += 1) total += distance(ring[index], ring[(index + 1) % ring.length]);
|
|
298
203
|
return total;
|
|
299
204
|
}
|
|
300
|
-
function
|
|
301
|
-
const len = ring.length;
|
|
302
|
-
const cumulative = new Float64Array(len + 1);
|
|
205
|
+
function polylineLength(path) {
|
|
303
206
|
let total = 0;
|
|
304
|
-
for (let
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
total += Math.sqrt(dx * dx + dy * dy);
|
|
310
|
-
}
|
|
311
|
-
cumulative[len] = total;
|
|
312
|
-
return cumulative;
|
|
313
|
-
}
|
|
314
|
-
function buildOpenCumulativeDistances(path) {
|
|
315
|
-
const len = path.length;
|
|
316
|
-
const cumulative = new Float64Array(len);
|
|
317
|
-
let total = 0;
|
|
318
|
-
cumulative[0] = 0;
|
|
319
|
-
for (let i = 1; i < len; i++) {
|
|
320
|
-
const dx = path[i][0] - path[i - 1][0];
|
|
321
|
-
const dy = path[i][1] - path[i - 1][1];
|
|
322
|
-
total += Math.sqrt(dx * dx + dy * dy);
|
|
323
|
-
cumulative[i] = total;
|
|
324
|
-
}
|
|
325
|
-
return cumulative;
|
|
326
|
-
}
|
|
327
|
-
function binarySearchSegment(cumulative, d) {
|
|
328
|
-
let lo = 0, hi = cumulative.length - 2;
|
|
329
|
-
while (lo < hi) {
|
|
330
|
-
const mid = lo + hi + 1 >>> 1;
|
|
331
|
-
if (cumulative[mid] <= d) lo = mid;
|
|
332
|
-
else hi = mid - 1;
|
|
333
|
-
}
|
|
334
|
-
return lo;
|
|
335
|
-
}
|
|
336
|
-
function pointAtClosedPathDistanceFast(ring, cumulative, distanceAlong) {
|
|
337
|
-
const perimeter = cumulative[ring.length];
|
|
207
|
+
for (let index = 0; index < path.length - 1; index += 1) total += distance(path[index], path[index + 1]);
|
|
208
|
+
return total;
|
|
209
|
+
}
|
|
210
|
+
function pointAtClosedPathDistance(ring, distanceAlong) {
|
|
211
|
+
const perimeter = ringPerimeter(ring);
|
|
338
212
|
if (perimeter <= 1e-9) return [ring[0][0], ring[0][1]];
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
213
|
+
let remaining = mod(distanceAlong, perimeter);
|
|
214
|
+
for (let index = 0; index < ring.length; index += 1) {
|
|
215
|
+
const start = ring[index];
|
|
216
|
+
const end = ring[(index + 1) % ring.length];
|
|
217
|
+
const segmentLength = distance(start, end);
|
|
218
|
+
if (segmentLength <= 1e-9) continue;
|
|
219
|
+
if (remaining <= segmentLength) {
|
|
220
|
+
const t = remaining / segmentLength;
|
|
221
|
+
return [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t];
|
|
222
|
+
}
|
|
223
|
+
remaining -= segmentLength;
|
|
224
|
+
}
|
|
225
|
+
return [ring[ring.length - 1][0], ring[ring.length - 1][1]];
|
|
226
|
+
}
|
|
227
|
+
function pointAtOpenPathDistance(path, distanceAlong) {
|
|
349
228
|
if (distanceAlong <= 0) return [path[0][0], path[0][1]];
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
229
|
+
let remaining = distanceAlong;
|
|
230
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
231
|
+
const start = path[index];
|
|
232
|
+
const end = path[index + 1];
|
|
233
|
+
const segmentLength = distance(start, end);
|
|
234
|
+
if (segmentLength <= 1e-9) continue;
|
|
235
|
+
if (remaining <= segmentLength) {
|
|
236
|
+
const t = remaining / segmentLength;
|
|
237
|
+
return [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t];
|
|
238
|
+
}
|
|
239
|
+
remaining -= segmentLength;
|
|
240
|
+
}
|
|
241
|
+
return [path[path.length - 1][0], path[path.length - 1][1]];
|
|
358
242
|
}
|
|
359
243
|
function segmentIntersectsRing(a, b, ring, ignoredEdges, epsilon) {
|
|
360
244
|
for (let index = 0; index < ring.length; index += 1) {
|
|
@@ -436,28 +320,23 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
|
|
|
436
320
|
current = null;
|
|
437
321
|
return;
|
|
438
322
|
}
|
|
439
|
-
if (!samePoint(current.cursor, current.start))
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
y: current.start.y
|
|
448
|
-
}
|
|
449
|
-
});
|
|
323
|
+
if (!samePoint(current.cursor, current.start)) {
|
|
324
|
+
current.segments.push({
|
|
325
|
+
type: "L",
|
|
326
|
+
from: cloneRawPoint(current.cursor),
|
|
327
|
+
to: cloneRawPoint(current.start)
|
|
328
|
+
});
|
|
329
|
+
current.cursor = cloneRawPoint(current.start);
|
|
330
|
+
}
|
|
450
331
|
contours.push({
|
|
451
332
|
id: contourId++,
|
|
452
|
-
start: current.start,
|
|
333
|
+
start: cloneRawPoint(current.start),
|
|
453
334
|
segments: current.segments
|
|
454
335
|
});
|
|
455
336
|
current = null;
|
|
456
337
|
};
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const type = cmd.type;
|
|
460
|
-
if (type === "M") {
|
|
338
|
+
commands.forEach((cmd) => {
|
|
339
|
+
if (cmd.type === "M") {
|
|
461
340
|
finishCurrentContour();
|
|
462
341
|
current = {
|
|
463
342
|
start: {
|
|
@@ -470,49 +349,47 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
|
|
|
470
349
|
},
|
|
471
350
|
segments: []
|
|
472
351
|
};
|
|
473
|
-
|
|
352
|
+
return;
|
|
474
353
|
}
|
|
475
|
-
if (!current)
|
|
476
|
-
if (type === "L") {
|
|
354
|
+
if (!current) return;
|
|
355
|
+
if (cmd.type === "L") {
|
|
477
356
|
const to = {
|
|
478
357
|
x: cmd.x,
|
|
479
358
|
y: cmd.y
|
|
480
359
|
};
|
|
481
360
|
current.segments.push({
|
|
482
361
|
type: "L",
|
|
483
|
-
from: current.cursor,
|
|
362
|
+
from: cloneRawPoint(current.cursor),
|
|
484
363
|
to
|
|
485
364
|
});
|
|
486
|
-
current.cursor = to;
|
|
487
|
-
|
|
365
|
+
current.cursor = cloneRawPoint(to);
|
|
366
|
+
return;
|
|
488
367
|
}
|
|
489
|
-
if (type === "Q") {
|
|
490
|
-
const from = current.cursor;
|
|
368
|
+
if (cmd.type === "Q") {
|
|
491
369
|
const to = {
|
|
492
370
|
x: cmd.x,
|
|
493
371
|
y: cmd.y
|
|
494
372
|
};
|
|
495
373
|
current.segments.push({
|
|
496
374
|
type: "Q",
|
|
497
|
-
from,
|
|
375
|
+
from: cloneRawPoint(current.cursor),
|
|
498
376
|
c1: {
|
|
499
377
|
x: cmd.x1,
|
|
500
378
|
y: cmd.y1
|
|
501
379
|
},
|
|
502
380
|
to
|
|
503
381
|
});
|
|
504
|
-
current.cursor = to;
|
|
505
|
-
|
|
382
|
+
current.cursor = cloneRawPoint(to);
|
|
383
|
+
return;
|
|
506
384
|
}
|
|
507
|
-
if (type === "C") {
|
|
508
|
-
const from = current.cursor;
|
|
385
|
+
if (cmd.type === "C") {
|
|
509
386
|
const to = {
|
|
510
387
|
x: cmd.x,
|
|
511
388
|
y: cmd.y
|
|
512
389
|
};
|
|
513
390
|
current.segments.push({
|
|
514
391
|
type: "C",
|
|
515
|
-
from,
|
|
392
|
+
from: cloneRawPoint(current.cursor),
|
|
516
393
|
c1: {
|
|
517
394
|
x: cmd.x1,
|
|
518
395
|
y: cmd.y1
|
|
@@ -523,11 +400,11 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
|
|
|
523
400
|
},
|
|
524
401
|
to
|
|
525
402
|
});
|
|
526
|
-
current.cursor = to;
|
|
527
|
-
|
|
403
|
+
current.cursor = cloneRawPoint(to);
|
|
404
|
+
return;
|
|
528
405
|
}
|
|
529
|
-
if (type === "Z") finishCurrentContour();
|
|
530
|
-
}
|
|
406
|
+
if (cmd.type === "Z") finishCurrentContour();
|
|
407
|
+
});
|
|
531
408
|
finishCurrentContour();
|
|
532
409
|
return {
|
|
533
410
|
glyphIndex: glyph.index,
|
|
@@ -551,25 +428,18 @@ function flattenGlyphTopology(topology, options) {
|
|
|
551
428
|
}
|
|
552
429
|
function flattenContour(contour, scale, tolerance) {
|
|
553
430
|
const ring = [];
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const type = segment.type;
|
|
560
|
-
if (type === "L") {
|
|
561
|
-
cursor = toScreenPoint(segment.to, scale);
|
|
562
|
-
pushUniquePoint(ring, cursor);
|
|
563
|
-
} else if (type === "Q") {
|
|
564
|
-
const to = toScreenPoint(segment.to, scale);
|
|
565
|
-
flattenQuadratic(cursor, toScreenPoint(segment.c1, scale), to, tolerance, ring);
|
|
566
|
-
cursor = to;
|
|
567
|
-
} else if (type === "C") {
|
|
568
|
-
const to = toScreenPoint(segment.to, scale);
|
|
569
|
-
flattenCubic(cursor, toScreenPoint(segment.c1, scale), toScreenPoint(segment.c2, scale), to, tolerance, ring);
|
|
570
|
-
cursor = to;
|
|
431
|
+
pushUniquePoint(ring, toScreenPoint(contour.start, scale));
|
|
432
|
+
contour.segments.forEach((segment) => {
|
|
433
|
+
if (segment.type === "L") {
|
|
434
|
+
pushUniquePoint(ring, toScreenPoint(segment.to, scale));
|
|
435
|
+
return;
|
|
571
436
|
}
|
|
572
|
-
|
|
437
|
+
if (segment.type === "Q") {
|
|
438
|
+
flattenQuadratic(toScreenPoint(segment.from, scale), toScreenPoint(segment.c1, scale), toScreenPoint(segment.to, scale), tolerance, ring);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (segment.type === "C") flattenCubic(toScreenPoint(segment.from, scale), toScreenPoint(segment.c1, scale), toScreenPoint(segment.c2, scale), toScreenPoint(segment.to, scale), tolerance, ring);
|
|
442
|
+
});
|
|
573
443
|
if (ring.length > 1 && pointsEqual2D(ring[0], ring[ring.length - 1])) ring.pop();
|
|
574
444
|
if (ring.length < 3) return null;
|
|
575
445
|
const bbox = ringBounds(ring);
|
|
@@ -582,45 +452,37 @@ function flattenContour(contour, scale, tolerance) {
|
|
|
582
452
|
};
|
|
583
453
|
}
|
|
584
454
|
function classifyContours(contours) {
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
for (let i = 0; i < len; i++) result[i] = {
|
|
588
|
-
...contours[i],
|
|
455
|
+
const result = contours.map((contour) => ({
|
|
456
|
+
...contour,
|
|
589
457
|
parentId: null,
|
|
590
458
|
depth: 0,
|
|
591
459
|
role: "outer"
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
for (let ci = 0; ci < len; ci++) {
|
|
595
|
-
const child = result[ci];
|
|
596
|
-
const childAbsArea = Math.abs(child.area);
|
|
460
|
+
}));
|
|
461
|
+
result.forEach((child) => {
|
|
597
462
|
let parent = null;
|
|
598
463
|
let parentArea = Number.POSITIVE_INFINITY;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
464
|
+
result.forEach((candidate) => {
|
|
465
|
+
if (candidate.id === child.id) return;
|
|
466
|
+
if (Math.abs(candidate.area) <= Math.abs(child.area)) return;
|
|
467
|
+
if (!rectContainsRect(candidate.bbox, child.bbox)) return;
|
|
468
|
+
if (!pointInRing(child.ring[0], candidate.ring)) return;
|
|
469
|
+
const candidateArea = Math.abs(candidate.area);
|
|
470
|
+
if (candidateArea < parentArea) {
|
|
471
|
+
parent = candidate;
|
|
472
|
+
parentArea = candidateArea;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
610
475
|
if (parent) child.parentId = parent.id;
|
|
611
|
-
}
|
|
476
|
+
});
|
|
612
477
|
const byId = new Map(result.map((contour) => [contour.id, contour]));
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
contour.depth = depth;
|
|
622
|
-
contour.role = depth % 2 === 0 ? "outer" : "hole";
|
|
623
|
-
}
|
|
478
|
+
const resolveDepth = (contour) => {
|
|
479
|
+
if (contour.parentId == null) return 0;
|
|
480
|
+
return resolveDepth(byId.get(contour.parentId)) + 1;
|
|
481
|
+
};
|
|
482
|
+
result.forEach((contour) => {
|
|
483
|
+
contour.depth = resolveDepth(contour);
|
|
484
|
+
contour.role = contour.depth % 2 === 0 ? "outer" : "hole";
|
|
485
|
+
});
|
|
624
486
|
return result;
|
|
625
487
|
}
|
|
626
488
|
function buildParts(contours) {
|
|
@@ -635,26 +497,16 @@ function buildParts(contours) {
|
|
|
635
497
|
});
|
|
636
498
|
}
|
|
637
499
|
function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const partsLen = srcParts.length;
|
|
641
|
-
const contoursLen = srcContours.length;
|
|
642
|
-
const parts = new Array(partsLen);
|
|
643
|
-
const contours = new Array(contoursLen);
|
|
644
|
-
for (let i = 0; i < partsLen; i++) {
|
|
645
|
-
const part = srcParts[i];
|
|
646
|
-
parts[i] = {
|
|
500
|
+
return {
|
|
501
|
+
parts: geometry.parts.map((part, partIndex) => ({
|
|
647
502
|
outer: translateRing(part.outer, tx, ty),
|
|
648
503
|
holes: part.holes.map((hole) => translateRing(hole, tx, ty)),
|
|
649
504
|
bbox: translateRect(part.bbox, tx, ty),
|
|
650
505
|
area: part.area,
|
|
651
506
|
glyphPosition,
|
|
652
|
-
partIndex
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
for (let i = 0; i < contoursLen; i++) {
|
|
656
|
-
const contour = srcContours[i];
|
|
657
|
-
contours[i] = {
|
|
507
|
+
partIndex
|
|
508
|
+
})),
|
|
509
|
+
contours: geometry.contours.map((contour) => ({
|
|
658
510
|
id: `${glyphPosition}:${contour.id}`,
|
|
659
511
|
sourceId: contour.id,
|
|
660
512
|
glyphPosition,
|
|
@@ -664,11 +516,7 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
|
|
|
664
516
|
area: contour.area,
|
|
665
517
|
bbox: translateRect(contour.bbox, tx, ty),
|
|
666
518
|
ring: translateRing(contour.ring, tx, ty)
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return {
|
|
670
|
-
parts,
|
|
671
|
-
contours,
|
|
519
|
+
})),
|
|
672
520
|
bbox: translateRect(geometry.bbox, tx, ty)
|
|
673
521
|
};
|
|
674
522
|
}
|
|
@@ -844,31 +692,26 @@ var PAShape = class {
|
|
|
844
692
|
function createTextShape(layout, opts, fontInstance) {
|
|
845
693
|
const parts = [];
|
|
846
694
|
const contours = [];
|
|
847
|
-
const
|
|
848
|
-
const
|
|
849
|
-
const glyphs = new Array(glyphCount);
|
|
850
|
-
const sourceLayoutGlyphs = Array.isArray(layoutGlyphs) ? layoutGlyphs.slice() : [];
|
|
695
|
+
const glyphs = [];
|
|
696
|
+
const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
|
|
851
697
|
const variantOptions = {
|
|
852
698
|
openWidth: normalizePositive(opts.openWidth, 0),
|
|
853
699
|
step: normalizePositive(opts.step, 0)
|
|
854
700
|
};
|
|
855
|
-
|
|
856
|
-
const item = layoutGlyphs[glyphPosition];
|
|
701
|
+
layout.glyphs.forEach((item, glyphPosition) => {
|
|
857
702
|
const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
for (let j = 0; j < tContours.length; j++) contours.push(tContours[j]);
|
|
862
|
-
glyphs[glyphPosition] = {
|
|
703
|
+
parts.push(...translated.parts);
|
|
704
|
+
contours.push(...translated.contours);
|
|
705
|
+
glyphs.push({
|
|
863
706
|
position: glyphPosition,
|
|
864
707
|
glyphIndex: item.glyph.index,
|
|
865
708
|
x: item.x,
|
|
866
709
|
y: item.y,
|
|
867
710
|
size: item.size,
|
|
868
711
|
bbox: translated.bbox,
|
|
869
|
-
partCount:
|
|
870
|
-
};
|
|
871
|
-
}
|
|
712
|
+
partCount: translated.parts.length
|
|
713
|
+
});
|
|
714
|
+
});
|
|
872
715
|
const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
|
|
873
716
|
return new PAShape({
|
|
874
717
|
text: layout.text,
|
|
@@ -1102,105 +945,73 @@ function openPartWithSlit(part, width, epsilon) {
|
|
|
1102
945
|
let outer = copyRing(part.outer);
|
|
1103
946
|
const holes = part.holes.map((hole) => copyRing(hole));
|
|
1104
947
|
const anchors = [];
|
|
1105
|
-
const blockers = [];
|
|
1106
948
|
while (holes.length > 0) {
|
|
1107
949
|
let selectedHoleIndex = -1;
|
|
1108
950
|
let selectedBridge = null;
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
const candidate = findBestHoleBridge(outer, holes[hi], blockers, epsilon);
|
|
1114
|
-
if (candidate && candidate.distance < selectedDist) {
|
|
951
|
+
holes.forEach((hole, holeIndex) => {
|
|
952
|
+
const blockers = holes.filter((_, index) => index !== holeIndex);
|
|
953
|
+
const candidate = findBestHoleBridge(outer, hole, blockers, epsilon);
|
|
954
|
+
if (candidate && (!selectedBridge || candidate.distance < selectedBridge.distance)) {
|
|
1115
955
|
selectedBridge = candidate;
|
|
1116
|
-
selectedHoleIndex =
|
|
1117
|
-
selectedDist = candidate.distance;
|
|
956
|
+
selectedHoleIndex = holeIndex;
|
|
1118
957
|
}
|
|
1119
|
-
}
|
|
958
|
+
});
|
|
1120
959
|
if (!selectedBridge || selectedHoleIndex === -1) break;
|
|
1121
960
|
const [hole] = holes.splice(selectedHoleIndex, 1);
|
|
1122
961
|
const merged = mergeHoleIntoOuter(outer, hole, selectedBridge, width, epsilon);
|
|
1123
962
|
outer = merged.ring;
|
|
1124
|
-
|
|
1125
|
-
for (let i = 0; i < mergedAnchors.length; i++) anchors.push(mergedAnchors[i]);
|
|
963
|
+
anchors.push(...merged.anchors);
|
|
1126
964
|
}
|
|
1127
|
-
let holeArea = 0;
|
|
1128
|
-
for (let i = 0; i < holes.length; i++) holeArea += Math.abs(signedArea(holes[i]));
|
|
1129
965
|
return {
|
|
1130
966
|
...part,
|
|
1131
967
|
outer,
|
|
1132
968
|
holes,
|
|
1133
969
|
anchors,
|
|
1134
970
|
bbox: ringBounds(outer),
|
|
1135
|
-
area: Math.abs(signedArea(outer)) -
|
|
971
|
+
area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
|
|
1136
972
|
};
|
|
1137
973
|
}
|
|
1138
974
|
function resamplePart(part, step) {
|
|
1139
975
|
const outer = resampleRing(part.outer, step, part.anchors ?? []);
|
|
1140
|
-
const
|
|
1141
|
-
const holes = new Array(srcHoles.length);
|
|
1142
|
-
let holeArea = 0;
|
|
1143
|
-
for (let i = 0; i < srcHoles.length; i++) {
|
|
1144
|
-
holes[i] = resampleRing(srcHoles[i], step);
|
|
1145
|
-
holeArea += Math.abs(signedArea(holes[i]));
|
|
1146
|
-
}
|
|
976
|
+
const holes = part.holes.map((hole) => resampleRing(hole, step));
|
|
1147
977
|
return {
|
|
1148
978
|
...part,
|
|
1149
979
|
outer,
|
|
1150
980
|
holes,
|
|
1151
981
|
anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
|
|
1152
982
|
bbox: ringBounds(outer),
|
|
1153
|
-
area: Math.abs(signedArea(outer)) -
|
|
983
|
+
area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
|
|
1154
984
|
};
|
|
1155
985
|
}
|
|
1156
986
|
function findBestHoleBridge(outer, hole, blockers, epsilon) {
|
|
1157
987
|
let best = null;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
const dx = op[0] - hp[0], dy = op[1] - hp[1];
|
|
1166
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1167
|
-
if (dist >= bestDist) continue;
|
|
1168
|
-
if (!isVisibleBridge(outer, hole, blockers, op, hp, oi, hi, epsilon)) continue;
|
|
1169
|
-
best = {
|
|
1170
|
-
outerIndex: oi,
|
|
1171
|
-
holeIndex: hi,
|
|
1172
|
-
distance: dist
|
|
988
|
+
outer.forEach((outerPoint, outerIndex) => {
|
|
989
|
+
hole.forEach((holePoint, holeIndex) => {
|
|
990
|
+
if (!isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon)) return;
|
|
991
|
+
const candidate = {
|
|
992
|
+
outerIndex,
|
|
993
|
+
holeIndex,
|
|
994
|
+
distance: distance(outerPoint, holePoint)
|
|
1173
995
|
};
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
996
|
+
if (!best || candidate.distance < best.distance) best = candidate;
|
|
997
|
+
});
|
|
998
|
+
});
|
|
1177
999
|
if (best) return best;
|
|
1178
1000
|
return findNearestBridge(outer, hole);
|
|
1179
1001
|
}
|
|
1180
1002
|
function findNearestBridge(outer, hole) {
|
|
1181
|
-
let
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
bestDistSq = distSq;
|
|
1194
|
-
bestOi = oi;
|
|
1195
|
-
bestHi = hi;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
return {
|
|
1200
|
-
outerIndex: bestOi,
|
|
1201
|
-
holeIndex: bestHi,
|
|
1202
|
-
distance: Math.sqrt(bestDistSq)
|
|
1203
|
-
};
|
|
1003
|
+
let best = null;
|
|
1004
|
+
outer.forEach((outerPoint, outerIndex) => {
|
|
1005
|
+
hole.forEach((holePoint, holeIndex) => {
|
|
1006
|
+
const candidate = {
|
|
1007
|
+
outerIndex,
|
|
1008
|
+
holeIndex,
|
|
1009
|
+
distance: distance(outerPoint, holePoint)
|
|
1010
|
+
};
|
|
1011
|
+
if (!best || candidate.distance < best.distance) best = candidate;
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
return best;
|
|
1204
1015
|
}
|
|
1205
1016
|
function isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon) {
|
|
1206
1017
|
if (distance(outerPoint, holePoint) <= epsilon) return false;
|
|
@@ -1367,14 +1178,13 @@ function resampleRingWithAnchors(ring, step, anchorIndices) {
|
|
|
1367
1178
|
return result;
|
|
1368
1179
|
}
|
|
1369
1180
|
function resampleClosedPath(ring, step) {
|
|
1370
|
-
const
|
|
1371
|
-
const perimeter = cumulative[ring.length];
|
|
1181
|
+
const perimeter = ringPerimeter(ring);
|
|
1372
1182
|
if (perimeter <= 1e-9) return copyRing(ring);
|
|
1373
1183
|
const pointCount = Math.max(3, Math.round(perimeter / step));
|
|
1374
1184
|
if (pointCount >= ring.length) return copyRing(ring);
|
|
1375
1185
|
const sampled = [];
|
|
1376
|
-
for (let index = 0; index < pointCount; index
|
|
1377
|
-
const point =
|
|
1186
|
+
for (let index = 0; index < pointCount; index += 1) {
|
|
1187
|
+
const point = pointAtClosedPathDistance(ring, perimeter * index / pointCount);
|
|
1378
1188
|
if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
|
|
1379
1189
|
}
|
|
1380
1190
|
if (sampled.length < 3 || sampled.length >= ring.length) return copyRing(ring);
|
|
@@ -1382,13 +1192,12 @@ function resampleClosedPath(ring, step) {
|
|
|
1382
1192
|
}
|
|
1383
1193
|
function resampleOpenPath(path, step) {
|
|
1384
1194
|
if (path.length <= 2) return path.map((point) => [point[0], point[1]]);
|
|
1385
|
-
const
|
|
1386
|
-
const length = cumulative[path.length - 1];
|
|
1195
|
+
const length = polylineLength(path);
|
|
1387
1196
|
if (length <= step) return [[path[0][0], path[0][1]], [path[path.length - 1][0], path[path.length - 1][1]]];
|
|
1388
1197
|
const divisionCount = Math.max(1, Math.round(length / step));
|
|
1389
1198
|
const sampled = [];
|
|
1390
|
-
for (let index = 0; index <= divisionCount; index
|
|
1391
|
-
const point =
|
|
1199
|
+
for (let index = 0; index <= divisionCount; index += 1) {
|
|
1200
|
+
const point = pointAtOpenPathDistance(path, length * index / divisionCount);
|
|
1392
1201
|
if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
|
|
1393
1202
|
}
|
|
1394
1203
|
return sampled;
|
|
@@ -3486,6 +3295,7 @@ function layoutWithLines(prepared, maxWidth, lineHeight) {
|
|
|
3486
3295
|
//#endregion
|
|
3487
3296
|
//#region src/paFont/paragraphLayout.js
|
|
3488
3297
|
var DEFAULT_LINE_HEIGHT_RATIO = 1.2;
|
|
3298
|
+
var DEFAULT_PARAGRAPH_GAP = .5;
|
|
3489
3299
|
var HUGE_LAYOUT_WIDTH = 1e9;
|
|
3490
3300
|
var JUSTIFY_EPSILON = 1e-6;
|
|
3491
3301
|
var QUOTE_RE = /"/g;
|
|
@@ -3499,13 +3309,14 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3499
3309
|
const textValue = String(text ?? "");
|
|
3500
3310
|
const layoutBox = resolveLayoutBox(normalized, state);
|
|
3501
3311
|
const retainedPreparedState = resolveRetainedPreparedState(state, normalized);
|
|
3502
|
-
const
|
|
3503
|
-
const
|
|
3312
|
+
const paragraphs = splitParagraphText(textValue, normalized.whiteSpace);
|
|
3313
|
+
const pretextState = shouldAttemptPretextLayout(normalized) ? layoutParagraphsWithPretext(paragraphs, normalized, retainedPreparedState, layoutBox) : null;
|
|
3314
|
+
const layoutState = pretextState != null && canUsePretextLayout(pretextState, normalized) ? pretextState : layoutParagraphsWithNative(fontInstance, paragraphs, normalized, layoutBox);
|
|
3504
3315
|
const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
|
|
3505
3316
|
const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
|
|
3506
3317
|
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3507
3318
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3508
|
-
const textHeight = lines
|
|
3319
|
+
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3509
3320
|
const finalLayoutBox = finalizeLayoutBox(layoutBox, normalized, textHeight);
|
|
3510
3321
|
const cachedPrepared = pretextState?.prepared ?? retainedPreparedState.prepared ?? null;
|
|
3511
3322
|
const cachedPreparedWhiteSpace = pretextState?.preparedWhiteSpace ?? retainedPreparedState.preparedWhiteSpace ?? null;
|
|
@@ -3542,8 +3353,10 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3542
3353
|
], null));
|
|
3543
3354
|
const width = normalizeDimension(options.width);
|
|
3544
3355
|
const height = normalizeDimension(options.height);
|
|
3356
|
+
const gap = normalizeGap(options.gap);
|
|
3545
3357
|
if (options.width != null && width == null) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
|
|
3546
3358
|
if (options.height != null && height == null) throw new TypeError("font.paragraph() option \"height\" must be a positive number.");
|
|
3359
|
+
if (options.gap != null && gap == null) throw new TypeError("font.paragraph() option \"gap\" must be a non-negative number.");
|
|
3547
3360
|
const font = resolveCanvasFont(fontInstance, textOptions.size, options);
|
|
3548
3361
|
return {
|
|
3549
3362
|
...textOptions,
|
|
@@ -3558,6 +3371,7 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3558
3371
|
], wrapDefaults.overflowWrap)),
|
|
3559
3372
|
width,
|
|
3560
3373
|
height,
|
|
3374
|
+
gap: gap ?? DEFAULT_PARAGRAPH_GAP,
|
|
3561
3375
|
lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
|
|
3562
3376
|
align: normalizeEnum(options.align, [
|
|
3563
3377
|
"left",
|
|
@@ -3608,6 +3422,44 @@ function resolveCanvasFont(fontInstance, size, options = {}) {
|
|
|
3608
3422
|
function canReusePreparedParagraphState(previousState, previousOptions, nextOptions) {
|
|
3609
3423
|
return previousState?.prepared != null && previousState.preparedWhiteSpace === resolvePretextWhiteSpace(nextOptions.whiteSpace) && previousOptions?.font === nextOptions.font;
|
|
3610
3424
|
}
|
|
3425
|
+
function layoutParagraphsWithPretext(paragraphs, options, state, layoutBox) {
|
|
3426
|
+
const retainedPrepared = Array.isArray(state.prepared) ? state.prepared : state.prepared != null ? [state.prepared] : [];
|
|
3427
|
+
const paragraphStates = paragraphs.map((paragraph, paragraphIndex) => layoutWithPretext(paragraph, options, {
|
|
3428
|
+
prepared: retainedPrepared[paragraphIndex] ?? null,
|
|
3429
|
+
preparedWhiteSpace: state.preparedWhiteSpace,
|
|
3430
|
+
font: options.font
|
|
3431
|
+
}, layoutBox));
|
|
3432
|
+
return {
|
|
3433
|
+
lines: annotateParagraphLines(paragraphStates),
|
|
3434
|
+
prepared: paragraphStates.map((paragraphState) => paragraphState.prepared ?? null),
|
|
3435
|
+
preparedWhiteSpace: paragraphStates[0]?.preparedWhiteSpace ?? resolvePretextWhiteSpace(options.whiteSpace),
|
|
3436
|
+
layoutEngine: "pretext",
|
|
3437
|
+
usedOverflowWrapFallbackBreaks: paragraphStates.some((paragraphState) => paragraphState.usedOverflowWrapFallbackBreaks)
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
function layoutParagraphsWithNative(fontInstance, paragraphs, options, layoutBox) {
|
|
3441
|
+
return {
|
|
3442
|
+
lines: annotateParagraphLines(paragraphs.map((paragraph) => layoutWithNative(fontInstance, paragraph, options, layoutBox))),
|
|
3443
|
+
prepared: null,
|
|
3444
|
+
preparedWhiteSpace: null,
|
|
3445
|
+
layoutEngine: "native"
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
function annotateParagraphLines(paragraphStates) {
|
|
3449
|
+
const lines = [];
|
|
3450
|
+
paragraphStates.forEach((paragraphState, paragraphIndex) => {
|
|
3451
|
+
paragraphState.lines.forEach((line, lineIndex) => {
|
|
3452
|
+
lines.push({
|
|
3453
|
+
...line,
|
|
3454
|
+
start: null,
|
|
3455
|
+
end: null,
|
|
3456
|
+
paragraphIndex,
|
|
3457
|
+
paragraphEnd: lineIndex === paragraphState.lines.length - 1
|
|
3458
|
+
});
|
|
3459
|
+
});
|
|
3460
|
+
});
|
|
3461
|
+
return lines;
|
|
3462
|
+
}
|
|
3611
3463
|
function layoutWithPretext(text, options, state, layoutBox) {
|
|
3612
3464
|
const preparedWhiteSpace = resolvePretextWhiteSpace(options.whiteSpace);
|
|
3613
3465
|
const prepared = state.prepared != null && state.preparedWhiteSpace === preparedWhiteSpace && state.font === options.font ? state.prepared : prepareWithSegments(text, options.font, { whiteSpace: preparedWhiteSpace });
|
|
@@ -3732,7 +3584,7 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
|
|
|
3732
3584
|
const result = lines.map((line) => ({ ...line }));
|
|
3733
3585
|
let lineLimit = options.maxLines;
|
|
3734
3586
|
if (options.overflow === "hidden" && layoutBox.clipContentHeight != null) {
|
|
3735
|
-
const visibleLineCount =
|
|
3587
|
+
const visibleLineCount = countVisibleLinesForHeight(result, options, layoutBox.clipContentHeight);
|
|
3736
3588
|
lineLimit = lineLimit == null ? visibleLineCount : Math.min(lineLimit, visibleLineCount);
|
|
3737
3589
|
}
|
|
3738
3590
|
let clippedByCount = false;
|
|
@@ -3744,14 +3596,30 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
|
|
|
3744
3596
|
}
|
|
3745
3597
|
}
|
|
3746
3598
|
if (result.length === 0) return result;
|
|
3747
|
-
if (options.overflow === "hidden" && options.whiteSpace === "nowrap"
|
|
3748
|
-
|
|
3599
|
+
if (options.overflow === "hidden" && options.whiteSpace === "nowrap") {
|
|
3600
|
+
for (let index = 0; index < result.length; index += 1) if (result[index].width > contentWidth + JUSTIFY_EPSILON) result[index] = truncateLineToWidth(result[index], contentWidth, measureWidth, options.textOverflow === "ellipsis" ? options.ellipsis : false);
|
|
3601
|
+
}
|
|
3602
|
+
if (clippedByCount && shouldEllipsizeClampedLines(options)) {
|
|
3749
3603
|
const lastIndex = result.length - 1;
|
|
3750
3604
|
result[lastIndex] = truncateLineToWidth(result[lastIndex], contentWidth, measureWidth, options.ellipsis);
|
|
3751
3605
|
result[lastIndex].hardBreak = false;
|
|
3606
|
+
result[lastIndex].paragraphEnd = true;
|
|
3752
3607
|
}
|
|
3753
3608
|
return result;
|
|
3754
3609
|
}
|
|
3610
|
+
function countVisibleLinesForHeight(lines, options, maxHeight) {
|
|
3611
|
+
let visibleLineCount = 0;
|
|
3612
|
+
let consumedHeight = 0;
|
|
3613
|
+
const lineBoxHeight = options.lineHeight;
|
|
3614
|
+
const paragraphGap = lineBoxHeight * options.gap;
|
|
3615
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3616
|
+
if (consumedHeight + lineBoxHeight > maxHeight + JUSTIFY_EPSILON) break;
|
|
3617
|
+
consumedHeight += lineBoxHeight;
|
|
3618
|
+
visibleLineCount += 1;
|
|
3619
|
+
if (lines[index].paragraphEnd && index < lines.length - 1) consumedHeight += paragraphGap;
|
|
3620
|
+
}
|
|
3621
|
+
return visibleLineCount;
|
|
3622
|
+
}
|
|
3755
3623
|
function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
|
|
3756
3624
|
const suffixText = suffix === false ? "" : suffix;
|
|
3757
3625
|
if ((suffixText.length > 0 ? measureWidth(suffixText) : 0) > maxWidth + JUSTIFY_EPSILON) return {
|
|
@@ -3760,25 +3628,9 @@ function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
|
|
|
3760
3628
|
width: 0,
|
|
3761
3629
|
hardBreak: false
|
|
3762
3630
|
};
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
return {
|
|
3767
|
-
...line,
|
|
3768
|
-
text,
|
|
3769
|
-
width: measureWidth(text),
|
|
3770
|
-
hardBreak: false
|
|
3771
|
-
};
|
|
3772
|
-
}
|
|
3773
|
-
const segmenter = getGraphemeSegmenter();
|
|
3774
|
-
const graphemes = Array.from(segmenter.segment(trimmed), (seg) => seg.segment);
|
|
3775
|
-
let lo = 0, hi = graphemes.length;
|
|
3776
|
-
while (lo < hi) {
|
|
3777
|
-
const mid = lo + hi + 1 >>> 1;
|
|
3778
|
-
if (measureWidth(graphemes.slice(0, mid).join("") + suffixText) <= maxWidth + JUSTIFY_EPSILON) lo = mid;
|
|
3779
|
-
else hi = mid - 1;
|
|
3780
|
-
}
|
|
3781
|
-
const text = (lo > 0 ? graphemes.slice(0, lo).join("") : "") + suffixText;
|
|
3631
|
+
let nextText = trimTrailingWhitespace(line.text);
|
|
3632
|
+
while (nextText.length > 0 && measureWidth(`${nextText}${suffixText}`) > maxWidth + JUSTIFY_EPSILON) nextText = trimLastGrapheme(nextText);
|
|
3633
|
+
const text = `${nextText}${suffixText}`;
|
|
3782
3634
|
return {
|
|
3783
3635
|
...line,
|
|
3784
3636
|
text,
|
|
@@ -3792,13 +3644,17 @@ function shouldEllipsizeClampedLines(options) {
|
|
|
3792
3644
|
function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
3793
3645
|
const ascent = getFontAscender(fontInstance, options.size);
|
|
3794
3646
|
const lineBoxHeight = options.lineHeight;
|
|
3647
|
+
const paragraphGap = lineBoxHeight * options.gap;
|
|
3795
3648
|
let cursor = 0;
|
|
3649
|
+
let cursorY = layoutBox.contentY;
|
|
3796
3650
|
return lines.map((line, index) => {
|
|
3797
3651
|
const justified = shouldJustifyLine(line, index, lines.length, options);
|
|
3798
3652
|
const offsetX = justified ? 0 : resolveAlignOffset(options.align, line.width, layoutBox.contentWidth);
|
|
3799
3653
|
const x = layoutBox.contentX + offsetX;
|
|
3800
|
-
const y =
|
|
3654
|
+
const y = cursorY;
|
|
3801
3655
|
const baseline = y + ascent;
|
|
3656
|
+
const start = line.start ?? cursor;
|
|
3657
|
+
const end = line.end ?? start + line.text.length;
|
|
3802
3658
|
const fragments = justified ? buildJustifiedFragments(line, layoutBox.contentX, layoutBox.contentWidth, measureWidth) : [{
|
|
3803
3659
|
text: line.text,
|
|
3804
3660
|
x,
|
|
@@ -3815,8 +3671,8 @@ function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
|
3815
3671
|
const positioned = {
|
|
3816
3672
|
index,
|
|
3817
3673
|
text: line.text,
|
|
3818
|
-
start
|
|
3819
|
-
end
|
|
3674
|
+
start,
|
|
3675
|
+
end,
|
|
3820
3676
|
x,
|
|
3821
3677
|
y,
|
|
3822
3678
|
width,
|
|
@@ -3826,10 +3682,21 @@ function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
|
3826
3682
|
hardBreak: line.hardBreak,
|
|
3827
3683
|
fragments
|
|
3828
3684
|
};
|
|
3829
|
-
cursor =
|
|
3685
|
+
cursor = end;
|
|
3686
|
+
if (line.hardBreak) cursor += 1;
|
|
3687
|
+
if (line.paragraphEnd && index < lines.length - 1) {
|
|
3688
|
+
cursor += 2;
|
|
3689
|
+
cursorY += paragraphGap;
|
|
3690
|
+
}
|
|
3691
|
+
cursorY += lineBoxHeight;
|
|
3830
3692
|
return positioned;
|
|
3831
3693
|
});
|
|
3832
3694
|
}
|
|
3695
|
+
function resolvePositionedTextHeight(lines, contentY) {
|
|
3696
|
+
if (lines.length === 0) return 0;
|
|
3697
|
+
const lastLine = lines[lines.length - 1];
|
|
3698
|
+
return lastLine.y + lastLine.height - contentY;
|
|
3699
|
+
}
|
|
3833
3700
|
function buildJustifiedFragments(line, contentX, contentWidth, measureWidth) {
|
|
3834
3701
|
const tokens = splitPreservingWhitespace(line.text);
|
|
3835
3702
|
const expandable = tokens.reduce((count, token, index) => {
|
|
@@ -3860,7 +3727,7 @@ function buildJustifiedFragments(line, contentX, contentWidth, measureWidth) {
|
|
|
3860
3727
|
return fragments;
|
|
3861
3728
|
}
|
|
3862
3729
|
function shouldJustifyLine(line, index, lineCount, options) {
|
|
3863
|
-
return options.align === "justify" && index < lineCount - 1 && !line.hardBreak && /\S\s+\S/u.test(line.text);
|
|
3730
|
+
return options.align === "justify" && index < lineCount - 1 && !line.hardBreak && !line.paragraphEnd && /\S\s+\S/u.test(line.text);
|
|
3864
3731
|
}
|
|
3865
3732
|
function resolveAlignOffset(align, lineWidth, maxWidth) {
|
|
3866
3733
|
if (align === "center") return (maxWidth - lineWidth) * .5;
|
|
@@ -3970,6 +3837,9 @@ function normalizeMaxLines(value) {
|
|
|
3970
3837
|
function normalizeDimension(value) {
|
|
3971
3838
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
3972
3839
|
}
|
|
3840
|
+
function normalizeGap(value) {
|
|
3841
|
+
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3842
|
+
}
|
|
3973
3843
|
function normalizeSpacing(value) {
|
|
3974
3844
|
if (value == null) return zeroSpacing();
|
|
3975
3845
|
if (Number.isFinite(value)) {
|
|
@@ -4084,6 +3954,27 @@ function resolveRetainedPreparedState(state, options) {
|
|
|
4084
3954
|
function isHardBreak(prepared, line) {
|
|
4085
3955
|
return prepared.kinds?.[line.end.segmentIndex] === "hard-break";
|
|
4086
3956
|
}
|
|
3957
|
+
function splitParagraphText(text, whiteSpace) {
|
|
3958
|
+
const normalized = String(text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3959
|
+
const paragraphs = [];
|
|
3960
|
+
const lines = normalized.split("\n");
|
|
3961
|
+
let currentLines = [];
|
|
3962
|
+
const pushCurrentParagraph = () => {
|
|
3963
|
+
if (currentLines.length === 0) return;
|
|
3964
|
+
const paragraph = whiteSpace === "pre-wrap" ? currentLines.join("\n") : currentLines.join("\n").replace(/\s+/gu, " ").trim();
|
|
3965
|
+
currentLines = [];
|
|
3966
|
+
if (paragraph.length > 0) paragraphs.push(paragraph);
|
|
3967
|
+
};
|
|
3968
|
+
lines.forEach((line) => {
|
|
3969
|
+
if (/^[\t ]*$/u.test(line)) {
|
|
3970
|
+
pushCurrentParagraph();
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
currentLines.push(line);
|
|
3974
|
+
});
|
|
3975
|
+
pushCurrentParagraph();
|
|
3976
|
+
return paragraphs;
|
|
3977
|
+
}
|
|
4087
3978
|
function normalizeNativeText(text, whiteSpace) {
|
|
4088
3979
|
if (whiteSpace === "pre-wrap") return String(text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4089
3980
|
return String(text ?? "").replace(/\s+/gu, " ").trim();
|
|
@@ -4315,6 +4206,12 @@ function getGraphemeSegmenter() {
|
|
|
4315
4206
|
function trimTrailingWhitespace(value) {
|
|
4316
4207
|
return value.replace(/\s+$/u, "");
|
|
4317
4208
|
}
|
|
4209
|
+
function trimLastGrapheme(value) {
|
|
4210
|
+
const segmenter = getGraphemeSegmenter();
|
|
4211
|
+
const graphemes = Array.from(segmenter.segment(value), (segment) => segment.segment);
|
|
4212
|
+
graphemes.pop();
|
|
4213
|
+
return graphemes.join("");
|
|
4214
|
+
}
|
|
4318
4215
|
function splitPreservingWhitespace(value) {
|
|
4319
4216
|
return value.split(/(\s+)/u).filter((token) => token.length > 0);
|
|
4320
4217
|
}
|