pa_font 0.2.2 → 0.2.4

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
@@ -556,13 +556,14 @@ function toArrayBuffer(value) {
556
556
  //#endregion
557
557
  //#region src/paFont/shape.js
558
558
  var PAShape = class {
559
- constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon }) {
559
+ constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon, rawData }) {
560
560
  this.text = text;
561
561
  this.parts = parts;
562
562
  this.bbox = bbox;
563
563
  this.metrics = metrics;
564
564
  this.edgeEpsilon = edgeEpsilon;
565
565
  this.raw = {
566
+ ...rawData ?? {},
566
567
  contours,
567
568
  glyphs
568
569
  };
@@ -595,12 +596,23 @@ var PAShape = class {
595
596
  toRegions(options = {}) {
596
597
  return createRegionCollection(this.toShape(options));
597
598
  }
599
+ toRegionViews(options = {}) {
600
+ return this.toShape(options)._getRegionViews();
601
+ }
598
602
  openHoles(width) {
599
603
  const slitWidth = normalizePositive(width, 0);
600
604
  if (slitWidth <= 0 || !this.parts.some((part) => part.holes.length > 0)) return this;
601
605
  const cacheKey = toCacheKey(slitWidth);
602
606
  const cached = this._cache.openHoles.get(cacheKey);
603
607
  if (cached) return cached;
608
+ const glyphVariant = createGlyphDerivedShape(this, {
609
+ openWidth: slitWidth,
610
+ step: 0
611
+ });
612
+ if (glyphVariant) {
613
+ this._cache.openHoles.set(cacheKey, glyphVariant);
614
+ return glyphVariant;
615
+ }
604
616
  const geometryEpsilon = resolveGeometryEpsilon(slitWidth);
605
617
  const shape = createDerivedShape(this, this.parts.map((part) => openPartWithSlit(part, slitWidth, geometryEpsilon)));
606
618
  this._cache.openHoles.set(cacheKey, shape);
@@ -612,6 +624,14 @@ var PAShape = class {
612
624
  const cacheKey = toCacheKey(spacing);
613
625
  const cached = this._cache.resample.get(cacheKey);
614
626
  if (cached) return cached;
627
+ const glyphVariant = createGlyphDerivedShape(this, {
628
+ openWidth: 0,
629
+ step: spacing
630
+ });
631
+ if (glyphVariant) {
632
+ this._cache.resample.set(cacheKey, glyphVariant);
633
+ return glyphVariant;
634
+ }
615
635
  const shape = createDerivedShape(this, this.parts.map((part) => resamplePart(part, spacing)));
616
636
  this._cache.resample.set(cacheKey, shape);
617
637
  return shape;
@@ -673,8 +693,13 @@ function createTextShape(layout, opts, fontInstance) {
673
693
  const parts = [];
674
694
  const contours = [];
675
695
  const glyphs = [];
696
+ const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
697
+ const variantOptions = {
698
+ openWidth: normalizePositive(opts.openWidth, 0),
699
+ step: normalizePositive(opts.step, 0)
700
+ };
676
701
  layout.glyphs.forEach((item, glyphPosition) => {
677
- const translated = translateGlyphGeometry(fontInstance._getFlattenedGlyph(item.glyph, opts), item.x, item.y, glyphPosition);
702
+ const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
678
703
  parts.push(...translated.parts);
679
704
  contours.push(...translated.contours);
680
705
  glyphs.push({
@@ -698,11 +723,21 @@ function createTextShape(layout, opts, fontInstance) {
698
723
  ...layout.metrics,
699
724
  bbox
700
725
  },
701
- edgeEpsilon: opts.edgeEpsilon
726
+ edgeEpsilon: opts.edgeEpsilon,
727
+ rawData: {
728
+ fontInstance,
729
+ sourceLayout: {
730
+ glyphs: sourceLayoutGlyphs,
731
+ metrics: { ...layout.metrics }
732
+ },
733
+ sourceOptions: extractShapeSourceOptions(opts),
734
+ variantOptions
735
+ }
702
736
  });
703
737
  }
704
738
  function createGlyphShape(shape, glyphPosition) {
705
739
  const glyphMeta = shape.raw.glyphs?.[glyphPosition] ?? null;
740
+ const sourceGlyph = shape.raw.sourceLayout?.glyphs?.[glyphPosition] ?? null;
706
741
  const parts = copyParts(shape.parts.filter((part) => part.glyphPosition === glyphPosition));
707
742
  const contours = copyContours((shape.raw.contours ?? []).filter((contour) => contour.glyphPosition === glyphPosition));
708
743
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? (glyphMeta?.bbox ? { ...glyphMeta.bbox } : emptyRect());
@@ -723,14 +758,28 @@ function createGlyphShape(shape, glyphPosition) {
723
758
  width: bbox.w,
724
759
  bbox
725
760
  },
726
- edgeEpsilon: shape.edgeEpsilon
761
+ edgeEpsilon: shape.edgeEpsilon,
762
+ rawData: sourceGlyph && shape.raw.fontInstance ? {
763
+ fontInstance: shape.raw.fontInstance,
764
+ sourceLayout: {
765
+ glyphs: [sourceGlyph],
766
+ metrics: {
767
+ x: glyphMeta?.x ?? shape.metrics?.x ?? 0,
768
+ y: glyphMeta?.y ?? shape.metrics?.y ?? 0,
769
+ size: glyphMeta?.size ?? shape.metrics?.size ?? 0,
770
+ width: glyphMeta?.bbox?.w ?? bbox.w
771
+ }
772
+ },
773
+ sourceOptions: shape.raw.sourceOptions ?? null,
774
+ variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
775
+ } : void 0
727
776
  });
728
777
  }
729
778
  function createDerivedShape(shape, parts) {
730
- const copiedParts = copyParts(parts);
731
- const bbox = combineRects(copiedParts.map((part) => part.bbox)) ?? emptyRect();
779
+ const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
780
+ const partsByGlyphPosition = groupPartsByGlyphPosition(parts);
732
781
  const glyphs = (shape.raw.glyphs ?? []).map((glyph, glyphPosition) => {
733
- const glyphParts = copiedParts.filter((part) => part.glyphPosition === glyphPosition);
782
+ const glyphParts = partsByGlyphPosition.get(glyphPosition) ?? [];
734
783
  return {
735
784
  ...glyph,
736
785
  bbox: combineRects(glyphParts.map((part) => part.bbox)) ?? (glyph.bbox ? { ...glyph.bbox } : emptyRect()),
@@ -739,7 +788,7 @@ function createDerivedShape(shape, parts) {
739
788
  });
740
789
  return new PAShape({
741
790
  text: shape.text,
742
- parts: copiedParts,
791
+ parts,
743
792
  contours: [],
744
793
  glyphs,
745
794
  bbox,
@@ -747,7 +796,11 @@ function createDerivedShape(shape, parts) {
747
796
  ...shape.metrics,
748
797
  bbox
749
798
  },
750
- edgeEpsilon: shape.edgeEpsilon
799
+ edgeEpsilon: shape.edgeEpsilon,
800
+ rawData: {
801
+ ...shape.raw,
802
+ variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
803
+ }
751
804
  });
752
805
  }
753
806
  function createRegionCollection(shape) {
@@ -776,6 +829,16 @@ function copyContours(contours) {
776
829
  ring: copyRing(contour.ring)
777
830
  }));
778
831
  }
832
+ function groupPartsByGlyphPosition(parts) {
833
+ const grouped = /* @__PURE__ */ new Map();
834
+ parts.forEach((part) => {
835
+ if (typeof part.glyphPosition !== "number") return;
836
+ const bucket = grouped.get(part.glyphPosition) ?? [];
837
+ bucket.push(part);
838
+ grouped.set(part.glyphPosition, bucket);
839
+ });
840
+ return grouped;
841
+ }
779
842
  function extractGlyphText(text, glyphPosition, glyphCount) {
780
843
  if (glyphCount <= 1) return text;
781
844
  return Array.from(text ?? "")[glyphPosition] ?? "";
@@ -804,6 +867,19 @@ function normalizeShapeOptions(options = {}) {
804
867
  openWidth: normalizePositive(options.openWidth, 0)
805
868
  };
806
869
  }
870
+ function extractShapeSourceOptions(opts) {
871
+ return {
872
+ size: opts.size,
873
+ flatten: opts.flatten,
874
+ edgeEpsilon: opts.edgeEpsilon,
875
+ kerning: opts.kerning,
876
+ letterSpacing: opts.letterSpacing,
877
+ tracking: opts.tracking,
878
+ script: opts.script,
879
+ language: opts.language,
880
+ features: opts.features
881
+ };
882
+ }
807
883
  function resolveShapeVariant(shape, options = {}) {
808
884
  const normalized = normalizeShapeOptions(options);
809
885
  if (normalized.step <= 0 && normalized.openWidth <= 0) return shape;
@@ -816,9 +892,51 @@ function resolveShapeVariant(shape, options = {}) {
816
892
  shape._cache.shapes.set(cacheKey, next);
817
893
  return next;
818
894
  }
895
+ function deriveGlyphGeometryVariant(geometry, options = {}) {
896
+ const normalized = normalizeShapeOptions(options);
897
+ if (normalized.step <= 0 && normalized.openWidth <= 0) return geometry;
898
+ let parts = geometry.parts;
899
+ if (normalized.openWidth > 0) {
900
+ const geometryEpsilon = resolveGeometryEpsilon(normalized.openWidth);
901
+ parts = parts.map((part) => openPartWithSlit(part, normalized.openWidth, geometryEpsilon));
902
+ }
903
+ if (normalized.step > 0) parts = parts.map((part) => resamplePart(part, normalized.step));
904
+ return {
905
+ ...geometry,
906
+ contours: [],
907
+ parts,
908
+ bbox: combineRects(parts.map((part) => part.bbox)) ?? emptyRect()
909
+ };
910
+ }
819
911
  function toCacheKey(value) {
820
912
  return normalizePositive(value, 0).toFixed(6);
821
913
  }
914
+ function zeroShapeVariant() {
915
+ return {
916
+ openWidth: 0,
917
+ step: 0
918
+ };
919
+ }
920
+ function createGlyphDerivedShape(shape, variant) {
921
+ if (!canUseGlyphDerivedShape(shape)) return null;
922
+ return createTextShape({
923
+ text: shape.text,
924
+ glyphs: shape.raw.sourceLayout.glyphs,
925
+ metrics: shape.raw.sourceLayout.metrics
926
+ }, {
927
+ ...shape.raw.sourceOptions,
928
+ ...mergeShapeVariantOptions(shape.raw.variantOptions, variant)
929
+ }, shape.raw.fontInstance);
930
+ }
931
+ function canUseGlyphDerivedShape(shape) {
932
+ return shape.raw?.fontInstance != null && shape.raw?.sourceOptions != null && shape.raw?.sourceLayout != null && Array.isArray(shape.raw.sourceLayout.glyphs);
933
+ }
934
+ function mergeShapeVariantOptions(previous, next) {
935
+ return {
936
+ openWidth: normalizePositive(next.openWidth, previous?.openWidth ?? 0),
937
+ step: normalizePositive(next.step, previous?.step ?? 0)
938
+ };
939
+ }
822
940
  function resolveGeometryEpsilon(width) {
823
941
  return Math.max(1e-4, normalizePositive(width, 1) * .001);
824
942
  }
@@ -3180,9 +3298,11 @@ var DEFAULT_LINE_HEIGHT_RATIO = 1.2;
3180
3298
  var HUGE_LAYOUT_WIDTH = 1e9;
3181
3299
  var JUSTIFY_EPSILON = 1e-6;
3182
3300
  var QUOTE_RE = /"/g;
3301
+ var SHARED_MEASURE_CACHE_LIMIT = 2048;
3183
3302
  var sharedMeasureContext = null;
3184
3303
  var sharedWordSegmenter = null;
3185
3304
  var sharedGraphemeSegmenter = null;
3305
+ var sharedMeasureCaches = /* @__PURE__ */ new WeakMap();
3186
3306
  function layoutParagraph(fontInstance, text, options = {}, state = {}) {
3187
3307
  const normalized = normalizeParagraphOptions(fontInstance, options);
3188
3308
  const textValue = String(text ?? "");
@@ -3862,15 +3982,24 @@ function createTextMeasurer(fontInstance, options) {
3862
3982
  const cache = /* @__PURE__ */ new Map();
3863
3983
  const openTypeMeasurer = createOpenTypeMeasurer(fontInstance, options);
3864
3984
  const context = getMeasureContext();
3985
+ const sharedCache = getSharedMeasureCache(fontInstance, "canvas");
3986
+ const sharedKeyPrefix = `${options.font}\u0000`;
3865
3987
  return (value) => {
3866
3988
  if (value.length === 0) return 0;
3867
3989
  if (cache.has(value)) return cache.get(value);
3990
+ const sharedKey = `${sharedKeyPrefix}${value}`;
3991
+ const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
3992
+ if (sharedWidth != null) {
3993
+ cache.set(value, sharedWidth);
3994
+ return sharedWidth;
3995
+ }
3868
3996
  let width;
3869
3997
  if (context) {
3870
3998
  context.font = options.font;
3871
3999
  width = context.measureText(value).width;
3872
4000
  } else width = openTypeMeasurer(value);
3873
4001
  cache.set(value, width);
4002
+ writeSharedMeasureCache(sharedCache, sharedKey, width);
3874
4003
  return width;
3875
4004
  };
3876
4005
  }
@@ -3883,6 +4012,7 @@ function createLazyTextMeasurer(fontInstance, options) {
3883
4012
  }
3884
4013
  function createOpenTypeMeasurer(fontInstance, options) {
3885
4014
  const cache = /* @__PURE__ */ new Map();
4015
+ const sharedCache = getSharedMeasureCache(fontInstance, "openType");
3886
4016
  const widthOptions = {
3887
4017
  x: 0,
3888
4018
  y: 0,
@@ -3896,11 +4026,19 @@ function createOpenTypeMeasurer(fontInstance, options) {
3896
4026
  language: options.language,
3897
4027
  features: options.features
3898
4028
  };
4029
+ const sharedKeyPrefix = createOpenTypeMeasureKeyPrefix(widthOptions);
3899
4030
  return (value) => {
3900
4031
  if (value.length === 0) return 0;
3901
4032
  if (cache.has(value)) return cache.get(value);
4033
+ const sharedKey = `${sharedKeyPrefix}${value}`;
4034
+ const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
4035
+ if (sharedWidth != null) {
4036
+ cache.set(value, sharedWidth);
4037
+ return sharedWidth;
4038
+ }
3902
4039
  const width = measureAdvanceWidth(fontInstance.font, value, widthOptions);
3903
4040
  cache.set(value, width);
4041
+ writeSharedMeasureCache(sharedCache, sharedKey, width);
3904
4042
  return width;
3905
4043
  };
3906
4044
  }
@@ -3916,6 +4054,49 @@ function getMeasureContext() {
3916
4054
  }
3917
4055
  return null;
3918
4056
  }
4057
+ function getSharedMeasureCache(fontInstance, bucket) {
4058
+ if (fontInstance == null || typeof fontInstance !== "object" && typeof fontInstance !== "function") return null;
4059
+ let caches = sharedMeasureCaches.get(fontInstance);
4060
+ if (!caches) {
4061
+ caches = {
4062
+ canvas: /* @__PURE__ */ new Map(),
4063
+ openType: /* @__PURE__ */ new Map()
4064
+ };
4065
+ sharedMeasureCaches.set(fontInstance, caches);
4066
+ }
4067
+ return caches[bucket];
4068
+ }
4069
+ function readSharedMeasureCache(cache, key) {
4070
+ if (!cache || !cache.has(key)) return null;
4071
+ const value = cache.get(key);
4072
+ cache.delete(key);
4073
+ cache.set(key, value);
4074
+ return value;
4075
+ }
4076
+ function writeSharedMeasureCache(cache, key, value) {
4077
+ if (!cache) return;
4078
+ if (cache.has(key)) cache.delete(key);
4079
+ cache.set(key, value);
4080
+ if (cache.size > SHARED_MEASURE_CACHE_LIMIT) {
4081
+ const oldestKey = cache.keys().next().value;
4082
+ cache.delete(oldestKey);
4083
+ }
4084
+ }
4085
+ function createOpenTypeMeasureKeyPrefix(options) {
4086
+ return [
4087
+ options.size,
4088
+ options.kerning ? 1 : 0,
4089
+ options.letterSpacing ?? "",
4090
+ options.tracking ?? "",
4091
+ options.script ?? "",
4092
+ options.language ?? "",
4093
+ serializeMeasureFeatures(options.features)
4094
+ ].join("|") + "\0";
4095
+ }
4096
+ function serializeMeasureFeatures(features) {
4097
+ if (!features || typeof features !== "object") return "";
4098
+ return Object.keys(features).sort().map((key) => `${key}:${features[key]}`).join(",");
4099
+ }
3919
4100
  function getWordSegmenter() {
3920
4101
  if (sharedWordSegmenter == null) sharedWordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" });
3921
4102
  return sharedWordSegmenter;
@@ -4011,6 +4192,10 @@ var paParagraph = class paParagraph {
4011
4192
  const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegions()");
4012
4193
  return this._getBaseShape(layout).toRegions(shapeOptions);
4013
4194
  }
4195
+ toRegionViews(options = {}) {
4196
+ const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegionViews()");
4197
+ return this._getBaseShape(layout).toRegionViews(shapeOptions);
4198
+ }
4014
4199
  toPoints(options = {}) {
4015
4200
  const { layout = "current", ...pointOptions } = normalizeParagraphPointOptions(options);
4016
4201
  return this._getBaseShape(layout).toPoints(pointOptions);
@@ -4146,6 +4331,7 @@ var paFont = class paFont {
4146
4331
  this.canvasFamily = this.family;
4147
4332
  this._glyphTopologyCache = /* @__PURE__ */ new Map();
4148
4333
  this._glyphFlatCache = /* @__PURE__ */ new Map();
4334
+ this._glyphVariantCache = /* @__PURE__ */ new Map();
4149
4335
  }
4150
4336
  static async load(source, options = {}) {
4151
4337
  const opts = normalizeLoadOptions(options);
@@ -4223,10 +4409,27 @@ var paFont = class paFont {
4223
4409
  }
4224
4410
  return this._glyphFlatCache.get(key);
4225
4411
  }
4412
+ _getGlyphGeometryVariant(glyph, opts) {
4413
+ const openWidth = normalizeShapeVariantValue(opts.openWidth);
4414
+ const step = normalizeShapeVariantValue(opts.step);
4415
+ if (openWidth <= 0 && step <= 0) return this._getFlattenedGlyph(glyph, opts);
4416
+ const key = `${glyph.index}:${opts.size}:${opts.flatten}:${toShapeVariantKey(openWidth)}:${toShapeVariantKey(step)}`;
4417
+ if (!this._glyphVariantCache.has(key)) this._glyphVariantCache.set(key, deriveGlyphGeometryVariant(this._getFlattenedGlyph(glyph, opts), {
4418
+ openWidth,
4419
+ step
4420
+ }));
4421
+ return this._glyphVariantCache.get(key);
4422
+ }
4226
4423
  _layoutText(value, opts) {
4227
4424
  return layoutGlyphs(this.font, value, opts);
4228
4425
  }
4229
4426
  };
4427
+ function normalizeShapeVariantValue(value) {
4428
+ return Number.isFinite(value) && value > 0 ? Number(value) : 0;
4429
+ }
4430
+ function toShapeVariantKey(value) {
4431
+ return normalizeShapeVariantValue(value).toFixed(6);
4432
+ }
4230
4433
  async function fetchFontBytes(source) {
4231
4434
  const response = await fetch(source);
4232
4435
  if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);