@clypra/engine 1.1.0 → 1.1.2

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,
@@ -87,6 +90,7 @@ __export(index_exports, {
87
90
  createPulseOpacityTrack: () => createPulseOpacityTrack,
88
91
  defaultConfig: () => defaultConfig,
89
92
  deleteKeyframe: () => deleteKeyframe,
93
+ disposeSharedCompositor: () => disposeSharedCompositor,
90
94
  downloadDotLottie: () => downloadDotLottie,
91
95
  downloadLottieJson: () => downloadLottieJson,
92
96
  downloadPngSequenceZip: () => downloadPngSequenceZip,
@@ -99,6 +103,7 @@ __export(index_exports, {
99
103
  encodeGif: () => encodeGif,
100
104
  ensureDefaultTimeline: () => ensureDefaultTimeline,
101
105
  ensureFontInLottie: () => ensureFontInLottie,
106
+ ensureFontsLoaded: () => ensureFontsLoaded,
102
107
  evaluateConfig: () => evaluateConfig,
103
108
  evaluateScene: () => evaluateScene,
104
109
  findTrackIndex: () => findTrackIndex,
@@ -106,6 +111,7 @@ __export(index_exports, {
106
111
  getAnimatableParamDef: () => getAnimatableParamDef,
107
112
  getAnimatableParamsForLayer: () => getAnimatableParamsForLayer,
108
113
  getDefaultText: () => getDefaultText,
114
+ getFontLoader: () => getFontLoader,
109
115
  getLayerById: () => getLayerById,
110
116
  getPresetScene: () => getPresetScene,
111
117
  getSceneTime: () => getSceneTime,
@@ -124,6 +130,7 @@ __export(index_exports, {
124
130
  injectText: () => injectText,
125
131
  injectTextStyle: () => injectTextStyle,
126
132
  isWebMExportSupported: () => isWebMExportSupported,
133
+ layerToTextEffectConfig: () => layerToTextEffectConfig,
127
134
  loadLottieFonts: () => loadLottieFonts,
128
135
  lottieColorToHex: () => lottieColorToHex,
129
136
  lottieJToAlign: () => lottieJToAlign,
@@ -146,10 +153,12 @@ __export(index_exports, {
146
153
  renderPngSequence: () => renderPngSequence,
147
154
  renderSceneWebM: () => renderSceneWebM,
148
155
  renderTextEffectCore: () => renderTextEffectCore,
156
+ resetFontLoader: () => resetFontLoader,
149
157
  resetSceneTime: () => resetSceneTime,
150
158
  resizeCharFillColors: () => resizeCharFillColors,
151
159
  resolveAnimatedScalar: () => resolveAnimatedScalar,
152
160
  resolveCustomEngineId: () => resolveCustomEngineId,
161
+ resolveFontFamilyName: () => resolveFontFamilyName,
153
162
  restoreLetterSpacing: () => restoreLetterSpacing,
154
163
  scanLottieFonts: () => scanLottieFonts,
155
164
  scanTextLayers: () => scanTextLayers,
@@ -296,7 +305,7 @@ function layoutWithFontSize(ctx, cfg, fontSize, lines) {
296
305
  ctx.font = fontStr;
297
306
  const lineWidths = lines.map((line) => measureLine(ctx, line, letterSpacing));
298
307
  const maxLineWidth = Math.max(...lineWidths, 1);
299
- const textBlockHeight = fontSize + (lines.length - 1) * fontSize * lineHeight;
308
+ const textBlockHeight = lines.length === 1 ? fontSize : (lines.length - 1) * fontSize * lineHeight + fontSize;
300
309
  let align = "center";
301
310
  let startX = cWidth / 2;
302
311
  if (cfg.textPosX === "left") {
@@ -557,9 +566,8 @@ function createCanvas(width, height) {
557
566
  }
558
567
  function releaseCanvas(canvas) {
559
568
  if (canvas instanceof OffscreenCanvas) return;
560
- if (typeof document !== "undefined" && canvas.parentNode) {
561
- canvas.parentNode.removeChild(canvas);
562
- }
569
+ canvas.width = 0;
570
+ canvas.height = 0;
563
571
  }
564
572
  function _resetPlatformCache() {
565
573
  _ctxFilter = null;
@@ -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) {
@@ -2086,7 +2134,7 @@ function renderTextEffectCore(ctx, cfg) {
2086
2134
  ctx.globalCompositeOperation = "source-atop";
2087
2135
  const renderCount = Math.max(1, Math.min(20, layer2.strength ?? 1));
2088
2136
  for (let i = 0; i < renderCount; i++) {
2089
- renderWithShadowTrick("fill", layer2.color, layer2.blur, 0, 0, layer2.opacity, "transparent", layer2.spread ?? 0);
2137
+ renderWithShadowTrick("fill", layer2.color, layer2.blur, 0, 0, layer2.opacity, "#000000", layer2.spread ?? 0);
2090
2138
  }
2091
2139
  ctx.restore();
2092
2140
  }
@@ -2094,7 +2142,7 @@ function renderTextEffectCore(ctx, cfg) {
2094
2142
  if (shadowEnabled && shadowType === "inner" && shadowOpacity > 0) {
2095
2143
  ctx.save();
2096
2144
  ctx.globalCompositeOperation = "source-atop";
2097
- renderWithShadowTrick("fill", shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY, shadowOpacity, "transparent");
2145
+ renderWithShadowTrick("fill", shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY, shadowOpacity, "#000000");
2098
2146
  ctx.restore();
2099
2147
  }
2100
2148
  if (isGlitch) {
@@ -2527,12 +2575,148 @@ async function initializeFontSystem() {
2527
2575
  }
2528
2576
  }
2529
2577
  function checkFontVariant(variantName) {
2530
- const canvas = document.createElement("canvas");
2531
- const ctx = canvas.getContext("2d");
2532
- if (!ctx) return false;
2533
- ctx.font = `16px "${variantName}"`;
2534
- const metrics = ctx.measureText("Test");
2535
- return metrics.width > 0;
2578
+ if (typeof document === "undefined" || !document.fonts) return false;
2579
+ return document.fonts.check(`16px "${variantName}"`);
2580
+ }
2581
+ var FontLoader = class {
2582
+ state = {
2583
+ loading: /* @__PURE__ */ new Set(),
2584
+ loaded: /* @__PURE__ */ new Set(),
2585
+ failed: /* @__PURE__ */ new Map(),
2586
+ promises: /* @__PURE__ */ new Map()
2587
+ };
2588
+ async ensureFont(descriptor) {
2589
+ const key = this.getFontKey(descriptor);
2590
+ if (this.state.loaded.has(key)) {
2591
+ return {
2592
+ font: descriptor,
2593
+ loaded: true,
2594
+ loadTimeMs: 0
2595
+ };
2596
+ }
2597
+ if (this.state.failed.has(key)) {
2598
+ return {
2599
+ font: descriptor,
2600
+ loaded: false,
2601
+ error: this.state.failed.get(key),
2602
+ loadTimeMs: 0
2603
+ };
2604
+ }
2605
+ if (this.state.promises.has(key)) {
2606
+ return this.state.promises.get(key);
2607
+ }
2608
+ const promise = this.loadFont(descriptor);
2609
+ this.state.promises.set(key, promise);
2610
+ return promise;
2611
+ }
2612
+ async ensureFonts(descriptors) {
2613
+ return Promise.all(descriptors.map((desc) => this.ensureFont(desc)));
2614
+ }
2615
+ async waitForFontsReady() {
2616
+ if (typeof document === "undefined" || !document.fonts) {
2617
+ return;
2618
+ }
2619
+ await document.fonts.ready;
2620
+ }
2621
+ isLoaded(descriptor) {
2622
+ const key = this.getFontKey(descriptor);
2623
+ return this.state.loaded.has(key);
2624
+ }
2625
+ getStats() {
2626
+ return {
2627
+ loaded: this.state.loaded.size,
2628
+ loading: this.state.loading.size,
2629
+ failed: this.state.failed.size
2630
+ };
2631
+ }
2632
+ clear() {
2633
+ this.state.loading.clear();
2634
+ this.state.loaded.clear();
2635
+ this.state.failed.clear();
2636
+ this.state.promises.clear();
2637
+ }
2638
+ async loadFont(descriptor) {
2639
+ const key = this.getFontKey(descriptor);
2640
+ const startTime = performance.now();
2641
+ this.state.loading.add(key);
2642
+ try {
2643
+ if (typeof document === "undefined" || !document.fonts) {
2644
+ throw new Error("Font API not available");
2645
+ }
2646
+ const weight = this.normalizeFontWeight(descriptor.weight);
2647
+ const style = descriptor.style || "normal";
2648
+ const fontFace = `${style} ${weight} 16px "${descriptor.family}"`;
2649
+ if (document.fonts.check(fontFace)) {
2650
+ this.state.loaded.add(key);
2651
+ this.state.loading.delete(key);
2652
+ this.state.promises.delete(key);
2653
+ return {
2654
+ font: descriptor,
2655
+ loaded: true,
2656
+ loadTimeMs: performance.now() - startTime
2657
+ };
2658
+ }
2659
+ await document.fonts.load(fontFace);
2660
+ if (!document.fonts.check(fontFace)) {
2661
+ throw new Error(`Font "${descriptor.family}" failed to load`);
2662
+ }
2663
+ this.state.loaded.add(key);
2664
+ this.state.loading.delete(key);
2665
+ this.state.promises.delete(key);
2666
+ return {
2667
+ font: descriptor,
2668
+ loaded: true,
2669
+ loadTimeMs: performance.now() - startTime
2670
+ };
2671
+ } catch (error) {
2672
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2673
+ this.state.failed.set(key, errorMessage);
2674
+ this.state.loading.delete(key);
2675
+ this.state.promises.delete(key);
2676
+ return {
2677
+ font: descriptor,
2678
+ loaded: false,
2679
+ error: errorMessage,
2680
+ loadTimeMs: performance.now() - startTime
2681
+ };
2682
+ }
2683
+ }
2684
+ getFontKey(descriptor) {
2685
+ const weight = this.normalizeFontWeight(descriptor.weight);
2686
+ const style = descriptor.style || "normal";
2687
+ return `${descriptor.family}|${weight}|${style}`;
2688
+ }
2689
+ normalizeFontWeight(weight) {
2690
+ if (typeof weight === "number") {
2691
+ return weight;
2692
+ }
2693
+ if (!weight) return 400;
2694
+ const asNum = parseInt(weight, 10);
2695
+ if (!isNaN(asNum) && asNum >= 100 && asNum <= 900) {
2696
+ return asNum;
2697
+ }
2698
+ const weightMap = {
2699
+ normal: 400,
2700
+ bold: 700,
2701
+ lighter: 300,
2702
+ bolder: 700
2703
+ };
2704
+ return weightMap[weight] ?? 400;
2705
+ }
2706
+ };
2707
+ var globalFontLoader = null;
2708
+ function getFontLoader() {
2709
+ if (!globalFontLoader) {
2710
+ globalFontLoader = new FontLoader();
2711
+ }
2712
+ return globalFontLoader;
2713
+ }
2714
+ function resetFontLoader() {
2715
+ globalFontLoader = null;
2716
+ }
2717
+ async function ensureFontsLoaded(descriptors) {
2718
+ const loader = getFontLoader();
2719
+ return loader.ensureFonts(descriptors);
2536
2720
  }
2537
2721
 
2538
2722
  // src/engine/timelineDefaults.ts
@@ -2905,6 +3089,212 @@ function mergeSceneIntoConfig(doc, base) {
2905
3089
  const out = sceneToConfig({ ...doc, legacyConfig: base });
2906
3090
  return out;
2907
3091
  }
3092
+ function resolveFontFamilyName(fontFamily) {
3093
+ const f = fontFamily?.toLowerCase() || "";
3094
+ if (f.includes("inter")) return "Inter Variable";
3095
+ if (f.includes("montserrat")) return "Montserrat Variable";
3096
+ if (f.includes("geist")) return "Geist Variable";
3097
+ if (f.includes("space grotesk") || f.includes("grotesk")) return "Space Grotesk Variable";
3098
+ if (f.includes("outfit")) return "Outfit Variable";
3099
+ if (f.includes("roboto variable")) return "Roboto Variable";
3100
+ if (f.includes("roboto condensed")) return "Roboto Condensed";
3101
+ if (f === "roboto") return "Roboto Variable";
3102
+ if (f.includes("open sans")) return "Open Sans Variable";
3103
+ if (f.includes("raleway")) return "Raleway Variable";
3104
+ if (f.includes("oswald")) return "Oswald Variable";
3105
+ if (f.includes("playfair display")) return "Playfair Display Variable";
3106
+ if (f.includes("nunito")) return "Nunito Variable";
3107
+ if (f.includes("dancing script")) return "Dancing Script Variable";
3108
+ if (f === "lato") return "Lato";
3109
+ if (f === "anton") return "Anton";
3110
+ if (f === "bebas neue") return "Bebas Neue";
3111
+ if (f === "poppins") return "Poppins";
3112
+ if (f === "permanent marker") return "Permanent Marker";
3113
+ if (f === "bangers") return "Bangers";
3114
+ if (f === "press start 2p") return "Press Start 2P";
3115
+ if (f === "pacifico") return "Pacifico";
3116
+ return fontFamily;
3117
+ }
3118
+ function _buildConfig(effect, text, fontSize, canvasWidth, canvasHeight, time, clipStartTime, clipDuration) {
3119
+ const fill = effect.fills?.[0];
3120
+ const stroke = effect.strokes?.[0];
3121
+ const shadow = effect.shadows?.[0];
3122
+ const bevel = effect.bevel;
3123
+ const panel = effect.panel;
3124
+ const ratio = fontSize / 100;
3125
+ const config = {
3126
+ // Canvas / text
3127
+ width: canvasWidth,
3128
+ height: canvasHeight,
3129
+ canvasWidth,
3130
+ canvasHeight,
3131
+ text,
3132
+ time: time ?? 0,
3133
+ clipStartTime: clipStartTime ?? 0,
3134
+ clipDuration: clipDuration ?? 5,
3135
+ // Font
3136
+ fontFamily: resolveFontFamilyName(effect.font.family),
3137
+ fontWeight: effect.font.weight,
3138
+ fontStyle: effect.font.style,
3139
+ fontSize,
3140
+ letterSpacing: effect.font.letterSpacing,
3141
+ lineHeight: effect.font.lineHeight
3142
+ };
3143
+ if (effect.animation) {
3144
+ config.animation = effect.animation;
3145
+ }
3146
+ if (fill) {
3147
+ if (fill.type !== void 0) config.fillType = fill.type;
3148
+ if (fill.color !== void 0) config.fillColor = fill.color;
3149
+ if (fill.gradient?.angle !== void 0) config.fillGradientAngle = fill.gradient.angle;
3150
+ if (fill.gradient?.stops !== void 0) config.fillGradientStops = fill.gradient.stops;
3151
+ if (fill.patternType !== void 0) config.patternType = fill.patternType;
3152
+ if (fill.perCharFillEnabled !== void 0) config.perCharFillEnabled = fill.perCharFillEnabled;
3153
+ if (fill.charFillColors !== void 0) config.charFillColors = fill.charFillColors;
3154
+ } else {
3155
+ config.fillType = "none";
3156
+ }
3157
+ config.strokeEnabled = !!stroke;
3158
+ if (stroke) {
3159
+ if (stroke.color !== void 0) config.strokeColor = stroke.color;
3160
+ if (stroke.width !== void 0) config.strokeWidth = stroke.width * ratio;
3161
+ if (stroke.position !== void 0) config.strokePosition = stroke.position;
3162
+ if (stroke.opacity !== void 0) config.strokeOpacity = stroke.opacity;
3163
+ if (stroke.lineJoin !== void 0) config.strokeLineJoin = stroke.lineJoin;
3164
+ if (stroke.blur !== void 0) config.strokeBlur = stroke.blur * ratio;
3165
+ if (stroke.type !== void 0) config.strokeType = stroke.type;
3166
+ if (stroke.colorSecondary !== void 0) config.strokeColorSecondary = stroke.colorSecondary;
3167
+ if (stroke.widthSecondary !== void 0) config.strokeWidthSecondary = stroke.widthSecondary * ratio;
3168
+ if (stroke.fadeRange !== void 0) config.strokeFadeRange = stroke.fadeRange;
3169
+ }
3170
+ config.shadowEnabled = !!shadow;
3171
+ if (shadow) {
3172
+ if (shadow.color !== void 0) config.shadowColor = shadow.color;
3173
+ if (shadow.blur !== void 0) config.shadowBlur = shadow.blur * ratio;
3174
+ if (shadow.offsetX !== void 0) config.shadowOffsetX = shadow.offsetX * ratio;
3175
+ if (shadow.offsetY !== void 0) config.shadowOffsetY = shadow.offsetY * ratio;
3176
+ if (shadow.opacity !== void 0) config.shadowOpacity = shadow.opacity;
3177
+ if (shadow.type !== void 0) config.shadowType = shadow.type;
3178
+ }
3179
+ config.bevelEnabled = !!bevel;
3180
+ if (bevel) {
3181
+ if (bevel.depth !== void 0) config.bevelDepth = Math.round(bevel.depth * ratio);
3182
+ if (bevel.highlightColor !== void 0) config.bevelHighlight = bevel.highlightColor;
3183
+ if (bevel.shadowColor !== void 0) config.bevelShadow = bevel.shadowColor;
3184
+ if (bevel.direction !== void 0) config.bevelDirection = bevel.direction;
3185
+ if (bevel.coreColor !== void 0) config.bevelCoreColor = bevel.coreColor;
3186
+ if (bevel.edgeColor !== void 0) config.bevelEdgeColor = bevel.edgeColor;
3187
+ if (bevel.edgeWidth !== void 0) config.bevelEdgeWidth = bevel.edgeWidth * ratio;
3188
+ if (bevel.blur !== void 0) config.bevelBlur = bevel.blur * ratio;
3189
+ if (bevel.blurColor !== void 0) config.bevelBlurColor = bevel.blurColor;
3190
+ if (bevel.perspectiveEnabled !== void 0) config.bevelPerspectiveEnabled = bevel.perspectiveEnabled;
3191
+ if (bevel.vanishingPointX !== void 0) config.bevelVanishingPointX = bevel.vanishingPointX;
3192
+ if (bevel.vanishingPointY !== void 0) config.bevelVanishingPointY = bevel.vanishingPointY;
3193
+ if (bevel.focalLength !== void 0) config.bevelFocalLength = bevel.focalLength;
3194
+ }
3195
+ if (effect.stack) {
3196
+ config.stackEnabled = !!effect.stack.count;
3197
+ if (effect.stack.count !== void 0) config.stackCount = effect.stack.count;
3198
+ if (effect.stack.offsetX !== void 0) config.stackOffsetX = effect.stack.offsetX * ratio;
3199
+ if (effect.stack.offsetY !== void 0) config.stackOffsetY = effect.stack.offsetY * ratio;
3200
+ if (effect.stack.opacityDecay !== void 0) config.stackOpacityDecay = effect.stack.opacityDecay;
3201
+ if (effect.stack.color1 !== void 0) config.stackColor1 = effect.stack.color1;
3202
+ if (effect.stack.color2 !== void 0) config.stackColor2 = effect.stack.color2;
3203
+ if (effect.stack.color3 !== void 0) config.stackColor3 = effect.stack.color3;
3204
+ if (effect.stack.color4 !== void 0) config.stackColor4 = effect.stack.color4;
3205
+ }
3206
+ config.panelEnabled = !!panel;
3207
+ if (panel) {
3208
+ if (panel.color !== void 0) config.panelColor = panel.color;
3209
+ if (panel.opacity !== void 0) config.panelOpacity = panel.opacity;
3210
+ if (panel.radius !== void 0) config.panelRadius = panel.radius;
3211
+ if (panel.paddingX !== void 0) config.panelPaddingX = panel.paddingX * ratio;
3212
+ if (panel.paddingY !== void 0) config.panelPaddingY = panel.paddingY * ratio;
3213
+ if (panel.stroke !== void 0) {
3214
+ config.panelStrokeEnabled = !!panel.stroke;
3215
+ if (panel.stroke.color !== void 0) config.panelStrokeColor = panel.stroke.color;
3216
+ if (panel.stroke.width !== void 0) config.panelStrokeWidth = panel.stroke.width * ratio;
3217
+ }
3218
+ }
3219
+ if (effect.glows) {
3220
+ config.glowLayers = effect.glows.map((g) => {
3221
+ const mappedGlow = {
3222
+ enabled: true,
3223
+ color: g.color,
3224
+ blur: typeof g.blur === "number" ? g.blur * ratio : g.blur ?? 0,
3225
+ opacity: g.opacity,
3226
+ type: g.type ?? "outer"
3227
+ };
3228
+ if (g.strength !== void 0) mappedGlow.strength = g.strength;
3229
+ if (g.spread !== void 0) mappedGlow.spread = g.spread * ratio;
3230
+ return mappedGlow;
3231
+ });
3232
+ }
3233
+ const standardKeys = /* @__PURE__ */ new Set([
3234
+ "id",
3235
+ "name",
3236
+ "category",
3237
+ "description",
3238
+ "tags",
3239
+ "font",
3240
+ "fills",
3241
+ "strokes",
3242
+ "shadows",
3243
+ "glows",
3244
+ "bevel",
3245
+ "panel",
3246
+ "text",
3247
+ "animation",
3248
+ "stack"
3249
+ ]);
3250
+ for (const key of Object.keys(effect)) {
3251
+ if (!standardKeys.has(key)) {
3252
+ config[key] = effect[key];
3253
+ }
3254
+ }
3255
+ return config;
3256
+ }
3257
+ function layerToTextEffectConfig(layer2) {
3258
+ const normWeight = typeof layer2.fontWeight === "number" ? layer2.fontWeight : layer2.fontWeight === "bold" ? 700 : 400;
3259
+ const config = {
3260
+ ...defaultConfig,
3261
+ width: layer2.width,
3262
+ height: layer2.height,
3263
+ canvasWidth: layer2.width,
3264
+ canvasHeight: layer2.height,
3265
+ text: layer2.text,
3266
+ fontFamily: resolveFontFamilyName(layer2.fontFamily),
3267
+ fontWeight: normWeight,
3268
+ fontStyle: layer2.fontStyle || "normal",
3269
+ fontSize: layer2.fontSize,
3270
+ letterSpacing: layer2.letterSpacing ?? 0,
3271
+ lineHeight: layer2.lineHeight ?? 1.2,
3272
+ fillType: layer2.color ? "solid" : "none",
3273
+ fillColor: layer2.color ?? "#FFFFFF",
3274
+ strokeEnabled: !!layer2.stroke,
3275
+ strokeColor: layer2.stroke?.color ?? "#000000",
3276
+ strokeWidth: layer2.stroke?.width ?? 0,
3277
+ strokePosition: "center",
3278
+ strokeOpacity: 100,
3279
+ strokeLineJoin: "round",
3280
+ shadowEnabled: !!layer2.shadow,
3281
+ shadowColor: layer2.shadow?.color ?? "#000000",
3282
+ shadowBlur: layer2.shadow?.blur ?? 0,
3283
+ shadowOffsetX: layer2.shadow?.offsetX ?? 0,
3284
+ shadowOffsetY: layer2.shadow?.offsetY ?? 0,
3285
+ shadowOpacity: 100,
3286
+ shadowType: "drop",
3287
+ panelEnabled: !!layer2.background,
3288
+ panelColor: layer2.background?.color ?? "#1E1E26",
3289
+ panelOpacity: 80,
3290
+ panelRadius: layer2.background?.borderRadius ?? 6,
3291
+ panelPaddingX: layer2.background?.padding ?? 12,
3292
+ panelPaddingY: layer2.background?.padding ?? 12,
3293
+ textPosX: layer2.textAlign || "center",
3294
+ textPosY: layer2.verticalAlign === "middle" ? "middle" : layer2.verticalAlign || "middle"
3295
+ };
3296
+ return config;
3297
+ }
2908
3298
 
2909
3299
  // src/engine/animation.ts
2910
3300
  function ease(t, kind = "linear") {
@@ -3190,6 +3580,10 @@ function getCompositor() {
3190
3580
  }
3191
3581
  return sharedCompositor;
3192
3582
  }
3583
+ function disposeSharedCompositor() {
3584
+ sharedCompositor?.dispose();
3585
+ sharedCompositor = null;
3586
+ }
3193
3587
  function evaluateScene(doc, time, ctx, options = {}) {
3194
3588
  const animated = applyTimelineAtTime(doc, time);
3195
3589
  const cfg = sceneToConfig(animated);
@@ -3210,47 +3604,23 @@ function evaluateScene(doc, time, ctx, options = {}) {
3210
3604
  finishFrame();
3211
3605
  return;
3212
3606
  }
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);
3607
+ const temp = CanvasDevice.acquire(w, h);
3608
+ const tctx = temp.getContext("2d");
3609
+ if (tctx) {
3610
+ tctx.clearRect(0, 0, w, h);
3611
+ renderTextEffectCore(tctx, cfg);
3612
+ applyMaskReveal(tctx, animated, w, h);
3223
3613
  const compositor = options.compositor ?? getCompositor();
3224
3614
  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
- }
3615
+ compositor.renderToContext(ctx, temp, comp);
3616
+ } else {
3247
3617
  ctx.clearRect(0, 0, w, h);
3248
3618
  ctx.drawImage(temp, 0, 0);
3249
- temp.width = 0;
3250
- temp.height = 0;
3251
- return;
3252
3619
  }
3620
+ CanvasDevice.release(temp);
3621
+ return;
3253
3622
  }
3623
+ CanvasDevice.release(temp);
3254
3624
  renderTextEffectCore(ctx, cfg);
3255
3625
  finishFrame();
3256
3626
  }
@@ -6218,6 +6588,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6218
6588
  0 && (module.exports = {
6219
6589
  COMPOSITION_PRESETS,
6220
6590
  CUSTOM_ENGINE_IDS,
6591
+ CanvasDevice,
6221
6592
  DEFAULT_CANVAS_HEIGHT,
6222
6593
  DEFAULT_CANVAS_WIDTH,
6223
6594
  DEFAULT_DURATION,
@@ -6229,6 +6600,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6229
6600
  ENTRANCE_PRESETS,
6230
6601
  EXIT_PRESETS,
6231
6602
  FONT_WEIGHT_OPTIONS,
6603
+ FontLoader,
6232
6604
  InkBrushEngine,
6233
6605
  LEGACY_RENDERER_MAP,
6234
6606
  LOOP_PRESETS,
@@ -6240,6 +6612,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6240
6612
  TextEffectRenderer,
6241
6613
  WEBM_EXPORT_MAX_FRAMES,
6242
6614
  WebGLCompositor,
6615
+ _buildConfig,
6243
6616
  _resetPlatformCache,
6244
6617
  addImageLayer,
6245
6618
  addKeyframeAtTime,
@@ -6284,6 +6657,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6284
6657
  createPulseOpacityTrack,
6285
6658
  defaultConfig,
6286
6659
  deleteKeyframe,
6660
+ disposeSharedCompositor,
6287
6661
  downloadDotLottie,
6288
6662
  downloadLottieJson,
6289
6663
  downloadPngSequenceZip,
@@ -6296,6 +6670,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6296
6670
  encodeGif,
6297
6671
  ensureDefaultTimeline,
6298
6672
  ensureFontInLottie,
6673
+ ensureFontsLoaded,
6299
6674
  evaluateConfig,
6300
6675
  evaluateScene,
6301
6676
  findTrackIndex,
@@ -6303,6 +6678,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6303
6678
  getAnimatableParamDef,
6304
6679
  getAnimatableParamsForLayer,
6305
6680
  getDefaultText,
6681
+ getFontLoader,
6306
6682
  getLayerById,
6307
6683
  getPresetScene,
6308
6684
  getSceneTime,
@@ -6321,6 +6697,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6321
6697
  injectText,
6322
6698
  injectTextStyle,
6323
6699
  isWebMExportSupported,
6700
+ layerToTextEffectConfig,
6324
6701
  loadLottieFonts,
6325
6702
  lottieColorToHex,
6326
6703
  lottieJToAlign,
@@ -6343,10 +6720,12 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6343
6720
  renderPngSequence,
6344
6721
  renderSceneWebM,
6345
6722
  renderTextEffectCore,
6723
+ resetFontLoader,
6346
6724
  resetSceneTime,
6347
6725
  resizeCharFillColors,
6348
6726
  resolveAnimatedScalar,
6349
6727
  resolveCustomEngineId,
6728
+ resolveFontFamilyName,
6350
6729
  restoreLetterSpacing,
6351
6730
  scanLottieFonts,
6352
6731
  scanTextLayers,