@clypra/engine 1.1.1 → 1.2.0

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/index.cjs CHANGED
@@ -21,6 +21,7 @@ var index_exports = {};
21
21
  __export(index_exports, {
22
22
  COMPOSITION_PRESETS: () => COMPOSITION_PRESETS,
23
23
  CUSTOM_ENGINE_IDS: () => CUSTOM_ENGINE_IDS,
24
+ CanvasDevice: () => CanvasDevice,
24
25
  DEFAULT_CANVAS_HEIGHT: () => DEFAULT_CANVAS_HEIGHT,
25
26
  DEFAULT_CANVAS_WIDTH: () => DEFAULT_CANVAS_WIDTH,
26
27
  DEFAULT_DURATION: () => DEFAULT_DURATION,
@@ -32,6 +33,7 @@ __export(index_exports, {
32
33
  ENTRANCE_PRESETS: () => ENTRANCE_PRESETS,
33
34
  EXIT_PRESETS: () => EXIT_PRESETS,
34
35
  FONT_WEIGHT_OPTIONS: () => FONT_WEIGHT_OPTIONS,
36
+ FontLoader: () => FontLoader,
35
37
  InkBrushEngine: () => InkBrushEngine,
36
38
  LEGACY_RENDERER_MAP: () => LEGACY_RENDERER_MAP,
37
39
  LOOP_PRESETS: () => LOOP_PRESETS,
@@ -43,6 +45,7 @@ __export(index_exports, {
43
45
  TextEffectRenderer: () => TextEffectRenderer,
44
46
  WEBM_EXPORT_MAX_FRAMES: () => WEBM_EXPORT_MAX_FRAMES,
45
47
  WebGLCompositor: () => WebGLCompositor,
48
+ _buildConfig: () => _buildConfig,
46
49
  _resetPlatformCache: () => _resetPlatformCache,
47
50
  addImageLayer: () => addImageLayer,
48
51
  addKeyframeAtTime: () => addKeyframeAtTime,
@@ -100,6 +103,7 @@ __export(index_exports, {
100
103
  encodeGif: () => encodeGif,
101
104
  ensureDefaultTimeline: () => ensureDefaultTimeline,
102
105
  ensureFontInLottie: () => ensureFontInLottie,
106
+ ensureFontsLoaded: () => ensureFontsLoaded,
103
107
  evaluateConfig: () => evaluateConfig,
104
108
  evaluateScene: () => evaluateScene,
105
109
  findTrackIndex: () => findTrackIndex,
@@ -107,6 +111,7 @@ __export(index_exports, {
107
111
  getAnimatableParamDef: () => getAnimatableParamDef,
108
112
  getAnimatableParamsForLayer: () => getAnimatableParamsForLayer,
109
113
  getDefaultText: () => getDefaultText,
114
+ getFontLoader: () => getFontLoader,
110
115
  getLayerById: () => getLayerById,
111
116
  getPresetScene: () => getPresetScene,
112
117
  getSceneTime: () => getSceneTime,
@@ -125,6 +130,7 @@ __export(index_exports, {
125
130
  injectText: () => injectText,
126
131
  injectTextStyle: () => injectTextStyle,
127
132
  isWebMExportSupported: () => isWebMExportSupported,
133
+ layerToTextEffectConfig: () => layerToTextEffectConfig,
128
134
  loadLottieFonts: () => loadLottieFonts,
129
135
  lottieColorToHex: () => lottieColorToHex,
130
136
  lottieJToAlign: () => lottieJToAlign,
@@ -147,10 +153,12 @@ __export(index_exports, {
147
153
  renderPngSequence: () => renderPngSequence,
148
154
  renderSceneWebM: () => renderSceneWebM,
149
155
  renderTextEffectCore: () => renderTextEffectCore,
156
+ resetFontLoader: () => resetFontLoader,
150
157
  resetSceneTime: () => resetSceneTime,
151
158
  resizeCharFillColors: () => resizeCharFillColors,
152
159
  resolveAnimatedScalar: () => resolveAnimatedScalar,
153
160
  resolveCustomEngineId: () => resolveCustomEngineId,
161
+ resolveFontFamilyName: () => resolveFontFamilyName,
154
162
  restoreLetterSpacing: () => restoreLetterSpacing,
155
163
  scanLottieFonts: () => scanLottieFonts,
156
164
  scanTextLayers: () => scanTextLayers,
@@ -568,6 +576,46 @@ function _resetPlatformCache() {
568
576
  _offscreenCanvas = null;
569
577
  _webgl2 = null;
570
578
  }
579
+ var CanvasDevice = class {
580
+ static canvases = [];
581
+ static maxPoolSize = 10;
582
+ /**
583
+ * Acquire a Canvas context from the pool or create a new one.
584
+ * If a canvas is pulled from the pool, it is resized to the target dimensions.
585
+ */
586
+ static acquire(width, height) {
587
+ let canvas;
588
+ if (this.canvases.length > 0) {
589
+ canvas = this.canvases.pop();
590
+ if (canvas.width !== width || canvas.height !== height) {
591
+ canvas.width = width;
592
+ canvas.height = height;
593
+ }
594
+ } else {
595
+ canvas = createCanvas(width, height);
596
+ }
597
+ return canvas;
598
+ }
599
+ /**
600
+ * Release a canvas back to the pool, or free its resources immediately if pool is full.
601
+ */
602
+ static release(canvas) {
603
+ if (this.canvases.length < this.maxPoolSize) {
604
+ this.canvases.push(canvas);
605
+ } else {
606
+ releaseCanvas(canvas);
607
+ }
608
+ }
609
+ /**
610
+ * Disposes all pooled canvases to release GPU/memory backing stores.
611
+ */
612
+ static clearPool() {
613
+ while (this.canvases.length > 0) {
614
+ const c = this.canvases.pop();
615
+ releaseCanvas(c);
616
+ }
617
+ }
618
+ };
571
619
 
572
620
  // src/engine/procedural/utils.ts
573
621
  function getCanvas2DContext(canvas) {
@@ -959,6 +1007,17 @@ function restoreLetterSpacing(ctx, saved) {
959
1007
  function getCanvas2DContext2(canvas) {
960
1008
  return canvas.getContext("2d");
961
1009
  }
1010
+ function ctxSupportsFilter(ctx) {
1011
+ try {
1012
+ const prev = ctx.filter;
1013
+ ctx.filter = "blur(4px)";
1014
+ const ok = typeof ctx.filter === "string" && ctx.filter.includes("blur");
1015
+ ctx.filter = prev;
1016
+ return ok;
1017
+ } catch {
1018
+ return false;
1019
+ }
1020
+ }
962
1021
  function renderTextEffectCore(ctx, cfg) {
963
1022
  if (cfg.customRenderer === "InkBrushEngine") {
964
1023
  const engine = new InkBrushEngine(cfg);
@@ -1268,19 +1327,31 @@ function renderTextEffectCore(ctx, cfg) {
1268
1327
  const vpy = cHeight / 2 + (bevelVanishingPointY !== void 0 ? bevelVanishingPointY : 80) / 100 * (cHeight / 2);
1269
1328
  const fl = Math.max(100, bevelFocalLength !== void 0 ? bevelFocalLength : 400);
1270
1329
  if (bevelBlur && bevelBlur > 0) {
1271
- ctx.save();
1272
- ctx.filter = `blur(${bevelBlur}px)`;
1273
1330
  const blurColor = bevelBlurColor || bevelShadow || "#000000";
1274
- for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1275
- const scale = fl / (fl + i);
1331
+ if (ctxSupportsFilter(ctx)) {
1276
1332
  ctx.save();
1277
- ctx.translate(vpx, vpy);
1278
- ctx.scale(scale, scale);
1279
- ctx.translate(-vpx, -vpy);
1280
- renderLines("fill", blurColor);
1333
+ ctx.filter = `blur(${bevelBlur}px)`;
1334
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1335
+ const scale = fl / (fl + i);
1336
+ ctx.save();
1337
+ ctx.translate(vpx, vpy);
1338
+ ctx.scale(scale, scale);
1339
+ ctx.translate(-vpx, -vpy);
1340
+ renderLines("fill", blurColor);
1341
+ ctx.restore();
1342
+ }
1281
1343
  ctx.restore();
1344
+ } else {
1345
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1346
+ const scale = fl / (fl + i);
1347
+ ctx.save();
1348
+ ctx.translate(vpx, vpy);
1349
+ ctx.scale(scale, scale);
1350
+ ctx.translate(-vpx, -vpy);
1351
+ renderWithShadowTrick("fill", blurColor, bevelBlur, 0, 0, 100);
1352
+ ctx.restore();
1353
+ }
1282
1354
  }
1283
- ctx.restore();
1284
1355
  }
1285
1356
  ctx.save();
1286
1357
  for (let i = bevelDepth; i > 0; i--) {
@@ -1327,14 +1398,21 @@ function renderTextEffectCore(ctx, cfg) {
1327
1398
  return { dx: i, dy: i };
1328
1399
  };
1329
1400
  if (bevelBlur && bevelBlur > 0) {
1330
- ctx.save();
1331
- ctx.filter = `blur(${bevelBlur}px)`;
1332
1401
  const blurColor = bevelBlurColor || bevelShadow || "#000000";
1333
- for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1334
- const { dx, dy } = getDirOffset(i);
1335
- renderLines("fill", blurColor, dx, dy);
1402
+ if (ctxSupportsFilter(ctx)) {
1403
+ ctx.save();
1404
+ ctx.filter = `blur(${bevelBlur}px)`;
1405
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1406
+ const { dx, dy } = getDirOffset(i);
1407
+ renderLines("fill", blurColor, dx, dy);
1408
+ }
1409
+ ctx.restore();
1410
+ } else {
1411
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1412
+ const { dx, dy } = getDirOffset(i);
1413
+ renderWithShadowTrick("fill", blurColor, bevelBlur, dx, dy, 100);
1414
+ }
1336
1415
  }
1337
- ctx.restore();
1338
1416
  }
1339
1417
  ctx.save();
1340
1418
  for (let i = bevelDepth; i > 0; i--) {
@@ -1388,24 +1466,51 @@ function renderTextEffectCore(ctx, cfg) {
1388
1466
  customStrokeStyle = grad;
1389
1467
  }
1390
1468
  const drawStrokeLayer = (color, width, blurAmount, opacity, position) => {
1391
- ctx.save();
1392
- ctx.globalAlpha = opacity / 100;
1393
- ctx.strokeStyle = color;
1394
- if (blurAmount > 0) {
1469
+ if (blurAmount > 0 && ctxSupportsFilter(ctx)) {
1470
+ ctx.save();
1471
+ ctx.globalAlpha = opacity / 100;
1472
+ ctx.strokeStyle = color;
1395
1473
  ctx.filter = `blur(${blurAmount}px)`;
1474
+ if (position === "outside") {
1475
+ ctx.lineWidth = width * 2;
1476
+ renderLines("stroke");
1477
+ } else if (position === "center") {
1478
+ ctx.lineWidth = width;
1479
+ renderLines("stroke");
1480
+ } else if (position === "inside") {
1481
+ ctx.globalCompositeOperation = "source-atop";
1482
+ ctx.lineWidth = width * 2;
1483
+ renderLines("stroke");
1484
+ }
1485
+ ctx.restore();
1486
+ } else if (blurAmount > 0) {
1487
+ const colorStr = typeof color === "string" ? color : strokeColor;
1488
+ const spread = position === "center" ? width / 2 : width;
1489
+ if (position === "inside") {
1490
+ ctx.save();
1491
+ ctx.globalCompositeOperation = "source-atop";
1492
+ renderWithShadowTrick("stroke", colorStr, blurAmount, 0, 0, opacity, void 0, spread);
1493
+ ctx.restore();
1494
+ } else {
1495
+ renderWithShadowTrick("stroke", colorStr, blurAmount, 0, 0, opacity, void 0, spread);
1496
+ }
1497
+ } else {
1498
+ ctx.save();
1499
+ ctx.globalAlpha = opacity / 100;
1500
+ ctx.strokeStyle = color;
1501
+ if (position === "outside") {
1502
+ ctx.lineWidth = width * 2;
1503
+ renderLines("stroke");
1504
+ } else if (position === "center") {
1505
+ ctx.lineWidth = width;
1506
+ renderLines("stroke");
1507
+ } else if (position === "inside") {
1508
+ ctx.globalCompositeOperation = "source-atop";
1509
+ ctx.lineWidth = width * 2;
1510
+ renderLines("stroke");
1511
+ }
1512
+ ctx.restore();
1396
1513
  }
1397
- if (position === "outside") {
1398
- ctx.lineWidth = width * 2;
1399
- renderLines("stroke");
1400
- } else if (position === "center") {
1401
- ctx.lineWidth = width;
1402
- renderLines("stroke");
1403
- } else if (position === "inside") {
1404
- ctx.globalCompositeOperation = "source-atop";
1405
- ctx.lineWidth = width * 2;
1406
- renderLines("stroke");
1407
- }
1408
- ctx.restore();
1409
1514
  };
1410
1515
  if (sType === "double") {
1411
1516
  const outerWidth = strokeWidth + sWidthSecondary;
@@ -2530,6 +2635,146 @@ function checkFontVariant(variantName) {
2530
2635
  if (typeof document === "undefined" || !document.fonts) return false;
2531
2636
  return document.fonts.check(`16px "${variantName}"`);
2532
2637
  }
2638
+ var FontLoader = class {
2639
+ state = {
2640
+ loading: /* @__PURE__ */ new Set(),
2641
+ loaded: /* @__PURE__ */ new Set(),
2642
+ failed: /* @__PURE__ */ new Map(),
2643
+ promises: /* @__PURE__ */ new Map()
2644
+ };
2645
+ async ensureFont(descriptor) {
2646
+ const key = this.getFontKey(descriptor);
2647
+ if (this.state.loaded.has(key)) {
2648
+ return {
2649
+ font: descriptor,
2650
+ loaded: true,
2651
+ loadTimeMs: 0
2652
+ };
2653
+ }
2654
+ if (this.state.failed.has(key)) {
2655
+ return {
2656
+ font: descriptor,
2657
+ loaded: false,
2658
+ error: this.state.failed.get(key),
2659
+ loadTimeMs: 0
2660
+ };
2661
+ }
2662
+ if (this.state.promises.has(key)) {
2663
+ return this.state.promises.get(key);
2664
+ }
2665
+ const promise = this.loadFont(descriptor);
2666
+ this.state.promises.set(key, promise);
2667
+ return promise;
2668
+ }
2669
+ async ensureFonts(descriptors) {
2670
+ return Promise.all(descriptors.map((desc) => this.ensureFont(desc)));
2671
+ }
2672
+ async waitForFontsReady() {
2673
+ if (typeof document === "undefined" || !document.fonts) {
2674
+ return;
2675
+ }
2676
+ await document.fonts.ready;
2677
+ }
2678
+ isLoaded(descriptor) {
2679
+ const key = this.getFontKey(descriptor);
2680
+ return this.state.loaded.has(key);
2681
+ }
2682
+ getStats() {
2683
+ return {
2684
+ loaded: this.state.loaded.size,
2685
+ loading: this.state.loading.size,
2686
+ failed: this.state.failed.size
2687
+ };
2688
+ }
2689
+ clear() {
2690
+ this.state.loading.clear();
2691
+ this.state.loaded.clear();
2692
+ this.state.failed.clear();
2693
+ this.state.promises.clear();
2694
+ }
2695
+ async loadFont(descriptor) {
2696
+ const key = this.getFontKey(descriptor);
2697
+ const startTime = performance.now();
2698
+ this.state.loading.add(key);
2699
+ try {
2700
+ if (typeof document === "undefined" || !document.fonts) {
2701
+ throw new Error("Font API not available");
2702
+ }
2703
+ const weight = this.normalizeFontWeight(descriptor.weight);
2704
+ const style = descriptor.style || "normal";
2705
+ const fontFace = `${style} ${weight} 16px "${descriptor.family}"`;
2706
+ if (document.fonts.check(fontFace)) {
2707
+ this.state.loaded.add(key);
2708
+ this.state.loading.delete(key);
2709
+ this.state.promises.delete(key);
2710
+ return {
2711
+ font: descriptor,
2712
+ loaded: true,
2713
+ loadTimeMs: performance.now() - startTime
2714
+ };
2715
+ }
2716
+ await document.fonts.load(fontFace);
2717
+ if (!document.fonts.check(fontFace)) {
2718
+ throw new Error(`Font "${descriptor.family}" failed to load`);
2719
+ }
2720
+ this.state.loaded.add(key);
2721
+ this.state.loading.delete(key);
2722
+ this.state.promises.delete(key);
2723
+ return {
2724
+ font: descriptor,
2725
+ loaded: true,
2726
+ loadTimeMs: performance.now() - startTime
2727
+ };
2728
+ } catch (error) {
2729
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2730
+ this.state.failed.set(key, errorMessage);
2731
+ this.state.loading.delete(key);
2732
+ this.state.promises.delete(key);
2733
+ return {
2734
+ font: descriptor,
2735
+ loaded: false,
2736
+ error: errorMessage,
2737
+ loadTimeMs: performance.now() - startTime
2738
+ };
2739
+ }
2740
+ }
2741
+ getFontKey(descriptor) {
2742
+ const weight = this.normalizeFontWeight(descriptor.weight);
2743
+ const style = descriptor.style || "normal";
2744
+ return `${descriptor.family}|${weight}|${style}`;
2745
+ }
2746
+ normalizeFontWeight(weight) {
2747
+ if (typeof weight === "number") {
2748
+ return weight;
2749
+ }
2750
+ if (!weight) return 400;
2751
+ const asNum = parseInt(weight, 10);
2752
+ if (!isNaN(asNum) && asNum >= 100 && asNum <= 900) {
2753
+ return asNum;
2754
+ }
2755
+ const weightMap = {
2756
+ normal: 400,
2757
+ bold: 700,
2758
+ lighter: 300,
2759
+ bolder: 700
2760
+ };
2761
+ return weightMap[weight] ?? 400;
2762
+ }
2763
+ };
2764
+ var globalFontLoader = null;
2765
+ function getFontLoader() {
2766
+ if (!globalFontLoader) {
2767
+ globalFontLoader = new FontLoader();
2768
+ }
2769
+ return globalFontLoader;
2770
+ }
2771
+ function resetFontLoader() {
2772
+ globalFontLoader = null;
2773
+ }
2774
+ async function ensureFontsLoaded(descriptors) {
2775
+ const loader = getFontLoader();
2776
+ return loader.ensureFonts(descriptors);
2777
+ }
2533
2778
 
2534
2779
  // src/engine/timelineDefaults.ts
2535
2780
  function ensureDefaultTimeline(doc) {
@@ -2901,6 +3146,212 @@ function mergeSceneIntoConfig(doc, base) {
2901
3146
  const out = sceneToConfig({ ...doc, legacyConfig: base });
2902
3147
  return out;
2903
3148
  }
3149
+ function resolveFontFamilyName(fontFamily) {
3150
+ const f = fontFamily?.toLowerCase() || "";
3151
+ if (f.includes("inter")) return "Inter Variable";
3152
+ if (f.includes("montserrat")) return "Montserrat Variable";
3153
+ if (f.includes("geist")) return "Geist Variable";
3154
+ if (f.includes("space grotesk") || f.includes("grotesk")) return "Space Grotesk Variable";
3155
+ if (f.includes("outfit")) return "Outfit Variable";
3156
+ if (f.includes("roboto variable")) return "Roboto Variable";
3157
+ if (f.includes("roboto condensed")) return "Roboto Condensed";
3158
+ if (f === "roboto") return "Roboto Variable";
3159
+ if (f.includes("open sans")) return "Open Sans Variable";
3160
+ if (f.includes("raleway")) return "Raleway Variable";
3161
+ if (f.includes("oswald")) return "Oswald Variable";
3162
+ if (f.includes("playfair display")) return "Playfair Display Variable";
3163
+ if (f.includes("nunito")) return "Nunito Variable";
3164
+ if (f.includes("dancing script")) return "Dancing Script Variable";
3165
+ if (f === "lato") return "Lato";
3166
+ if (f === "anton") return "Anton";
3167
+ if (f === "bebas neue") return "Bebas Neue";
3168
+ if (f === "poppins") return "Poppins";
3169
+ if (f === "permanent marker") return "Permanent Marker";
3170
+ if (f === "bangers") return "Bangers";
3171
+ if (f === "press start 2p") return "Press Start 2P";
3172
+ if (f === "pacifico") return "Pacifico";
3173
+ return fontFamily;
3174
+ }
3175
+ function _buildConfig(effect, text, fontSize, canvasWidth, canvasHeight, time, clipStartTime, clipDuration) {
3176
+ const fill = effect.fills?.[0];
3177
+ const stroke = effect.strokes?.[0];
3178
+ const shadow = effect.shadows?.[0];
3179
+ const bevel = effect.bevel;
3180
+ const panel = effect.panel;
3181
+ const ratio = fontSize / 100;
3182
+ const config = {
3183
+ // Canvas / text
3184
+ width: canvasWidth,
3185
+ height: canvasHeight,
3186
+ canvasWidth,
3187
+ canvasHeight,
3188
+ text,
3189
+ time: time ?? 0,
3190
+ clipStartTime: clipStartTime ?? 0,
3191
+ clipDuration: clipDuration ?? 5,
3192
+ // Font
3193
+ fontFamily: resolveFontFamilyName(effect.font.family),
3194
+ fontWeight: effect.font.weight,
3195
+ fontStyle: effect.font.style,
3196
+ fontSize,
3197
+ letterSpacing: effect.font.letterSpacing,
3198
+ lineHeight: effect.font.lineHeight
3199
+ };
3200
+ if (effect.animation) {
3201
+ config.animation = effect.animation;
3202
+ }
3203
+ if (fill) {
3204
+ if (fill.type !== void 0) config.fillType = fill.type;
3205
+ if (fill.color !== void 0) config.fillColor = fill.color;
3206
+ if (fill.gradient?.angle !== void 0) config.fillGradientAngle = fill.gradient.angle;
3207
+ if (fill.gradient?.stops !== void 0) config.fillGradientStops = fill.gradient.stops;
3208
+ if (fill.patternType !== void 0) config.patternType = fill.patternType;
3209
+ if (fill.perCharFillEnabled !== void 0) config.perCharFillEnabled = fill.perCharFillEnabled;
3210
+ if (fill.charFillColors !== void 0) config.charFillColors = fill.charFillColors;
3211
+ } else {
3212
+ config.fillType = "none";
3213
+ }
3214
+ config.strokeEnabled = !!stroke;
3215
+ if (stroke) {
3216
+ if (stroke.color !== void 0) config.strokeColor = stroke.color;
3217
+ if (stroke.width !== void 0) config.strokeWidth = stroke.width * ratio;
3218
+ if (stroke.position !== void 0) config.strokePosition = stroke.position;
3219
+ if (stroke.opacity !== void 0) config.strokeOpacity = stroke.opacity;
3220
+ if (stroke.lineJoin !== void 0) config.strokeLineJoin = stroke.lineJoin;
3221
+ if (stroke.blur !== void 0) config.strokeBlur = stroke.blur * ratio;
3222
+ if (stroke.type !== void 0) config.strokeType = stroke.type;
3223
+ if (stroke.colorSecondary !== void 0) config.strokeColorSecondary = stroke.colorSecondary;
3224
+ if (stroke.widthSecondary !== void 0) config.strokeWidthSecondary = stroke.widthSecondary * ratio;
3225
+ if (stroke.fadeRange !== void 0) config.strokeFadeRange = stroke.fadeRange;
3226
+ }
3227
+ config.shadowEnabled = !!shadow;
3228
+ if (shadow) {
3229
+ if (shadow.color !== void 0) config.shadowColor = shadow.color;
3230
+ if (shadow.blur !== void 0) config.shadowBlur = shadow.blur * ratio;
3231
+ if (shadow.offsetX !== void 0) config.shadowOffsetX = shadow.offsetX * ratio;
3232
+ if (shadow.offsetY !== void 0) config.shadowOffsetY = shadow.offsetY * ratio;
3233
+ if (shadow.opacity !== void 0) config.shadowOpacity = shadow.opacity;
3234
+ if (shadow.type !== void 0) config.shadowType = shadow.type;
3235
+ }
3236
+ config.bevelEnabled = !!bevel;
3237
+ if (bevel) {
3238
+ if (bevel.depth !== void 0) config.bevelDepth = Math.round(bevel.depth * ratio);
3239
+ if (bevel.highlightColor !== void 0) config.bevelHighlight = bevel.highlightColor;
3240
+ if (bevel.shadowColor !== void 0) config.bevelShadow = bevel.shadowColor;
3241
+ if (bevel.direction !== void 0) config.bevelDirection = bevel.direction;
3242
+ if (bevel.coreColor !== void 0) config.bevelCoreColor = bevel.coreColor;
3243
+ if (bevel.edgeColor !== void 0) config.bevelEdgeColor = bevel.edgeColor;
3244
+ if (bevel.edgeWidth !== void 0) config.bevelEdgeWidth = bevel.edgeWidth * ratio;
3245
+ if (bevel.blur !== void 0) config.bevelBlur = bevel.blur * ratio;
3246
+ if (bevel.blurColor !== void 0) config.bevelBlurColor = bevel.blurColor;
3247
+ if (bevel.perspectiveEnabled !== void 0) config.bevelPerspectiveEnabled = bevel.perspectiveEnabled;
3248
+ if (bevel.vanishingPointX !== void 0) config.bevelVanishingPointX = bevel.vanishingPointX;
3249
+ if (bevel.vanishingPointY !== void 0) config.bevelVanishingPointY = bevel.vanishingPointY;
3250
+ if (bevel.focalLength !== void 0) config.bevelFocalLength = bevel.focalLength;
3251
+ }
3252
+ if (effect.stack) {
3253
+ config.stackEnabled = !!effect.stack.count;
3254
+ if (effect.stack.count !== void 0) config.stackCount = effect.stack.count;
3255
+ if (effect.stack.offsetX !== void 0) config.stackOffsetX = effect.stack.offsetX * ratio;
3256
+ if (effect.stack.offsetY !== void 0) config.stackOffsetY = effect.stack.offsetY * ratio;
3257
+ if (effect.stack.opacityDecay !== void 0) config.stackOpacityDecay = effect.stack.opacityDecay;
3258
+ if (effect.stack.color1 !== void 0) config.stackColor1 = effect.stack.color1;
3259
+ if (effect.stack.color2 !== void 0) config.stackColor2 = effect.stack.color2;
3260
+ if (effect.stack.color3 !== void 0) config.stackColor3 = effect.stack.color3;
3261
+ if (effect.stack.color4 !== void 0) config.stackColor4 = effect.stack.color4;
3262
+ }
3263
+ config.panelEnabled = !!panel;
3264
+ if (panel) {
3265
+ if (panel.color !== void 0) config.panelColor = panel.color;
3266
+ if (panel.opacity !== void 0) config.panelOpacity = panel.opacity;
3267
+ if (panel.radius !== void 0) config.panelRadius = panel.radius;
3268
+ if (panel.paddingX !== void 0) config.panelPaddingX = panel.paddingX * ratio;
3269
+ if (panel.paddingY !== void 0) config.panelPaddingY = panel.paddingY * ratio;
3270
+ if (panel.stroke !== void 0) {
3271
+ config.panelStrokeEnabled = !!panel.stroke;
3272
+ if (panel.stroke.color !== void 0) config.panelStrokeColor = panel.stroke.color;
3273
+ if (panel.stroke.width !== void 0) config.panelStrokeWidth = panel.stroke.width * ratio;
3274
+ }
3275
+ }
3276
+ if (effect.glows) {
3277
+ config.glowLayers = effect.glows.map((g) => {
3278
+ const mappedGlow = {
3279
+ enabled: true,
3280
+ color: g.color,
3281
+ blur: typeof g.blur === "number" ? g.blur * ratio : g.blur ?? 0,
3282
+ opacity: g.opacity,
3283
+ type: g.type ?? "outer"
3284
+ };
3285
+ if (g.strength !== void 0) mappedGlow.strength = g.strength;
3286
+ if (g.spread !== void 0) mappedGlow.spread = g.spread * ratio;
3287
+ return mappedGlow;
3288
+ });
3289
+ }
3290
+ const standardKeys = /* @__PURE__ */ new Set([
3291
+ "id",
3292
+ "name",
3293
+ "category",
3294
+ "description",
3295
+ "tags",
3296
+ "font",
3297
+ "fills",
3298
+ "strokes",
3299
+ "shadows",
3300
+ "glows",
3301
+ "bevel",
3302
+ "panel",
3303
+ "text",
3304
+ "animation",
3305
+ "stack"
3306
+ ]);
3307
+ for (const key of Object.keys(effect)) {
3308
+ if (!standardKeys.has(key)) {
3309
+ config[key] = effect[key];
3310
+ }
3311
+ }
3312
+ return config;
3313
+ }
3314
+ function layerToTextEffectConfig(layer2) {
3315
+ const normWeight = typeof layer2.fontWeight === "number" ? layer2.fontWeight : layer2.fontWeight === "bold" ? 700 : 400;
3316
+ const config = {
3317
+ ...defaultConfig,
3318
+ width: layer2.width,
3319
+ height: layer2.height,
3320
+ canvasWidth: layer2.width,
3321
+ canvasHeight: layer2.height,
3322
+ text: layer2.text,
3323
+ fontFamily: resolveFontFamilyName(layer2.fontFamily),
3324
+ fontWeight: normWeight,
3325
+ fontStyle: layer2.fontStyle || "normal",
3326
+ fontSize: layer2.fontSize,
3327
+ letterSpacing: layer2.letterSpacing ?? 0,
3328
+ lineHeight: layer2.lineHeight ?? 1.2,
3329
+ fillType: layer2.color ? "solid" : "none",
3330
+ fillColor: layer2.color ?? "#FFFFFF",
3331
+ strokeEnabled: !!layer2.stroke,
3332
+ strokeColor: layer2.stroke?.color ?? "#000000",
3333
+ strokeWidth: layer2.stroke?.width ?? 0,
3334
+ strokePosition: "center",
3335
+ strokeOpacity: 100,
3336
+ strokeLineJoin: "round",
3337
+ shadowEnabled: !!layer2.shadow,
3338
+ shadowColor: layer2.shadow?.color ?? "#000000",
3339
+ shadowBlur: layer2.shadow?.blur ?? 0,
3340
+ shadowOffsetX: layer2.shadow?.offsetX ?? 0,
3341
+ shadowOffsetY: layer2.shadow?.offsetY ?? 0,
3342
+ shadowOpacity: 100,
3343
+ shadowType: "drop",
3344
+ panelEnabled: !!layer2.background,
3345
+ panelColor: layer2.background?.color ?? "#1E1E26",
3346
+ panelOpacity: 80,
3347
+ panelRadius: layer2.background?.borderRadius ?? 6,
3348
+ panelPaddingX: layer2.background?.padding ?? 12,
3349
+ panelPaddingY: layer2.background?.padding ?? 12,
3350
+ textPosX: layer2.textAlign || "center",
3351
+ textPosY: layer2.verticalAlign === "middle" ? "middle" : layer2.verticalAlign || "middle"
3352
+ };
3353
+ return config;
3354
+ }
2904
3355
 
2905
3356
  // src/engine/animation.ts
2906
3357
  function ease(t, kind = "linear") {
@@ -3210,47 +3661,23 @@ function evaluateScene(doc, time, ctx, options = {}) {
3210
3661
  finishFrame();
3211
3662
  return;
3212
3663
  }
3213
- if (supportsOffscreenCanvas()) {
3214
- const off = new OffscreenCanvas(w, h);
3215
- const offCtx = off.getContext("2d");
3216
- if (!offCtx) {
3217
- renderTextEffectCore(ctx, cfg);
3218
- finishFrame();
3219
- return;
3220
- }
3221
- renderTextEffectCore(offCtx, cfg);
3222
- applyMaskReveal(offCtx, animated, w, h);
3664
+ const temp = CanvasDevice.acquire(w, h);
3665
+ const tctx = temp.getContext("2d");
3666
+ if (tctx) {
3667
+ tctx.clearRect(0, 0, w, h);
3668
+ renderTextEffectCore(tctx, cfg);
3669
+ applyMaskReveal(tctx, animated, w, h);
3223
3670
  const compositor = options.compositor ?? getCompositor();
3224
3671
  if (compositor?.isSupported) {
3225
- compositor.renderToContext(ctx, off, comp);
3226
- return;
3227
- }
3228
- ctx.clearRect(0, 0, w, h);
3229
- ctx.drawImage(off, 0, 0);
3230
- return;
3231
- }
3232
- if (typeof document !== "undefined") {
3233
- const temp = document.createElement("canvas");
3234
- temp.width = w;
3235
- temp.height = h;
3236
- const tctx = temp.getContext("2d");
3237
- if (tctx) {
3238
- renderTextEffectCore(tctx, cfg);
3239
- applyMaskReveal(tctx, animated, w, h);
3240
- const compositor = options.compositor ?? getCompositor();
3241
- if (compositor?.isSupported) {
3242
- compositor.renderToContext(ctx, temp, comp);
3243
- temp.width = 0;
3244
- temp.height = 0;
3245
- return;
3246
- }
3672
+ compositor.renderToContext(ctx, temp, comp);
3673
+ } else {
3247
3674
  ctx.clearRect(0, 0, w, h);
3248
3675
  ctx.drawImage(temp, 0, 0);
3249
- temp.width = 0;
3250
- temp.height = 0;
3251
- return;
3252
3676
  }
3677
+ CanvasDevice.release(temp);
3678
+ return;
3253
3679
  }
3680
+ CanvasDevice.release(temp);
3254
3681
  renderTextEffectCore(ctx, cfg);
3255
3682
  finishFrame();
3256
3683
  }
@@ -6218,6 +6645,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6218
6645
  0 && (module.exports = {
6219
6646
  COMPOSITION_PRESETS,
6220
6647
  CUSTOM_ENGINE_IDS,
6648
+ CanvasDevice,
6221
6649
  DEFAULT_CANVAS_HEIGHT,
6222
6650
  DEFAULT_CANVAS_WIDTH,
6223
6651
  DEFAULT_DURATION,
@@ -6229,6 +6657,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6229
6657
  ENTRANCE_PRESETS,
6230
6658
  EXIT_PRESETS,
6231
6659
  FONT_WEIGHT_OPTIONS,
6660
+ FontLoader,
6232
6661
  InkBrushEngine,
6233
6662
  LEGACY_RENDERER_MAP,
6234
6663
  LOOP_PRESETS,
@@ -6240,6 +6669,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6240
6669
  TextEffectRenderer,
6241
6670
  WEBM_EXPORT_MAX_FRAMES,
6242
6671
  WebGLCompositor,
6672
+ _buildConfig,
6243
6673
  _resetPlatformCache,
6244
6674
  addImageLayer,
6245
6675
  addKeyframeAtTime,
@@ -6297,6 +6727,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6297
6727
  encodeGif,
6298
6728
  ensureDefaultTimeline,
6299
6729
  ensureFontInLottie,
6730
+ ensureFontsLoaded,
6300
6731
  evaluateConfig,
6301
6732
  evaluateScene,
6302
6733
  findTrackIndex,
@@ -6304,6 +6735,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6304
6735
  getAnimatableParamDef,
6305
6736
  getAnimatableParamsForLayer,
6306
6737
  getDefaultText,
6738
+ getFontLoader,
6307
6739
  getLayerById,
6308
6740
  getPresetScene,
6309
6741
  getSceneTime,
@@ -6322,6 +6754,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6322
6754
  injectText,
6323
6755
  injectTextStyle,
6324
6756
  isWebMExportSupported,
6757
+ layerToTextEffectConfig,
6325
6758
  loadLottieFonts,
6326
6759
  lottieColorToHex,
6327
6760
  lottieJToAlign,
@@ -6344,10 +6777,12 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6344
6777
  renderPngSequence,
6345
6778
  renderSceneWebM,
6346
6779
  renderTextEffectCore,
6780
+ resetFontLoader,
6347
6781
  resetSceneTime,
6348
6782
  resizeCharFillColors,
6349
6783
  resolveAnimatedScalar,
6350
6784
  resolveCustomEngineId,
6785
+ resolveFontFamilyName,
6351
6786
  restoreLetterSpacing,
6352
6787
  scanLottieFonts,
6353
6788
  scanTextLayers,