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/dist/paFont.js
CHANGED
|
@@ -272,10 +272,12 @@ function mod(value, divisor) {
|
|
|
272
272
|
}
|
|
273
273
|
//#endregion
|
|
274
274
|
//#region src/paFont/core.js
|
|
275
|
+
var DEFAULT_TEXT_SIZE = 72;
|
|
276
|
+
var DEFAULT_TEXT_SIZE_BASIS = "bbox";
|
|
275
277
|
function layoutGlyphs(font, value, opts) {
|
|
276
278
|
const glyphs = [];
|
|
277
279
|
const renderOptions = toRenderOptions(opts);
|
|
278
|
-
return {
|
|
280
|
+
return applyAnchorToGlyphLayout(font, value, {
|
|
279
281
|
text: value,
|
|
280
282
|
glyphs,
|
|
281
283
|
metrics: {
|
|
@@ -292,19 +294,45 @@ function layoutGlyphs(font, value, opts) {
|
|
|
292
294
|
y: opts.y,
|
|
293
295
|
size: opts.size
|
|
294
296
|
}
|
|
297
|
+
}, opts);
|
|
298
|
+
}
|
|
299
|
+
function applyAnchorToGlyphLayout(font, value, layout, opts) {
|
|
300
|
+
if (opts.anchor == null || layout.glyphs.length === 0) return layout;
|
|
301
|
+
const { bbox } = measureText(font, value, {
|
|
302
|
+
...opts,
|
|
303
|
+
anchor: null
|
|
304
|
+
});
|
|
305
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
306
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
307
|
+
if (tx === 0 && ty === 0) return layout;
|
|
308
|
+
return {
|
|
309
|
+
text: value,
|
|
310
|
+
glyphs: layout.glyphs.map((glyph) => ({
|
|
311
|
+
...glyph,
|
|
312
|
+
x: glyph.x + tx,
|
|
313
|
+
y: glyph.y + ty
|
|
314
|
+
})),
|
|
315
|
+
metrics: {
|
|
316
|
+
...layout.metrics,
|
|
317
|
+
x: layout.metrics.x + tx,
|
|
318
|
+
y: layout.metrics.y + ty
|
|
319
|
+
}
|
|
295
320
|
};
|
|
296
321
|
}
|
|
297
322
|
function measureText(font, value, opts) {
|
|
298
323
|
const renderOptions = toRenderOptions(opts);
|
|
299
|
-
const
|
|
324
|
+
const bbox = resolveMeasuredBBox(font.getPath(value, opts.x, opts.y, opts.size, renderOptions).getBoundingBox(), opts.x, opts.y);
|
|
325
|
+
if (opts.anchor == null) return {
|
|
326
|
+
size: opts.size,
|
|
327
|
+
width: measureAdvanceWidth(font, value, opts),
|
|
328
|
+
bbox
|
|
329
|
+
};
|
|
330
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
331
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
300
332
|
return {
|
|
333
|
+
size: opts.size,
|
|
301
334
|
width: measureAdvanceWidth(font, value, opts),
|
|
302
|
-
bbox:
|
|
303
|
-
x: box.x1,
|
|
304
|
-
y: box.y1,
|
|
305
|
-
w: box.x2 - box.x1,
|
|
306
|
-
h: box.y2 - box.y1
|
|
307
|
-
}
|
|
335
|
+
bbox: translateRect(bbox, tx, ty)
|
|
308
336
|
};
|
|
309
337
|
}
|
|
310
338
|
function measureAdvanceWidth(font, value, opts) {
|
|
@@ -520,8 +548,31 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
|
|
|
520
548
|
bbox: translateRect(geometry.bbox, tx, ty)
|
|
521
549
|
};
|
|
522
550
|
}
|
|
551
|
+
function resolveTextOptions(font, value, options = {}, config = {}) {
|
|
552
|
+
const sourceOptions = options ?? {};
|
|
553
|
+
return normalizeTextOptions({
|
|
554
|
+
...sourceOptions,
|
|
555
|
+
size: resolveTextSize(font, value, sourceOptions, config)
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function resolveTextSize(font, value, options = {}, config = {}) {
|
|
559
|
+
const sourceOptions = options ?? {};
|
|
560
|
+
const sizeOption = sourceOptions.size;
|
|
561
|
+
if (!isTextSizeConstraint(sizeOption)) return normalizePositive(sizeOption, DEFAULT_TEXT_SIZE);
|
|
562
|
+
const fit = normalizeTextSizeConstraint(sizeOption, config.methodName);
|
|
563
|
+
const referenceSize = DEFAULT_TEXT_SIZE;
|
|
564
|
+
const measurement = measureText(font, String(value ?? ""), normalizeTextOptions({
|
|
565
|
+
...sourceOptions,
|
|
566
|
+
anchor: null,
|
|
567
|
+
size: referenceSize
|
|
568
|
+
}));
|
|
569
|
+
const measuredWidth = fit.basis === "advance" ? measurement.width : measurement.bbox.w;
|
|
570
|
+
if (!Number.isFinite(measuredWidth) || measuredWidth <= 0) return clampTextSize(referenceSize, fit.min, fit.max);
|
|
571
|
+
return clampTextSize(referenceSize * fit.width / measuredWidth, fit.min, fit.max);
|
|
572
|
+
}
|
|
523
573
|
function normalizeTextOptions(options = {}) {
|
|
524
|
-
const size = normalizePositive(options.size,
|
|
574
|
+
const size = normalizePositive(options.size, DEFAULT_TEXT_SIZE);
|
|
575
|
+
const anchor = normalizeAnchor(options.anchor);
|
|
525
576
|
return {
|
|
526
577
|
x: normalizeNumber(options.x, 0),
|
|
527
578
|
y: normalizeNumber(options.y, 0),
|
|
@@ -531,14 +582,85 @@ function normalizeTextOptions(options = {}) {
|
|
|
531
582
|
kerning: options.kerning !== false,
|
|
532
583
|
letterSpacing: options.letterSpacing == null ? void 0 : normalizeNumber(options.letterSpacing, 0),
|
|
533
584
|
tracking: options.tracking == null ? void 0 : normalizeNumber(options.tracking, 0),
|
|
585
|
+
anchor,
|
|
534
586
|
script: options.script,
|
|
535
587
|
language: options.language,
|
|
536
588
|
features: options.features
|
|
537
589
|
};
|
|
538
590
|
}
|
|
591
|
+
function normalizeAnchor(value) {
|
|
592
|
+
if (value == null) return null;
|
|
593
|
+
if (Number.isFinite(value)) {
|
|
594
|
+
const next = Number(value);
|
|
595
|
+
return {
|
|
596
|
+
x: next,
|
|
597
|
+
y: next
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (Array.isArray(value)) {
|
|
601
|
+
if (value.length === 1 && Number.isFinite(value[0])) {
|
|
602
|
+
const next = Number(value[0]);
|
|
603
|
+
return {
|
|
604
|
+
x: next,
|
|
605
|
+
y: next
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
|
|
609
|
+
x: Number(value[0]),
|
|
610
|
+
y: Number(value[1])
|
|
611
|
+
};
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
if (typeof value === "object") {
|
|
615
|
+
const hasX = Number.isFinite(value.x);
|
|
616
|
+
const hasY = Number.isFinite(value.y);
|
|
617
|
+
if (!hasX && !hasY) return null;
|
|
618
|
+
return {
|
|
619
|
+
x: hasX ? Number(value.x) : 0,
|
|
620
|
+
y: hasY ? Number(value.y) : 0
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
function resolveMeasuredBBox(box, fallbackX, fallbackY) {
|
|
626
|
+
if (Number.isFinite(box?.x1) && Number.isFinite(box?.y1) && Number.isFinite(box?.x2) && Number.isFinite(box?.y2)) return {
|
|
627
|
+
x: box.x1,
|
|
628
|
+
y: box.y1,
|
|
629
|
+
w: box.x2 - box.x1,
|
|
630
|
+
h: box.y2 - box.y1
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
x: normalizeNumber(fallbackX, 0),
|
|
634
|
+
y: normalizeNumber(fallbackY, 0),
|
|
635
|
+
w: 0,
|
|
636
|
+
h: 0
|
|
637
|
+
};
|
|
638
|
+
}
|
|
539
639
|
function defaultEdgeEpsilon(size) {
|
|
540
640
|
return Math.min(1, Math.max(.01, size * .0025));
|
|
541
641
|
}
|
|
642
|
+
function isTextSizeConstraint(value) {
|
|
643
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
644
|
+
}
|
|
645
|
+
function normalizeTextSizeConstraint(value, methodName) {
|
|
646
|
+
const width = normalizeNonNegativeNumber(value.width, NaN);
|
|
647
|
+
if (!Number.isFinite(width)) throw new TypeError(`${methodName ?? "Text"} option "size.width" must be a non-negative number.`);
|
|
648
|
+
const min = normalizeNonNegativeNumber(value.min, 0);
|
|
649
|
+
const maxValue = value.max === Infinity ? Infinity : normalizeNonNegativeNumber(value.max, Number.POSITIVE_INFINITY);
|
|
650
|
+
const max = Number.isFinite(maxValue) ? Math.max(min, maxValue) : Infinity;
|
|
651
|
+
return {
|
|
652
|
+
width,
|
|
653
|
+
basis: value.basis === "advance" ? "advance" : DEFAULT_TEXT_SIZE_BASIS,
|
|
654
|
+
min,
|
|
655
|
+
max
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function normalizeNonNegativeNumber(value, fallback) {
|
|
659
|
+
return Number.isFinite(value) && value >= 0 ? Number(value) : fallback;
|
|
660
|
+
}
|
|
661
|
+
function clampTextSize(value, min, max) {
|
|
662
|
+
return Math.min(max, Math.max(min, normalizeNonNegativeNumber(value, min)));
|
|
663
|
+
}
|
|
542
664
|
function toRenderOptions(opts) {
|
|
543
665
|
const renderOptions = { kerning: opts.kerning };
|
|
544
666
|
if (opts.letterSpacing != null) renderOptions.letterSpacing = opts.letterSpacing;
|
|
@@ -3314,7 +3436,7 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3314
3436
|
const layoutState = pretextState != null && canUsePretextLayout(pretextState, normalized) ? pretextState : layoutParagraphsWithNative(fontInstance, paragraphs, normalized, layoutBox);
|
|
3315
3437
|
const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
|
|
3316
3438
|
const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
|
|
3317
|
-
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3439
|
+
const textBBox = normalized.anchor == null ? combineRects(lines.map((line) => line.bbox)) ?? emptyRect() : measurePositionedTextBBox(fontInstance, lines, normalized);
|
|
3318
3440
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3319
3441
|
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3320
3442
|
const anchoredLayout = applyParagraphAnchor(lines, textBBox, finalizeLayoutBox(layoutBox, normalized, textHeight), normalized.anchor);
|
|
@@ -3853,6 +3975,35 @@ function translateLayoutBox(layoutBox, tx, ty) {
|
|
|
3853
3975
|
clipBox: translateRect(layoutBox.clipBox, tx, ty)
|
|
3854
3976
|
};
|
|
3855
3977
|
}
|
|
3978
|
+
function measurePositionedTextBBox(fontInstance, lines, options) {
|
|
3979
|
+
const visibleBoxes = [];
|
|
3980
|
+
const measureOptions = {
|
|
3981
|
+
x: 0,
|
|
3982
|
+
y: 0,
|
|
3983
|
+
size: options.size,
|
|
3984
|
+
flatten: options.flatten,
|
|
3985
|
+
edgeEpsilon: options.edgeEpsilon,
|
|
3986
|
+
kerning: options.kerning,
|
|
3987
|
+
letterSpacing: options.letterSpacing,
|
|
3988
|
+
tracking: options.tracking,
|
|
3989
|
+
anchor: null,
|
|
3990
|
+
script: options.script,
|
|
3991
|
+
language: options.language,
|
|
3992
|
+
features: options.features
|
|
3993
|
+
};
|
|
3994
|
+
lines.forEach((line) => {
|
|
3995
|
+
line.fragments.forEach((fragment) => {
|
|
3996
|
+
if (fragment.text.length === 0) return;
|
|
3997
|
+
const { bbox } = measureText(fontInstance.font, fragment.text, {
|
|
3998
|
+
...measureOptions,
|
|
3999
|
+
x: fragment.x,
|
|
4000
|
+
y: line.baseline
|
|
4001
|
+
});
|
|
4002
|
+
if (bbox.w > 0 || bbox.h > 0) visibleBoxes.push(bbox);
|
|
4003
|
+
});
|
|
4004
|
+
});
|
|
4005
|
+
return combineRects(visibleBoxes) ?? combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
4006
|
+
}
|
|
3856
4007
|
function resolveContainerDimension(explicit, fallback) {
|
|
3857
4008
|
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
3858
4009
|
if (Number.isFinite(fallback) && fallback > 0) return fallback;
|
|
@@ -3881,40 +4032,6 @@ function normalizeDimension(value) {
|
|
|
3881
4032
|
function normalizeGap(value) {
|
|
3882
4033
|
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3883
4034
|
}
|
|
3884
|
-
function normalizeAnchor(value) {
|
|
3885
|
-
if (value == null) return null;
|
|
3886
|
-
if (Number.isFinite(value)) {
|
|
3887
|
-
const next = Number(value);
|
|
3888
|
-
return {
|
|
3889
|
-
x: next,
|
|
3890
|
-
y: next
|
|
3891
|
-
};
|
|
3892
|
-
}
|
|
3893
|
-
if (Array.isArray(value)) {
|
|
3894
|
-
if (value.length === 1 && Number.isFinite(value[0])) {
|
|
3895
|
-
const next = Number(value[0]);
|
|
3896
|
-
return {
|
|
3897
|
-
x: next,
|
|
3898
|
-
y: next
|
|
3899
|
-
};
|
|
3900
|
-
}
|
|
3901
|
-
if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
|
|
3902
|
-
x: Number(value[0]),
|
|
3903
|
-
y: Number(value[1])
|
|
3904
|
-
};
|
|
3905
|
-
return null;
|
|
3906
|
-
}
|
|
3907
|
-
if (typeof value === "object") {
|
|
3908
|
-
const hasX = Number.isFinite(value.x);
|
|
3909
|
-
const hasY = Number.isFinite(value.y);
|
|
3910
|
-
if (!hasX && !hasY) return null;
|
|
3911
|
-
return {
|
|
3912
|
-
x: hasX ? Number(value.x) : 0,
|
|
3913
|
-
y: hasY ? Number(value.y) : 0
|
|
3914
|
-
};
|
|
3915
|
-
}
|
|
3916
|
-
return null;
|
|
3917
|
-
}
|
|
3918
4035
|
function normalizeSpacing(value) {
|
|
3919
4036
|
if (value == null) return zeroSpacing();
|
|
3920
4037
|
if (Number.isFinite(value)) {
|
|
@@ -4524,14 +4641,20 @@ var paFont = class paFont {
|
|
|
4524
4641
|
return new paFont(await load(target, void 0, loadOptions));
|
|
4525
4642
|
}
|
|
4526
4643
|
text(value, options = {}) {
|
|
4527
|
-
const
|
|
4528
|
-
|
|
4644
|
+
const text = String(value ?? "");
|
|
4645
|
+
const sourceOptions = options ?? {};
|
|
4646
|
+
const opts = resolveTextOptions(this.font, text, sourceOptions, { methodName: "font.text()" });
|
|
4647
|
+
assertValidAnchorOption("font.text()", sourceOptions, opts.anchor);
|
|
4648
|
+
return createTextShape(this._layoutText(text, opts), opts, this);
|
|
4529
4649
|
}
|
|
4530
4650
|
glyph(value, options = {}) {
|
|
4531
|
-
const
|
|
4532
|
-
const
|
|
4533
|
-
|
|
4534
|
-
|
|
4651
|
+
const text = Array.from(String(value ?? ""))[0] ?? "";
|
|
4652
|
+
const sourceOptions = options ?? {};
|
|
4653
|
+
const opts = resolveTextOptions(this.font, text, sourceOptions, { methodName: "font.glyph()" });
|
|
4654
|
+
assertValidAnchorOption("font.glyph()", sourceOptions, opts.anchor);
|
|
4655
|
+
const glyph = this.font.charToGlyph(text);
|
|
4656
|
+
const layout = {
|
|
4657
|
+
text,
|
|
4535
4658
|
glyphs: [{
|
|
4536
4659
|
glyph,
|
|
4537
4660
|
x: opts.x,
|
|
@@ -4545,11 +4668,15 @@ var paFont = class paFont {
|
|
|
4545
4668
|
y: opts.y,
|
|
4546
4669
|
size: opts.size
|
|
4547
4670
|
}
|
|
4548
|
-
}
|
|
4671
|
+
};
|
|
4672
|
+
return createTextShape(anchorSingleGlyphLayout(this.font, text, layout, opts), opts, this);
|
|
4549
4673
|
}
|
|
4550
4674
|
metrics(value, options = {}) {
|
|
4551
|
-
const
|
|
4552
|
-
|
|
4675
|
+
const text = String(value ?? "");
|
|
4676
|
+
const sourceOptions = options ?? {};
|
|
4677
|
+
const opts = resolveTextOptions(this.font, text, sourceOptions, { methodName: "font.metrics()" });
|
|
4678
|
+
assertValidAnchorOption("font.metrics()", sourceOptions, opts.anchor);
|
|
4679
|
+
return measureText(this.font, text, opts);
|
|
4553
4680
|
}
|
|
4554
4681
|
paragraph(value, options = {}) {
|
|
4555
4682
|
return createParagraph(this, String(value ?? ""), options);
|
|
@@ -4603,6 +4730,32 @@ function normalizeShapeVariantValue(value) {
|
|
|
4603
4730
|
function toShapeVariantKey(value) {
|
|
4604
4731
|
return normalizeShapeVariantValue(value).toFixed(6);
|
|
4605
4732
|
}
|
|
4733
|
+
function assertValidAnchorOption(methodName, sourceOptions, anchor) {
|
|
4734
|
+
if (sourceOptions?.anchor != null && anchor == null) throw new TypeError(`${methodName} option "anchor" must be a number, [x, y], or { x, y }.`);
|
|
4735
|
+
}
|
|
4736
|
+
function anchorSingleGlyphLayout(font, text, layout, opts) {
|
|
4737
|
+
if (opts.anchor == null || text.length === 0) return layout;
|
|
4738
|
+
const { bbox } = measureText(font, text, {
|
|
4739
|
+
...opts,
|
|
4740
|
+
anchor: null
|
|
4741
|
+
});
|
|
4742
|
+
const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
|
|
4743
|
+
const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
|
|
4744
|
+
if (tx === 0 && ty === 0) return layout;
|
|
4745
|
+
return {
|
|
4746
|
+
...layout,
|
|
4747
|
+
glyphs: layout.glyphs.map((glyph) => ({
|
|
4748
|
+
...glyph,
|
|
4749
|
+
x: glyph.x + tx,
|
|
4750
|
+
y: glyph.y + ty
|
|
4751
|
+
})),
|
|
4752
|
+
metrics: {
|
|
4753
|
+
...layout.metrics,
|
|
4754
|
+
x: layout.metrics.x + tx,
|
|
4755
|
+
y: layout.metrics.y + ty
|
|
4756
|
+
}
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4606
4759
|
async function fetchFontBytes(source) {
|
|
4607
4760
|
const response = await fetch(source);
|
|
4608
4761
|
if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);
|