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