@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.js CHANGED
@@ -386,6 +386,46 @@ function _resetPlatformCache() {
386
386
  _offscreenCanvas = null;
387
387
  _webgl2 = null;
388
388
  }
389
+ var CanvasDevice = class {
390
+ static canvases = [];
391
+ static maxPoolSize = 10;
392
+ /**
393
+ * Acquire a Canvas context from the pool or create a new one.
394
+ * If a canvas is pulled from the pool, it is resized to the target dimensions.
395
+ */
396
+ static acquire(width, height) {
397
+ let canvas;
398
+ if (this.canvases.length > 0) {
399
+ canvas = this.canvases.pop();
400
+ if (canvas.width !== width || canvas.height !== height) {
401
+ canvas.width = width;
402
+ canvas.height = height;
403
+ }
404
+ } else {
405
+ canvas = createCanvas(width, height);
406
+ }
407
+ return canvas;
408
+ }
409
+ /**
410
+ * Release a canvas back to the pool, or free its resources immediately if pool is full.
411
+ */
412
+ static release(canvas) {
413
+ if (this.canvases.length < this.maxPoolSize) {
414
+ this.canvases.push(canvas);
415
+ } else {
416
+ releaseCanvas(canvas);
417
+ }
418
+ }
419
+ /**
420
+ * Disposes all pooled canvases to release GPU/memory backing stores.
421
+ */
422
+ static clearPool() {
423
+ while (this.canvases.length > 0) {
424
+ const c = this.canvases.pop();
425
+ releaseCanvas(c);
426
+ }
427
+ }
428
+ };
389
429
 
390
430
  // src/engine/procedural/utils.ts
