pa_font 0.2.8 → 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 +109 -11
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +109 -11
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +1 -0
- package/package.json +1 -1
package/dist/paFont.js
CHANGED
|
@@ -3295,6 +3295,7 @@ function layoutWithLines(prepared, maxWidth, lineHeight) {
|
|
|
3295
3295
|
//#endregion
|
|
3296
3296
|
//#region src/paFont/paragraphLayout.js
|
|
3297
3297
|
var DEFAULT_LINE_HEIGHT_RATIO = 1.2;
|
|
3298
|
+
var DEFAULT_PARAGRAPH_GAP = .5;
|
|
3298
3299
|
var HUGE_LAYOUT_WIDTH = 1e9;
|
|
3299
3300
|
var JUSTIFY_EPSILON = 1e-6;
|
|
3300
3301
|
var QUOTE_RE = /"/g;
|
|
@@ -3308,13 +3309,14 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3308
3309
|
const textValue = String(text ?? "");
|
|
3309
3310
|
const layoutBox = resolveLayoutBox(normalized, state);
|
|
3310
3311
|
const retainedPreparedState = resolveRetainedPreparedState(state, normalized);
|
|
3311
|
-
const
|
|
3312
|
-
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);
|
|
3313
3315
|
const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
|
|
3314
3316
|
const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
|
|
3315
3317
|
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3316
3318
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3317
|
-
const textHeight = lines
|
|
3319
|
+
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3318
3320
|
const finalLayoutBox = finalizeLayoutBox(layoutBox, normalized, textHeight);
|
|
3319
3321
|
const cachedPrepared = pretextState?.prepared ?? retainedPreparedState.prepared ?? null;
|
|
3320
3322
|
const cachedPreparedWhiteSpace = pretextState?.preparedWhiteSpace ?? retainedPreparedState.preparedWhiteSpace ?? null;
|
|
@@ -3351,8 +3353,10 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3351
3353
|
], null));
|
|
3352
3354
|
const width = normalizeDimension(options.width);
|
|
3353
3355
|
const height = normalizeDimension(options.height);
|
|
3356
|
+
const gap = normalizeGap(options.gap);
|
|
3354
3357
|
if (options.width != null && width == null) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
|
|
3355
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.");
|
|
3356
3360
|
const font = resolveCanvasFont(fontInstance, textOptions.size, options);
|
|
3357
3361
|
return {
|
|
3358
3362
|
...textOptions,
|
|
@@ -3367,6 +3371,7 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3367
3371
|
], wrapDefaults.overflowWrap)),
|
|
3368
3372
|
width,
|
|
3369
3373
|
height,
|
|
3374
|
+
gap: gap ?? DEFAULT_PARAGRAPH_GAP,
|
|
3370
3375
|
lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
|
|
3371
3376
|
align: normalizeEnum(options.align, [
|
|
3372
3377
|
"left",
|
|
@@ -3417,6 +3422,44 @@ function resolveCanvasFont(fontInstance, size, options = {}) {
|
|
|
3417
3422
|
function canReusePreparedParagraphState(previousState, previousOptions, nextOptions) {
|
|
3418
3423
|
return previousState?.prepared != null && previousState.preparedWhiteSpace === resolvePretextWhiteSpace(nextOptions.whiteSpace) && previousOptions?.font === nextOptions.font;
|
|
3419
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
|
+
}
|
|
3420
3463
|
function layoutWithPretext(text, options, state, layoutBox) {
|
|
3421
3464
|
const preparedWhiteSpace = resolvePretextWhiteSpace(options.whiteSpace);
|
|
3422
3465
|
const prepared = state.prepared != null && state.preparedWhiteSpace === preparedWhiteSpace && state.font === options.font ? state.prepared : prepareWithSegments(text, options.font, { whiteSpace: preparedWhiteSpace });
|
|
@@ -3541,7 +3584,7 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
|
|
|
3541
3584
|
const result = lines.map((line) => ({ ...line }));
|
|
3542
3585
|
let lineLimit = options.maxLines;
|
|
3543
3586
|
if (options.overflow === "hidden" && layoutBox.clipContentHeight != null) {
|
|
3544
|
-
const visibleLineCount =
|
|
3587
|
+
const visibleLineCount = countVisibleLinesForHeight(result, options, layoutBox.clipContentHeight);
|
|
3545
3588
|
lineLimit = lineLimit == null ? visibleLineCount : Math.min(lineLimit, visibleLineCount);
|
|
3546
3589
|
}
|
|
3547
3590
|
let clippedByCount = false;
|
|
@@ -3553,14 +3596,30 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
|
|
|
3553
3596
|
}
|
|
3554
3597
|
}
|
|
3555
3598
|
if (result.length === 0) return result;
|
|
3556
|
-
if (options.overflow === "hidden" && options.whiteSpace === "nowrap"
|
|
3557
|
-
|
|
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)) {
|
|
3558
3603
|
const lastIndex = result.length - 1;
|
|
3559
3604
|
result[lastIndex] = truncateLineToWidth(result[lastIndex], contentWidth, measureWidth, options.ellipsis);
|
|
3560
3605
|
result[lastIndex].hardBreak = false;
|
|
3606
|
+
result[lastIndex].paragraphEnd = true;
|
|
3561
3607
|
}
|
|
3562
3608
|
return result;
|
|
3563
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
|
+
}
|
|
3564
3623
|
function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
|
|
3565
3624
|
const suffixText = suffix === false ? "" : suffix;
|
|
3566
3625
|
if ((suffixText.length > 0 ? measureWidth(suffixText) : 0) > maxWidth + JUSTIFY_EPSILON) return {
|
|
@@ -3585,13 +3644,17 @@ function shouldEllipsizeClampedLines(options) {
|
|
|
3585
3644
|
function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
3586
3645
|
const ascent = getFontAscender(fontInstance, options.size);
|
|
3587
3646
|
const lineBoxHeight = options.lineHeight;
|
|
3647
|
+
const paragraphGap = lineBoxHeight * options.gap;
|
|
3588
3648
|
let cursor = 0;
|
|
3649
|
+
let cursorY = layoutBox.contentY;
|
|
3589
3650
|
return lines.map((line, index) => {
|
|
3590
3651
|
const justified = shouldJustifyLine(line, index, lines.length, options);
|
|
3591
3652
|
const offsetX = justified ? 0 : resolveAlignOffset(options.align, line.width, layoutBox.contentWidth);
|
|
3592
3653
|
const x = layoutBox.contentX + offsetX;
|
|
3593
|
-
const y =
|
|
3654
|
+
const y = cursorY;
|
|
3594
3655
|
const baseline = y + ascent;
|
|
3656
|
+
const start = line.start ?? cursor;
|
|
3657
|
+
const end = line.end ?? start + line.text.length;
|
|
3595
3658
|
const fragments = justified ? buildJustifiedFragments(line, layoutBox.contentX, layoutBox.contentWidth, measureWidth) : [{
|
|
3596
3659
|
text: line.text,
|
|
3597
3660
|
x,
|
|
@@ -3608,8 +3671,8 @@ function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
|
3608
3671
|
const positioned = {
|
|
3609
3672
|
index,
|
|
3610
3673
|
text: line.text,
|
|
3611
|
-
start
|
|
3612
|
-
end
|
|
3674
|
+
start,
|
|
3675
|
+
end,
|
|
3613
3676
|
x,
|
|
3614
3677
|
y,
|
|
3615
3678
|
width,
|
|
@@ -3619,10 +3682,21 @@ function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
|
3619
3682
|
hardBreak: line.hardBreak,
|
|
3620
3683
|
fragments
|
|
3621
3684
|
};
|
|
3622
|
-
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;
|
|
3623
3692
|
return positioned;
|
|
3624
3693
|
});
|
|
3625
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
|
+
}
|
|
3626
3700
|
function buildJustifiedFragments(line, contentX, contentWidth, measureWidth) {
|
|
3627
3701
|
const tokens = splitPreservingWhitespace(line.text);
|
|
3628
3702
|
const expandable = tokens.reduce((count, token, index) => {
|
|
@@ -3653,7 +3727,7 @@ function buildJustifiedFragments(line, contentX, contentWidth, measureWidth) {
|
|
|
3653
3727
|
return fragments;
|
|
3654
3728
|
}
|
|
3655
3729
|
function shouldJustifyLine(line, index, lineCount, options) {
|
|
3656
|
-
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);
|
|
3657
3731
|
}
|
|
3658
3732
|
function resolveAlignOffset(align, lineWidth, maxWidth) {
|
|
3659
3733
|
if (align === "center") return (maxWidth - lineWidth) * .5;
|
|
@@ -3763,6 +3837,9 @@ function normalizeMaxLines(value) {
|
|
|
3763
3837
|
function normalizeDimension(value) {
|
|
3764
3838
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
3765
3839
|
}
|
|
3840
|
+
function normalizeGap(value) {
|
|
3841
|
+
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3842
|
+
}
|
|
3766
3843
|
function normalizeSpacing(value) {
|
|
3767
3844
|
if (value == null) return zeroSpacing();
|
|
3768
3845
|
if (Number.isFinite(value)) {
|
|
@@ -3877,6 +3954,27 @@ function resolveRetainedPreparedState(state, options) {
|
|
|
3877
3954
|
function isHardBreak(prepared, line) {
|
|
3878
3955
|
return prepared.kinds?.[line.end.segmentIndex] === "hard-break";
|
|
3879
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
|
+
}
|
|
3880
3978
|
function normalizeNativeText(text, whiteSpace) {
|
|
3881
3979
|
if (whiteSpace === "pre-wrap") return String(text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3882
3980
|
return String(text ?? "").replace(/\s+/gu, " ").trim();
|