pa_font 0.3.3 → 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/dist/paFont.js CHANGED
@@ -275,7 +275,7 @@ function mod(value, divisor) {
275
275
  function layoutGlyphs(font, value, opts) {
276
276
  const glyphs = [];
277
277
  const renderOptions = toRenderOptions(opts);
278
- return {
278
+ return applyAnchorToGlyphLayout(font, value, {
279
279
  text: value,
280
280
  glyphs,
281
281
  metrics: {
@@ -292,19 +292,43 @@ function layoutGlyphs(font, value, opts) {
292
292
  y: opts.y,
293
293
  size: opts.size
294
294
  }
295
+ }, opts);
296
+ }
297
+ function applyAnchorToGlyphLayout(font, value, layout, opts) {
298
+ if (opts.anchor == null || layout.glyphs.length === 0) return layout;
299
+ const { bbox } = measureText(font, value, {
300
+ ...opts,
301
+ anchor: null
302
+ });
303
+ const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
304
+ const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
305
+ if (tx === 0 && ty === 0) return layout;
306
+ return {
307
+ text: value,
308
+ glyphs: layout.glyphs.map((glyph) => ({
309
+ ...glyph,
310
+ x: glyph.x + tx,
311
+ y: glyph.y + ty
312
+ })),
313
+ metrics: {
314
+ ...layout.metrics,
315
+ x: layout.metrics.x + tx,
316
+ y: layout.metrics.y + ty
317
+ }
295
318
  };
296
319
  }
297
320
  function measureText(font, value, opts) {
298
321
  const renderOptions = toRenderOptions(opts);
299
- const box = font.getPath(value, opts.x, opts.y, opts.size, renderOptions).getBoundingBox();
322
+ const bbox = resolveMeasuredBBox(font.getPath(value, opts.x, opts.y, opts.size, renderOptions).getBoundingBox(), opts.x, opts.y);
323
+ if (opts.anchor == null) return {
324
+ width: measureAdvanceWidth(font, value, opts),
325
+ bbox
326
+ };
327
+ const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
328
+ const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
300
329
  return {
301
330
  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
- }
331
+ bbox: translateRect(bbox, tx, ty)
308
332
  };
309
333
  }
310
334
  function measureAdvanceWidth(font, value, opts) {
@@ -522,6 +546,7 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
522
546
  }
523
547
  function normalizeTextOptions(options = {}) {
524
548
  const size = normalizePositive(options.size, 72);
549
+ const anchor = normalizeAnchor(options.anchor);
525
550
  return {
526
551
  x: normalizeNumber(options.x, 0),
527
552
  y: normalizeNumber(options.y, 0),
@@ -531,11 +556,60 @@ function normalizeTextOptions(options = {}) {
531
556
  kerning: options.kerning !== false,
532
557
  letterSpacing: options.letterSpacing == null ? void 0 : normalizeNumber(options.letterSpacing, 0),
533
558
  tracking: options.tracking == null ? void 0 : normalizeNumber(options.tracking, 0),
559
+ anchor,
534
560
  script: options.script,
535
561
  language: options.language,
536
562
  features: options.features
537
563
  };
538
564
  }
565
+ function normalizeAnchor(value) {
566
+ if (value == null) return null;
567
+ if (Number.isFinite(value)) {
568
+ const next = Number(value);
569
+ return {
570
+ x: next,
571
+ y: next
572
+ };
573
+ }
574
+ if (Array.isArray(value)) {
575
+ if (value.length === 1 && Number.isFinite(value[0])) {
576
+ const next = Number(value[0]);
577
+ return {
578
+ x: next,
579
+ y: next
580
+ };
581
+ }
582
+ if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
583
+ x: Number(value[0]),
584
+ y: Number(value[1])
585
+ };
586
+ return null;
587
+ }
588
+ if (typeof value === "object") {
589
+ const hasX = Number.isFinite(value.x);
590
+ const hasY = Number.isFinite(value.y);
591
+ if (!hasX && !hasY) return null;
592
+ return {
593
+ x: hasX ? Number(value.x) : 0,
594
+ y: hasY ? Number(value.y) : 0
595
+ };
596
+ }
597
+ return null;
598
+ }
599
+ function resolveMeasuredBBox(box, fallbackX, fallbackY) {
600
+ if (Number.isFinite(box?.x1) && Number.isFinite(box?.y1) && Number.isFinite(box?.x2) && Number.isFinite(box?.y2)) return {
601
+ x: box.x1,
602
+ y: box.y1,
603
+ w: box.x2 - box.x1,
604
+ h: box.y2 - box.y1
605
+ };
606
+ return {
607
+ x: normalizeNumber(fallbackX, 0),
608
+ y: normalizeNumber(fallbackY, 0),
609
+ w: 0,
610
+ h: 0
611
+ };
612
+ }
539
613
  function defaultEdgeEpsilon(size) {
540
614
  return Math.min(1, Math.max(.01, size * .0025));
541
615
  }
@@ -3314,7 +3388,7 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
3314
3388
  const layoutState = pretextState != null && canUsePretextLayout(pretextState, normalized) ? pretextState : layoutParagraphsWithNative(fontInstance, paragraphs, normalized, layoutBox);
3315
3389
  const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
3316
3390
  const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
3317
- const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
3391
+ const textBBox = normalized.anchor == null ? combineRects(lines.map((line) => line.bbox)) ?? emptyRect() : measurePositionedTextBBox(fontInstance, lines, normalized);
3318
3392
  const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
3319
3393
  const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
3320
3394
  const anchoredLayout = applyParagraphAnchor(lines, textBBox, finalizeLayoutBox(layoutBox, normalized, textHeight), normalized.anchor);
@@ -3374,10 +3448,7 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
3374
3448
  width,
3375
3449
  height,
3376
3450
  gap: gap ?? DEFAULT_PARAGRAPH_GAP,
3377
- anchor: anchor ?? {
3378
- x: 0,
3379
- y: 0
3380
- },
3451
+ anchor,
3381
3452
  lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
3382
3453
  align: normalizeEnum(options.align, [
3383
3454
  "left",
@@ -3819,13 +3890,13 @@ function finalizeLayoutBox(layoutBox, options, textHeight) {
3819
3890
  };
3820
3891
  }
3821
3892
  function applyParagraphAnchor(lines, textBBox, layoutBox, anchor) {
3822
- if (anchor == null || Math.abs(anchor.x) <= JUSTIFY_EPSILON && Math.abs(anchor.y) <= JUSTIFY_EPSILON) return {
3893
+ if (anchor == null) return {
3823
3894
  lines,
3824
3895
  textBBox,
3825
3896
  layoutBox
3826
3897
  };
3827
- const tx = -layoutBox.contentBox.w * anchor.x;
3828
- const ty = -layoutBox.contentBox.h * anchor.y;
3898
+ const tx = layoutBox.contentX - (textBBox.x + textBBox.w * anchor.x);
3899
+ const ty = layoutBox.contentY - (textBBox.y + textBBox.h * anchor.y);
3829
3900
  return {
3830
3901
  lines: translatePositionedLines(lines, tx, ty),
3831
3902
  textBBox: translateRect(textBBox, tx, ty),
@@ -3856,6 +3927,35 @@ function translateLayoutBox(layoutBox, tx, ty) {
3856
3927
  clipBox: translateRect(layoutBox.clipBox, tx, ty)
3857
3928
  };
3858
3929
  }
3930
+ function measurePositionedTextBBox(fontInstance, lines, options) {
3931
+ const visibleBoxes = [];
3932
+ const measureOptions = {
3933
+ x: 0,
3934
+ y: 0,
3935
+ size: options.size,
3936
+ flatten: options.flatten,
3937
+ edgeEpsilon: options.edgeEpsilon,
3938
+ kerning: options.kerning,
3939
+ letterSpacing: options.letterSpacing,
3940
+ tracking: options.tracking,
3941
+ anchor: null,
3942
+ script: options.script,
3943
+ language: options.language,
3944
+ features: options.features
3945
+ };
3946
+ lines.forEach((line) => {
3947
+ line.fragments.forEach((fragment) => {
3948
+ if (fragment.text.length === 0) return;
3949
+ const { bbox } = measureText(fontInstance.font, fragment.text, {
3950
+ ...measureOptions,
3951
+ x: fragment.x,
3952
+ y: line.baseline
3953
+ });
3954
+ if (bbox.w > 0 || bbox.h > 0) visibleBoxes.push(bbox);
3955
+ });
3956
+ });
3957
+ return combineRects(visibleBoxes) ?? combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
3958
+ }
3859
3959
  function resolveContainerDimension(explicit, fallback) {
3860
3960
  if (Number.isFinite(explicit) && explicit > 0) return explicit;
3861
3961
  if (Number.isFinite(fallback) && fallback > 0) return fallback;
@@ -3884,40 +3984,6 @@ function normalizeDimension(value) {
3884
3984
  function normalizeGap(value) {
3885
3985
  return Number.isFinite(value) && value >= 0 ? Number(value) : null;
3886
3986
  }
3887
- function normalizeAnchor(value) {
3888
- if (value == null) return null;
3889
- if (Number.isFinite(value)) {
3890
- const next = Number(value);
3891
- return {
3892
- x: next,
3893
- y: next
3894
- };
3895
- }
3896
- if (Array.isArray(value)) {
3897
- if (value.length === 1 && Number.isFinite(value[0])) {
3898
- const next = Number(value[0]);
3899
- return {
3900
- x: next,
3901
- y: next
3902
- };
3903
- }
3904
- if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
3905
- x: Number(value[0]),
3906
- y: Number(value[1])
3907
- };
3908
- return null;
3909
- }
3910
- if (typeof value === "object") {
3911
- const hasX = Number.isFinite(value.x);
3912
- const hasY = Number.isFinite(value.y);
3913
- if (!hasX && !hasY) return null;
3914
- return {
3915
- x: hasX ? Number(value.x) : 0,
3916
- y: hasY ? Number(value.y) : 0
3917
- };
3918
- }
3919
- return null;
3920
- }
3921
3987
  function normalizeSpacing(value) {
3922
3988
  if (value == null) return zeroSpacing();
3923
3989
  if (Number.isFinite(value)) {
@@ -4528,13 +4594,16 @@ var paFont = class paFont {
4528
4594
  }
4529
4595
  text(value, options = {}) {
4530
4596
  const opts = normalizeTextOptions(options);
4597
+ assertValidAnchorOption("font.text()", options, opts.anchor);
4531
4598
  return createTextShape(this._layoutText(String(value ?? ""), opts), opts, this);
4532
4599
  }
4533
4600
  glyph(value, options = {}) {
4534
4601
  const opts = normalizeTextOptions(options);
4535
- const glyph = this.font.charToGlyph(String(value ?? ""));
4536
- return createTextShape({
4537
- text: String(value ?? ""),
4602
+ assertValidAnchorOption("font.glyph()", options, opts.anchor);
4603
+ const text = Array.from(String(value ?? ""))[0] ?? "";
4604
+ const glyph = this.font.charToGlyph(text);
4605
+ const layout = {
4606
+ text,
4538
4607
  glyphs: [{
4539
4608
  glyph,
4540
4609
  x: opts.x,
@@ -4548,10 +4617,12 @@ var paFont = class paFont {
4548
4617
  y: opts.y,
4549
4618
  size: opts.size
4550
4619
  }
4551
- }, opts, this);
4620
+ };
4621
+ return createTextShape(anchorSingleGlyphLayout(this.font, text, layout, opts), opts, this);
4552
4622
  }
4553
4623
  metrics(value, options = {}) {
4554
4624
  const opts = normalizeTextOptions(options);
4625
+ assertValidAnchorOption("font.metrics()", options, opts.anchor);
4555
4626
  return measureText(this.font, String(value ?? ""), opts);
4556
4627
  }
4557
4628
  paragraph(value, options = {}) {
@@ -4606,6 +4677,32 @@ function normalizeShapeVariantValue(value) {
4606
4677
  function toShapeVariantKey(value) {
4607
4678
  return normalizeShapeVariantValue(value).toFixed(6);
4608
4679
  }
4680
+ function assertValidAnchorOption(methodName, sourceOptions, anchor) {
4681
+ if (sourceOptions?.anchor != null && anchor == null) throw new TypeError(`${methodName} option "anchor" must be a number, [x, y], or { x, y }.`);
4682
+ }
4683
+ function anchorSingleGlyphLayout(font, text, layout, opts) {
4684
+ if (opts.anchor == null || text.length === 0) return layout;
4685
+ const { bbox } = measureText(font, text, {
4686
+ ...opts,
4687
+ anchor: null
4688
+ });
4689
+ const tx = opts.x - (bbox.x + bbox.w * opts.anchor.x);
4690
+ const ty = opts.y - (bbox.y + bbox.h * opts.anchor.y);
4691
+ if (tx === 0 && ty === 0) return layout;
4692
+ return {
4693
+ ...layout,
4694
+ glyphs: layout.glyphs.map((glyph) => ({
4695
+ ...glyph,
4696
+ x: glyph.x + tx,
4697
+ y: glyph.y + ty
4698
+ })),
4699
+ metrics: {
4700
+ ...layout.metrics,
4701
+ x: layout.metrics.x + tx,
4702
+ y: layout.metrics.y + ty
4703
+ }
4704
+ };
4705
+ }
4609
4706
  async function fetchFontBytes(source) {
4610
4707
  const response = await fetch(source);
4611
4708
  if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);