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/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);
@@ -3853,6 +3927,35 @@ function translateLayoutBox(layoutBox, tx, ty) {
3853
3927
  clipBox: translateRect(layoutBox.clipBox, tx, ty)
3854
3928
  };
3855
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
+ }
3856
3959
  function resolveContainerDimension(explicit, fallback) {
3857
3960
  if (Number.isFinite(explicit) && explicit > 0) return explicit;
3858
3961
  if (Number.isFinite(fallback) && fallback > 0) return fallback;
@@ -3881,40 +3984,6 @@ function normalizeDimension(value) {
3881
3984
  function normalizeGap(value) {
3882
3985
  return Number.isFinite(value) && value >= 0 ? Number(value) : null;
3883
3986
  }
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
3987
  function normalizeSpacing(value) {
3919
3988
  if (value == null) return zeroSpacing();
3920
3989
  if (Number.isFinite(value)) {
@@ -4525,13 +4594,16 @@ var paFont = class paFont {
4525
4594
  }
4526
4595
  text(value, options = {}) {
4527
4596
  const opts = normalizeTextOptions(options);
4597
+ assertValidAnchorOption("font.text()", options, opts.anchor);
4528
4598
  return createTextShape(this._layoutText(String(value ?? ""), opts), opts, this);
4529
4599
  }
4530
4600
  glyph(value, options = {}) {
4531
4601
  const opts = normalizeTextOptions(options);
4532
- const glyph = this.font.charToGlyph(String(value ?? ""));
4533
- return createTextShape({
4534
- 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,
4535
4607
  glyphs: [{
4536
4608
  glyph,
4537
4609
  x: opts.x,
@@ -4545,10 +4617,12 @@ var paFont = class paFont {
4545
4617
  y: opts.y,
4546
4618
  size: opts.size
4547
4619
  }
4548
- }, opts, this);
4620
+ };
4621
+ return createTextShape(anchorSingleGlyphLayout(this.font, text, layout, opts), opts, this);
4549
4622
  }
4550
4623
  metrics(value, options = {}) {
4551
4624
  const opts = normalizeTextOptions(options);
4625
+ assertValidAnchorOption("font.metrics()", options, opts.anchor);
4552
4626
  return measureText(this.font, String(value ?? ""), opts);
4553
4627
  }
4554
4628
  paragraph(value, options = {}) {
@@ -4603,6 +4677,32 @@ function normalizeShapeVariantValue(value) {
4603
4677
  function toShapeVariantKey(value) {
4604
4678
  return normalizeShapeVariantValue(value).toFixed(6);
4605
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
+ }
4606
4706
  async function fetchFontBytes(source) {
4607
4707
  const response = await fetch(source);
4608
4708
  if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);