391
431
  function getCanvas2DContext(canvas) {
@@ -777,6 +817,17 @@ function restoreLetterSpacing(ctx, saved) {
777
817
  function getCanvas2DContext2(canvas) {
778
818
  return canvas.getContext("2d");
779
819
  }
820
+ function ctxSupportsFilter(ctx) {
821
+ try {
822
+ const prev = ctx.filter;
823
+ ctx.filter = "blur(4px)";
824
+ const ok = typeof ctx.filter === "string" && ctx.filter.includes("blur");
825
+ ctx.filter = prev;
826
+ return ok;
827
+ } catch {
828
+ return false;
829
+ }
830
+ }
780
831
  function renderTextEffectCore(ctx, cfg) {
781
832
  if (cfg.customRenderer === "InkBrushEngine") {
782
833
  const engine = new InkBrushEngine(cfg);
@@ -1086,19 +1137,31 @@ function renderTextEffectCore(ctx, cfg) {
1086
1137
  const vpy = cHeight / 2 + (bevelVanishingPointY !== void 0 ? bevelVanishingPointY : 80) / 100 * (cHeight / 2);
1087
1138
  const fl = Math.max(100, bevelFocalLength !== void 0 ? bevelFocalLength : 400);
1088
1139
  if (bevelBlur && bevelBlur > 0) {
1089
- ctx.save();
1090
- ctx.filter = `blur(${bevelBlur}px)`;
1091
1140
  const blurColor = bevelBlurColor || bevelShadow || "#000000";
1092
- for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1093
- const scale = fl / (fl + i);
1141
+ if (ctxSupportsFilter(ctx)) {
1094
1142
  ctx.save();
1095
- ctx.translate(vpx, vpy);
1096
- ctx.scale(scale, scale);
1097
- ctx.translate(-vpx, -vpy);
1098
- renderLines("fill", blurColor);
1143
+ ctx.filter = `blur(${bevelBlur}px)`;
1144
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1145
+ const scale = fl / (fl + i);
1146
+ ctx.save();
1147
+ ctx.translate(vpx, vpy);
1148
+ ctx.scale(scale, scale);
1149
+ ctx.translate(-vpx, -vpy);
1150
+ renderLines("fill", blurColor);
1151
+ ctx.restore();
1152
+ }
1099
1153
  ctx.restore();
1154
+ } else {
1155
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1156
+ const scale = fl / (fl + i);
1157
+ ctx.save();
1158
+ ctx.translate(vpx, vpy);
1159
+ ctx.scale(scale, scale);
1160
+ ctx.translate(-vpx, -vpy);
1161
+ renderWithShadowTrick("fill", blurColor, bevelBlur, 0, 0, 100);
1162
+ ctx.restore();
1163
+ }
1100
1164
  }
1101
- ctx.restore();
1102
1165
  }
1103
1166
  ctx.save();
1104
1167
  for (let i = bevelDepth; i > 0; i--) {
@@ -1145,14 +1208,21 @@ function renderTextEffectCore(ctx, cfg) {
1145
1208
  return { dx: i, dy: i };
1146
1209
  };
1147
1210
  if (bevelBlur && bevelBlur > 0) {
1148
- ctx.save();
1149
- ctx.filter = `blur(${bevelBlur}px)`;
1150
1211
  const blurColor = bevelBlurColor || bevelShadow || "#000000";
1151
- for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1152
- const { dx, dy } = getDirOffset(i);
1153
- renderLines("fill", blurColor, dx, dy);
1212
+ if (ctxSupportsFilter(ctx)) {
1213
+ ctx.save();
1214
+ ctx.filter = `blur(${bevelBlur}px)`;
1215
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1216
+ const { dx, dy } = getDirOffset(i);
1217
+ renderLines("fill", blurColor, dx, dy);
1218
+ }
1219
+ ctx.restore();
1220
+ } else {
1221
+ for (let i = bevelDepth; i > 0; i -= Math.max(1, Math.floor(bevelDepth / 4))) {
1222
+ const { dx, dy } = getDirOffset(i);
1223
+ renderWithShadowTrick("fill", blurColor, bevelBlur, dx, dy, 100);
1224
+ }
1154
1225
  }
1155
- ctx.restore();
1156
1226
  }
1157
1227
  ctx.save();
1158
1228
  for (let i = bevelDepth; i > 0; i--) {
@@ -1206,24 +1276,51 @@ function renderTextEffectCore(ctx, cfg) {
1206
1276
  customStrokeStyle = grad;
1207
1277
  }
1208
1278
  const drawStrokeLayer = (color, width, blurAmount, opacity, position) => {
1209
- ctx.save();
1210
- ctx.globalAlpha = opacity / 100;
1211
- ctx.strokeStyle = color;
1212
- if (blurAmount > 0) {
1279
+ if (blurAmount > 0 && ctxSupportsFilter(ctx)) {
1280
+ ctx.save();
1281
+ ctx.globalAlpha = opacity / 100;
1282
+ ctx.strokeStyle = color;
1213
1283
  ctx.filter = `blur(${blurAmount}px)`;
1284
+ if (position === "outside") {
1285
+ ctx.lineWidth = width * 2;
1286
+ renderLines("stroke");
1287
+ } else if (position === "center") {
1288
+ ctx.lineWidth = width;
1289
+ renderLines("stroke");
1290
+ } else if (position === "inside") {
1291
+ ctx.globalCompositeOperation = "source-atop";
1292
+ ctx.lineWidth = width * 2;
1293
+ renderLines("stroke");
1294
+ }
1295
+ ctx.restore();
1296
+ } else if (blurAmount > 0) {
1297
+ const colorStr = typeof color === "string" ? color : strokeColor;
1298
+ const spread = position === "center" ? width / 2 : width;
1299
+ if (position === "inside") {
1300
+ ctx.save();
1301
+ ctx.globalCompositeOperation = "source-atop";
1302
+ renderWithShadowTrick("stroke", colorStr, blurAmount, 0, 0, opacity, void 0, spread);
1303
+ ctx.restore();
1304
+ } else {
1305
+ renderWithShadowTrick("stroke", colorStr, blurAmount, 0, 0, opacity, void 0, spread);
1306
+ }
1307
+ } else {
1308
+ ctx.save();
1309
+ ctx.globalAlpha = opacity / 100;
1310
+ ctx.strokeStyle = color;
1311
+ if (position === "outside") {
1312
+ ctx.lineWidth = width * 2;
1313
+ renderLines("stroke");
1314
+ } else if (position === "center") {
1315
+ ctx.lineWidth = width;
1316
+ renderLines("stroke");
1317
+ } else if (position === "inside") {
1318
+ ctx.globalCompositeOperation = "source-atop";
1319
+ ctx.lineWidth = width * 2;
1320
+ renderLines("stroke");
1321
+ }
1322
+ ctx.restore();
1214
1323
  }
1215
- if (position === "outside") {
1216
- ctx.lineWidth = width * 2;
1217
- renderLines("stroke");
1218
- } else if (position === "center") {
1219
- ctx.lineWidth = width;
1220
- renderLines("stroke");
1221
- } else if (position === "inside") {
1222
- ctx.globalCompositeOperation = "source-atop";
1223
- ctx.lineWidth = width * 2;
1224
- renderLines("stroke");
1225
- }
1226
- ctx.restore();
1227
1324
  };
1228
1325
  if (sType === "double") {
1229
1326
  const outerWidth = strokeWidth + sWidthSecondary;
@@ -2348,6 +2445,146 @@ function checkFontVariant(variantName) {
2348
2445
  if (typeof document === "undefined" || !document.fonts) return false;
2349
2446
  return document.fonts.check(`16px "${variantName}"`);
2350
2447
  }
2448
+ var FontLoader = class {
2449
+ state = {
2450
+ loading: /* @__PURE__ */ new Set(),
2451
+ loaded: /* @__PURE__ */ new Set(),
2452
+ failed: /* @__PURE__ */ new Map(),
2453
+ promises: /* @__PURE__ */ new Map()
2454
+ };
2455
+ async ensureFont(descriptor) {
2456
+ const key = this.getFontKey(descriptor);
2457
+ if (this.state.loaded.has(key)) {
2458
+ return {
2459
+ font: descriptor,
2460
+ loaded: true,
2461
+ loadTimeMs: 0
2462
+ };
2463
+ }
2464
+ if (this.state.failed.has(key)) {
2465
+ return {
2466
+ font: descriptor,
2467
+ loaded: false,
2468
+ error: this.state.failed.get(key),
2469
+ loadTimeMs: 0
2470
+ };
2471
+ }
2472
+ if (this.state.promises.has(key)) {
2473
+ return this.state.promises.get(key);
2474
+ }
2475
+ const promise = this.loadFont(descriptor);
2476
+ this.state.promises.set(key, promise);
2477
+ return promise;
2478
+ }
2479
+ async ensureFonts(descriptors) {
2480
+ return Promise.all(descriptors.map((desc) => this.ensureFont(desc)));
2481
+ }
2482
+ async waitForFontsReady() {
2483
+ if (typeof document === "undefined" || !document.fonts) {
2484
+ return;
2485
+ }
2486
+ await document.fonts.ready;
2487
+ }
2488
+ isLoaded(descriptor) {
2489
+ const key = this.getFontKey(descriptor);
2490
+ return this.state.loaded.has(key);
2491
+ }
2492
+ getStats() {
2493
+ return {
2494
+ loaded: this.state.loaded.size,
2495
+ loading: this.state.loading.size,
2496
+ failed: this.state.failed.size
2497
+ };
2498
+ }
2499
+ clear() {
2500
+ this.state.loading.clear();
2501
+ this.state.loaded.clear();
2502
+ this.state.failed.clear();
2503
+ this.state.promises.clear();
2504
+ }
2505
+ async loadFont(descriptor) {
2506
+ const key = this.getFontKey(descriptor);
2507
+ const startTime = performance.now();
2508
+ this.state.loading.add(key);
2509
+ try {
2510
+ if (typeof document === "undefined" || !document.fonts) {
2511
+ throw new Error("Font API not available");
2512
+ }
2513
+ const weight = this.normalizeFontWeight(descriptor.weight);
2514
+ const style = descriptor.style || "normal";
2515
+ const fontFace = `${style} ${weight} 16px "${descriptor.family}"`;
2516
+ if (document.fonts.check(fontFace)) {
2517
+ this.state.loaded.add(key);
2518
+ this.state.loading.delete(key);
2519
+ this.state.promises.delete(key);
2520
+ return {
2521
+ font: descriptor,
2522
+ loaded: true,
2523
+ loadTimeMs: performance.now() - startTime
2524
+ };
2525
+ }
2526
+ await document.fonts.load(fontFace);
2527
+ if (!document.fonts.check(fontFace)) {
2528
+ throw new Error(`Font "${descriptor.family}" failed to load`);
2529
+ }
2530
+ this.state.loaded.add(key);
2531
+ this.state.loading.delete(key);
2532
+ this.state.promises.delete(key);
2533
+ return {
2534
+ font: descriptor,
2535
+ loaded: true,
2536
+ loadTimeMs: performance.now() - startTime
2537
+ };
2538
+ } catch (error) {
2539
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2540
+ this.state.failed.set(key, errorMessage);
2541
+ this.state.loading.delete(key);
2542
+ this.state.promises.delete(key);
2543
+ return {
2544
+ font: descriptor,
2545
+ loaded: false,
2546
+ error: errorMessage,
2547
+ loadTimeMs: performance.now() - startTime
2548
+ };
2549
+ }
2550
+ }
2551
+ getFontKey(descriptor) {
2552
+ const weight = this.normalizeFontWeight(descriptor.weight);
2553
+ const style = descriptor.style || "normal";
2554
+ return `${descriptor.family}|${weight}|${style}`;
2555
+ }
2556
+ normalizeFontWeight(weight) {
2557
+ if (typeof weight === "number") {
2558
+ return weight;
2559
+ }
2560
+ if (!weight) return 400;
2561
+ const asNum = parseInt(weight, 10);
2562
+ if (!isNaN(asNum) && asNum >= 100 && asNum <= 900) {
2563
+ return asNum;
2564
+ }
2565
+ const weightMap = {
2566
+ normal: 400,
2567
+ bold: 700,
2568
+ lighter: 300,
2569
+ bolder: 700
2570
+ };
2571
+ return weightMap[weight] ?? 400;
2572
+ }
2573
+ };
2574
+ var globalFontLoader = null;
2575
+ function getFontLoader() {
2576
+ if (!globalFontLoader) {
2577
+ globalFontLoader = new FontLoader();
2578
+ }
2579
+ return globalFontLoader;
2580
+ }
2581
+ function resetFontLoader() {
2582
+ globalFontLoader = null;
2583
+ }
2584
+ async function ensureFontsLoaded(descriptors) {
2585
+ const loader = getFontLoader();
2586
+ return loader.ensureFonts(descriptors);
2587
+ }
2351
2588
 
2352
2589
  // src/engine/timelineDefaults.ts
2353
2590
  function ensureDefaultTimeline(doc) {
@@ -2719,6 +2956,212 @@ function mergeSceneIntoConfig(doc, base) {
2719
2956
  const out = sceneToConfig({ ...doc, legacyConfig: base });
2720
2957
  return out;
2721
2958
  }
2959
+ function resolveFontFamilyName(fontFamily) {
2960
+ const f = fontFamily?.toLowerCase() || "";
2961
+ if (f.includes("inter")) return "Inter Variable";
2962
+ if (f.includes("montserrat")) return "Montserrat Variable";
2963
+ if (f.includes("geist")) return "Geist Variable";
2964
+ if (f.includes("space grotesk") || f.includes("grotesk")) return "Space Grotesk Variable";
2965
+ if (f.includes("outfit")) return "Outfit Variable";
2966
+ if (f.includes("roboto variable")) return "Roboto Variable";
2967
+ if (f.includes("roboto condensed")) return "Roboto Condensed";
2968
+ if (f === "roboto") return "Roboto Variable";
2969
+ if (f.includes("open sans")) return "Open Sans Variable";
2970
+ if (f.includes("raleway")) return "Raleway Variable";
2971
+ if (f.includes("oswald")) return "Oswald Variable";
2972
+ if (f.includes("playfair display")) return "Playfair Display Variable";
2973
+ if (f.includes("nunito")) return "Nunito Variable";
2974
+ if (f.includes("dancing script")) return "Dancing Script Variable";
2975
+ if (f === "lato") return "Lato";
2976
+ if (f === "anton") return "Anton";
2977
+ if (f === "bebas neue") return "Bebas Neue";
2978
+ if (f === "poppins") return "Poppins";
2979
+ if (f === "permanent marker") return "Permanent Marker";
2980
+ if (f === "bangers") return "Bangers";
2981
+ if (f === "press start 2p") return "Press Start 2P";
2982
+ if (f === "pacifico") return "Pacifico";
2983
+ return fontFamily;
2984
+ }
2985
+ function _buildConfig(effect, text, fontSize, canvasWidth, canvasHeight, time, clipStartTime, clipDuration) {
2986
+ const fill = effect.fills?.[0];
2987
+ const stroke = effect.strokes?.[0];
2988
+ const shadow = effect.shadows?.[0];
2989
+ const bevel = effect.bevel;
2990
+ const panel = effect.panel;
2991
+ const ratio = fontSize / 100;
2992
+ const config = {
2993
+ // Canvas / text
2994
+ width: canvasWidth,
2995
+ height: canvasHeight,
2996
+ canvasWidth,
2997
+ canvasHeight,
2998
+ text,
2999
+ time: time ?? 0,
3000
+ clipStartTime: clipStartTime ?? 0,
3001
+ clipDuration: clipDuration ?? 5,
3002
+ // Font
3003
+ fontFamily: resolveFontFamilyName(effect.font.family),
3004
+ fontWeight: effect.font.weight,
3005
+ fontStyle: effect.font.style,
3006
+ fontSize,
3007
+ letterSpacing: effect.font.letterSpacing,
3008
+ lineHeight: effect.font.lineHeight
3009
+ };
3010
+ if (effect.animation) {
3011
+ config.animation = effect.animation;
3012
+ }
3013
+ if (fill) {
3014
+ if (fill.type !== void 0) config.fillType = fill.type;
3015
+ if (fill.color !== void 0) config.fillColor = fill.color;
3016
+ if (fill.gradient?.angle !== void 0) config.fillGradientAngle = fill.gradient.angle;
3017
+ if (fill.gradient?.stops !== void 0) config.fillGradientStops = fill.gradient.stops;
3018
+ if (fill.patternType !== void 0) config.patternType = fill.patternType;
3019
+ if (fill.perCharFillEnabled !== void 0) config.perCharFillEnabled = fill.perCharFillEnabled;
3020
+ if (fill.charFillColors !== void 0) config.charFillColors = fill.charFillColors;
3021
+ } else {
3022
+ config.fillType = "none";
3023
+ }
3024
+ config.strokeEnabled = !!stroke;
3025
+ if (stroke) {
3026
+ if (stroke.color !== void 0) config.strokeColor = stroke.color;
3027
+ if (stroke.width !== void 0) config.strokeWidth = stroke.width * ratio;
3028
+ if (stroke.position !== void 0) config.strokePosition = stroke.position;
3029
+ if (stroke.opacity !== void 0) config.strokeOpacity = stroke.opacity;
3030
+ if (stroke.lineJoin !== void 0) config.strokeLineJoin = stroke.lineJoin;
3031
+ if (stroke.blur !== void 0) config.strokeBlur = stroke.blur * ratio;
3032
+ if (stroke.type !== void 0) config.strokeType = stroke.type;
3033
+ if (stroke.colorSecondary !== void 0) config.strokeColorSecondary = stroke.colorSecondary;
3034
+ if (stroke.widthSecondary !== void 0) config.strokeWidthSecondary = stroke.widthSecondary * ratio;
3035
+ if (stroke.fadeRange !== void 0) config.strokeFadeRange = stroke.fadeRange;
3036
+ }
3037
+ config.shadowEnabled = !!shadow;
3038
+ if (shadow) {
3039
+ if (shadow.color !== void 0) config.shadowColor = shadow.color;
3040
+ if (shadow.blur !== void 0) config.shadowBlur = shadow.blur * ratio;
3041
+ if (shadow.offsetX !== void 0) config.shadowOffsetX = shadow.offsetX * ratio;
3042
+ if (shadow.offsetY !== void 0) config.shadowOffsetY = shadow.offsetY * ratio;
3043
+ if (shadow.opacity !== void 0) config.shadowOpacity = shadow.opacity;
3044
+ if (shadow.type !== void 0) config.shadowType = shadow.type;
3045
+ }
3046
+ config.bevelEnabled = !!bevel;
3047
+ if (bevel) {
3048
+ if (bevel.depth !== void 0) config.bevelDepth = Math.round(bevel.depth * ratio);
3049
+ if (bevel.highlightColor !== void 0) config.bevelHighlight = bevel.highlightColor;
3050
+ if (bevel.shadowColor !== void 0) config.bevelShadow = bevel.shadowColor;
3051
+ if (bevel.direction !== void 0) config.bevelDirection = bevel.direction;
3052
+ if (bevel.coreColor !== void 0) config.bevelCoreColor = bevel.coreColor;
3053
+ if (bevel.edgeColor !== void 0) config.bevelEdgeColor = bevel.edgeColor;
3054
+ if (bevel.edgeWidth !== void 0) config.bevelEdgeWidth = bevel.edgeWidth * ratio;
3055
+ if (bevel.blur !== void 0) config.bevelBlur = bevel.blur * ratio;
3056
+ if (bevel.blurColor !== void 0) config.bevelBlurColor = bevel.blurColor;
3057
+ if (bevel.perspectiveEnabled !== void 0) config.bevelPerspectiveEnabled = bevel.perspectiveEnabled;
3058
+ if (bevel.vanishingPointX !== void 0) config.bevelVanishingPointX = bevel.vanishingPointX;
3059
+ if (bevel.vanishingPointY !== void 0) config.bevelVanishingPointY = bevel.vanishingPointY;
3060
+ if (bevel.focalLength !== void 0) config.bevelFocalLength = bevel.focalLength;
3061
+ }
3062
+ if (effect.stack) {
3063
+ config.stackEnabled = !!effect.stack.count;
3064
+ if (effect.stack.count !== void 0) config.stackCount = effect.stack.count;
3065
+ if (effect.stack.offsetX !== void 0) config.stackOffsetX = effect.stack.offsetX * ratio;
3066
+ if (effect.stack.offsetY !== void 0) config.stackOffsetY = effect.stack.offsetY * ratio;
3067
+ if (effect.stack.opacityDecay !== void 0) config.stackOpacityDecay = effect.stack.opacityDecay;
3068
+ if (effect.stack.color1 !== void 0) config.stackColor1 = effect.stack.color1;
3069
+ if (effect.stack.color2 !== void 0) config.stackColor2 = effect.stack.color2;
3070
+ if (effect.stack.color3 !== void 0) config.stackColor3 = effect.stack.color3;
3071
+ if (effect.stack.color4 !== void 0) config.stackColor4 = effect.stack.color4;
3072
+ }
3073
+ config.panelEnabled = !!panel;
3074
+ if (panel) {
3075
+ if (panel.color !== void 0) config.panelColor = panel.color;
3076
+ if (panel.opacity !== void 0) config.panelOpacity = panel.opacity;
3077
+ if (panel.radius !== void 0) config.panelRadius = panel.radius;
3078
+ if (panel.paddingX !== void 0) config.panelPaddingX = panel.paddingX * ratio;
3079
+ if (panel.paddingY !== void 0) config.panelPaddingY = panel.paddingY * ratio;
3080
+ if (panel.stroke !== void 0) {
3081
+ config.panelStrokeEnabled = !!panel.stroke;
3082
+ if (panel.stroke.color !== void 0) config.panelStrokeColor = panel.stroke.color;
3083
+ if (panel.stroke.width !== void 0) config.panelStrokeWidth = panel.stroke.width * ratio;
3084
+ }
3085
+ }
3086
+ if (effect.glows) {
3087
+ config.glowLayers = effect.glows.map((g) => {
3088
+ const mappedGlow = {
3089
+ enabled: true,
3090
+ color: g.color,
3091
+ blur: typeof g.blur === "number" ? g.blur * ratio : g.blur ?? 0,
3092
+ opacity: g.opacity,
3093
+ type: g.type ?? "outer"
3094
+ };
3095
+ if (g.strength !== void 0) mappedGlow.strength = g.strength;
3096
+ if (g.spread !== void 0) mappedGlow.spread = g.spread * ratio;
3097
+ return mappedGlow;
3098
+ });
3099
+ }
3100
+ const standardKeys = /* @__PURE__ */ new Set([
3101
+ "id",
3102
+ "name",
3103
+ "category",
3104
+ "description",
3105
+ "tags",
3106
+ "font",
3107
+ "fills",
3108
+ "strokes",
3109
+ "shadows",
3110
+ "glows",
3111
+ "bevel",
3112
+ "panel",
3113
+ "text",
3114
+ "animation",
3115
+ "stack"
3116
+ ]);
3117
+ for (const key of Object.keys(effect)) {
3118
+ if (!standardKeys.has(key)) {
3119
+ config[key] = effect[key];
3120
+ }
3121
+ }
3122
+ return config;
3123
+ }
3124
+ function layerToTextEffectConfig(layer2) {
3125
+ const normWeight = typeof layer2.fontWeight === "number" ? layer2.fontWeight : layer2.fontWeight === "bold" ? 700 : 400;
3126
+ const config = {
3127
+ ...defaultConfig,
3128
+ width: layer2.width,
3129
+ height: layer2.height,
3130
+ canvasWidth: layer2.width,
3131
+ canvasHeight: layer2.height,
3132
+ text: layer2.text,
3133
+ fontFamily: resolveFontFamilyName(layer2.fontFamily),
3134
+ fontWeight: normWeight,
3135
+ fontStyle: layer2.fontStyle || "normal",
3136
+ fontSize: layer2.fontSize,
3137
+ letterSpacing: layer2.letterSpacing ?? 0,
3138
+ lineHeight: layer2.lineHeight ?? 1.2,
3139
+ fillType: layer2.color ? "solid" : "none",
3140
+ fillColor: layer2.color ?? "#FFFFFF",
3141
+ strokeEnabled: !!layer2.stroke,
3142
+ strokeColor: layer2.stroke?.color ?? "#000000",
3143
+ strokeWidth: layer2.stroke?.width ?? 0,
3144
+ strokePosition: "center",
3145
+ strokeOpacity: 100,
3146
+ strokeLineJoin: "round",
3147
+ shadowEnabled: !!layer2.shadow,
3148
+ shadowColor: layer2.shadow?.color ?? "#000000",
3149
+ shadowBlur: layer2.shadow?.blur ?? 0,
3150
+ shadowOffsetX: layer2.shadow?.offsetX ?? 0,
3151
+ shadowOffsetY: layer2.shadow?.offsetY ?? 0,
3152
+ shadowOpacity: 100,
3153
+ shadowType: "drop",
3154
+ panelEnabled: !!layer2.background,
3155
+ panelColor: layer2.background?.color ?? "#1E1E26",
3156
+ panelOpacity: 80,
3157
+ panelRadius: layer2.background?.borderRadius ?? 6,
3158
+ panelPaddingX: layer2.background?.padding ?? 12,
3159
+ panelPaddingY: layer2.background?.padding ?? 12,
3160
+ textPosX: layer2.textAlign || "center",
3161
+ textPosY: layer2.verticalAlign === "middle" ? "middle" : layer2.verticalAlign || "middle"
3162
+ };
3163
+ return config;
3164
+ }
2722
3165
 
2723
3166
  // src/engine/animation.ts
2724
3167
  function ease(t, kind = "linear") {
@@ -3028,47 +3471,23 @@ function evaluateScene(doc, time, ctx, options = {}) {
3028
3471
  finishFrame();
3029
3472
  return;
3030
3473
  }
3031
- if (supportsOffscreenCanvas()) {
3032
- const off = new OffscreenCanvas(w, h);
3033
- const offCtx = off.getContext("2d");
3034
- if (!offCtx) {
3035
- renderTextEffectCore(ctx, cfg);
3036
- finishFrame();
3037
- return;
3038
- }
3039
- renderTextEffectCore(offCtx, cfg);
3040
- applyMaskReveal(offCtx, animated, w, h);
3474
+ const temp = CanvasDevice.acquire(w, h);
3475
+ const tctx = temp.getContext("2d");
3476
+ if (tctx) {
3477
+ tctx.clearRect(0, 0, w, h);
3478
+ renderTextEffectCore(tctx, cfg);
3479
+ applyMaskReveal(tctx, animated, w, h);
3041
3480
  const compositor = options.compositor ?? getCompositor();
3042
3481
  if (compositor?.isSupported) {
3043
- compositor.renderToContext(ctx, off, comp);
3044
- return;
3045
- }
3046
- ctx.clearRect(0, 0, w, h);
3047
- ctx.drawImage(off, 0, 0);
3048
- return;
3049
- }
3050
- if (typeof document !== "undefined") {
3051
- const temp = document.createElement("canvas");
3052
- temp.width = w;
3053
- temp.height = h;
3054
- const tctx = temp.getContext("2d");
3055
- if (tctx) {
3056
- renderTextEffectCore(tctx, cfg);
3057
- applyMaskReveal(tctx, animated, w, h);
3058
- const compositor = options.compositor ?? getCompositor();
3059
- if (compositor?.isSupported) {
3060
- compositor.renderToContext(ctx, temp, comp);
3061
- temp.width = 0;
3062
- temp.height = 0;
3063
- return;
3064
- }
3482
+ compositor.renderToContext(ctx, temp, comp);
3483
+ } else {
3065
3484
  ctx.clearRect(0, 0, w, h);
3066
3485
  ctx.drawImage(temp, 0, 0);
3067
- temp.width = 0;
3068
- temp.height = 0;
3069
- return;
3070
3486
  }
3487
+ CanvasDevice.release(temp);
3488
+ return;
3071
3489
  }
3490
+ CanvasDevice.release(temp);
3072
3491
  renderTextEffectCore(ctx, cfg);
3073
3492
  finishFrame();
3074
3493
  }
@@ -6035,6 +6454,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6035
6454
  export {
6036
6455
  COMPOSITION_PRESETS,
6037
6456
  CUSTOM_ENGINE_IDS,
6457
+ CanvasDevice,
6038
6458
  DEFAULT_CANVAS_HEIGHT,
6039
6459
  DEFAULT_CANVAS_WIDTH,
6040
6460
  DEFAULT_DURATION,
@@ -6046,6 +6466,7 @@ export {
6046
6466
  ENTRANCE_PRESETS,
6047
6467
  EXIT_PRESETS,
6048
6468
  FONT_WEIGHT_OPTIONS,
6469
+ FontLoader,
6049
6470
  InkBrushEngine,
6050
6471
  LEGACY_RENDERER_MAP,
6051
6472
  LOOP_PRESETS,
@@ -6057,6 +6478,7 @@ export {
6057
6478
  TextEffectRenderer,
6058
6479
  WEBM_EXPORT_MAX_FRAMES,
6059
6480
  WebGLCompositor,
6481
+ _buildConfig,
6060
6482
  _resetPlatformCache,
6061
6483
  addImageLayer,
6062
6484
  addKeyframeAtTime,
@@ -6114,6 +6536,7 @@ export {
6114
6536
  encodeGif,
6115
6537
  ensureDefaultTimeline,
6116
6538
  ensureFontInLottie,
6539
+ ensureFontsLoaded,
6117
6540
  evaluateConfig,
6118
6541
  evaluateScene,
6119
6542
  findTrackIndex,
@@ -6121,6 +6544,7 @@ export {
6121
6544
  getAnimatableParamDef,
6122
6545
  getAnimatableParamsForLayer,
6123
6546
  getDefaultText,
6547
+ getFontLoader,
6124
6548
  getLayerById,
6125
6549
  getPresetScene,
6126
6550
  getSceneTime,
@@ -6139,6 +6563,7 @@ export {
6139
6563
  injectText,
6140
6564
  injectTextStyle,
6141
6565
  isWebMExportSupported,
6566
+ layerToTextEffectConfig,
6142
6567
  loadLottieFonts,
6143
6568
  lottieColorToHex,
6144
6569
  lottieJToAlign,
@@ -6161,10 +6586,12 @@ export {
6161
6586
  renderPngSequence,
6162
6587
  renderSceneWebM,
6163
6588
  renderTextEffectCore,
6589
+ resetFontLoader,
6164
6590
  resetSceneTime,
6165
6591
  resizeCharFillColors,
6166
6592
  resolveAnimatedScalar,
6167
6593
  resolveCustomEngineId,
6594
+ resolveFontFamilyName,
6168
6595
  restoreLetterSpacing,
6169
6596
  scanLottieFonts,
6170
6597
  scanTextLayers,