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/README.md +0 -39
- package/USAGE.md +9 -0
- package/dist/paFont.cjs +212 -9
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +212 -9
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# paFont
|
|
2
|
-
|
|
3
|
-
`paFont`는 OpenType 폰트에서 텍스트 geometry를 만들고, 같은 폰트로 canvas 문단 레이아웃까지 처리하는 작은 라이브러리입니다.
|
|
4
|
-
|
|
5
|
-
## 설치
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
npm install pa_font
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## 빠른 예시
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
import paFont from "pa_font";
|
|
15
|
-
|
|
16
|
-
const font = await paFont.load("/assets/font.otf");
|
|
17
|
-
|
|
18
|
-
const paragraph = font.paragraph("Canvas paragraph with fast wrapping.", {
|
|
19
|
-
x: 40,
|
|
20
|
-
y: 80,
|
|
21
|
-
size: 32,
|
|
22
|
-
width: 420,
|
|
23
|
-
wrap: "word",
|
|
24
|
-
padding: { x: 24, y: 20 },
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
paragraph.drawText(ctx, { fillStyle: "#111" });
|
|
28
|
-
|
|
29
|
-
const shape = paragraph.toShape({ step: 6 });
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## 문단 API 핵심
|
|
33
|
-
|
|
34
|
-
- `wrap: "word" | "char" | "keep"`로 대부분의 줄바꿈 모드를 짧게 지정할 수 있습니다.
|
|
35
|
-
- `padding` / `margin`은 `24`, `"24 32"`, `[24, 32]`, `{ x: 32, y: 24 }`, `{ top, right, bottom, left }`를 지원합니다.
|
|
36
|
-
- 기본 문단 레이아웃은 `pretext` hot path를 우선 사용하고, 필요한 경우만 native 측정으로 fallback 합니다.
|
|
37
|
-
- 실제 사용 엔진은 `paragraph.layoutEngine`에서 확인할 수 있습니다.
|
|
38
|
-
|
|
39
|
-
자세한 사용법은 [USAGE.md](./USAGE.md)를 참고하세요.
|
package/USAGE.md
CHANGED
|
@@ -189,6 +189,10 @@ const shape = paragraph.toShape({
|
|
|
189
189
|
|
|
190
190
|
문단 전체를 plain region 데이터로 반환합니다.
|
|
191
191
|
|
|
192
|
+
### `paragraph.toRegionViews(options?)`
|
|
193
|
+
|
|
194
|
+
문단 전체를 zero-copy region view로 반환합니다.
|
|
195
|
+
|
|
192
196
|
### `paragraph.toPoints(options?)`
|
|
193
197
|
|
|
194
198
|
문단 전체를 점 샘플로 반환합니다.
|
|
@@ -234,6 +238,11 @@ const shape = paragraph.toShape({
|
|
|
234
238
|
|
|
235
239
|
최종 polygon 데이터를 반환합니다.
|
|
236
240
|
|
|
241
|
+
### `shape.toRegionViews({ step, openWidth })`
|
|
242
|
+
|
|
243
|
+
내부 polygon을 복사하지 않는 read-only view를 반환합니다.
|
|
244
|
+
반복 후처리나 시각화처럼 바로 순회만 할 때 `toRegions()`보다 가볍습니다.
|
|
245
|
+
|
|
237
246
|
반환 형태:
|
|
238
247
|
|
|
239
248
|
```js
|
package/dist/paFont.cjs
CHANGED
|
@@ -560,13 +560,14 @@ function toArrayBuffer(value) {
|
|
|
560
560
|
//#endregion
|
|
561
561
|
//#region src/paFont/shape.js
|
|
562
562
|
var PAShape = class {
|
|
563
|
-
constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon }) {
|
|
563
|
+
constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon, rawData }) {
|
|
564
564
|
this.text = text;
|
|
565
565
|
this.parts = parts;
|
|
566
566
|
this.bbox = bbox;
|
|
567
567
|
this.metrics = metrics;
|
|
568
568
|
this.edgeEpsilon = edgeEpsilon;
|
|
569
569
|
this.raw = {
|
|
570
|
+
...rawData ?? {},
|
|
570
571
|
contours,
|
|
571
572
|
glyphs
|
|
572
573
|
};
|
|
@@ -599,12 +600,23 @@ var PAShape = class {
|
|
|
599
600
|
toRegions(options = {}) {
|
|
600
601
|
return createRegionCollection(this.toShape(options));
|
|
601
602
|
}
|
|
603
|
+
toRegionViews(options = {}) {
|
|
604
|
+
return this.toShape(options)._getRegionViews();
|
|
605
|
+
}
|
|
602
606
|
openHoles(width) {
|
|
603
607
|
const slitWidth = normalizePositive(width, 0);
|
|
604
608
|
if (slitWidth <= 0 || !this.parts.some((part) => part.holes.length > 0)) return this;
|
|
605
609
|
const cacheKey = toCacheKey(slitWidth);
|
|
606
610
|
const cached = this._cache.openHoles.get(cacheKey);
|
|
607
611
|
if (cached) return cached;
|
|
612
|
+
const glyphVariant = createGlyphDerivedShape(this, {
|
|
613
|
+
openWidth: slitWidth,
|
|
614
|
+
step: 0
|
|
615
|
+
});
|
|
616
|
+
if (glyphVariant) {
|
|
617
|
+
this._cache.openHoles.set(cacheKey, glyphVariant);
|
|
618
|
+
return glyphVariant;
|
|
619
|
+
}
|
|
608
620
|
const geometryEpsilon = resolveGeometryEpsilon(slitWidth);
|
|
609
621
|
const shape = createDerivedShape(this, this.parts.map((part) => openPartWithSlit(part, slitWidth, geometryEpsilon)));
|
|
610
622
|
this._cache.openHoles.set(cacheKey, shape);
|
|
@@ -616,6 +628,14 @@ var PAShape = class {
|
|
|
616
628
|
const cacheKey = toCacheKey(spacing);
|
|
617
629
|
const cached = this._cache.resample.get(cacheKey);
|
|
618
630
|
if (cached) return cached;
|
|
631
|
+
const glyphVariant = createGlyphDerivedShape(this, {
|
|
632
|
+
openWidth: 0,
|
|
633
|
+
step: spacing
|
|
634
|
+
});
|
|
635
|
+
if (glyphVariant) {
|
|
636
|
+
this._cache.resample.set(cacheKey, glyphVariant);
|
|
637
|
+
return glyphVariant;
|
|
638
|
+
}
|
|
619
639
|
const shape = createDerivedShape(this, this.parts.map((part) => resamplePart(part, spacing)));
|
|
620
640
|
this._cache.resample.set(cacheKey, shape);
|
|
621
641
|
return shape;
|
|
@@ -677,8 +697,13 @@ function createTextShape(layout, opts, fontInstance) {
|
|
|
677
697
|
const parts = [];
|
|
678
698
|
const contours = [];
|
|
679
699
|
const glyphs = [];
|
|
700
|
+
const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
|
|
701
|
+
const variantOptions = {
|
|
702
|
+
openWidth: normalizePositive(opts.openWidth, 0),
|
|
703
|
+
step: normalizePositive(opts.step, 0)
|
|
704
|
+
};
|
|
680
705
|
layout.glyphs.forEach((item, glyphPosition) => {
|
|
681
|
-
const translated = translateGlyphGeometry(fontInstance.
|
|
706
|
+
const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
|
|
682
707
|
parts.push(...translated.parts);
|
|
683
708
|
contours.push(...translated.contours);
|
|
684
709
|
glyphs.push({
|
|
@@ -702,11 +727,21 @@ function createTextShape(layout, opts, fontInstance) {
|
|
|
702
727
|
...layout.metrics,
|
|
703
728
|
bbox
|
|
704
729
|
},
|
|
705
|
-
edgeEpsilon: opts.edgeEpsilon
|
|
730
|
+
edgeEpsilon: opts.edgeEpsilon,
|
|
731
|
+
rawData: {
|
|
732
|
+
fontInstance,
|
|
733
|
+
sourceLayout: {
|
|
734
|
+
glyphs: sourceLayoutGlyphs,
|
|
735
|
+
metrics: { ...layout.metrics }
|
|
736
|
+
},
|
|
737
|
+
sourceOptions: extractShapeSourceOptions(opts),
|
|
738
|
+
variantOptions
|
|
739
|
+
}
|
|
706
740
|
});
|
|
707
741
|
}
|
|
708
742
|
function createGlyphShape(shape, glyphPosition) {
|
|
709
743
|
const glyphMeta = shape.raw.glyphs?.[glyphPosition] ?? null;
|
|
744
|
+
const sourceGlyph = shape.raw.sourceLayout?.glyphs?.[glyphPosition] ?? null;
|
|
710
745
|
const parts = copyParts(shape.parts.filter((part) => part.glyphPosition === glyphPosition));
|
|
711
746
|
const contours = copyContours((shape.raw.contours ?? []).filter((contour) => contour.glyphPosition === glyphPosition));
|
|
712
747
|
const bbox = combineRects(parts.map((part) => part.bbox)) ?? (glyphMeta?.bbox ? { ...glyphMeta.bbox } : emptyRect());
|
|
@@ -727,14 +762,28 @@ function createGlyphShape(shape, glyphPosition) {
|
|
|
727
762
|
width: bbox.w,
|
|
728
763
|
bbox
|
|
729
764
|
},
|
|
730
|
-
edgeEpsilon: shape.edgeEpsilon
|
|
765
|
+
edgeEpsilon: shape.edgeEpsilon,
|
|
766
|
+
rawData: sourceGlyph && shape.raw.fontInstance ? {
|
|
767
|
+
fontInstance: shape.raw.fontInstance,
|
|
768
|
+
sourceLayout: {
|
|
769
|
+
glyphs: [sourceGlyph],
|
|
770
|
+
metrics: {
|
|
771
|
+
x: glyphMeta?.x ?? shape.metrics?.x ?? 0,
|
|
772
|
+
y: glyphMeta?.y ?? shape.metrics?.y ?? 0,
|
|
773
|
+
size: glyphMeta?.size ?? shape.metrics?.size ?? 0,
|
|
774
|
+
width: glyphMeta?.bbox?.w ?? bbox.w
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
sourceOptions: shape.raw.sourceOptions ?? null,
|
|
778
|
+
variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
|
|
779
|
+
} : void 0
|
|
731
780
|
});
|
|
732
781
|
}
|
|
733
782
|
function createDerivedShape(shape, parts) {
|
|
734
|
-
const
|
|
735
|
-
const
|
|
783
|
+
const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
|
|
784
|
+
const partsByGlyphPosition = groupPartsByGlyphPosition(parts);
|
|
736
785
|
const glyphs = (shape.raw.glyphs ?? []).map((glyph, glyphPosition) => {
|
|
737
|
-
const glyphParts =
|
|
786
|
+
const glyphParts = partsByGlyphPosition.get(glyphPosition) ?? [];
|
|
738
787
|
return {
|
|
739
788
|
...glyph,
|
|
740
789
|
bbox: combineRects(glyphParts.map((part) => part.bbox)) ?? (glyph.bbox ? { ...glyph.bbox } : emptyRect()),
|
|
@@ -743,7 +792,7 @@ function createDerivedShape(shape, parts) {
|
|
|
743
792
|
});
|
|
744
793
|
return new PAShape({
|
|
745
794
|
text: shape.text,
|
|
746
|
-
parts
|
|
795
|
+
parts,
|
|
747
796
|
contours: [],
|
|
748
797
|
glyphs,
|
|
749
798
|
bbox,
|
|
@@ -751,7 +800,11 @@ function createDerivedShape(shape, parts) {
|
|
|
751
800
|
...shape.metrics,
|
|
752
801
|
bbox
|
|
753
802
|
},
|
|
754
|
-
edgeEpsilon: shape.edgeEpsilon
|
|
803
|
+
edgeEpsilon: shape.edgeEpsilon,
|
|
804
|
+
rawData: {
|
|
805
|
+
...shape.raw,
|
|
806
|
+
variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
|
|
807
|
+
}
|
|
755
808
|
});
|
|
756
809
|
}
|
|
757
810
|
function createRegionCollection(shape) {
|
|
@@ -780,6 +833,16 @@ function copyContours(contours) {
|
|
|
780
833
|
ring: copyRing(contour.ring)
|
|
781
834
|
}));
|
|
782
835
|
}
|
|
836
|
+
function groupPartsByGlyphPosition(parts) {
|
|
837
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
838
|
+
parts.forEach((part) => {
|
|
839
|
+
if (typeof part.glyphPosition !== "number") return;
|
|
840
|
+
const bucket = grouped.get(part.glyphPosition) ?? [];
|
|
841
|
+
bucket.push(part);
|
|
842
|
+
grouped.set(part.glyphPosition, bucket);
|
|
843
|
+
});
|
|
844
|
+
return grouped;
|
|
845
|
+
}
|
|
783
846
|
function extractGlyphText(text, glyphPosition, glyphCount) {
|
|
784
847
|
if (glyphCount <= 1) return text;
|
|
785
848
|
return Array.from(text ?? "")[glyphPosition] ?? "";
|
|
@@ -808,6 +871,19 @@ function normalizeShapeOptions(options = {}) {
|
|
|
808
871
|
openWidth: normalizePositive(options.openWidth, 0)
|
|
809
872
|
};
|
|
810
873
|
}
|
|
874
|
+
function extractShapeSourceOptions(opts) {
|
|
875
|
+
return {
|
|
876
|
+
size: opts.size,
|
|
877
|
+
flatten: opts.flatten,
|
|
878
|
+
edgeEpsilon: opts.edgeEpsilon,
|
|
879
|
+
kerning: opts.kerning,
|
|
880
|
+
letterSpacing: opts.letterSpacing,
|
|
881
|
+
tracking: opts.tracking,
|
|
882
|
+
script: opts.script,
|
|
883
|
+
language: opts.language,
|
|
884
|
+
features: opts.features
|
|
885
|
+
};
|
|
886
|
+
}
|
|
811
887
|
function resolveShapeVariant(shape, options = {}) {
|
|
812
888
|
const normalized = normalizeShapeOptions(options);
|
|
813
889
|
if (normalized.step <= 0 && normalized.openWidth <= 0) return shape;
|
|
@@ -820,9 +896,51 @@ function resolveShapeVariant(shape, options = {}) {
|
|
|
820
896
|
shape._cache.shapes.set(cacheKey, next);
|
|
821
897
|
return next;
|
|
822
898
|
}
|
|
899
|
+
function deriveGlyphGeometryVariant(geometry, options = {}) {
|
|
900
|
+
const normalized = normalizeShapeOptions(options);
|
|
901
|
+
if (normalized.step <= 0 && normalized.openWidth <= 0) return geometry;
|
|
902
|
+
let parts = geometry.parts;
|
|
903
|
+
if (normalized.openWidth > 0) {
|
|
904
|
+
const geometryEpsilon = resolveGeometryEpsilon(normalized.openWidth);
|
|
905
|
+
parts = parts.map((part) => openPartWithSlit(part, normalized.openWidth, geometryEpsilon));
|
|
906
|
+
}
|
|
907
|
+
if (normalized.step > 0) parts = parts.map((part) => resamplePart(part, normalized.step));
|
|
908
|
+
return {
|
|
909
|
+
...geometry,
|
|
910
|
+
contours: [],
|
|
911
|
+
parts,
|
|
912
|
+
bbox: combineRects(parts.map((part) => part.bbox)) ?? emptyRect()
|
|
913
|
+
};
|
|
914
|
+
}
|
|
823
915
|
function toCacheKey(value) {
|
|
824
916
|
return normalizePositive(value, 0).toFixed(6);
|
|
825
917
|
}
|
|
918
|
+
function zeroShapeVariant() {
|
|
919
|
+
return {
|
|
920
|
+
openWidth: 0,
|
|
921
|
+
step: 0
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function createGlyphDerivedShape(shape, variant) {
|
|
925
|
+
if (!canUseGlyphDerivedShape(shape)) return null;
|
|
926
|
+
return createTextShape({
|
|
927
|
+
text: shape.text,
|
|
928
|
+
glyphs: shape.raw.sourceLayout.glyphs,
|
|
929
|
+
metrics: shape.raw.sourceLayout.metrics
|
|
930
|
+
}, {
|
|
931
|
+
...shape.raw.sourceOptions,
|
|
932
|
+
...mergeShapeVariantOptions(shape.raw.variantOptions, variant)
|
|
933
|
+
}, shape.raw.fontInstance);
|
|
934
|
+
}
|
|
935
|
+
function canUseGlyphDerivedShape(shape) {
|
|
936
|
+
return shape.raw?.fontInstance != null && shape.raw?.sourceOptions != null && shape.raw?.sourceLayout != null && Array.isArray(shape.raw.sourceLayout.glyphs);
|
|
937
|
+
}
|
|
938
|
+
function mergeShapeVariantOptions(previous, next) {
|
|
939
|
+
return {
|
|
940
|
+
openWidth: normalizePositive(next.openWidth, previous?.openWidth ?? 0),
|
|
941
|
+
step: normalizePositive(next.step, previous?.step ?? 0)
|
|
942
|
+
};
|
|
943
|
+
}
|
|
826
944
|
function resolveGeometryEpsilon(width) {
|
|
827
945
|
return Math.max(1e-4, normalizePositive(width, 1) * .001);
|
|
828
946
|
}
|
|
@@ -3184,9 +3302,11 @@ var DEFAULT_LINE_HEIGHT_RATIO = 1.2;
|
|
|
3184
3302
|
var HUGE_LAYOUT_WIDTH = 1e9;
|
|
3185
3303
|
var JUSTIFY_EPSILON = 1e-6;
|
|
3186
3304
|
var QUOTE_RE = /"/g;
|
|
3305
|
+
var SHARED_MEASURE_CACHE_LIMIT = 2048;
|
|
3187
3306
|
var sharedMeasureContext = null;
|
|
3188
3307
|
var sharedWordSegmenter = null;
|
|
3189
3308
|
var sharedGraphemeSegmenter = null;
|
|
3309
|
+
var sharedMeasureCaches = /* @__PURE__ */ new WeakMap();
|
|
3190
3310
|
function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
3191
3311
|
const normalized = normalizeParagraphOptions(fontInstance, options);
|
|
3192
3312
|
const textValue = String(text ?? "");
|
|
@@ -3866,15 +3986,24 @@ function createTextMeasurer(fontInstance, options) {
|
|
|
3866
3986
|
const cache = /* @__PURE__ */ new Map();
|
|
3867
3987
|
const openTypeMeasurer = createOpenTypeMeasurer(fontInstance, options);
|
|
3868
3988
|
const context = getMeasureContext();
|
|
3989
|
+
const sharedCache = getSharedMeasureCache(fontInstance, "canvas");
|
|
3990
|
+
const sharedKeyPrefix = `${options.font}\u0000`;
|
|
3869
3991
|
return (value) => {
|
|
3870
3992
|
if (value.length === 0) return 0;
|
|
3871
3993
|
if (cache.has(value)) return cache.get(value);
|
|
3994
|
+
const sharedKey = `${sharedKeyPrefix}${value}`;
|
|
3995
|
+
const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
|
|
3996
|
+
if (sharedWidth != null) {
|
|
3997
|
+
cache.set(value, sharedWidth);
|
|
3998
|
+
return sharedWidth;
|
|
3999
|
+
}
|
|
3872
4000
|
let width;
|
|
3873
4001
|
if (context) {
|
|
3874
4002
|
context.font = options.font;
|
|
3875
4003
|
width = context.measureText(value).width;
|
|
3876
4004
|
} else width = openTypeMeasurer(value);
|
|
3877
4005
|
cache.set(value, width);
|
|
4006
|
+
writeSharedMeasureCache(sharedCache, sharedKey, width);
|
|
3878
4007
|
return width;
|
|
3879
4008
|
};
|
|
3880
4009
|
}
|
|
@@ -3887,6 +4016,7 @@ function createLazyTextMeasurer(fontInstance, options) {
|
|
|
3887
4016
|
}
|
|
3888
4017
|
function createOpenTypeMeasurer(fontInstance, options) {
|
|
3889
4018
|
const cache = /* @__PURE__ */ new Map();
|
|
4019
|
+
const sharedCache = getSharedMeasureCache(fontInstance, "openType");
|
|
3890
4020
|
const widthOptions = {
|
|
3891
4021
|
x: 0,
|
|
3892
4022
|
y: 0,
|
|
@@ -3900,11 +4030,19 @@ function createOpenTypeMeasurer(fontInstance, options) {
|
|
|
3900
4030
|
language: options.language,
|
|
3901
4031
|
features: options.features
|
|
3902
4032
|
};
|
|
4033
|
+
const sharedKeyPrefix = createOpenTypeMeasureKeyPrefix(widthOptions);
|
|
3903
4034
|
return (value) => {
|
|
3904
4035
|
if (value.length === 0) return 0;
|
|
3905
4036
|
if (cache.has(value)) return cache.get(value);
|
|
4037
|
+
const sharedKey = `${sharedKeyPrefix}${value}`;
|
|
4038
|
+
const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
|
|
4039
|
+
if (sharedWidth != null) {
|
|
4040
|
+
cache.set(value, sharedWidth);
|
|
4041
|
+
return sharedWidth;
|
|
4042
|
+
}
|
|
3906
4043
|
const width = measureAdvanceWidth(fontInstance.font, value, widthOptions);
|
|
3907
4044
|
cache.set(value, width);
|
|
4045
|
+
writeSharedMeasureCache(sharedCache, sharedKey, width);
|
|
3908
4046
|
return width;
|
|
3909
4047
|
};
|
|
3910
4048
|
}
|
|
@@ -3920,6 +4058,49 @@ function getMeasureContext() {
|
|
|
3920
4058
|
}
|
|
3921
4059
|
return null;
|
|
3922
4060
|
}
|
|
4061
|
+
function getSharedMeasureCache(fontInstance, bucket) {
|
|
4062
|
+
if (fontInstance == null || typeof fontInstance !== "object" && typeof fontInstance !== "function") return null;
|
|
4063
|
+
let caches = sharedMeasureCaches.get(fontInstance);
|
|
4064
|
+
if (!caches) {
|
|
4065
|
+
caches = {
|
|
4066
|
+
canvas: /* @__PURE__ */ new Map(),
|
|
4067
|
+
openType: /* @__PURE__ */ new Map()
|
|
4068
|
+
};
|
|
4069
|
+
sharedMeasureCaches.set(fontInstance, caches);
|
|
4070
|
+
}
|
|
4071
|
+
return caches[bucket];
|
|
4072
|
+
}
|
|
4073
|
+
function readSharedMeasureCache(cache, key) {
|
|
4074
|
+
if (!cache || !cache.has(key)) return null;
|
|
4075
|
+
const value = cache.get(key);
|
|
4076
|
+
cache.delete(key);
|
|
4077
|
+
cache.set(key, value);
|
|
4078
|
+
return value;
|
|
4079
|
+
}
|
|
4080
|
+
function writeSharedMeasureCache(cache, key, value) {
|
|
4081
|
+
if (!cache) return;
|
|
4082
|
+
if (cache.has(key)) cache.delete(key);
|
|
4083
|
+
cache.set(key, value);
|
|
4084
|
+
if (cache.size > SHARED_MEASURE_CACHE_LIMIT) {
|
|
4085
|
+
const oldestKey = cache.keys().next().value;
|
|
4086
|
+
cache.delete(oldestKey);
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
function createOpenTypeMeasureKeyPrefix(options) {
|
|
4090
|
+
return [
|
|
4091
|
+
options.size,
|
|
4092
|
+
options.kerning ? 1 : 0,
|
|
4093
|
+
options.letterSpacing ?? "",
|
|
4094
|
+
options.tracking ?? "",
|
|
4095
|
+
options.script ?? "",
|
|
4096
|
+
options.language ?? "",
|
|
4097
|
+
serializeMeasureFeatures(options.features)
|
|
4098
|
+
].join("|") + "\0";
|
|
4099
|
+
}
|
|
4100
|
+
function serializeMeasureFeatures(features) {
|
|
4101
|
+
if (!features || typeof features !== "object") return "";
|
|
4102
|
+
return Object.keys(features).sort().map((key) => `${key}:${features[key]}`).join(",");
|
|
4103
|
+
}
|
|
3923
4104
|
function getWordSegmenter() {
|
|
3924
4105
|
if (sharedWordSegmenter == null) sharedWordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" });
|
|
3925
4106
|
return sharedWordSegmenter;
|
|
@@ -4015,6 +4196,10 @@ var paParagraph = class paParagraph {
|
|
|
4015
4196
|
const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegions()");
|
|
4016
4197
|
return this._getBaseShape(layout).toRegions(shapeOptions);
|
|
4017
4198
|
}
|
|
4199
|
+
toRegionViews(options = {}) {
|
|
4200
|
+
const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegionViews()");
|
|
4201
|
+
return this._getBaseShape(layout).toRegionViews(shapeOptions);
|
|
4202
|
+
}
|
|
4018
4203
|
toPoints(options = {}) {
|
|
4019
4204
|
const { layout = "current", ...pointOptions } = normalizeParagraphPointOptions(options);
|
|
4020
4205
|
return this._getBaseShape(layout).toPoints(pointOptions);
|
|
@@ -4150,6 +4335,7 @@ var paFont = class paFont {
|
|
|
4150
4335
|
this.canvasFamily = this.family;
|
|
4151
4336
|
this._glyphTopologyCache = /* @__PURE__ */ new Map();
|
|
4152
4337
|
this._glyphFlatCache = /* @__PURE__ */ new Map();
|
|
4338
|
+
this._glyphVariantCache = /* @__PURE__ */ new Map();
|
|
4153
4339
|
}
|
|
4154
4340
|
static async load(source, options = {}) {
|
|
4155
4341
|
const opts = normalizeLoadOptions(options);
|
|
@@ -4227,10 +4413,27 @@ var paFont = class paFont {
|
|
|
4227
4413
|
}
|
|
4228
4414
|
return this._glyphFlatCache.get(key);
|
|
4229
4415
|
}
|
|
4416
|
+
_getGlyphGeometryVariant(glyph, opts) {
|
|
4417
|
+
const openWidth = normalizeShapeVariantValue(opts.openWidth);
|
|
4418
|
+
const step = normalizeShapeVariantValue(opts.step);
|
|
4419
|
+
if (openWidth <= 0 && step <= 0) return this._getFlattenedGlyph(glyph, opts);
|
|
4420
|
+
const key = `${glyph.index}:${opts.size}:${opts.flatten}:${toShapeVariantKey(openWidth)}:${toShapeVariantKey(step)}`;
|
|
4421
|
+
if (!this._glyphVariantCache.has(key)) this._glyphVariantCache.set(key, deriveGlyphGeometryVariant(this._getFlattenedGlyph(glyph, opts), {
|
|
4422
|
+
openWidth,
|
|
4423
|
+
step
|
|
4424
|
+
}));
|
|
4425
|
+
return this._glyphVariantCache.get(key);
|
|
4426
|
+
}
|
|
4230
4427
|
_layoutText(value, opts) {
|
|
4231
4428
|
return layoutGlyphs(this.font, value, opts);
|
|
4232
4429
|
}
|
|
4233
4430
|
};
|
|
4431
|
+
function normalizeShapeVariantValue(value) {
|
|
4432
|
+
return Number.isFinite(value) && value > 0 ? Number(value) : 0;
|
|
4433
|
+
}
|
|
4434
|
+
function toShapeVariantKey(value) {
|
|
4435
|
+
return normalizeShapeVariantValue(value).toFixed(6);
|
|
4436
|
+
}
|
|
4234
4437
|
async function fetchFontBytes(source) {
|
|
4235
4438
|
const response = await fetch(source);
|
|
4236
4439
|
if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);
|