pa_font 0.3.1 → 0.3.3
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 -25
- package/dist/paFont.cjs +92 -44
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +92 -44
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +8 -4
- package/package.json +1 -1
package/USAGE.md
CHANGED
|
@@ -111,7 +111,7 @@ console.log(metrics.width, metrics.bbox);
|
|
|
111
111
|
- `margin`, `padding`: `24`, `"24 32"`, `[24, 32]`, `{ x: 32, y: 24 }`, `{ top, right, bottom, left }`
|
|
112
112
|
- `width`: 생략하면 `drawText(ctx)` 시 현재 canvas 폭 기준으로 자동 계산
|
|
113
113
|
- `height`: `overflow: "hidden"`과 같이 쓰면 clip/clamp
|
|
114
|
-
- `x`, `y`, `size`, `lineHeight`, `gap`, `align`
|
|
114
|
+
- `x`, `y`, `size`, `lineHeight`, `gap`, `anchor`, `align`
|
|
115
115
|
|
|
116
116
|
세부 제어 옵션:
|
|
117
117
|
|
|
@@ -119,7 +119,6 @@ console.log(metrics.width, metrics.bbox);
|
|
|
119
119
|
- `overflowWrap`: `normal | break-word | anywhere`
|
|
120
120
|
- `wordBreak`: `normal | break-all | keep-all`
|
|
121
121
|
- `overflow`: `visible | hidden`
|
|
122
|
-
- `overflowY`: `visible | hidden | scroll`
|
|
123
122
|
- `textOverflow`: `clip | ellipsis`
|
|
124
123
|
- `fontFamily`, `fontWeight`, `fontStyle`, `font`
|
|
125
124
|
- `engine`: `pretext | native`
|
|
@@ -136,9 +135,15 @@ console.log(metrics.width, metrics.bbox);
|
|
|
136
135
|
빈 줄 하나 이상은 새 문단으로 처리됩니다.
|
|
137
136
|
|
|
138
137
|
- `gap`: 문단 사이 간격. `lineHeight` 배수이며 기본값은 `0.5`
|
|
138
|
+
- `anchor`: 기준점을 옮깁니다. `anchor: 0.5`면 CSS의 `translate(-50%, -50%)`처럼 동작
|
|
139
139
|
- `whiteSpace: "normal"`이면 문단 안 단일 줄바꿈은 공백으로 정리
|
|
140
140
|
- `whiteSpace: "pre-wrap"`이면 문단 안 단일 줄바꿈 유지
|
|
141
|
-
|
|
141
|
+
|
|
142
|
+
`anchor`는 다음 형식을 지원합니다.
|
|
143
|
+
|
|
144
|
+
- `0.5` -> `{ x: 0.5, y: 0.5 }`
|
|
145
|
+
- `[0.5, 0]`
|
|
146
|
+
- `{ x: 0.5, y: 0.5 }`
|
|
142
147
|
|
|
143
148
|
## paParagraph API
|
|
144
149
|
|
|
@@ -168,6 +173,19 @@ const mobile = paragraph.relayout({ width: 280 });
|
|
|
168
173
|
엄격한 `overflowWrap: "normal"`이 필요한 경우만 native로 내려갑니다.
|
|
169
174
|
실제로 사용된 엔진은 `paragraph.layoutEngine`에서 확인할 수 있습니다.
|
|
170
175
|
|
|
176
|
+
가운데를 기준점으로 두고 싶다면:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const paragraph = font.paragraph("asdf", {
|
|
180
|
+
x: window.innerWidth * 0.5,
|
|
181
|
+
y: window.innerHeight * 0.5,
|
|
182
|
+
width: window.innerWidth,
|
|
183
|
+
size: window.innerHeight * 0.5,
|
|
184
|
+
align: "center",
|
|
185
|
+
anchor: 0.5,
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
171
189
|
### `paragraph.drawText(ctx, options?)`
|
|
172
190
|
|
|
173
191
|
현재 문단 레이아웃을 canvas 텍스트로 그립니다.
|
|
@@ -184,28 +202,6 @@ paragraph.drawText(ctx, {
|
|
|
184
202
|
- `strokeStyle`
|
|
185
203
|
- `fill`
|
|
186
204
|
- `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`에서 확인할 수 있습니다.
|
|
209
205
|
|
|
210
206
|
### `paragraph.toShape(options?)`
|
|
211
207
|
|
package/dist/paFont.cjs
CHANGED
|
@@ -3321,30 +3321,28 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3321
3321
|
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3322
3322
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3323
3323
|
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3324
|
-
const
|
|
3324
|
+
const anchoredLayout = applyParagraphAnchor(lines, textBBox, finalizeLayoutBox(layoutBox, normalized, textHeight), normalized.anchor);
|
|
3325
3325
|
const cachedPrepared = pretextState?.prepared ?? retainedPreparedState.prepared ?? null;
|
|
3326
3326
|
const cachedPreparedWhiteSpace = pretextState?.preparedWhiteSpace ?? retainedPreparedState.preparedWhiteSpace ?? null;
|
|
3327
3327
|
return {
|
|
3328
3328
|
options: normalized,
|
|
3329
|
-
lines,
|
|
3329
|
+
lines: anchoredLayout.lines,
|
|
3330
3330
|
metrics: {
|
|
3331
|
-
x:
|
|
3332
|
-
y:
|
|
3331
|
+
x: anchoredLayout.layoutBox.contentBox.x,
|
|
3332
|
+
y: anchoredLayout.layoutBox.contentBox.y,
|
|
3333
3333
|
width: textWidth,
|
|
3334
3334
|
height: textHeight,
|
|
3335
|
-
viewportHeight: finalLayoutBox.contentBox.h,
|
|
3336
|
-
maxScrollTop: Math.max(0, textHeight - finalLayoutBox.contentBox.h),
|
|
3337
3335
|
lineCount: lines.length,
|
|
3338
|
-
bbox: textBBox,
|
|
3339
|
-
contentBox: { ...
|
|
3340
|
-
paddingBox: { ...
|
|
3341
|
-
marginBox: { ...
|
|
3342
|
-
clipBox: { ...
|
|
3336
|
+
bbox: anchoredLayout.textBBox,
|
|
3337
|
+
contentBox: { ...anchoredLayout.layoutBox.contentBox },
|
|
3338
|
+
paddingBox: { ...anchoredLayout.layoutBox.paddingBox },
|
|
3339
|
+
marginBox: { ...anchoredLayout.layoutBox.marginBox },
|
|
3340
|
+
clipBox: { ...anchoredLayout.layoutBox.clipBox }
|
|
3343
3341
|
},
|
|
3344
3342
|
prepared: cachedPrepared,
|
|
3345
3343
|
preparedWhiteSpace: cachedPreparedWhiteSpace,
|
|
3346
3344
|
layoutEngine: layoutState.layoutEngine,
|
|
3347
|
-
layoutBox:
|
|
3345
|
+
layoutBox: anchoredLayout.layoutBox,
|
|
3348
3346
|
containerWidth: layoutBox.containerWidth,
|
|
3349
3347
|
containerHeight: layoutBox.containerHeight
|
|
3350
3348
|
};
|
|
@@ -3360,12 +3358,11 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3360
3358
|
const width = normalizeDimension(options.width);
|
|
3361
3359
|
const height = normalizeDimension(options.height);
|
|
3362
3360
|
const gap = normalizeGap(options.gap);
|
|
3363
|
-
const
|
|
3364
|
-
const overflowY = resolveOverflowY(options.overflowY, overflow);
|
|
3361
|
+
const anchor = normalizeAnchor(options.anchor);
|
|
3365
3362
|
if (options.width != null && width == null) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
|
|
3366
3363
|
if (options.height != null && height == null) throw new TypeError("font.paragraph() option \"height\" must be a positive number.");
|
|
3367
3364
|
if (options.gap != null && gap == null) throw new TypeError("font.paragraph() option \"gap\" must be a non-negative number.");
|
|
3368
|
-
if (
|
|
3365
|
+
if (options.anchor != null && anchor == null) throw new TypeError("font.paragraph() option \"anchor\" must be a number, [x, y], or { x, y }.");
|
|
3369
3366
|
const font = resolveCanvasFont(fontInstance, textOptions.size, options);
|
|
3370
3367
|
return {
|
|
3371
3368
|
...textOptions,
|
|
@@ -3381,6 +3378,10 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3381
3378
|
width,
|
|
3382
3379
|
height,
|
|
3383
3380
|
gap: gap ?? DEFAULT_PARAGRAPH_GAP,
|
|
3381
|
+
anchor: anchor ?? {
|
|
3382
|
+
x: 0,
|
|
3383
|
+
y: 0
|
|
3384
|
+
},
|
|
3384
3385
|
lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
|
|
3385
3386
|
align: normalizeEnum(options.align, [
|
|
3386
3387
|
"left",
|
|
@@ -3403,8 +3404,7 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3403
3404
|
"break-all",
|
|
3404
3405
|
"keep-all"
|
|
3405
3406
|
], wrapDefaults.wordBreak),
|
|
3406
|
-
overflow,
|
|
3407
|
-
overflowY,
|
|
3407
|
+
overflow: normalizeEnum(options.overflow, ["visible", "hidden"], "visible"),
|
|
3408
3408
|
textOverflow: normalizeNullableEnum(options.textOverflow, ["clip", "ellipsis"], null),
|
|
3409
3409
|
maxLines: normalizeMaxLines(options.maxLines),
|
|
3410
3410
|
ellipsis: normalizeEllipsis(options.ellipsis),
|
|
@@ -3593,7 +3593,7 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
|
|
|
3593
3593
|
const contentWidth = layoutBox.contentWidth;
|
|
3594
3594
|
const result = lines.map((line) => ({ ...line }));
|
|
3595
3595
|
let lineLimit = options.maxLines;
|
|
3596
|
-
if (
|
|
3596
|
+
if (options.overflow === "hidden" && layoutBox.clipContentHeight != null) {
|
|
3597
3597
|
const visibleLineCount = countVisibleLinesForHeight(result, options, layoutBox.clipContentHeight);
|
|
3598
3598
|
lineLimit = lineLimit == null ? visibleLineCount : Math.min(lineLimit, visibleLineCount);
|
|
3599
3599
|
}
|
|
@@ -3818,10 +3818,48 @@ function finalizeLayoutBox(layoutBox, options, textHeight) {
|
|
|
3818
3818
|
x: layoutBox.contentX,
|
|
3819
3819
|
y: layoutBox.contentY,
|
|
3820
3820
|
w: layoutBox.contentWidth,
|
|
3821
|
-
h:
|
|
3821
|
+
h: options.overflow === "hidden" ? contentHeight : textHeight
|
|
3822
3822
|
}
|
|
3823
3823
|
};
|
|
3824
3824
|
}
|
|
3825
|
+
function applyParagraphAnchor(lines, textBBox, layoutBox, anchor) {
|
|
3826
|
+
if (anchor == null || Math.abs(anchor.x) <= JUSTIFY_EPSILON && Math.abs(anchor.y) <= JUSTIFY_EPSILON) return {
|
|
3827
|
+
lines,
|
|
3828
|
+
textBBox,
|
|
3829
|
+
layoutBox
|
|
3830
|
+
};
|
|
3831
|
+
const tx = -layoutBox.contentBox.w * anchor.x;
|
|
3832
|
+
const ty = -layoutBox.contentBox.h * anchor.y;
|
|
3833
|
+
return {
|
|
3834
|
+
lines: translatePositionedLines(lines, tx, ty),
|
|
3835
|
+
textBBox: translateRect(textBBox, tx, ty),
|
|
3836
|
+
layoutBox: translateLayoutBox(layoutBox, tx, ty)
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
function translatePositionedLines(lines, tx, ty) {
|
|
3840
|
+
return lines.map((line) => ({
|
|
3841
|
+
...line,
|
|
3842
|
+
x: line.x + tx,
|
|
3843
|
+
y: line.y + ty,
|
|
3844
|
+
baseline: line.baseline + ty,
|
|
3845
|
+
bbox: translateRect(line.bbox, tx, ty),
|
|
3846
|
+
fragments: line.fragments.map((fragment) => ({
|
|
3847
|
+
...fragment,
|
|
3848
|
+
x: fragment.x + tx
|
|
3849
|
+
}))
|
|
3850
|
+
}));
|
|
3851
|
+
}
|
|
3852
|
+
function translateLayoutBox(layoutBox, tx, ty) {
|
|
3853
|
+
return {
|
|
3854
|
+
...layoutBox,
|
|
3855
|
+
contentX: layoutBox.contentX + tx,
|
|
3856
|
+
contentY: layoutBox.contentY + ty,
|
|
3857
|
+
contentBox: translateRect(layoutBox.contentBox, tx, ty),
|
|
3858
|
+
paddingBox: translateRect(layoutBox.paddingBox, tx, ty),
|
|
3859
|
+
marginBox: translateRect(layoutBox.marginBox, tx, ty),
|
|
3860
|
+
clipBox: translateRect(layoutBox.clipBox, tx, ty)
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3825
3863
|
function resolveContainerDimension(explicit, fallback) {
|
|
3826
3864
|
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
3827
3865
|
if (Number.isFinite(fallback) && fallback > 0) return fallback;
|
|
@@ -3834,19 +3872,6 @@ function normalizeNullableEnum(value, supported, fallback) {
|
|
|
3834
3872
|
if (value == null) return fallback;
|
|
3835
3873
|
return typeof value === "string" && supported.includes(value) ? value : fallback;
|
|
3836
3874
|
}
|
|
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
|
-
}
|
|
3850
3875
|
function normalizeEllipsis(value) {
|
|
3851
3876
|
if (value === false) return false;
|
|
3852
3877
|
if (typeof value === "string") return value;
|
|
@@ -3863,6 +3888,40 @@ function normalizeDimension(value) {
|
|
|
3863
3888
|
function normalizeGap(value) {
|
|
3864
3889
|
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3865
3890
|
}
|
|
3891
|
+
function normalizeAnchor(value) {
|
|
3892
|
+
if (value == null) return null;
|
|
3893
|
+
if (Number.isFinite(value)) {
|
|
3894
|
+
const next = Number(value);
|
|
3895
|
+
return {
|
|
3896
|
+
x: next,
|
|
3897
|
+
y: next
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
if (Array.isArray(value)) {
|
|
3901
|
+
if (value.length === 1 && Number.isFinite(value[0])) {
|
|
3902
|
+
const next = Number(value[0]);
|
|
3903
|
+
return {
|
|
3904
|
+
x: next,
|
|
3905
|
+
y: next
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
|
|
3909
|
+
x: Number(value[0]),
|
|
3910
|
+
y: Number(value[1])
|
|
3911
|
+
};
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
if (typeof value === "object") {
|
|
3915
|
+
const hasX = Number.isFinite(value.x);
|
|
3916
|
+
const hasY = Number.isFinite(value.y);
|
|
3917
|
+
if (!hasX && !hasY) return null;
|
|
3918
|
+
return {
|
|
3919
|
+
x: hasX ? Number(value.x) : 0,
|
|
3920
|
+
y: hasY ? Number(value.y) : 0
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
return null;
|
|
3924
|
+
}
|
|
3866
3925
|
function normalizeSpacing(value) {
|
|
3867
3926
|
if (value == null) return zeroSpacing();
|
|
3868
3927
|
if (Number.isFinite(value)) {
|
|
@@ -4283,7 +4342,6 @@ var paParagraph = class paParagraph {
|
|
|
4283
4342
|
if (!ctx || typeof ctx.fillText !== "function") throw new TypeError("drawText() expects a CanvasRenderingContext2D.");
|
|
4284
4343
|
const fill = options.fill !== false;
|
|
4285
4344
|
const stroke = options.stroke === true;
|
|
4286
|
-
const scrollTop = clampScrollTop(options.scrollTop, this.metrics.maxScrollTop);
|
|
4287
4345
|
if (!fill && !stroke) return;
|
|
4288
4346
|
this._syncLayoutWithContext(ctx);
|
|
4289
4347
|
ctx.save();
|
|
@@ -4292,13 +4350,12 @@ var paParagraph = class paParagraph {
|
|
|
4292
4350
|
ctx.textBaseline = "alphabetic";
|
|
4293
4351
|
if (options.fillStyle != null) ctx.fillStyle = options.fillStyle;
|
|
4294
4352
|
if (options.strokeStyle != null) ctx.strokeStyle = options.strokeStyle;
|
|
4295
|
-
if (
|
|
4353
|
+
if (this.options.overflow === "hidden" && this._state.layoutBox?.clipBox) {
|
|
4296
4354
|
const clipBox = this._state.layoutBox.clipBox;
|
|
4297
4355
|
ctx.beginPath();
|
|
4298
4356
|
ctx.rect(clipBox.x, clipBox.y, clipBox.w, clipBox.h);
|
|
4299
4357
|
ctx.clip();
|
|
4300
4358
|
}
|
|
4301
|
-
if (scrollTop > 0) ctx.translate(0, -scrollTop);
|
|
4302
4359
|
this._state.lines.forEach((line) => {
|
|
4303
4360
|
line.fragments.forEach((fragment) => {
|
|
4304
4361
|
if (fill) ctx.fillText(fragment.text, fragment.x, line.baseline);
|
|
@@ -4349,8 +4406,6 @@ var paParagraph = class paParagraph {
|
|
|
4349
4406
|
this.metrics = {
|
|
4350
4407
|
...state.metrics,
|
|
4351
4408
|
bbox: { ...state.metrics.bbox },
|
|
4352
|
-
viewportHeight: state.metrics.viewportHeight,
|
|
4353
|
-
maxScrollTop: state.metrics.maxScrollTop,
|
|
4354
4409
|
contentBox: state.metrics.contentBox ? { ...state.metrics.contentBox } : void 0,
|
|
4355
4410
|
paddingBox: state.metrics.paddingBox ? { ...state.metrics.paddingBox } : void 0,
|
|
4356
4411
|
marginBox: state.metrics.marginBox ? { ...state.metrics.marginBox } : void 0,
|
|
@@ -4445,13 +4500,6 @@ function normalizeParagraphPointOptions(options = {}) {
|
|
|
4445
4500
|
layout: options.layout ?? "current"
|
|
4446
4501
|
};
|
|
4447
4502
|
}
|
|
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
|
-
}
|
|
4455
4503
|
//#endregion
|
|
4456
4504
|
//#region src/paFont/paFont.js
|
|
4457
4505
|
var browserFontRegistrationId = 0;
|