@shotstack/shotstack-canvas 1.3.7 → 1.3.9
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/entry.node.cjs +72 -12
- package/dist/entry.node.d.cts +5 -0
- package/dist/entry.node.d.ts +5 -0
- package/dist/entry.node.js +72 -12
- package/dist/entry.web.d.ts +5 -0
- package/dist/entry.web.js +72 -12
- package/package.json +1 -1
package/dist/entry.node.cjs
CHANGED
|
@@ -370,10 +370,14 @@ var FontRegistry = class _FontRegistry {
|
|
|
370
370
|
fontkitFonts = /* @__PURE__ */ new Map();
|
|
371
371
|
wasmBaseURL;
|
|
372
372
|
initPromise;
|
|
373
|
+
emojiFallbackDesc;
|
|
373
374
|
static fallbackLoader;
|
|
374
375
|
static setFallbackLoader(loader) {
|
|
375
376
|
_FontRegistry.fallbackLoader = loader;
|
|
376
377
|
}
|
|
378
|
+
setEmojiFallback(desc) {
|
|
379
|
+
this.emojiFallbackDesc = desc;
|
|
380
|
+
}
|
|
377
381
|
constructor(wasmBaseURL) {
|
|
378
382
|
this.wasmBaseURL = wasmBaseURL;
|
|
379
383
|
}
|
|
@@ -531,7 +535,8 @@ var FontRegistry = class _FontRegistry {
|
|
|
531
535
|
if (fkFont) {
|
|
532
536
|
const glyph = fkFont.getGlyph(glyphId);
|
|
533
537
|
if (glyph && glyph.path) {
|
|
534
|
-
|
|
538
|
+
const path2 = glyph.path.toSVG();
|
|
539
|
+
return path2 && path2 !== "" ? path2 : "M 0 0";
|
|
535
540
|
}
|
|
536
541
|
}
|
|
537
542
|
const font = await this.getFont(desc);
|
|
@@ -587,6 +592,18 @@ var FontRegistry = class _FontRegistry {
|
|
|
587
592
|
};
|
|
588
593
|
|
|
589
594
|
// src/core/layout.ts
|
|
595
|
+
function isEmoji(char) {
|
|
596
|
+
const code = char.codePointAt(0);
|
|
597
|
+
if (!code) return false;
|
|
598
|
+
return code >= 127744 && code <= 129535 || // Emoticons, symbols, pictographs
|
|
599
|
+
code >= 9728 && code <= 9983 || // Miscellaneous symbols
|
|
600
|
+
code >= 9984 && code <= 10175 || // Dingbats
|
|
601
|
+
code >= 65024 && code <= 65039 || // Variation selectors
|
|
602
|
+
code >= 128512 && code <= 128591 || // Emoticons
|
|
603
|
+
code >= 128640 && code <= 128767 || // Transport and map symbols
|
|
604
|
+
code >= 129280 && code <= 129535 || // Supplemental symbols and pictographs
|
|
605
|
+
code >= 129648 && code <= 129791;
|
|
606
|
+
}
|
|
590
607
|
var LayoutEngine = class {
|
|
591
608
|
constructor(fonts) {
|
|
592
609
|
this.fonts = fonts;
|
|
@@ -634,14 +651,43 @@ var LayoutEngine = class {
|
|
|
634
651
|
}
|
|
635
652
|
async layout(params) {
|
|
636
653
|
try {
|
|
637
|
-
const { textTransform, desc, fontSize, letterSpacing, width } = params;
|
|
654
|
+
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
638
655
|
const input = this.transformText(params.text, textTransform);
|
|
639
656
|
if (!input || input.length === 0) {
|
|
640
657
|
return [];
|
|
641
658
|
}
|
|
642
659
|
let shaped;
|
|
643
660
|
try {
|
|
644
|
-
|
|
661
|
+
if (!emojiFallback) {
|
|
662
|
+
shaped = await this.shapeFull(input, desc);
|
|
663
|
+
} else {
|
|
664
|
+
const chars = Array.from(input);
|
|
665
|
+
const runs = [];
|
|
666
|
+
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
667
|
+
for (let i = 0; i < chars.length; i++) {
|
|
668
|
+
const char = chars[i];
|
|
669
|
+
const charIsEmoji = isEmoji(char);
|
|
670
|
+
if (i === 0) {
|
|
671
|
+
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
672
|
+
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
673
|
+
currentRun.text += char;
|
|
674
|
+
} else {
|
|
675
|
+
runs.push(currentRun);
|
|
676
|
+
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (currentRun.text) runs.push(currentRun);
|
|
680
|
+
shaped = [];
|
|
681
|
+
for (const run of runs) {
|
|
682
|
+
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
683
|
+
const runShaped = await this.shapeFull(run.text, runFont);
|
|
684
|
+
for (const glyph of runShaped) {
|
|
685
|
+
glyph.cl += run.startIndex;
|
|
686
|
+
glyph.fontDesc = runFont;
|
|
687
|
+
}
|
|
688
|
+
shaped.push(...runShaped);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
645
691
|
} catch (err) {
|
|
646
692
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
647
693
|
}
|
|
@@ -667,7 +713,9 @@ var LayoutEngine = class {
|
|
|
667
713
|
xOffset: g.dx * scale,
|
|
668
714
|
yOffset: -g.dy * scale,
|
|
669
715
|
cluster: g.cl,
|
|
670
|
-
char
|
|
716
|
+
char,
|
|
717
|
+
fontDesc: g.fontDesc
|
|
718
|
+
// Preserve font descriptor
|
|
671
719
|
};
|
|
672
720
|
});
|
|
673
721
|
const lines = [];
|
|
@@ -777,18 +825,21 @@ async function buildDrawOps(p) {
|
|
|
777
825
|
if (p.lines.length === 0) return ops;
|
|
778
826
|
const upem = Math.max(1, await p.getUnitsPerEm());
|
|
779
827
|
const scale = p.font.size / upem;
|
|
780
|
-
const
|
|
828
|
+
const numLines = p.lines.length;
|
|
829
|
+
const lineHeightPx = p.font.size * p.style.lineHeight;
|
|
781
830
|
let blockY;
|
|
782
831
|
switch (p.align.vertical) {
|
|
783
832
|
case "top":
|
|
784
833
|
blockY = p.font.size;
|
|
785
834
|
break;
|
|
786
835
|
case "bottom":
|
|
787
|
-
blockY = p.textRect.height -
|
|
836
|
+
blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
|
|
788
837
|
break;
|
|
789
838
|
case "middle":
|
|
790
839
|
default:
|
|
791
|
-
|
|
840
|
+
const capHeightRatio = 0.35;
|
|
841
|
+
const visualOffset = p.font.size * capHeightRatio;
|
|
842
|
+
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
|
|
792
843
|
break;
|
|
793
844
|
}
|
|
794
845
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
@@ -809,9 +860,10 @@ async function buildDrawOps(p) {
|
|
|
809
860
|
break;
|
|
810
861
|
}
|
|
811
862
|
let xCursor = lineX;
|
|
812
|
-
const
|
|
863
|
+
const lineIndex = p.lines.indexOf(line);
|
|
864
|
+
const baselineY = blockY + lineIndex * lineHeightPx;
|
|
813
865
|
for (const glyph of line.glyphs) {
|
|
814
|
-
const path = await p.glyphPathProvider(glyph.id);
|
|
866
|
+
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
815
867
|
if (!path || path === "M 0 0") {
|
|
816
868
|
xCursor += glyph.xAdvance;
|
|
817
869
|
continue;
|
|
@@ -2075,6 +2127,13 @@ async function createTextEngine(opts = {}) {
|
|
|
2075
2127
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
2076
2128
|
let lines;
|
|
2077
2129
|
try {
|
|
2130
|
+
const emojiDesc = { family: "NotoEmoji", weight: "400", style: "normal" };
|
|
2131
|
+
let emojiAvailable = false;
|
|
2132
|
+
try {
|
|
2133
|
+
await fonts.getFace(emojiDesc);
|
|
2134
|
+
emojiAvailable = true;
|
|
2135
|
+
} catch {
|
|
2136
|
+
}
|
|
2078
2137
|
lines = await layout.layout({
|
|
2079
2138
|
text: asset.text,
|
|
2080
2139
|
width: asset.width ?? width,
|
|
@@ -2082,7 +2141,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2082
2141
|
fontSize: main.size,
|
|
2083
2142
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
2084
2143
|
desc,
|
|
2085
|
-
textTransform: asset.style?.textTransform ?? "none"
|
|
2144
|
+
textTransform: asset.style?.textTransform ?? "none",
|
|
2145
|
+
emojiFallback: emojiAvailable ? emojiDesc : void 0
|
|
2086
2146
|
});
|
|
2087
2147
|
} catch (err) {
|
|
2088
2148
|
throw new Error(
|
|
@@ -2124,8 +2184,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2124
2184
|
vertical: asset.align?.vertical ?? "middle"
|
|
2125
2185
|
},
|
|
2126
2186
|
background: asset.background,
|
|
2127
|
-
glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
|
|
2128
|
-
getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
|
|
2187
|
+
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2188
|
+
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2129
2189
|
});
|
|
2130
2190
|
} catch (err) {
|
|
2131
2191
|
throw new Error(
|
package/dist/entry.node.d.cts
CHANGED
package/dist/entry.node.d.ts
CHANGED
package/dist/entry.node.js
CHANGED
|
@@ -331,10 +331,14 @@ var FontRegistry = class _FontRegistry {
|
|
|
331
331
|
fontkitFonts = /* @__PURE__ */ new Map();
|
|
332
332
|
wasmBaseURL;
|
|
333
333
|
initPromise;
|
|
334
|
+
emojiFallbackDesc;
|
|
334
335
|
static fallbackLoader;
|
|
335
336
|
static setFallbackLoader(loader) {
|
|
336
337
|
_FontRegistry.fallbackLoader = loader;
|
|
337
338
|
}
|
|
339
|
+
setEmojiFallback(desc) {
|
|
340
|
+
this.emojiFallbackDesc = desc;
|
|
341
|
+
}
|
|
338
342
|
constructor(wasmBaseURL) {
|
|
339
343
|
this.wasmBaseURL = wasmBaseURL;
|
|
340
344
|
}
|
|
@@ -492,7 +496,8 @@ var FontRegistry = class _FontRegistry {
|
|
|
492
496
|
if (fkFont) {
|
|
493
497
|
const glyph = fkFont.getGlyph(glyphId);
|
|
494
498
|
if (glyph && glyph.path) {
|
|
495
|
-
|
|
499
|
+
const path2 = glyph.path.toSVG();
|
|
500
|
+
return path2 && path2 !== "" ? path2 : "M 0 0";
|
|
496
501
|
}
|
|
497
502
|
}
|
|
498
503
|
const font = await this.getFont(desc);
|
|
@@ -548,6 +553,18 @@ var FontRegistry = class _FontRegistry {
|
|
|
548
553
|
};
|
|
549
554
|
|
|
550
555
|
// src/core/layout.ts
|
|
556
|
+
function isEmoji(char) {
|
|
557
|
+
const code = char.codePointAt(0);
|
|
558
|
+
if (!code) return false;
|
|
559
|
+
return code >= 127744 && code <= 129535 || // Emoticons, symbols, pictographs
|
|
560
|
+
code >= 9728 && code <= 9983 || // Miscellaneous symbols
|
|
561
|
+
code >= 9984 && code <= 10175 || // Dingbats
|
|
562
|
+
code >= 65024 && code <= 65039 || // Variation selectors
|
|
563
|
+
code >= 128512 && code <= 128591 || // Emoticons
|
|
564
|
+
code >= 128640 && code <= 128767 || // Transport and map symbols
|
|
565
|
+
code >= 129280 && code <= 129535 || // Supplemental symbols and pictographs
|
|
566
|
+
code >= 129648 && code <= 129791;
|
|
567
|
+
}
|
|
551
568
|
var LayoutEngine = class {
|
|
552
569
|
constructor(fonts) {
|
|
553
570
|
this.fonts = fonts;
|
|
@@ -595,14 +612,43 @@ var LayoutEngine = class {
|
|
|
595
612
|
}
|
|
596
613
|
async layout(params) {
|
|
597
614
|
try {
|
|
598
|
-
const { textTransform, desc, fontSize, letterSpacing, width } = params;
|
|
615
|
+
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
599
616
|
const input = this.transformText(params.text, textTransform);
|
|
600
617
|
if (!input || input.length === 0) {
|
|
601
618
|
return [];
|
|
602
619
|
}
|
|
603
620
|
let shaped;
|
|
604
621
|
try {
|
|
605
|
-
|
|
622
|
+
if (!emojiFallback) {
|
|
623
|
+
shaped = await this.shapeFull(input, desc);
|
|
624
|
+
} else {
|
|
625
|
+
const chars = Array.from(input);
|
|
626
|
+
const runs = [];
|
|
627
|
+
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
628
|
+
for (let i = 0; i < chars.length; i++) {
|
|
629
|
+
const char = chars[i];
|
|
630
|
+
const charIsEmoji = isEmoji(char);
|
|
631
|
+
if (i === 0) {
|
|
632
|
+
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
633
|
+
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
634
|
+
currentRun.text += char;
|
|
635
|
+
} else {
|
|
636
|
+
runs.push(currentRun);
|
|
637
|
+
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (currentRun.text) runs.push(currentRun);
|
|
641
|
+
shaped = [];
|
|
642
|
+
for (const run of runs) {
|
|
643
|
+
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
644
|
+
const runShaped = await this.shapeFull(run.text, runFont);
|
|
645
|
+
for (const glyph of runShaped) {
|
|
646
|
+
glyph.cl += run.startIndex;
|
|
647
|
+
glyph.fontDesc = runFont;
|
|
648
|
+
}
|
|
649
|
+
shaped.push(...runShaped);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
606
652
|
} catch (err) {
|
|
607
653
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
608
654
|
}
|
|
@@ -628,7 +674,9 @@ var LayoutEngine = class {
|
|
|
628
674
|
xOffset: g.dx * scale,
|
|
629
675
|
yOffset: -g.dy * scale,
|
|
630
676
|
cluster: g.cl,
|
|
631
|
-
char
|
|
677
|
+
char,
|
|
678
|
+
fontDesc: g.fontDesc
|
|
679
|
+
// Preserve font descriptor
|
|
632
680
|
};
|
|
633
681
|
});
|
|
634
682
|
const lines = [];
|
|
@@ -738,18 +786,21 @@ async function buildDrawOps(p) {
|
|
|
738
786
|
if (p.lines.length === 0) return ops;
|
|
739
787
|
const upem = Math.max(1, await p.getUnitsPerEm());
|
|
740
788
|
const scale = p.font.size / upem;
|
|
741
|
-
const
|
|
789
|
+
const numLines = p.lines.length;
|
|
790
|
+
const lineHeightPx = p.font.size * p.style.lineHeight;
|
|
742
791
|
let blockY;
|
|
743
792
|
switch (p.align.vertical) {
|
|
744
793
|
case "top":
|
|
745
794
|
blockY = p.font.size;
|
|
746
795
|
break;
|
|
747
796
|
case "bottom":
|
|
748
|
-
blockY = p.textRect.height -
|
|
797
|
+
blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
|
|
749
798
|
break;
|
|
750
799
|
case "middle":
|
|
751
800
|
default:
|
|
752
|
-
|
|
801
|
+
const capHeightRatio = 0.35;
|
|
802
|
+
const visualOffset = p.font.size * capHeightRatio;
|
|
803
|
+
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
|
|
753
804
|
break;
|
|
754
805
|
}
|
|
755
806
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
@@ -770,9 +821,10 @@ async function buildDrawOps(p) {
|
|
|
770
821
|
break;
|
|
771
822
|
}
|
|
772
823
|
let xCursor = lineX;
|
|
773
|
-
const
|
|
824
|
+
const lineIndex = p.lines.indexOf(line);
|
|
825
|
+
const baselineY = blockY + lineIndex * lineHeightPx;
|
|
774
826
|
for (const glyph of line.glyphs) {
|
|
775
|
-
const path = await p.glyphPathProvider(glyph.id);
|
|
827
|
+
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
776
828
|
if (!path || path === "M 0 0") {
|
|
777
829
|
xCursor += glyph.xAdvance;
|
|
778
830
|
continue;
|
|
@@ -2036,6 +2088,13 @@ async function createTextEngine(opts = {}) {
|
|
|
2036
2088
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
2037
2089
|
let lines;
|
|
2038
2090
|
try {
|
|
2091
|
+
const emojiDesc = { family: "NotoEmoji", weight: "400", style: "normal" };
|
|
2092
|
+
let emojiAvailable = false;
|
|
2093
|
+
try {
|
|
2094
|
+
await fonts.getFace(emojiDesc);
|
|
2095
|
+
emojiAvailable = true;
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2039
2098
|
lines = await layout.layout({
|
|
2040
2099
|
text: asset.text,
|
|
2041
2100
|
width: asset.width ?? width,
|
|
@@ -2043,7 +2102,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2043
2102
|
fontSize: main.size,
|
|
2044
2103
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
2045
2104
|
desc,
|
|
2046
|
-
textTransform: asset.style?.textTransform ?? "none"
|
|
2105
|
+
textTransform: asset.style?.textTransform ?? "none",
|
|
2106
|
+
emojiFallback: emojiAvailable ? emojiDesc : void 0
|
|
2047
2107
|
});
|
|
2048
2108
|
} catch (err) {
|
|
2049
2109
|
throw new Error(
|
|
@@ -2085,8 +2145,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2085
2145
|
vertical: asset.align?.vertical ?? "middle"
|
|
2086
2146
|
},
|
|
2087
2147
|
background: asset.background,
|
|
2088
|
-
glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
|
|
2089
|
-
getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
|
|
2148
|
+
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2149
|
+
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2090
2150
|
});
|
|
2091
2151
|
} catch (err) {
|
|
2092
2152
|
throw new Error(
|
package/dist/entry.web.d.ts
CHANGED
package/dist/entry.web.js
CHANGED
|
@@ -336,11 +336,15 @@ var _FontRegistry = class _FontRegistry {
|
|
|
336
336
|
__publicField(this, "fontkitFonts", /* @__PURE__ */ new Map());
|
|
337
337
|
__publicField(this, "wasmBaseURL");
|
|
338
338
|
__publicField(this, "initPromise");
|
|
339
|
+
__publicField(this, "emojiFallbackDesc");
|
|
339
340
|
this.wasmBaseURL = wasmBaseURL;
|
|
340
341
|
}
|
|
341
342
|
static setFallbackLoader(loader) {
|
|
342
343
|
_FontRegistry.fallbackLoader = loader;
|
|
343
344
|
}
|
|
345
|
+
setEmojiFallback(desc) {
|
|
346
|
+
this.emojiFallbackDesc = desc;
|
|
347
|
+
}
|
|
344
348
|
async init() {
|
|
345
349
|
if (this.initPromise) {
|
|
346
350
|
await this.initPromise;
|
|
@@ -495,7 +499,8 @@ var _FontRegistry = class _FontRegistry {
|
|
|
495
499
|
if (fkFont) {
|
|
496
500
|
const glyph = fkFont.getGlyph(glyphId);
|
|
497
501
|
if (glyph && glyph.path) {
|
|
498
|
-
|
|
502
|
+
const path2 = glyph.path.toSVG();
|
|
503
|
+
return path2 && path2 !== "" ? path2 : "M 0 0";
|
|
499
504
|
}
|
|
500
505
|
}
|
|
501
506
|
const font = await this.getFont(desc);
|
|
@@ -553,6 +558,18 @@ __publicField(_FontRegistry, "fallbackLoader");
|
|
|
553
558
|
var FontRegistry = _FontRegistry;
|
|
554
559
|
|
|
555
560
|
// src/core/layout.ts
|
|
561
|
+
function isEmoji(char) {
|
|
562
|
+
const code = char.codePointAt(0);
|
|
563
|
+
if (!code) return false;
|
|
564
|
+
return code >= 127744 && code <= 129535 || // Emoticons, symbols, pictographs
|
|
565
|
+
code >= 9728 && code <= 9983 || // Miscellaneous symbols
|
|
566
|
+
code >= 9984 && code <= 10175 || // Dingbats
|
|
567
|
+
code >= 65024 && code <= 65039 || // Variation selectors
|
|
568
|
+
code >= 128512 && code <= 128591 || // Emoticons
|
|
569
|
+
code >= 128640 && code <= 128767 || // Transport and map symbols
|
|
570
|
+
code >= 129280 && code <= 129535 || // Supplemental symbols and pictographs
|
|
571
|
+
code >= 129648 && code <= 129791;
|
|
572
|
+
}
|
|
556
573
|
var LayoutEngine = class {
|
|
557
574
|
constructor(fonts) {
|
|
558
575
|
this.fonts = fonts;
|
|
@@ -600,14 +617,43 @@ var LayoutEngine = class {
|
|
|
600
617
|
}
|
|
601
618
|
async layout(params) {
|
|
602
619
|
try {
|
|
603
|
-
const { textTransform, desc, fontSize, letterSpacing, width } = params;
|
|
620
|
+
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
604
621
|
const input = this.transformText(params.text, textTransform);
|
|
605
622
|
if (!input || input.length === 0) {
|
|
606
623
|
return [];
|
|
607
624
|
}
|
|
608
625
|
let shaped;
|
|
609
626
|
try {
|
|
610
|
-
|
|
627
|
+
if (!emojiFallback) {
|
|
628
|
+
shaped = await this.shapeFull(input, desc);
|
|
629
|
+
} else {
|
|
630
|
+
const chars = Array.from(input);
|
|
631
|
+
const runs = [];
|
|
632
|
+
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
633
|
+
for (let i = 0; i < chars.length; i++) {
|
|
634
|
+
const char = chars[i];
|
|
635
|
+
const charIsEmoji = isEmoji(char);
|
|
636
|
+
if (i === 0) {
|
|
637
|
+
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
638
|
+
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
639
|
+
currentRun.text += char;
|
|
640
|
+
} else {
|
|
641
|
+
runs.push(currentRun);
|
|
642
|
+
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (currentRun.text) runs.push(currentRun);
|
|
646
|
+
shaped = [];
|
|
647
|
+
for (const run of runs) {
|
|
648
|
+
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
649
|
+
const runShaped = await this.shapeFull(run.text, runFont);
|
|
650
|
+
for (const glyph of runShaped) {
|
|
651
|
+
glyph.cl += run.startIndex;
|
|
652
|
+
glyph.fontDesc = runFont;
|
|
653
|
+
}
|
|
654
|
+
shaped.push(...runShaped);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
611
657
|
} catch (err) {
|
|
612
658
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
613
659
|
}
|
|
@@ -633,7 +679,9 @@ var LayoutEngine = class {
|
|
|
633
679
|
xOffset: g.dx * scale,
|
|
634
680
|
yOffset: -g.dy * scale,
|
|
635
681
|
cluster: g.cl,
|
|
636
|
-
char
|
|
682
|
+
char,
|
|
683
|
+
fontDesc: g.fontDesc
|
|
684
|
+
// Preserve font descriptor
|
|
637
685
|
};
|
|
638
686
|
});
|
|
639
687
|
const lines = [];
|
|
@@ -743,18 +791,21 @@ async function buildDrawOps(p) {
|
|
|
743
791
|
if (p.lines.length === 0) return ops;
|
|
744
792
|
const upem = Math.max(1, await p.getUnitsPerEm());
|
|
745
793
|
const scale = p.font.size / upem;
|
|
746
|
-
const
|
|
794
|
+
const numLines = p.lines.length;
|
|
795
|
+
const lineHeightPx = p.font.size * p.style.lineHeight;
|
|
747
796
|
let blockY;
|
|
748
797
|
switch (p.align.vertical) {
|
|
749
798
|
case "top":
|
|
750
799
|
blockY = p.font.size;
|
|
751
800
|
break;
|
|
752
801
|
case "bottom":
|
|
753
|
-
blockY = p.textRect.height -
|
|
802
|
+
blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
|
|
754
803
|
break;
|
|
755
804
|
case "middle":
|
|
756
805
|
default:
|
|
757
|
-
|
|
806
|
+
const capHeightRatio = 0.35;
|
|
807
|
+
const visualOffset = p.font.size * capHeightRatio;
|
|
808
|
+
blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
|
|
758
809
|
break;
|
|
759
810
|
}
|
|
760
811
|
const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
|
|
@@ -775,9 +826,10 @@ async function buildDrawOps(p) {
|
|
|
775
826
|
break;
|
|
776
827
|
}
|
|
777
828
|
let xCursor = lineX;
|
|
778
|
-
const
|
|
829
|
+
const lineIndex = p.lines.indexOf(line);
|
|
830
|
+
const baselineY = blockY + lineIndex * lineHeightPx;
|
|
779
831
|
for (const glyph of line.glyphs) {
|
|
780
|
-
const path = await p.glyphPathProvider(glyph.id);
|
|
832
|
+
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
781
833
|
if (!path || path === "M 0 0") {
|
|
782
834
|
xCursor += glyph.xAdvance;
|
|
783
835
|
continue;
|
|
@@ -1757,6 +1809,13 @@ async function createTextEngine(opts = {}) {
|
|
|
1757
1809
|
const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
|
|
1758
1810
|
let lines;
|
|
1759
1811
|
try {
|
|
1812
|
+
const emojiDesc = { family: "NotoEmoji", weight: "400", style: "normal" };
|
|
1813
|
+
let emojiAvailable = false;
|
|
1814
|
+
try {
|
|
1815
|
+
await fonts.getFace(emojiDesc);
|
|
1816
|
+
emojiAvailable = true;
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1760
1819
|
lines = await layout.layout({
|
|
1761
1820
|
text: asset.text,
|
|
1762
1821
|
width: asset.width ?? width,
|
|
@@ -1764,7 +1823,8 @@ async function createTextEngine(opts = {}) {
|
|
|
1764
1823
|
fontSize: main.size,
|
|
1765
1824
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
1766
1825
|
desc,
|
|
1767
|
-
textTransform: asset.style?.textTransform ?? "none"
|
|
1826
|
+
textTransform: asset.style?.textTransform ?? "none",
|
|
1827
|
+
emojiFallback: emojiAvailable ? emojiDesc : void 0
|
|
1768
1828
|
});
|
|
1769
1829
|
} catch (err) {
|
|
1770
1830
|
throw new Error(
|
|
@@ -1806,8 +1866,8 @@ async function createTextEngine(opts = {}) {
|
|
|
1806
1866
|
vertical: asset.align?.vertical ?? "middle"
|
|
1807
1867
|
},
|
|
1808
1868
|
background: asset.background,
|
|
1809
|
-
glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
|
|
1810
|
-
getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
|
|
1869
|
+
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
1870
|
+
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
1811
1871
|
});
|
|
1812
1872
|
} catch (err) {
|
|
1813
1873
|
throw new Error(
|
package/package.json
CHANGED