pa_font 0.3.4 → 0.3.5
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 +4 -2
- package/dist/paFont.cjs +147 -47
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +147 -47
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +1 -1
- package/package.json +1 -1
package/USAGE.md
CHANGED
|
@@ -82,7 +82,8 @@ const regions = paragraph.toRegions({ step: 6, openWidth: 1 });
|
|
|
82
82
|
|
|
83
83
|
주요 옵션:
|
|
84
84
|
|
|
85
|
-
- `x`, `y`: baseline 시작 위치
|
|
85
|
+
- `x`, `y`: 기본값은 baseline 시작 위치
|
|
86
|
+
- `anchor`: 주면 `x`, `y`를 실제 글자 bbox 기준점으로 사용
|
|
86
87
|
- `size`: 글자 크기
|
|
87
88
|
- `flatten`: 곡선 평탄화 허용 오차
|
|
88
89
|
- `kerning`, `letterSpacing`, `tracking`
|
|
@@ -95,6 +96,7 @@ const regions = paragraph.toRegions({ step: 6, openWidth: 1 });
|
|
|
95
96
|
### `font.metrics(value, options?)`
|
|
96
97
|
|
|
97
98
|
텍스트 폭과 bounding box만 빠르게 계산합니다.
|
|
99
|
+
`anchor`를 주면 반환되는 `bbox`도 같은 기준점으로 이동된 값입니다.
|
|
98
100
|
|
|
99
101
|
```js
|
|
100
102
|
const metrics = font.metrics("안녕", { size: 120 });
|
|
@@ -135,7 +137,7 @@ console.log(metrics.width, metrics.bbox);
|
|
|
135
137
|
빈 줄 하나 이상은 새 문단으로 처리됩니다.
|
|
136
138
|
|
|
137
139
|
- `gap`: 문단 사이 간격. `lineHeight` 배수이며 기본값은 `0.5`
|
|
138
|
-
- `anchor`: 기준점을 옮깁니다. `anchor: 0.5`면 CSS의 `translate(-50%, -50%)`처럼 동작
|
|
140
|
+
- `anchor`: 실제 렌더된 글자 bbox 기준으로 기준점을 옮깁니다. `anchor: 0.5`면 CSS의 `translate(-50%, -50%)`처럼 동작
|
|
139
141
|
- `whiteSpace: "normal"`이면 문단 안 단일 줄바꿈은 공백으로 정리
|
|
140
142
|
- `whiteSpace: "pre-wrap"`이면 문단 안 단일 줄바꿈 유지
|
|
141
143
|
|
package/dist/paFont.cjs
CHANGED
|
@@ -279,7 +279,7 @@ function mod(value, divisor) {
|
|
|
279
279
|
function layoutGlyphs(font, value, opts) {
|
|
280
280
|
const glyphs = [];
|
|
281
281
|
const renderOptions = toRenderOptions(opts);
|
|
282
|
-
return {
|
|
282
|
+
return applyAnchorToGlyphLayout(font, value, {
|
|
283
283
|
text: value,
|
|
284
284
|
glyphs,
|
|
285
285
|
metrics: {
|
|
@@ -296,19 +296,43 @@ function layoutGlyphs(font, value, opts) {
|
|
|
296
296
|
y: opts.y,
|
|
297
297
|
size: opts.size
|
|
298
298
|
}
|
|
299
|
+
}, opts);
|
|
300
|
+
}
|
|
301
|
+
function applyAnchorToGlyphLayout(font, value, layout, opts) {
|
|
302
|
+
if (opts.anchor == null || layout.glyphs.length === 0) return layout;
|
|
303
|
+
const { bbox } = measureText(font, value, {
|
|
304
|
+
...opts,
|
|
305
|
+
anchor: null
|
|
306
|
+
});
|
|
307
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
308
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
309
|
+
if (tx === 0 && ty === 0) return layout;
|
|
310
|
+
return {
|
|
311
|
+
text: value,
|
|
312
|
+
glyphs: layout.glyphs.map((glyph) => ({
|
|
313
|
+
...glyph,
|
|
314
|
+
x: glyph.x + tx,
|
|
315
|
+
y: glyph.y + ty
|
|
316
|
+
})),
|
|
317
|
+
metrics: {
|
|
318
|
+
...layout.metrics,
|
|
319
|
+
x: layout.metrics.x + tx,
|
|
320
|
+
y: layout.metrics.y + ty
|
|
321
|
+
}
|
|
299
322
|
};
|
|
300
323
|
}
|
|
301
324
|
function measureText(font, value, opts) {
|
|
302
325
|
const renderOptions = toRenderOptions(opts);
|
|
303
|
-
const
|
|
326
|
+
const bbox = resolveMeasuredBBox(font.getPath(value, opts.x, opts.y, opts.size, renderOptions).getBoundingBox(), opts.x, opts.y);
|
|
327
|
+
if (opts.anchor == null) return {
|
|
328
|
+
width: measureAdvanceWidth(font, value, opts),
|
|
329
|
+
bbox
|
|
330
|
+
};
|
|
331
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
332
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
304
333
|
return {
|
|
305
334
|
width: measureAdvanceWidth(font, value, opts),
|
|
306
|
-
bbox:
|
|
307
|
-
x: box.x1,
|
|
308
|
-
y: box.y1,
|
|
309
|
-
w: box.x2 - box.x1,
|
|
310
|
-
h: box.y2 - box.y1
|
|
311
|
-
}
|
|
335
|
+
bbox: translateRect(bbox, tx, ty)
|
|
312
336
|
};
|
|
313
337
|
}
|
|
314
338
|
function measureAdvanceWidth(font, value, opts) {
|
|
@@ -526,6 +550,7 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
|
|
|
526
550
|
}
|
|
527
551
|
function normalizeTextOptions(options = {}) {
|
|
528
552
|
const size = normalizePositive(options.size, 72);
|
|
553
|
+
const anchor = normalizeAnchor(options.anchor);
|
|
529
554
|
return {
|
|
530
555
|
x: normalizeNumber(options.x, 0),
|
|
531
556
|
y: normalizeNumber(options.y, 0),
|
|
@@ -535,11 +560,60 @@ function normalizeTextOptions(options = {}) {
|
|
|
535
560
|
kerning: options.kerning !== false,
|
|
536
561
|
letterSpacing: options.letterSpacing == null ? void 0 : normalizeNumber(options.letterSpacing, 0),
|
|
537
562
|
tracking: options.tracking == null ? void 0 : normalizeNumber(options.tracking, 0),
|
|
563
|
+
anchor,
|
|
538
564
|
script: options.script,
|
|
539
565
|
language: options.language,
|
|
540
566
|
features: options.features
|
|
541
567
|
};
|
|
542
568
|
}
|
|
569
|
+
function normalizeAnchor(value) {
|
|
570
|
+
if (value == null) return null;
|
|
571
|
+
if (Number.isFinite(value)) {
|
|
572
|
+
const next = Number(value);
|
|
573
|
+
return {
|
|
574
|
+
x: next,
|
|
575
|
+
y: next
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (Array.isArray(value)) {
|
|
579
|
+
if (value.length === 1 && Number.isFinite(value[0])) {
|
|
580
|
+
const next = Number(value[0]);
|
|
581
|
+
return {
|
|
582
|
+
x: next,
|
|
583
|
+
y: next
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
|
|
587
|
+
x: Number(value[0]),
|
|
588
|
+
y: Number(value[1])
|
|
589
|
+
};
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (typeof value === "object") {
|
|
593
|
+
const hasX = Number.isFinite(value.x);
|
|
594
|
+
const hasY = Number.isFinite(value.y);
|
|
595
|
+
if (!hasX && !hasY) return null;
|
|
596
|
+
return {
|
|
597
|
+
x: hasX ? Number(value.x) : 0,
|
|
598
|
+
y: hasY ? Number(value.y) : 0
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
function resolveMeasuredBBox(box, fallbackX, fallbackY) {
|
|
604
|
+
if (Number.isFinite(box?.x1) && Number.isFinite(box?.y1) && Number.isFinite(box?.x2) && Number.isFinite(box?.y2)) return {
|
|
605
|
+
x: box.x1,
|
|
606
|
+
y: box.y1,
|
|
607
|
+
w: box.x2 - box.x1,
|
|
608
|
+
h: box.y2 - box.y1
|
|
609
|
+
};
|
|
610
|
+
return {
|
|
611
|
+
x: normalizeNumber(fallbackX, 0),
|
|
612
|
+
y: normalizeNumber(fallbackY, 0),
|
|
613
|
+
w: 0,
|
|
614
|
+
h: 0
|
|
615
|
+
};
|
|
616
|
+
}
|
|
543
617
|
function defaultEdgeEpsilon(size) {
|
|
544
618
|
return Math.min(1, Math.max(.01, size * .0025));
|
|
545
619
|
}
|
|
@@ -3318,7 +3392,7 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3318
3392
|
const layoutState = pretextState != null && canUsePretextLayout(pretextState, normalized) ? pretextState : layoutParagraphsWithNative(fontInstance, paragraphs, normalized, layoutBox);
|
|
3319
3393
|
const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
|
|
3320
3394
|
const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
|
|
3321
|
-
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3395
|
+
const textBBox = normalized.anchor == null ? combineRects(lines.map((line) => line.bbox)) ?? emptyRect() : measurePositionedTextBBox(fontInstance, lines, normalized);
|
|
3322
3396
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3323
3397
|
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3324
3398
|
const anchoredLayout = applyParagraphAnchor(lines, textBBox, finalizeLayoutBox(layoutBox, normalized, textHeight), normalized.anchor);
|
|
@@ -3857,6 +3931,35 @@ function translateLayoutBox(layoutBox, tx, ty) {
|
|
|
3857
3931
|
clipBox: translateRect(layoutBox.clipBox, tx, ty)
|
|
3858
3932
|
};
|
|
3859
3933
|
}
|
|
3934
|
+
function measurePositionedTextBBox(fontInstance, lines, options) {
|
|
3935
|
+
const visibleBoxes = [];
|
|
3936
|
+
const measureOptions = {
|
|
3937
|
+
x: 0,
|
|
3938
|
+
y: 0,
|
|
3939
|
+
size: options.size,
|
|
3940
|
+
flatten: options.flatten,
|
|
3941
|
+
edgeEpsilon: options.edgeEpsilon,
|
|
3942
|
+
kerning: options.kerning,
|
|
3943
|
+
letterSpacing: options.letterSpacing,
|
|
3944
|
+
tracking: options.tracking,
|
|
3945
|
+
anchor: null,
|
|
3946
|
+
script: options.script,
|
|
3947
|
+
language: options.language,
|
|
3948
|
+
features: options.features
|
|
3949
|
+
};
|
|
3950
|
+
lines.forEach((line) => {
|
|
3951
|
+
line.fragments.forEach((fragment) => {
|
|
3952
|
+
if (fragment.text.length === 0) return;
|
|
3953
|
+
const { bbox } = measureText(fontInstance.font, fragment.text, {
|
|
3954
|
+
...measureOptions,
|
|
3955
|
+
x: fragment.x,
|
|
3956
|
+
y: line.baseline
|
|
3957
|
+
});
|
|
3958
|
+
if (bbox.w > 0 || bbox.h > 0) visibleBoxes.push(bbox);
|
|
3959
|
+
});
|
|
3960
|
+
});
|
|
3961
|
+
return combineRects(visibleBoxes) ?? combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3962
|
+
}
|
|
3860
3963
|
function resolveContainerDimension(explicit, fallback) {
|
|
3861
3964
|
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
3862
3965
|
if (Number.isFinite(fallback) && fallback > 0) return fallback;
|
|
@@ -3885,40 +3988,6 @@ function normalizeDimension(value) {
|
|
|
3885
3988
|
function normalizeGap(value) {
|
|
3886
3989
|
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3887
3990
|
}
|
|
3888
|
-
function normalizeAnchor(value) {
|
|
3889
|
-
if (value == null) return null;
|
|
3890
|
-
if (Number.isFinite(value)) {
|
|
3891
|
-
const next = Number(value);
|
|
3892
|
-
return {
|
|
3893
|
-
x: next,
|
|
3894
|
-
y: next
|
|
3895
|
-
};
|
|
3896
|
-
}
|
|
3897
|
-
if (Array.isArray(value)) {
|
|
3898
|
-
if (value.length === 1 && Number.isFinite(value[0])) {
|
|
3899
|
-
const next = Number(value[0]);
|
|
3900
|
-
return {
|
|
3901
|
-
x: next,
|
|
3902
|
-
y: next
|
|
3903
|
-
};
|
|
3904
|
-
}
|
|
3905
|
-
if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
|
|
3906
|
-
x: Number(value[0]),
|
|
3907
|
-
y: Number(value[1])
|
|
3908
|
-
};
|
|
3909
|
-
return null;
|
|
3910
|
-
}
|
|
3911
|
-
if (typeof value === "object") {
|
|
3912
|
-
const hasX = Number.isFinite(value.x);
|
|
3913
|
-
const hasY = Number.isFinite(value.y);
|
|
3914
|
-
if (!hasX && !hasY) return null;
|
|
3915
|
-
return {
|
|
3916
|
-
x: hasX ? Number(value.x) : 0,
|
|
3917
|
-
y: hasY ? Number(value.y) : 0
|
|
3918
|
-
};
|
|
3919
|
-
}
|
|
3920
|
-
return null;
|
|
3921
|
-
}
|
|
3922
3991
|
function normalizeSpacing(value) {
|
|
3923
3992
|
if (value == null) return zeroSpacing();
|
|
3924
3993
|
if (Number.isFinite(value)) {
|
|
@@ -4529,13 +4598,16 @@ var paFont = class paFont {
|
|
|
4529
4598
|
}
|
|
4530
4599
|
text(value, options = {}) {
|
|
4531
4600
|
const opts = normalizeTextOptions(options);
|
|
4601
|
+
assertValidAnchorOption("font.text()", options, opts.anchor);
|
|
4532
4602
|
return createTextShape(this._layoutText(String(value ?? ""), opts), opts, this);
|
|
4533
4603
|
}
|
|
4534
4604
|
glyph(value, options = {}) {
|
|
4535
4605
|
const opts = normalizeTextOptions(options);
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4606
|
+
assertValidAnchorOption("font.glyph()", options, opts.anchor);
|
|
4607
|
+
const text = Array.from(String(value ?? ""))[0] ?? "";
|
|
4608
|
+
const glyph = this.font.charToGlyph(text);
|
|
4609
|
+
const layout = {
|
|
4610
|
+
text,
|
|
4539
4611
|
glyphs: [{
|
|
4540
4612
|
glyph,
|
|
4541
4613
|
x: opts.x,
|
|
@@ -4549,10 +4621,12 @@ var paFont = class paFont {
|
|
|
4549
4621
|
y: opts.y,
|
|
4550
4622
|
size: opts.size
|
|
4551
4623
|
}
|
|
4552
|
-
}
|
|
4624
|
+
};
|
|
4625
|
+
return createTextShape(anchorSingleGlyphLayout(this.font, text, layout, opts), opts, this);
|
|
4553
4626
|
}
|
|
4554
4627
|
metrics(value, options = {}) {
|
|
4555
4628
|
const opts = normalizeTextOptions(options);
|
|
4629
|
+
assertValidAnchorOption("font.metrics()", options, opts.anchor);
|
|
4556
4630
|
return measureText(this.font, String(value ?? ""), opts);
|
|
4557
4631
|
}
|
|
4558
4632
|
paragraph(value, options = {}) {
|
|
@@ -4607,6 +4681,32 @@ function normalizeShapeVariantValue(value) {
|
|
|
4607
4681
|
function toShapeVariantKey(value) {
|
|
4608
4682
|
return normalizeShapeVariantValue(value).toFixed(6);
|
|
4609
4683
|
}
|
|
4684
|
+
function assertValidAnchorOption(methodName, sourceOptions, anchor) {
|
|
4685
|
+
if (sourceOptions?.anchor != null && anchor == null) throw new TypeError(`${methodName} option "anchor" must be a number, [x, y], or { x, y }.`);
|
|
4686
|
+
}
|
|
4687
|
+
function anchorSingleGlyphLayout(font, text, layout, opts) {
|
|
4688
|
+
if (opts.anchor == null || text.length === 0) return layout;
|
|
4689
|
+
const { bbox } = measureText(font, text, {
|
|
4690
|
+
...opts,
|
|
4691
|
+
anchor: null
|
|
4692
|
+
});
|
|
4693
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
4694
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
4695
|
+
if (tx === 0 && ty === 0) return layout;
|
|
4696
|
+
return {
|
|
4697
|
+
...layout,
|
|
4698
|
+
glyphs: layout.glyphs.map((glyph) => ({
|
|
4699
|
+
...glyph,
|
|
4700
|
+
x: glyph.x + tx,
|
|
4701
|
+
y: glyph.y + ty
|
|
4702
|
+
})),
|
|
4703
|
+
metrics: {
|
|
4704
|
+
...layout.metrics,
|
|
4705
|
+
x: layout.metrics.x + tx,
|
|
4706
|
+
y: layout.metrics.y + ty
|
|
4707
|
+
}
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4610
4710
|
async function fetchFontBytes(source) {
|
|
4611
4711
|
const response = await fetch(source);
|
|
4612
4712
|
if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);
|