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/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 box = font.getPath(value, opts.x, opts.y, opts.size, renderOptions).getBoundingBox();
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, 72);
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 opts = normalizeTextOptions(options);
4528
- return createTextShape(this._layoutText(String(value ?? ""), opts), opts, this);
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 opts = normalizeTextOptions(options);
4532
- const glyph = this.font.charToGlyph(String(value ?? ""));
4533
- return createTextShape({
4534
- text: String(value ?? ""),
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
- }, opts, this);
4671
+ };
4672
+ return createTextShape(anchorSingleGlyphLayout(this.font, text, layout, opts), opts, this);
4549
4673
  }
4550
4674
  metrics(value, options = {}) {
4551
- const opts = normalizeTextOptions(options);
4552
- return measureText(this.font, String(value ?? ""), opts);
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}`);