pa_font 0.3.4 → 0.3.7
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 +43 -4
- package/dist/paFont.cjs +206 -53
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +206 -53
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +14 -3
- package/package.json +1 -1
package/USAGE.md
CHANGED
|
@@ -39,6 +39,26 @@ const regions = shape.toRegions();
|
|
|
39
39
|
const points = shape.toPoints({ step: 8 });
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
문자열 길이에 따라 비슷한 최종 폭으로 맞추고 싶다면 `size`에 객체를 넣을 수 있습니다.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const short = font.text("펑", {
|
|
46
|
+
x: canvasWidth * 0.5,
|
|
47
|
+
y: canvasHeight * 0.5,
|
|
48
|
+
anchor: 0.5,
|
|
49
|
+
size: { width: canvasWidth * 0.7, basis: "bbox" },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const long = font.text("왕밤빵", {
|
|
53
|
+
x: canvasWidth * 0.5,
|
|
54
|
+
y: canvasHeight * 0.5,
|
|
55
|
+
anchor: 0.5,
|
|
56
|
+
size: { width: canvasWidth * 0.7, basis: "bbox", min: 48, max: 320 },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
console.log(short.metrics.size, long.metrics.size);
|
|
60
|
+
```
|
|
61
|
+
|
|
42
62
|
### 2. 문단을 canvas에 그리고, 필요하면 geometry로 바꾸기
|
|
43
63
|
|
|
44
64
|
```js
|
|
@@ -82,12 +102,20 @@ const regions = paragraph.toRegions({ step: 6, openWidth: 1 });
|
|
|
82
102
|
|
|
83
103
|
주요 옵션:
|
|
84
104
|
|
|
85
|
-
- `x`, `y`: baseline 시작 위치
|
|
86
|
-
- `
|
|
105
|
+
- `x`, `y`: 기본값은 baseline 시작 위치
|
|
106
|
+
- `anchor`: 주면 `x`, `y`를 실제 글자 bbox 기준점으로 사용
|
|
107
|
+
- `size`: 글자 크기. `number` 또는 `{ width, basis?, min?, max? }`
|
|
87
108
|
- `flatten`: 곡선 평탄화 허용 오차
|
|
88
109
|
- `kerning`, `letterSpacing`, `tracking`
|
|
89
110
|
- `script`, `language`, `features`
|
|
90
111
|
|
|
112
|
+
`size` 객체를 주면 내부에서 최종 숫자 size를 계산한 뒤 기존 텍스트 생성 로직으로 이어집니다.
|
|
113
|
+
|
|
114
|
+
- `width`: 목표 폭
|
|
115
|
+
- `basis`: `"bbox"` 또는 `"advance"`, 기본값은 `"bbox"`
|
|
116
|
+
- `min`: 최소 size, 기본값은 `0`
|
|
117
|
+
- `max`: 최대 size, 기본값은 `Infinity`
|
|
118
|
+
|
|
91
119
|
### `font.glyph(value, options?)`
|
|
92
120
|
|
|
93
121
|
한 글자만 `PAShape`로 만듭니다.
|
|
@@ -95,10 +123,19 @@ const regions = paragraph.toRegions({ step: 6, openWidth: 1 });
|
|
|
95
123
|
### `font.metrics(value, options?)`
|
|
96
124
|
|
|
97
125
|
텍스트 폭과 bounding box만 빠르게 계산합니다.
|
|
126
|
+
`anchor`를 주면 반환되는 `bbox`도 같은 기준점으로 이동된 값입니다.
|
|
98
127
|
|
|
99
128
|
```js
|
|
100
129
|
const metrics = font.metrics("안녕", { size: 120 });
|
|
101
|
-
console.log(metrics.width, metrics.bbox);
|
|
130
|
+
console.log(metrics.size, metrics.width, metrics.bbox);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const metrics = font.metrics("왕밤빵", {
|
|
135
|
+
size: { width: 600, basis: "advance", min: 80, max: 240 },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(metrics.size, metrics.width, metrics.bbox);
|
|
102
139
|
```
|
|
103
140
|
|
|
104
141
|
### `font.paragraph(value, options?)`
|
|
@@ -113,6 +150,8 @@ console.log(metrics.width, metrics.bbox);
|
|
|
113
150
|
- `height`: `overflow: "hidden"`과 같이 쓰면 clip/clamp
|
|
114
151
|
- `x`, `y`, `size`, `lineHeight`, `gap`, `anchor`, `align`
|
|
115
152
|
|
|
153
|
+
`font.paragraph()`의 `size`는 현재도 숫자만 받습니다.
|
|
154
|
+
|
|
116
155
|
세부 제어 옵션:
|
|
117
156
|
|
|
118
157
|
- `whiteSpace`: `normal | pre-wrap | nowrap`
|
|
@@ -135,7 +174,7 @@ console.log(metrics.width, metrics.bbox);
|
|
|
135
174
|
빈 줄 하나 이상은 새 문단으로 처리됩니다.
|
|
136
175
|
|
|
137
176
|
- `gap`: 문단 사이 간격. `lineHeight` 배수이며 기본값은 `0.5`
|
|
138
|
-
- `anchor`: 기준점을 옮깁니다. `anchor: 0.5`면 CSS의 `translate(-50%, -50%)`처럼 동작
|
|
177
|
+
- `anchor`: 실제 렌더된 글자 bbox 기준으로 기준점을 옮깁니다. `anchor: 0.5`면 CSS의 `translate(-50%, -50%)`처럼 동작
|
|
139
178
|
- `whiteSpace: "normal"`이면 문단 안 단일 줄바꿈은 공백으로 정리
|
|
140
179
|
- `whiteSpace: "pre-wrap"`이면 문단 안 단일 줄바꿈 유지
|
|
141
180
|
|
package/dist/paFont.cjs
CHANGED
|
@@ -276,10 +276,12 @@ function mod(value, divisor) {
|
|
|
276
276
|
}
|
|
277
277
|
//#endregion
|
|
278
278
|
//#region src/paFont/core.js
|
|
279
|
+
var DEFAULT_TEXT_SIZE = 72;
|
|
280
|
+
var DEFAULT_TEXT_SIZE_BASIS = "bbox";
|
|
279
281
|
function layoutGlyphs(font, value, opts) {
|
|
280
282
|
const glyphs = [];
|
|
281
283
|
const renderOptions = toRenderOptions(opts);
|
|
282
|
-
return {
|
|
284
|
+
return applyAnchorToGlyphLayout(font, value, {
|
|
283
285
|
text: value,
|
|
284
286
|
glyphs,
|
|
285
287
|
metrics: {
|
|
@@ -296,19 +298,45 @@ function layoutGlyphs(font, value, opts) {
|
|
|
296
298
|
y: opts.y,
|
|
297
299
|
size: opts.size
|
|
298
300
|
}
|
|
301
|
+
}, opts);
|
|
302
|
+
}
|
|
303
|
+
function applyAnchorToGlyphLayout(font, value, layout, opts) {
|
|
304
|
+
if (opts.anchor == null || layout.glyphs.length === 0) return layout;
|
|
305
|
+
const { bbox } = measureText(font, value, {
|
|
306
|
+
...opts,
|
|
307
|
+
anchor: null
|
|
308
|
+
});
|
|
309
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
310
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
311
|
+
if (tx === 0 && ty === 0) return layout;
|
|
312
|
+
return {
|
|
313
|
+
text: value,
|
|
314
|
+
glyphs: layout.glyphs.map((glyph) => ({
|
|
315
|
+
...glyph,
|
|
316
|
+
x: glyph.x + tx,
|
|
317
|
+
y: glyph.y + ty
|
|
318
|
+
})),
|
|
319
|
+
metrics: {
|
|
320
|
+
...layout.metrics,
|
|
321
|
+
x: layout.metrics.x + tx,
|
|
322
|
+
y: layout.metrics.y + ty
|
|
323
|
+
}
|
|
299
324
|
};
|
|
300
325
|
}
|
|
301
326
|
function measureText(font, value, opts) {
|
|
302
327
|
const renderOptions = toRenderOptions(opts);
|
|
303
|
-
const
|
|
328
|
+
const bbox = resolveMeasuredBBox(font.getPath(value, opts.x, opts.y, opts.size, renderOptions).getBoundingBox(), opts.x, opts.y);
|
|
329
|
+
if (opts.anchor == null) return {
|
|
330
|
+
size: opts.size,
|
|
331
|
+
width: measureAdvanceWidth(font, value, opts),
|
|
332
|
+
bbox
|
|
333
|
+
};
|
|
334
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
335
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
304
336
|
return {
|
|
337
|
+
size: opts.size,
|
|
305
338
|
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
|
-
}
|
|
339
|
+
bbox: translateRect(bbox, tx, ty)
|
|
312
340
|
};
|
|
313
341
|
}
|
|
314
342
|
function measureAdvanceWidth(font, value, opts) {
|
|
@@ -524,8 +552,31 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
|
|
|
524
552
|
bbox: translateRect(geometry.bbox, tx, ty)
|
|
525
553
|
};
|
|
526
554
|
}
|
|
555
|
+
function resolveTextOptions(font, value, options = {}, config = {}) {
|
|
556
|
+
const sourceOptions = options ?? {};
|
|
557
|
+
return normalizeTextOptions({
|
|
558
|
+
...sourceOptions,
|
|
559
|
+
size: resolveTextSize(font, value, sourceOptions, config)
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function resolveTextSize(font, value, options = {}, config = {}) {
|
|
563
|
+
const sourceOptions = options ?? {};
|
|
564
|
+
const sizeOption = sourceOptions.size;
|
|
565
|
+
if (!isTextSizeConstraint(sizeOption)) return normalizePositive(sizeOption, DEFAULT_TEXT_SIZE);
|
|
566
|
+
const fit = normalizeTextSizeConstraint(sizeOption, config.methodName);
|
|
567
|
+
const referenceSize = DEFAULT_TEXT_SIZE;
|
|
568
|
+
const measurement = measureText(font, String(value ?? ""), normalizeTextOptions({
|
|
569
|
+
...sourceOptions,
|
|
570
|
+
anchor: null,
|
|
571
|
+
size: referenceSize
|
|
572
|
+
}));
|
|
573
|
+
const measuredWidth = fit.basis === "advance" ? measurement.width : measurement.bbox.w;
|
|
574
|
+
if (!Number.isFinite(measuredWidth) || measuredWidth <= 0) return clampTextSize(referenceSize, fit.min, fit.max);
|
|
575
|
+
return clampTextSize(referenceSize * fit.width / measuredWidth, fit.min, fit.max);
|
|
576
|
+
}
|
|
527
577
|
function normalizeTextOptions(options = {}) {
|
|
528
|
-
const size = normalizePositive(options.size,
|
|
578
|
+
const size = normalizePositive(options.size, DEFAULT_TEXT_SIZE);
|
|
579
|
+
const anchor = normalizeAnchor(options.anchor);
|
|
529
580
|
return {
|
|
530
581
|
x: normalizeNumber(options.x, 0),
|
|
531
582
|
y: normalizeNumber(options.y, 0),
|
|
@@ -535,14 +586,85 @@ function normalizeTextOptions(options = {}) {
|
|
|
535
586
|
kerning: options.kerning !== false,
|
|
536
587
|
letterSpacing: options.letterSpacing == null ? void 0 : normalizeNumber(options.letterSpacing, 0),
|
|
537
588
|
tracking: options.tracking == null ? void 0 : normalizeNumber(options.tracking, 0),
|
|
589
|
+
anchor,
|
|
538
590
|
script: options.script,
|
|
539
591
|
language: options.language,
|
|
540
592
|
features: options.features
|
|
541
593
|
};
|
|
542
594
|
}
|
|
595
|
+
function normalizeAnchor(value) {
|
|
596
|
+
if (value == null) return null;
|
|
597
|
+
if (Number.isFinite(value)) {
|
|
598
|
+
const next = Number(value);
|
|
599
|
+
return {
|
|
600
|
+
x: next,
|
|
601
|
+
y: next
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
if (Array.isArray(value)) {
|
|
605
|
+
if (value.length === 1 && Number.isFinite(value[0])) {
|
|
606
|
+
const next = Number(value[0]);
|
|
607
|
+
return {
|
|
608
|
+
x: next,
|
|
609
|
+
y: next
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
|
|
613
|
+
x: Number(value[0]),
|
|
614
|
+
y: Number(value[1])
|
|
615
|
+
};
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
if (typeof value === "object") {
|
|
619
|
+
const hasX = Number.isFinite(value.x);
|
|
620
|
+
const hasY = Number.isFinite(value.y);
|
|
621
|
+
if (!hasX && !hasY) return null;
|
|
622
|
+
return {
|
|
623
|
+
x: hasX ? Number(value.x) : 0,
|
|
624
|
+
y: hasY ? Number(value.y) : 0
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
function resolveMeasuredBBox(box, fallbackX, fallbackY) {
|
|
630
|
+
if (Number.isFinite(box?.x1) && Number.isFinite(box?.y1) && Number.isFinite(box?.x2) && Number.isFinite(box?.y2)) return {
|
|
631
|
+
x: box.x1,
|
|
632
|
+
y: box.y1,
|
|
633
|
+
w: box.x2 - box.x1,
|
|
634
|
+
h: box.y2 - box.y1
|
|
635
|
+
};
|
|
636
|
+
return {
|
|
637
|
+
x: normalizeNumber(fallbackX, 0),
|
|
638
|
+
y: normalizeNumber(fallbackY, 0),
|
|
639
|
+
w: 0,
|
|
640
|
+
h: 0
|
|
641
|
+
};
|
|
642
|
+
}
|
|
543
643
|
function defaultEdgeEpsilon(size) {
|
|
544
644
|
return Math.min(1, Math.max(.01, size * .0025));
|
|
545
645
|
}
|
|
646
|
+
function isTextSizeConstraint(value) {
|
|
647
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
648
|
+
}
|
|
649
|
+
function normalizeTextSizeConstraint(value, methodName) {
|
|
650
|
+
const width = normalizeNonNegativeNumber(value.width, NaN);
|
|
651
|
+
if (!Number.isFinite(width)) throw new TypeError(`${methodName ?? "Text"} option "size.width" must be a non-negative number.`);
|
|
652
|
+
const min = normalizeNonNegativeNumber(value.min, 0);
|
|
653
|
+
const maxValue = value.max === Infinity ? Infinity : normalizeNonNegativeNumber(value.max, Number.POSITIVE_INFINITY);
|
|
654
|
+
const max = Number.isFinite(maxValue) ? Math.max(min, maxValue) : Infinity;
|
|
655
|
+
return {
|
|
656
|
+
width,
|
|
657
|
+
basis: value.basis === "advance" ? "advance" : DEFAULT_TEXT_SIZE_BASIS,
|
|
658
|
+
min,
|
|
659
|
+
max
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function normalizeNonNegativeNumber(value, fallback) {
|
|
663
|
+
return Number.isFinite(value) && value >= 0 ? Number(value) : fallback;
|
|
664
|
+
}
|
|
665
|
+
function clampTextSize(value, min, max) {
|
|
666
|
+
return Math.min(max, Math.max(min, normalizeNonNegativeNumber(value, min)));
|
|
667
|
+
}
|
|
546
668
|
function toRenderOptions(opts) {
|
|
547
669
|
const renderOptions = { kerning: opts.kerning };
|
|
548
670
|
if (opts.letterSpacing != null) renderOptions.letterSpacing = opts.letterSpacing;
|
|
@@ -3318,7 +3440,7 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3318
3440
|
const layoutState = pretextState != null && canUsePretextLayout(pretextState, normalized) ? pretextState : layoutParagraphsWithNative(fontInstance, paragraphs, normalized, layoutBox);
|
|
3319
3441
|
const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
|
|
3320
3442
|
const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
|
|
3321
|
-
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3443
|
+
const textBBox = normalized.anchor == null ? combineRects(lines.map((line) => line.bbox)) ?? emptyRect() : measurePositionedTextBBox(fontInstance, lines, normalized);
|
|
3322
3444
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3323
3445
|
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3324
3446
|
const anchoredLayout = applyParagraphAnchor(lines, textBBox, finalizeLayoutBox(layoutBox, normalized, textHeight), normalized.anchor);
|
|
@@ -3857,6 +3979,35 @@ function translateLayoutBox(layoutBox, tx, ty) {
|
|
|
3857
3979
|
clipBox: translateRect(layoutBox.clipBox, tx, ty)
|
|
3858
3980
|
};
|
|
3859
3981
|
}
|
|
3982
|
+
function measurePositionedTextBBox(fontInstance, lines, options) {
|
|
3983
|
+
const visibleBoxes = [];
|
|
3984
|
+
const measureOptions = {
|
|
3985
|
+
x: 0,
|
|
3986
|
+
y: 0,
|
|
3987
|
+
size: options.size,
|
|
3988
|
+
flatten: options.flatten,
|
|
3989
|
+
edgeEpsilon: options.edgeEpsilon,
|
|
3990
|
+
kerning: options.kerning,
|
|
3991
|
+
letterSpacing: options.letterSpacing,
|
|
3992
|
+
tracking: options.tracking,
|
|
3993
|
+
anchor: null,
|
|
3994
|
+
script: options.script,
|
|
3995
|
+
language: options.language,
|
|
3996
|
+
features: options.features
|
|
3997
|
+
};
|
|
3998
|
+
lines.forEach((line) => {
|
|
3999
|
+
line.fragments.forEach((fragment) => {
|
|
4000
|
+
if (fragment.text.length === 0) return;
|
|
4001
|
+
const { bbox } = measureText(fontInstance.font, fragment.text, {
|
|
4002
|
+
...measureOptions,
|
|
4003
|
+
x: fragment.x,
|
|
4004
|
+
y: line.baseline
|
|
4005
|
+
});
|
|
4006
|
+
if (bbox.w > 0 || bbox.h > 0) visibleBoxes.push(bbox);
|
|
4007
|
+
});
|
|
4008
|
+
});
|
|
4009
|
+
return combineRects(visibleBoxes) ?? combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
4010
|
+
}
|
|
3860
4011
|
function resolveContainerDimension(explicit, fallback) {
|
|
3861
4012
|
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
3862
4013
|
if (Number.isFinite(fallback) && fallback > 0) return fallback;
|
|
@@ -3885,40 +4036,6 @@ function normalizeDimension(value) {
|
|
|
3885
4036
|
function normalizeGap(value) {
|
|
3886
4037
|
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3887
4038
|
}
|
|
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
4039
|
function normalizeSpacing(value) {
|
|
3923
4040
|
if (value == null) return zeroSpacing();
|
|
3924
4041
|
if (Number.isFinite(value)) {
|
|
@@ -4528,14 +4645,20 @@ var paFont = class paFont {
|
|
|
4528
4645
|
return new paFont(await (0, opentype_js.load)(target, void 0, loadOptions));
|
|
4529
4646
|
}
|
|
4530
4647
|
text(value, options = {}) {
|
|
4531
|
-
const
|
|
4532
|
-
|
|
4648
|
+
const text = String(value ?? "");
|
|
4649
|
+
const sourceOptions = options ?? {};
|
|
4650
|
+
const opts = resolveTextOptions(this.font, text, sourceOptions, { methodName: "font.text()" });
|
|
4651
|
+
assertValidAnchorOption("font.text()", sourceOptions, opts.anchor);
|
|
4652
|
+
return createTextShape(this._layoutText(text, opts), opts, this);
|
|
4533
4653
|
}
|
|
4534
4654
|
glyph(value, options = {}) {
|
|
4535
|
-
const
|
|
4536
|
-
const
|
|
4537
|
-
|
|
4538
|
-
|
|
4655
|
+
const text = Array.from(String(value ?? ""))[0] ?? "";
|
|
4656
|
+
const sourceOptions = options ?? {};
|
|
4657
|
+
const opts = resolveTextOptions(this.font, text, sourceOptions, { methodName: "font.glyph()" });
|
|
4658
|
+
assertValidAnchorOption("font.glyph()", sourceOptions, opts.anchor);
|
|
4659
|
+
const glyph = this.font.charToGlyph(text);
|
|
4660
|
+
const layout = {
|
|
4661
|
+
text,
|
|
4539
4662
|
glyphs: [{
|
|
4540
4663
|
glyph,
|
|
4541
4664
|
x: opts.x,
|
|
@@ -4549,11 +4672,15 @@ var paFont = class paFont {
|
|
|
4549
4672
|
y: opts.y,
|
|
4550
4673
|
size: opts.size
|
|
4551
4674
|
}
|
|
4552
|
-
}
|
|
4675
|
+
};
|
|
4676
|
+
return createTextShape(anchorSingleGlyphLayout(this.font, text, layout, opts), opts, this);
|
|
4553
4677
|
}
|
|
4554
4678
|
metrics(value, options = {}) {
|
|
4555
|
-
const
|
|
4556
|
-
|
|
4679
|
+
const text = String(value ?? "");
|
|
4680
|
+
const sourceOptions = options ?? {};
|
|
4681
|
+
const opts = resolveTextOptions(this.font, text, sourceOptions, { methodName: "font.metrics()" });
|
|
4682
|
+
assertValidAnchorOption("font.metrics()", sourceOptions, opts.anchor);
|
|
4683
|
+
return measureText(this.font, text, opts);
|
|
4557
4684
|
}
|
|
4558
4685
|
paragraph(value, options = {}) {
|
|
4559
4686
|
return createParagraph(this, String(value ?? ""), options);
|
|
@@ -4607,6 +4734,32 @@ function normalizeShapeVariantValue(value) {
|
|
|
4607
4734
|
function toShapeVariantKey(value) {
|
|
4608
4735
|
return normalizeShapeVariantValue(value).toFixed(6);
|
|
4609
4736
|
}
|
|
4737
|
+
function assertValidAnchorOption(methodName, sourceOptions, anchor) {
|
|
4738
|
+
if (sourceOptions?.anchor != null && anchor == null) throw new TypeError(`${methodName} option "anchor" must be a number, [x, y], or { x, y }.`);
|
|
4739
|
+
}
|
|
4740
|
+
function anchorSingleGlyphLayout(font, text, layout, opts) {
|
|
4741
|
+
if (opts.anchor == null || text.length === 0) return layout;
|
|
4742
|
+
const { bbox } = measureText(font, text, {
|
|
4743
|
+
...opts,
|
|
4744
|
+
anchor: null
|
|
4745
|
+
});
|
|
4746
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
4747
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
4748
|
+
if (tx === 0 && ty === 0) return layout;
|
|
4749
|
+
return {
|
|
4750
|
+
...layout,
|
|
4751
|
+
glyphs: layout.glyphs.map((glyph) => ({
|
|
4752
|
+
...glyph,
|
|
4753
|
+
x: glyph.x + tx,
|
|
4754
|
+
y: glyph.y + ty
|
|
4755
|
+
})),
|
|
4756
|
+
metrics: {
|
|
4757
|
+
...layout.metrics,
|
|
4758
|
+
x: layout.metrics.x + tx,
|
|
4759
|
+
y: layout.metrics.y + ty
|
|
4760
|
+
}
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4610
4763
|
async function fetchFontBytes(source) {
|
|
4611
4764
|
const response = await fetch(source);
|
|
4612
4765
|
if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);
|