pa_font 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +45 -10
- package/dist/paFont.cjs +143 -15
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +143 -15
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +5 -0
- package/package.json +1 -1
package/USAGE.md
CHANGED
|
@@ -42,15 +42,19 @@ const points = shape.toPoints({ step: 8 });
|
|
|
42
42
|
### 2. 문단을 canvas에 그리고, 필요하면 geometry로 바꾸기
|
|
43
43
|
|
|
44
44
|
```js
|
|
45
|
-
const paragraph = font.paragraph(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
const paragraph = font.paragraph(
|
|
46
|
+
"첫 번째 문단입니다. 자동 줄바꿈됩니다.\n\n두 번째 문단입니다.",
|
|
47
|
+
{
|
|
48
|
+
x: 40,
|
|
49
|
+
y: 80,
|
|
50
|
+
size: 32,
|
|
51
|
+
lineHeight: 1.4,
|
|
52
|
+
gap: 0.75,
|
|
53
|
+
align: "left",
|
|
54
|
+
wrap: "word",
|
|
55
|
+
padding: { x: 24, y: 20 },
|
|
56
|
+
}
|
|
57
|
+
);
|
|
54
58
|
|
|
55
59
|
paragraph.drawText(ctx, {
|
|
56
60
|
fillStyle: "#111",
|
|
@@ -107,7 +111,7 @@ console.log(metrics.width, metrics.bbox);
|
|
|
107
111
|
- `margin`, `padding`: `24`, `"24 32"`, `[24, 32]`, `{ x: 32, y: 24 }`, `{ top, right, bottom, left }`
|
|
108
112
|
- `width`: 생략하면 `drawText(ctx)` 시 현재 canvas 폭 기준으로 자동 계산
|
|
109
113
|
- `height`: `overflow: "hidden"`과 같이 쓰면 clip/clamp
|
|
110
|
-
- `x`, `y`, `size`, `lineHeight`, `align`
|
|
114
|
+
- `x`, `y`, `size`, `lineHeight`, `gap`, `align`
|
|
111
115
|
|
|
112
116
|
세부 제어 옵션:
|
|
113
117
|
|
|
@@ -115,6 +119,7 @@ console.log(metrics.width, metrics.bbox);
|
|
|
115
119
|
- `overflowWrap`: `normal | break-word | anywhere`
|
|
116
120
|
- `wordBreak`: `normal | break-all | keep-all`
|
|
117
121
|
- `overflow`: `visible | hidden`
|
|
122
|
+
- `overflowY`: `visible | hidden | scroll`
|
|
118
123
|
- `textOverflow`: `clip | ellipsis`
|
|
119
124
|
- `fontFamily`, `fontWeight`, `fontStyle`, `font`
|
|
120
125
|
- `engine`: `pretext | native`
|
|
@@ -128,6 +133,13 @@ console.log(metrics.width, metrics.bbox);
|
|
|
128
133
|
|
|
129
134
|
`wordBreak` / `overflowWrap`를 직접 주면 `wrap`보다 우선합니다.
|
|
130
135
|
|
|
136
|
+
빈 줄 하나 이상은 새 문단으로 처리됩니다.
|
|
137
|
+
|
|
138
|
+
- `gap`: 문단 사이 간격. `lineHeight` 배수이며 기본값은 `0.5`
|
|
139
|
+
- `whiteSpace: "normal"`이면 문단 안 단일 줄바꿈은 공백으로 정리
|
|
140
|
+
- `whiteSpace: "pre-wrap"`이면 문단 안 단일 줄바꿈 유지
|
|
141
|
+
- `overflowY: "scroll"`을 쓰려면 `height`가 필요
|
|
142
|
+
|
|
131
143
|
## paParagraph API
|
|
132
144
|
|
|
133
145
|
`font.paragraph()`는 `paParagraph`를 반환합니다.
|
|
@@ -172,6 +184,28 @@ paragraph.drawText(ctx, {
|
|
|
172
184
|
- `strokeStyle`
|
|
173
185
|
- `fill`
|
|
174
186
|
- `stroke`
|
|
187
|
+
- `scrollTop`
|
|
188
|
+
|
|
189
|
+
스크롤 가능한 viewport를 만들고 싶다면:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
const paragraph = font.paragraph(text, {
|
|
193
|
+
width: 320,
|
|
194
|
+
height: 220,
|
|
195
|
+
overflowY: "scroll",
|
|
196
|
+
lineHeight: 1.5,
|
|
197
|
+
gap: 0.5,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
paragraph.drawText(ctx, {
|
|
201
|
+
fillStyle: "#111",
|
|
202
|
+
scrollTop: 48,
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
전체 콘텐츠 높이는 `paragraph.metrics.height`,
|
|
207
|
+
현재 viewport 높이는 `paragraph.metrics.viewportHeight`,
|
|
208
|
+
최대 스크롤 값은 `paragraph.metrics.maxScrollTop`에서 확인할 수 있습니다.
|
|
175
209
|
|
|
176
210
|
### `paragraph.toShape(options?)`
|
|
177
211
|
|
|
@@ -281,6 +315,7 @@ const paragraph = font.paragraph(text, {
|
|
|
281
315
|
y: 0,
|
|
282
316
|
size: 20,
|
|
283
317
|
lineHeight: 1.6,
|
|
318
|
+
gap: 0.5,
|
|
284
319
|
padding: { x: 32, y: 24 },
|
|
285
320
|
margin: "20 0 0 20",
|
|
286
321
|
wrap: "char",
|
package/dist/paFont.cjs
CHANGED
|
@@ -3299,6 +3299,7 @@ function layoutWithLines(prepared, maxWidth, lineHeight) {
|
|
|
3299
3299
|
//#endregion
|
|
3300
3300
|
//#region src/paFont/paragraphLayout.js
|
|
3301
3301
|
var DEFAULT_LINE_HEIGHT_RATIO = 1.2;
|
|
3302
|
+
var DEFAULT_PARAGRAPH_GAP = .5;
|
|
3302
3303
|
var HUGE_LAYOUT_WIDTH = 1e9;
|
|
3303
3304
|
var JUSTIFY_EPSILON = 1e-6;
|
|
3304
3305
|
var QUOTE_RE = /"/g;
|
|
@@ -3312,13 +3313,14 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3312
3313
|
const textValue = String(text ?? "");
|
|
3313
3314
|
const layoutBox = resolveLayoutBox(normalized, state);
|
|
3314
3315
|
const retainedPreparedState = resolveRetainedPreparedState(state, normalized);
|
|
3315
|
-
const
|
|
3316
|
-
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);
|
|
3317
3319
|
const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
|
|
3318
3320
|
const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
|
|
3319
3321
|
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3320
3322
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3321
|
-
const textHeight = lines
|
|
3323
|
+
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3322
3324
|
const finalLayoutBox = finalizeLayoutBox(layoutBox, normalized, textHeight);
|
|
3323
3325
|
const cachedPrepared = pretextState?.prepared ?? retainedPreparedState.prepared ?? null;
|
|
3324
3326
|
const cachedPreparedWhiteSpace = pretextState?.preparedWhiteSpace ?? retainedPreparedState.preparedWhiteSpace ?? null;
|
|
@@ -3330,6 +3332,8 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3330
3332
|
y: finalLayoutBox.contentBox.y,
|
|
3331
3333
|
width: textWidth,
|
|
3332
3334
|
height: textHeight,
|
|
3335
|
+
viewportHeight: finalLayoutBox.contentBox.h,
|
|
3336
|
+
maxScrollTop: Math.max(0, textHeight - finalLayoutBox.contentBox.h),
|
|
3333
3337
|
lineCount: lines.length,
|
|
3334
3338
|
bbox: textBBox,
|
|
3335
3339
|
contentBox: { ...finalLayoutBox.contentBox },
|
|
@@ -3355,8 +3359,13 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3355
3359
|
], null));
|
|
3356
3360
|
const width = normalizeDimension(options.width);
|
|
3357
3361
|
const height = normalizeDimension(options.height);
|
|
3362
|
+
const gap = normalizeGap(options.gap);
|
|
3363
|
+
const overflow = normalizeEnum(options.overflow, ["visible", "hidden"], "visible");
|
|
3364
|
+
const overflowY = resolveOverflowY(options.overflowY, overflow);
|
|
3358
3365
|
if (options.width != null && width == null) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
|
|
3359
3366
|
if (options.height != null && height == null) throw new TypeError("font.paragraph() option \"height\" must be a positive number.");
|
|
3367
|
+
if (options.gap != null && gap == null) throw new TypeError("font.paragraph() option \"gap\" must be a non-negative number.");
|
|
3368
|
+
if (overflowY === "scroll" && height == null) throw new TypeError("font.paragraph() option \"height\" is required when \"overflowY\" is \"scroll\".");
|
|
3360
3369
|
const font = resolveCanvasFont(fontInstance, textOptions.size, options);
|
|
3361
3370
|
return {
|
|
3362
3371
|
...textOptions,
|
|
@@ -3371,6 +3380,7 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3371
3380
|
], wrapDefaults.overflowWrap)),
|
|
3372
3381
|
width,
|
|
3373
3382
|
height,
|
|
3383
|
+
gap: gap ?? DEFAULT_PARAGRAPH_GAP,
|
|
3374
3384
|
lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
|
|
3375
3385
|
align: normalizeEnum(options.align, [
|
|
3376
3386
|
"left",
|
|
@@ -3393,7 +3403,8 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3393
3403
|
"break-all",
|
|
3394
3404
|
"keep-all"
|
|
3395
3405
|
], wrapDefaults.wordBreak),
|
|
3396
|
-
overflow
|
|
3406
|
+
overflow,
|
|
3407
|
+
overflowY,
|
|
3397
3408
|
textOverflow: normalizeNullableEnum(options.textOverflow, ["clip", "ellipsis"], null),
|
|
3398
3409
|
maxLines: normalizeMaxLines(options.maxLines),
|
|
3399
3410
|
ellipsis: normalizeEllipsis(options.ellipsis),
|
|
@@ -3421,6 +3432,44 @@ function resolveCanvasFont(fontInstance, size, options = {}) {
|
|
|
3421
3432
|
function canReusePreparedParagraphState(previousState, previousOptions, nextOptions) {
|
|
3422
3433
|
return previousState?.prepared != null && previousState.preparedWhiteSpace === resolvePretextWhiteSpace(nextOptions.whiteSpace) && previousOptions?.font === nextOptions.font;
|
|
3423
3434
|
}
|
|
3435
|
+
function layoutParagraphsWithPretext(paragraphs, options, state, layoutBox) {
|
|
3436
|
+
const retainedPrepared = Array.isArray(state.prepared) ? state.prepared : state.prepared != null ? [state.prepared] : [];
|
|
3437
|
+
const paragraphStates = paragraphs.map((paragraph, paragraphIndex) => layoutWithPretext(paragraph, options, {
|
|
3438
|
+
prepared: retainedPrepared[paragraphIndex] ?? null,
|
|
3439
|
+
preparedWhiteSpace: state.preparedWhiteSpace,
|
|
3440
|
+
font: options.font
|
|
3441
|
+
}, layoutBox));
|
|
3442
|
+
return {
|
|
3443
|
+
lines: annotateParagraphLines(paragraphStates),
|
|
3444
|
+
prepared: paragraphStates.map((paragraphState) => paragraphState.prepared ?? null),
|
|
3445
|
+
preparedWhiteSpace: paragraphStates[0]?.preparedWhiteSpace ?? resolvePretextWhiteSpace(options.whiteSpace),
|
|
3446
|
+
layoutEngine: "pretext",
|
|
3447
|
+
usedOverflowWrapFallbackBreaks: paragraphStates.some((paragraphState) => paragraphState.usedOverflowWrapFallbackBreaks)
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
function layoutParagraphsWithNative(fontInstance, paragraphs, options, layoutBox) {
|
|
3451
|
+
return {
|
|
3452
|
+
lines: annotateParagraphLines(paragraphs.map((paragraph) => layoutWithNative(fontInstance, paragraph, options, layoutBox))),
|
|
3453
|
+
prepared: null,
|
|
3454
|
+
preparedWhiteSpace: null,
|
|
3455
|
+
layoutEngine: "native"
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
function annotateParagraphLines(paragraphStates) {
|
|
3459
|
+
const lines = [];
|
|
3460
|
+
paragraphStates.forEach((paragraphState, paragraphIndex) => {
|
|
3461
|
+
paragraphState.lines.forEach((line, lineIndex) => {
|
|
3462
|
+
lines.push({
|
|
3463
|
+
...line,
|
|
3464
|
+
start: null,
|
|
3465
|
+
end: null,
|
|
3466
|
+
paragraphIndex,
|
|
3467
|
+
paragraphEnd: lineIndex === paragraphState.lines.length - 1
|
|
3468
|
+
});
|
|
3469
|
+
});
|
|
3470
|
+
});
|
|
3471
|
+
return lines;
|
|
3472
|
+
}
|
|
3424
3473
|
function layoutWithPretext(text, options, state, layoutBox) {
|
|
3425
3474
|
const preparedWhiteSpace = resolvePretextWhiteSpace(options.whiteSpace);
|
|
3426
3475
|
const prepared = state.prepared != null && state.preparedWhiteSpace === preparedWhiteSpace && state.font === options.font ? state.prepared : prepareWithSegments(text, options.font, { whiteSpace: preparedWhiteSpace });
|
|
@@ -3544,8 +3593,8 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
|
|
|
3544
3593
|
const contentWidth = layoutBox.contentWidth;
|
|
3545
3594
|
const result = lines.map((line) => ({ ...line }));
|
|
3546
3595
|
let lineLimit = options.maxLines;
|
|
3547
|
-
if (options
|
|
3548
|
-
const visibleLineCount =
|
|
3596
|
+
if (shouldClampLinesToHeight(options) && layoutBox.clipContentHeight != null) {
|
|
3597
|
+
const visibleLineCount = countVisibleLinesForHeight(result, options, layoutBox.clipContentHeight);
|
|
3549
3598
|
lineLimit = lineLimit == null ? visibleLineCount : Math.min(lineLimit, visibleLineCount);
|
|
3550
3599
|
}
|
|
3551
3600
|
let clippedByCount = false;
|
|
@@ -3557,14 +3606,30 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
|
|
|
3557
3606
|
}
|
|
3558
3607
|
}
|
|
3559
3608
|
if (result.length === 0) return result;
|
|
3560
|
-
if (options.overflow === "hidden" && options.whiteSpace === "nowrap"
|
|
3561
|
-
|
|
3609
|
+
if (options.overflow === "hidden" && options.whiteSpace === "nowrap") {
|
|
3610
|
+
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);
|
|
3611
|
+
}
|
|
3612
|
+
if (clippedByCount && shouldEllipsizeClampedLines(options)) {
|
|
3562
3613
|
const lastIndex = result.length - 1;
|
|
3563
3614
|
result[lastIndex] = truncateLineToWidth(result[lastIndex], contentWidth, measureWidth, options.ellipsis);
|
|
3564
3615
|
result[lastIndex].hardBreak = false;
|
|
3616
|
+
result[lastIndex].paragraphEnd = true;
|
|
3565
3617
|
}
|
|
3566
3618
|
return result;
|
|
3567
3619
|
}
|
|
3620
|
+
function countVisibleLinesForHeight(lines, options, maxHeight) {
|
|
3621
|
+
let visibleLineCount = 0;
|
|
3622
|
+
let consumedHeight = 0;
|
|
3623
|
+
const lineBoxHeight = options.lineHeight;
|
|
3624
|
+
const paragraphGap = lineBoxHeight * options.gap;
|
|
3625
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3626
|
+
if (consumedHeight + lineBoxHeight > maxHeight + JUSTIFY_EPSILON) break;
|
|
3627
|
+
consumedHeight += lineBoxHeight;
|
|
3628
|
+
visibleLineCount += 1;
|
|
3629
|
+
if (lines[index].paragraphEnd && index < lines.length - 1) consumedHeight += paragraphGap;
|
|
3630
|
+
}
|
|
3631
|
+
return visibleLineCount;
|
|
3632
|
+
}
|
|
3568
3633
|
function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
|
|
3569
3634
|
const suffixText = suffix === false ? "" : suffix;
|
|
3570
3635
|
if ((suffixText.length > 0 ? measureWidth(suffixText) : 0) > maxWidth + JUSTIFY_EPSILON) return {
|
|
@@ -3589,13 +3654,17 @@ function shouldEllipsizeClampedLines(options) {
|
|
|
3589
3654
|
function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
3590
3655
|
const ascent = getFontAscender(fontInstance, options.size);
|
|
3591
3656
|
const lineBoxHeight = options.lineHeight;
|
|
3657
|
+
const paragraphGap = lineBoxHeight * options.gap;
|
|
3592
3658
|
let cursor = 0;
|
|
3659
|
+
let cursorY = layoutBox.contentY;
|
|
3593
3660
|
return lines.map((line, index) => {
|
|
3594
3661
|
const justified = shouldJustifyLine(line, index, lines.length, options);
|
|
3595
3662
|
const offsetX = justified ? 0 : resolveAlignOffset(options.align, line.width, layoutBox.contentWidth);
|
|
3596
3663
|
const x = layoutBox.contentX + offsetX;
|
|
3597
|
-
const y =
|
|
3664
|
+
const y = cursorY;
|
|
3598
3665
|
const baseline = y + ascent;
|
|
3666
|
+
const start = line.start ?? cursor;
|
|
3667
|
+
const end = line.end ?? start + line.text.length;
|
|
3599
3668
|
const fragments = justified ? buildJustifiedFragments(line, layoutBox.contentX, layoutBox.contentWidth, measureWidth) : [{
|
|
3600
3669
|
text: line.text,
|
|
3601
3670
|
x,
|
|
@@ -3612,8 +3681,8 @@ function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
|
3612
3681
|
const positioned = {
|
|
3613
3682
|
index,
|
|
3614
3683
|
text: line.text,
|
|
3615
|
-
start
|
|
3616
|
-
end
|
|
3684
|
+
start,
|
|
3685
|
+
end,
|
|
3617
3686
|
x,
|
|
3618
3687
|
y,
|
|
3619
3688
|
width,
|
|
@@ -3623,10 +3692,21 @@ function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
|
|
|
3623
3692
|
hardBreak: line.hardBreak,
|
|
3624
3693
|
fragments
|
|
3625
3694
|
};
|
|
3626
|
-
cursor =
|
|
3695
|
+
cursor = end;
|
|
3696
|
+
if (line.hardBreak) cursor += 1;
|
|
3697
|
+
if (line.paragraphEnd && index < lines.length - 1) {
|
|
3698
|
+
cursor += 2;
|
|
3699
|
+
cursorY += paragraphGap;
|
|
3700
|
+
}
|
|
3701
|
+
cursorY += lineBoxHeight;
|
|
3627
3702
|
return positioned;
|
|
3628
3703
|
});
|
|
3629
3704
|
}
|
|
3705
|
+
function resolvePositionedTextHeight(lines, contentY) {
|
|
3706
|
+
if (lines.length === 0) return 0;
|
|
3707
|
+
const lastLine = lines[lines.length - 1];
|
|
3708
|
+
return lastLine.y + lastLine.height - contentY;
|
|
3709
|
+
}
|
|
3630
3710
|
function buildJustifiedFragments(line, contentX, contentWidth, measureWidth) {
|
|
3631
3711
|
const tokens = splitPreservingWhitespace(line.text);
|
|
3632
3712
|
const expandable = tokens.reduce((count, token, index) => {
|
|
@@ -3657,7 +3737,7 @@ function buildJustifiedFragments(line, contentX, contentWidth, measureWidth) {
|
|
|
3657
3737
|
return fragments;
|
|
3658
3738
|
}
|
|
3659
3739
|
function shouldJustifyLine(line, index, lineCount, options) {
|
|
3660
|
-
return options.align === "justify" && index < lineCount - 1 && !line.hardBreak && /\S\s+\S/u.test(line.text);
|
|
3740
|
+
return options.align === "justify" && index < lineCount - 1 && !line.hardBreak && !line.paragraphEnd && /\S\s+\S/u.test(line.text);
|
|
3661
3741
|
}
|
|
3662
3742
|
function resolveAlignOffset(align, lineWidth, maxWidth) {
|
|
3663
3743
|
if (align === "center") return (maxWidth - lineWidth) * .5;
|
|
@@ -3738,7 +3818,7 @@ function finalizeLayoutBox(layoutBox, options, textHeight) {
|
|
|
3738
3818
|
x: layoutBox.contentX,
|
|
3739
3819
|
y: layoutBox.contentY,
|
|
3740
3820
|
w: layoutBox.contentWidth,
|
|
3741
|
-
h: options
|
|
3821
|
+
h: shouldClipToContentHeight(options) ? contentHeight : textHeight
|
|
3742
3822
|
}
|
|
3743
3823
|
};
|
|
3744
3824
|
}
|
|
@@ -3754,6 +3834,19 @@ function normalizeNullableEnum(value, supported, fallback) {
|
|
|
3754
3834
|
if (value == null) return fallback;
|
|
3755
3835
|
return typeof value === "string" && supported.includes(value) ? value : fallback;
|
|
3756
3836
|
}
|
|
3837
|
+
function resolveOverflowY(value, overflow) {
|
|
3838
|
+
return normalizeEnum(value, [
|
|
3839
|
+
"visible",
|
|
3840
|
+
"hidden",
|
|
3841
|
+
"scroll"
|
|
3842
|
+
], overflow === "hidden" ? "hidden" : "visible");
|
|
3843
|
+
}
|
|
3844
|
+
function shouldClampLinesToHeight(options) {
|
|
3845
|
+
return options.overflowY === "hidden";
|
|
3846
|
+
}
|
|
3847
|
+
function shouldClipToContentHeight(options) {
|
|
3848
|
+
return options.overflow === "hidden" || options.overflowY === "hidden" || options.overflowY === "scroll";
|
|
3849
|
+
}
|
|
3757
3850
|
function normalizeEllipsis(value) {
|
|
3758
3851
|
if (value === false) return false;
|
|
3759
3852
|
if (typeof value === "string") return value;
|
|
@@ -3767,6 +3860,9 @@ function normalizeMaxLines(value) {
|
|
|
3767
3860
|
function normalizeDimension(value) {
|
|
3768
3861
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
3769
3862
|
}
|
|
3863
|
+
function normalizeGap(value) {
|
|
3864
|
+
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3865
|
+
}
|
|
3770
3866
|
function normalizeSpacing(value) {
|
|
3771
3867
|
if (value == null) return zeroSpacing();
|
|
3772
3868
|
if (Number.isFinite(value)) {
|
|
@@ -3881,6 +3977,27 @@ function resolveRetainedPreparedState(state, options) {
|
|
|
3881
3977
|
function isHardBreak(prepared, line) {
|
|
3882
3978
|
return prepared.kinds?.[line.end.segmentIndex] === "hard-break";
|
|
3883
3979
|
}
|
|
3980
|
+
function splitParagraphText(text, whiteSpace) {
|
|
3981
|
+
const normalized = String(text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3982
|
+
const paragraphs = [];
|
|
3983
|
+
const lines = normalized.split("\n");
|
|
3984
|
+
let currentLines = [];
|
|
3985
|
+
const pushCurrentParagraph = () => {
|
|
3986
|
+
if (currentLines.length === 0) return;
|
|
3987
|
+
const paragraph = whiteSpace === "pre-wrap" ? currentLines.join("\n") : currentLines.join("\n").replace(/\s+/gu, " ").trim();
|
|
3988
|
+
currentLines = [];
|
|
3989
|
+
if (paragraph.length > 0) paragraphs.push(paragraph);
|
|
3990
|
+
};
|
|
3991
|
+
lines.forEach((line) => {
|
|
3992
|
+
if (/^[\t ]*$/u.test(line)) {
|
|
3993
|
+
pushCurrentParagraph();
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
currentLines.push(line);
|
|
3997
|
+
});
|
|
3998
|
+
pushCurrentParagraph();
|
|
3999
|
+
return paragraphs;
|
|
4000
|
+
}
|
|
3884
4001
|
function normalizeNativeText(text, whiteSpace) {
|
|
3885
4002
|
if (whiteSpace === "pre-wrap") return String(text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3886
4003
|
return String(text ?? "").replace(/\s+/gu, " ").trim();
|
|
@@ -4166,6 +4283,7 @@ var paParagraph = class paParagraph {
|
|
|
4166
4283
|
if (!ctx || typeof ctx.fillText !== "function") throw new TypeError("drawText() expects a CanvasRenderingContext2D.");
|
|
4167
4284
|
const fill = options.fill !== false;
|
|
4168
4285
|
const stroke = options.stroke === true;
|
|
4286
|
+
const scrollTop = clampScrollTop(options.scrollTop, this.metrics.maxScrollTop);
|
|
4169
4287
|
if (!fill && !stroke) return;
|
|
4170
4288
|
this._syncLayoutWithContext(ctx);
|
|
4171
4289
|
ctx.save();
|
|
@@ -4174,12 +4292,13 @@ var paParagraph = class paParagraph {
|
|
|
4174
4292
|
ctx.textBaseline = "alphabetic";
|
|
4175
4293
|
if (options.fillStyle != null) ctx.fillStyle = options.fillStyle;
|
|
4176
4294
|
if (options.strokeStyle != null) ctx.strokeStyle = options.strokeStyle;
|
|
4177
|
-
if (this.options
|
|
4295
|
+
if (shouldClipParagraph(this.options) && this._state.layoutBox?.clipBox) {
|
|
4178
4296
|
const clipBox = this._state.layoutBox.clipBox;
|
|
4179
4297
|
ctx.beginPath();
|
|
4180
4298
|
ctx.rect(clipBox.x, clipBox.y, clipBox.w, clipBox.h);
|
|
4181
4299
|
ctx.clip();
|
|
4182
4300
|
}
|
|
4301
|
+
if (scrollTop > 0) ctx.translate(0, -scrollTop);
|
|
4183
4302
|
this._state.lines.forEach((line) => {
|
|
4184
4303
|
line.fragments.forEach((fragment) => {
|
|
4185
4304
|
if (fill) ctx.fillText(fragment.text, fragment.x, line.baseline);
|
|
@@ -4230,6 +4349,8 @@ var paParagraph = class paParagraph {
|
|
|
4230
4349
|
this.metrics = {
|
|
4231
4350
|
...state.metrics,
|
|
4232
4351
|
bbox: { ...state.metrics.bbox },
|
|
4352
|
+
viewportHeight: state.metrics.viewportHeight,
|
|
4353
|
+
maxScrollTop: state.metrics.maxScrollTop,
|
|
4233
4354
|
contentBox: state.metrics.contentBox ? { ...state.metrics.contentBox } : void 0,
|
|
4234
4355
|
paddingBox: state.metrics.paddingBox ? { ...state.metrics.paddingBox } : void 0,
|
|
4235
4356
|
marginBox: state.metrics.marginBox ? { ...state.metrics.marginBox } : void 0,
|
|
@@ -4324,6 +4445,13 @@ function normalizeParagraphPointOptions(options = {}) {
|
|
|
4324
4445
|
layout: options.layout ?? "current"
|
|
4325
4446
|
};
|
|
4326
4447
|
}
|
|
4448
|
+
function shouldClipParagraph(options) {
|
|
4449
|
+
return options.overflow === "hidden" || options.overflowY === "hidden" || options.overflowY === "scroll";
|
|
4450
|
+
}
|
|
4451
|
+
function clampScrollTop(value, maxScrollTop) {
|
|
4452
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
4453
|
+
return Math.min(Number(value), maxScrollTop ?? 0);
|
|
4454
|
+
}
|
|
4327
4455
|
//#endregion
|
|
4328
4456
|
//#region src/paFont/paFont.js
|
|
4329
4457
|
var browserFontRegistrationId = 0;
|