@a-company/atelier 0.17.3 → 0.18.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.
@@ -153,6 +153,30 @@ var ShadowSchema = z6.object({
153
153
  offsetX: z6.number().optional(),
154
154
  offsetY: z6.number().optional()
155
155
  });
156
+ var BlendModeSchema = z7.enum([
157
+ "normal",
158
+ "multiply",
159
+ "screen",
160
+ "overlay",
161
+ "darken",
162
+ "lighten",
163
+ "color-dodge",
164
+ "color-burn",
165
+ "hard-light",
166
+ "soft-light",
167
+ "difference",
168
+ "exclusion",
169
+ "hue",
170
+ "saturation",
171
+ "color",
172
+ "luminosity"
173
+ ]);
174
+ var MotionPathSchema = z7.object({
175
+ points: z7.array(PathPointSchema).min(2, "Motion path must have at least 2 points"),
176
+ closed: z7.boolean().optional(),
177
+ autoRotate: z7.boolean().optional(),
178
+ autoRotateOffset: z7.number().optional()
179
+ });
156
180
  var ShapeVisualSchema = z7.object({
157
181
  type: z7.literal("shape"),
158
182
  shape: ShapeSchema,
@@ -196,7 +220,10 @@ var LayerSchema = z7.object({
196
220
  rotation: z7.number().optional(),
197
221
  scale: z7.object({ x: z7.number(), y: z7.number() }).optional(),
198
222
  visible: z7.boolean().optional(),
199
- shadow: ShadowSchema.optional()
223
+ shadow: ShadowSchema.optional(),
224
+ blendMode: BlendModeSchema.optional(),
225
+ motionPath: MotionPathSchema.optional(),
226
+ clipPath: ShapeSchema.optional()
200
227
  });
201
228
  var AnimatablePropertySchema = z8.enum([
202
229
  "frame.x",
@@ -220,7 +247,12 @@ var AnimatablePropertySchema = z8.enum([
220
247
  "shadow.color",
221
248
  "shadow.blur",
222
249
  "shadow.offsetX",
223
- "shadow.offsetY"
250
+ "shadow.offsetY",
251
+ "motionPath.progress",
252
+ "visual.fill.angle",
253
+ "visual.fill.center.x",
254
+ "visual.fill.center.y",
255
+ "visual.fill.radius"
224
256
  ]);
225
257
  var FrameRangeSchema = z8.tuple([
226
258
  z8.number().int().min(0, "Frame start must be >= 0"),
@@ -240,11 +272,19 @@ var DeltaSchema = z8.object({
240
272
  description: z8.string().optional(),
241
273
  tags: z8.array(z8.string()).optional()
242
274
  });
275
+ var AudioSchema = z9.object({
276
+ src: z9.string().min(1, "Audio src is required"),
277
+ offset: z9.number().min(0, "Audio offset must be non-negative").optional(),
278
+ volume: z9.number().min(0).max(1, "Audio volume must be 0\u20131").optional(),
279
+ loop: z9.boolean().optional(),
280
+ startFrame: z9.number().int().min(0, "Audio startFrame must be a non-negative integer").optional()
281
+ });
243
282
  var StateSchema = z9.object({
244
283
  description: z9.string().optional(),
245
284
  tags: z9.array(z9.string()).optional(),
246
285
  duration: z9.number().int().positive("State duration must be a positive integer (frames)"),
247
- deltas: z9.array(DeltaSchema)
286
+ deltas: z9.array(DeltaSchema),
287
+ audio: AudioSchema.optional()
248
288
  });
249
289
  var PresetDeltaSchema = z10.object({
250
290
  property: AnimatablePropertySchema,
@@ -264,7 +304,7 @@ var VariableSchema = z11.object({
264
304
  default: z11.unknown().optional(),
265
305
  description: z11.string().optional()
266
306
  });
267
- var AssetTypeSchema = z12.enum(["image", "svg", "font", "animation"]);
307
+ var AssetTypeSchema = z12.enum(["image", "svg", "font", "animation", "audio"]);
268
308
  var AssetSchema = z12.object({
269
309
  type: AssetTypeSchema,
270
310
  src: z12.string().min(1, "Asset src is required"),
@@ -426,6 +466,75 @@ function lerpRgba(a, b, t) {
426
466
  a: lerp(a.a, b.a, t)
427
467
  };
428
468
  }
469
+ function bezierSegmentLength(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, steps = 32) {
470
+ let length = 0;
471
+ let prevX = p0x;
472
+ let prevY = p0y;
473
+ for (let i = 1; i <= steps; i++) {
474
+ const t = i / steps;
475
+ const mt = 1 - t;
476
+ const x = mt * mt * mt * p0x + 3 * mt * mt * t * c0x + 3 * mt * t * t * c1x + t * t * t * p1x;
477
+ const y = mt * mt * mt * p0y + 3 * mt * mt * t * c0y + 3 * mt * t * t * c1y + t * t * t * p1y;
478
+ const dx = x - prevX;
479
+ const dy = y - prevY;
480
+ length += Math.sqrt(dx * dx + dy * dy);
481
+ prevX = x;
482
+ prevY = y;
483
+ }
484
+ return length;
485
+ }
486
+ function evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, t) {
487
+ const mt = 1 - t;
488
+ const x = mt * mt * mt * p0x + 3 * mt * mt * t * c0x + 3 * mt * t * t * c1x + t * t * t * p1x;
489
+ const y = mt * mt * mt * p0y + 3 * mt * mt * t * c0y + 3 * mt * t * t * c1y + t * t * t * p1y;
490
+ const tx = 3 * mt * mt * (c0x - p0x) + 6 * mt * t * (c1x - c0x) + 3 * t * t * (p1x - c1x);
491
+ const ty = 3 * mt * mt * (c0y - p0y) + 6 * mt * t * (c1y - c0y) + 3 * t * t * (p1y - c1y);
492
+ const angle = Math.atan2(ty, tx) * (180 / Math.PI);
493
+ return { x, y, angle };
494
+ }
495
+ function buildSegmentLengths(points, closed) {
496
+ const segCount = closed ? points.length : points.length - 1;
497
+ const lengths = [];
498
+ for (let i = 0; i < segCount; i++) {
499
+ const p0 = points[i];
500
+ const p1 = points[(i + 1) % points.length];
501
+ const c0x = p0.x + (p0.out?.x ?? 0);
502
+ const c0y = p0.y + (p0.out?.y ?? 0);
503
+ const c1x = p1.x + (p1.in?.x ?? 0);
504
+ const c1y = p1.y + (p1.in?.y ?? 0);
505
+ lengths.push(bezierSegmentLength(p0.x, p0.y, c0x, c0y, c1x, c1y, p1.x, p1.y));
506
+ }
507
+ return lengths;
508
+ }
509
+ function evaluatePathAtProgress(points, progress, closed = false) {
510
+ if (points.length < 2) {
511
+ return { x: points[0]?.x ?? 0, y: points[0]?.y ?? 0, angle: 0 };
512
+ }
513
+ const t = Math.max(0, Math.min(1, progress));
514
+ const segLengths = buildSegmentLengths(points, closed);
515
+ const totalLength = segLengths.reduce((a, b) => a + b, 0);
516
+ if (totalLength === 0) {
517
+ return { x: points[0].x, y: points[0].y, angle: 0 };
518
+ }
519
+ const targetLength = t * totalLength;
520
+ let accumulated = 0;
521
+ for (let i = 0; i < segLengths.length; i++) {
522
+ const segLen = segLengths[i];
523
+ if (accumulated + segLen >= targetLength || i === segLengths.length - 1) {
524
+ const segProgress = segLen === 0 ? 0 : (targetLength - accumulated) / segLen;
525
+ const p0 = points[i];
526
+ const p1 = points[(i + 1) % points.length];
527
+ const c0x = p0.x + (p0.out?.x ?? 0);
528
+ const c0y = p0.y + (p0.out?.y ?? 0);
529
+ const c1x = p1.x + (p1.in?.x ?? 0);
530
+ const c1y = p1.y + (p1.in?.y ?? 0);
531
+ return evalBezier(p0.x, p0.y, c0x, c0y, c1x, c1y, p1.x, p1.y, segProgress);
532
+ }
533
+ accumulated += segLen;
534
+ }
535
+ const last = points[points.length - 1];
536
+ return { x: last.x, y: last.y, angle: 0 };
537
+ }
429
538
 
430
539
  // ../core/dist/index.js
431
540
  function resolveEasing(easing) {
@@ -460,6 +569,261 @@ function resolveEasing(easing) {
460
569
  return linear;
461
570
  }
462
571
  }
572
+ function tokenize(expr) {
573
+ const tokens = [];
574
+ let i = 0;
575
+ while (i < expr.length) {
576
+ const ch = expr[i];
577
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
578
+ i++;
579
+ continue;
580
+ }
581
+ if (ch >= "0" && ch <= "9" || ch === ".") {
582
+ let num = "";
583
+ while (i < expr.length && (expr[i] >= "0" && expr[i] <= "9" || expr[i] === ".")) {
584
+ num += expr[i++];
585
+ }
586
+ tokens.push({ type: "number", value: num });
587
+ continue;
588
+ }
589
+ if (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_") {
590
+ let id = "";
591
+ while (i < expr.length && (expr[i] >= "a" && expr[i] <= "z" || expr[i] >= "A" && expr[i] <= "Z" || expr[i] >= "0" && expr[i] <= "9" || expr[i] === "_")) {
592
+ id += expr[i++];
593
+ }
594
+ tokens.push({ type: "ident", value: id });
595
+ continue;
596
+ }
597
+ if (ch === "<" || ch === ">" || ch === "!" || ch === "=") {
598
+ if (i + 1 < expr.length && expr[i + 1] === "=") {
599
+ tokens.push({ type: "compare", value: ch + "=" });
600
+ i += 2;
601
+ continue;
602
+ }
603
+ if (ch === "<" || ch === ">") {
604
+ tokens.push({ type: "compare", value: ch });
605
+ i++;
606
+ continue;
607
+ }
608
+ }
609
+ if (ch === "*" && i + 1 < expr.length && expr[i + 1] === "*") {
610
+ tokens.push({ type: "op", value: "**" });
611
+ i += 2;
612
+ continue;
613
+ }
614
+ if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%") {
615
+ tokens.push({ type: "op", value: ch });
616
+ i++;
617
+ continue;
618
+ }
619
+ if (ch === "(") {
620
+ tokens.push({ type: "lparen", value: "(" });
621
+ i++;
622
+ continue;
623
+ }
624
+ if (ch === ")") {
625
+ tokens.push({ type: "rparen", value: ")" });
626
+ i++;
627
+ continue;
628
+ }
629
+ if (ch === ",") {
630
+ tokens.push({ type: "comma", value: "," });
631
+ i++;
632
+ continue;
633
+ }
634
+ if (ch === "?") {
635
+ tokens.push({ type: "question", value: "?" });
636
+ i++;
637
+ continue;
638
+ }
639
+ if (ch === ":") {
640
+ tokens.push({ type: "colon", value: ":" });
641
+ i++;
642
+ continue;
643
+ }
644
+ throw new Error(`Expression: unexpected character '${ch}' at position ${i}`);
645
+ }
646
+ tokens.push({ type: "eof", value: "" });
647
+ return tokens;
648
+ }
649
+ var CONSTANTS = {
650
+ pi: Math.PI,
651
+ PI: Math.PI,
652
+ tau: Math.PI * 2,
653
+ TAU: Math.PI * 2,
654
+ e: Math.E,
655
+ E: Math.E
656
+ };
657
+ var FUNCTIONS = {
658
+ sin: Math.sin,
659
+ cos: Math.cos,
660
+ tan: Math.tan,
661
+ abs: Math.abs,
662
+ floor: Math.floor,
663
+ ceil: Math.ceil,
664
+ round: Math.round,
665
+ sqrt: Math.sqrt,
666
+ sign: Math.sign,
667
+ log: Math.log,
668
+ min: (...args) => Math.min(...args),
669
+ max: (...args) => Math.max(...args),
670
+ pow: (a, b) => Math.pow(a, b),
671
+ clamp: (v, lo, hi) => Math.min(Math.max(v, lo), hi)
672
+ };
673
+ var Parser = class {
674
+ tokens;
675
+ pos = 0;
676
+ ctx;
677
+ constructor(tokens, ctx) {
678
+ this.tokens = tokens;
679
+ this.ctx = ctx;
680
+ }
681
+ peek() {
682
+ return this.tokens[this.pos];
683
+ }
684
+ consume(expectedType) {
685
+ const tok = this.tokens[this.pos++];
686
+ if (expectedType && tok.type !== expectedType) {
687
+ throw new Error(`Expression: expected ${expectedType} but got ${tok.type} '${tok.value}'`);
688
+ }
689
+ return tok;
690
+ }
691
+ /** Entry: ternary (lowest precedence) */
692
+ parse() {
693
+ const result = this.parseTernary();
694
+ if (this.peek().type !== "eof") {
695
+ throw new Error(`Expression: unexpected token '${this.peek().value}'`);
696
+ }
697
+ return result;
698
+ }
699
+ /** ternary: comparison ? expr : expr */
700
+ parseTernary() {
701
+ const condition = this.parseComparison();
702
+ if (this.peek().type === "question") {
703
+ this.consume();
704
+ const trueVal = this.parseTernary();
705
+ this.consume("colon");
706
+ const falseVal = this.parseTernary();
707
+ return condition ? trueVal : falseVal;
708
+ }
709
+ return condition;
710
+ }
711
+ /** comparison: additive (< | > | <= | >= | == | !=) additive */
712
+ parseComparison() {
713
+ let left = this.parseAdditive();
714
+ while (this.peek().type === "compare") {
715
+ const op = this.consume().value;
716
+ const right = this.parseAdditive();
717
+ switch (op) {
718
+ case "<":
719
+ left = left < right ? 1 : 0;
720
+ break;
721
+ case ">":
722
+ left = left > right ? 1 : 0;
723
+ break;
724
+ case "<=":
725
+ left = left <= right ? 1 : 0;
726
+ break;
727
+ case ">=":
728
+ left = left >= right ? 1 : 0;
729
+ break;
730
+ case "==":
731
+ left = left === right ? 1 : 0;
732
+ break;
733
+ case "!=":
734
+ left = left !== right ? 1 : 0;
735
+ break;
736
+ }
737
+ }
738
+ return left;
739
+ }
740
+ /** additive: multiplicative (('+' | '-') multiplicative)* */
741
+ parseAdditive() {
742
+ let left = this.parseMultiplicative();
743
+ while (this.peek().type === "op" && (this.peek().value === "+" || this.peek().value === "-")) {
744
+ const op = this.consume().value;
745
+ const right = this.parseMultiplicative();
746
+ left = op === "+" ? left + right : left - right;
747
+ }
748
+ return left;
749
+ }
750
+ /** multiplicative: power (('*' | '/' | '%') power)* */
751
+ parseMultiplicative() {
752
+ let left = this.parsePower();
753
+ while (this.peek().type === "op" && (this.peek().value === "*" || this.peek().value === "/" || this.peek().value === "%")) {
754
+ const op = this.consume().value;
755
+ const right = this.parsePower();
756
+ if (op === "*") left = left * right;
757
+ else if (op === "/") left = right !== 0 ? left / right : 0;
758
+ else left = left % right;
759
+ }
760
+ return left;
761
+ }
762
+ /** power: unary ('**' unary)* (right-associative) */
763
+ parsePower() {
764
+ const base = this.parseUnary();
765
+ if (this.peek().type === "op" && this.peek().value === "**") {
766
+ this.consume();
767
+ const exp = this.parsePower();
768
+ return Math.pow(base, exp);
769
+ }
770
+ return base;
771
+ }
772
+ /** unary: ('-' | '+') unary | primary */
773
+ parseUnary() {
774
+ if (this.peek().type === "op" && (this.peek().value === "-" || this.peek().value === "+")) {
775
+ const op = this.consume().value;
776
+ const val = this.parseUnary();
777
+ return op === "-" ? -val : val;
778
+ }
779
+ return this.parsePrimary();
780
+ }
781
+ /** primary: number | ident | function(args) | '(' expr ')' */
782
+ parsePrimary() {
783
+ const tok = this.peek();
784
+ if (tok.type === "number") {
785
+ this.consume();
786
+ return parseFloat(tok.value);
787
+ }
788
+ if (tok.type === "ident") {
789
+ this.consume();
790
+ const name = tok.value;
791
+ if (this.peek().type === "lparen") {
792
+ this.consume();
793
+ const args = [];
794
+ if (this.peek().type !== "rparen") {
795
+ args.push(this.parseTernary());
796
+ while (this.peek().type === "comma") {
797
+ this.consume();
798
+ args.push(this.parseTernary());
799
+ }
800
+ }
801
+ this.consume("rparen");
802
+ const fn = FUNCTIONS[name];
803
+ if (!fn) throw new Error(`Expression: unknown function '${name}'`);
804
+ return fn(...args);
805
+ }
806
+ if (name in CONSTANTS) return CONSTANTS[name];
807
+ if (name in this.ctx) return this.ctx[name];
808
+ throw new Error(`Expression: unknown variable '${name}'`);
809
+ }
810
+ if (tok.type === "lparen") {
811
+ this.consume();
812
+ const val = this.parseTernary();
813
+ this.consume("rparen");
814
+ return val;
815
+ }
816
+ throw new Error(`Expression: unexpected token '${tok.value}'`);
817
+ }
818
+ };
819
+ function isExpression(value) {
820
+ return typeof value === "object" && value !== null && "expr" in value && typeof value.expr === "string";
821
+ }
822
+ function evaluateExpression(expr, ctx) {
823
+ const tokens = tokenize(expr);
824
+ const parser = new Parser(tokens, ctx);
825
+ return parser.parse();
826
+ }
463
827
  function isFrameInRange(frame, range) {
464
828
  return frame >= range[0] && frame <= range[1];
465
829
  }
@@ -475,7 +839,15 @@ function resolveDeltaValue(delta, frame) {
475
839
  const progress = computeProgress(frame, delta.range);
476
840
  const easingFn = resolveEasing(delta.easing);
477
841
  const easedProgress = easingFn(progress);
478
- return interpolateValue(delta.from, delta.to, easedProgress);
842
+ const exprCtx = {
843
+ t: easedProgress,
844
+ progress,
845
+ frame,
846
+ duration: delta.range[1] - delta.range[0]
847
+ };
848
+ const from = isExpression(delta.from) ? evaluateExpression(delta.from.expr, exprCtx) : delta.from;
849
+ const to = isExpression(delta.to) ? evaluateExpression(delta.to.expr, exprCtx) : delta.to;
850
+ return interpolateValue(from, to, easedProgress);
479
851
  }
480
852
  function interpolateValue(from, to, t) {
481
853
  if (typeof from === "number" && typeof to === "number") {
@@ -503,7 +875,16 @@ function resolvePropertyAtFrame(deltas, frame) {
503
875
  }
504
876
  }
505
877
  }
506
- return lastCompleted?.to;
878
+ if (!lastCompleted) return void 0;
879
+ if (isExpression(lastCompleted.to)) {
880
+ return evaluateExpression(lastCompleted.to.expr, {
881
+ t: 1,
882
+ progress: 1,
883
+ frame,
884
+ duration: lastCompleted.range[1] - lastCompleted.range[0]
885
+ });
886
+ }
887
+ return lastCompleted.to;
507
888
  }
508
889
  function resolveFrame(doc, stateName, frame) {
509
890
  const state = doc.states[stateName];
@@ -844,11 +1225,23 @@ function buildEffectiveLayer(resolved, parentWidth, parentHeight) {
844
1225
  const { layer, computedProperties } = resolved;
845
1226
  const cp = computedProperties;
846
1227
  const hasShadow = layer.shadow || cp["shadow.blur"] !== void 0 || cp["shadow.color"] !== void 0;
1228
+ let x = resolveUnit(cp["frame.x"] ?? layer.frame.x, parentWidth);
1229
+ let y = resolveUnit(cp["frame.y"] ?? layer.frame.y, parentHeight);
1230
+ let motionPathAngle = 0;
1231
+ const motionProgress = cp["motionPath.progress"];
1232
+ if (motionProgress !== void 0 && layer.motionPath && layer.motionPath.points.length >= 2) {
1233
+ const pos = evaluatePathAtProgress(layer.motionPath.points, motionProgress, layer.motionPath.closed);
1234
+ x = pos.x;
1235
+ y = pos.y;
1236
+ if (layer.motionPath.autoRotate) {
1237
+ motionPathAngle = pos.angle + (layer.motionPath.autoRotateOffset ?? 0);
1238
+ }
1239
+ }
847
1240
  return {
848
1241
  layer,
849
1242
  visual: buildEffectiveVisual(layer.visual, cp),
850
- x: resolveUnit(cp["frame.x"] ?? layer.frame.x, parentWidth),
851
- y: resolveUnit(cp["frame.y"] ?? layer.frame.y, parentHeight),
1243
+ x,
1244
+ y,
852
1245
  width: resolveUnit(cp["bounds.width"] ?? layer.bounds.width, parentWidth),
853
1246
  height: resolveUnit(cp["bounds.height"] ?? layer.bounds.height, parentHeight),
854
1247
  opacity: cp["opacity"] ?? layer.opacity ?? 1,
@@ -862,19 +1255,42 @@ function buildEffectiveLayer(resolved, parentWidth, parentHeight) {
862
1255
  blur: cp["shadow.blur"] ?? layer.shadow?.blur ?? 0,
863
1256
  offsetX: cp["shadow.offsetX"] ?? layer.shadow?.offsetX ?? 0,
864
1257
  offsetY: cp["shadow.offsetY"] ?? layer.shadow?.offsetY ?? 0
865
- } : void 0
1258
+ } : void 0,
1259
+ blendMode: layer.blendMode ?? "normal",
1260
+ motionPathAngle
866
1261
  };
867
1262
  }
868
1263
  function buildEffectiveVisual(visual, cp) {
869
- const hasVisualOverride = cp["visual.fill.color"] !== void 0 || cp["visual.stroke.color"] !== void 0 || cp["visual.stroke.width"] !== void 0 || cp["visual.stroke.start"] !== void 0 || cp["visual.stroke.end"] !== void 0 || cp["visual.shape.cornerRadius"] !== void 0 || cp["visual.style.fontSize"] !== void 0 || cp["visual.style.color"] !== void 0;
1264
+ const hasVisualOverride = cp["visual.fill.color"] !== void 0 || cp["visual.fill.angle"] !== void 0 || cp["visual.fill.center.x"] !== void 0 || cp["visual.fill.center.y"] !== void 0 || cp["visual.fill.radius"] !== void 0 || cp["visual.stroke.color"] !== void 0 || cp["visual.stroke.width"] !== void 0 || cp["visual.stroke.start"] !== void 0 || cp["visual.stroke.end"] !== void 0 || cp["visual.shape.cornerRadius"] !== void 0 || cp["visual.style.fontSize"] !== void 0 || cp["visual.style.color"] !== void 0;
870
1265
  if (!hasVisualOverride) return visual;
871
1266
  if (visual.type === "shape") {
872
1267
  const v = { ...visual };
873
1268
  if (cp["visual.shape.cornerRadius"] !== void 0 && v.shape.type === "rect") {
874
1269
  v.shape = { ...v.shape, cornerRadius: cp["visual.shape.cornerRadius"] };
875
1270
  }
876
- if (v.fill && cp["visual.fill.color"] !== void 0) {
877
- v.fill = v.fill.type === "solid" ? { ...v.fill, color: cp["visual.fill.color"] } : v.fill;
1271
+ if (v.fill) {
1272
+ if (cp["visual.fill.color"] !== void 0 && v.fill.type === "solid") {
1273
+ v.fill = { ...v.fill, color: cp["visual.fill.color"] };
1274
+ }
1275
+ if (cp["visual.fill.angle"] !== void 0 && v.fill.type === "linear-gradient") {
1276
+ v.fill = { ...v.fill, angle: cp["visual.fill.angle"] };
1277
+ }
1278
+ if (v.fill.type === "radial-gradient") {
1279
+ const cx = cp["visual.fill.center.x"];
1280
+ const cy = cp["visual.fill.center.y"];
1281
+ const r = cp["visual.fill.radius"];
1282
+ if (cx !== void 0 || cy !== void 0 || r !== void 0) {
1283
+ const f = v.fill;
1284
+ v.fill = {
1285
+ ...f,
1286
+ center: {
1287
+ x: cx !== void 0 ? cx : f.center.x,
1288
+ y: cy !== void 0 ? cy : f.center.y
1289
+ },
1290
+ radius: r !== void 0 ? r : f.radius
1291
+ };
1292
+ }
1293
+ }
878
1294
  }
879
1295
  if (v.stroke) {
880
1296
  const strokeColor = cp["visual.stroke.color"] ?? v.stroke.color;
@@ -1088,13 +1504,17 @@ function renderFrame(ctx, resolvedFrame, doc, imageCache) {
1088
1504
  ctx.save();
1089
1505
  applyAncestorTransforms(ctx, layer.id, effMap, doc);
1090
1506
  ctx.globalAlpha = eff.opacity;
1507
+ if (eff.blendMode !== "normal") {
1508
+ ctx.globalCompositeOperation = blendModeToComposite(eff.blendMode);
1509
+ }
1091
1510
  ctx.translate(eff.x, eff.y);
1092
1511
  const anchorPixelX = eff.anchorX * eff.width;
1093
1512
  const anchorPixelY = eff.anchorY * eff.height;
1094
- if (eff.rotation !== 0 || eff.scaleX !== 1 || eff.scaleY !== 1) {
1513
+ const totalRotation = eff.rotation + eff.motionPathAngle;
1514
+ if (totalRotation !== 0 || eff.scaleX !== 1 || eff.scaleY !== 1) {
1095
1515
  ctx.translate(anchorPixelX, anchorPixelY);
1096
- if (eff.rotation !== 0) {
1097
- ctx.rotate(eff.rotation * Math.PI / 180);
1516
+ if (totalRotation !== 0) {
1517
+ ctx.rotate(totalRotation * Math.PI / 180);
1098
1518
  }
1099
1519
  if (eff.scaleX !== 1 || eff.scaleY !== 1) {
1100
1520
  ctx.scale(eff.scaleX, eff.scaleY);
@@ -1107,6 +1527,9 @@ function renderFrame(ctx, resolvedFrame, doc, imageCache) {
1107
1527
  ctx.shadowOffsetX = eff.shadow.offsetX;
1108
1528
  ctx.shadowOffsetY = eff.shadow.offsetY;
1109
1529
  }
1530
+ if (layer.clipPath) {
1531
+ applyClipPath(ctx, layer.clipPath, eff.width, eff.height);
1532
+ }
1110
1533
  switch (layer.visual.type) {
1111
1534
  case "shape":
1112
1535
  renderShape(ctx, eff);
@@ -1126,6 +1549,68 @@ function renderFrame(ctx, resolvedFrame, doc, imageCache) {
1126
1549
  ctx.restore();
1127
1550
  }
1128
1551
  }
1552
+ function applyClipPath(ctx, shape, width, height) {
1553
+ ctx.beginPath();
1554
+ switch (shape.type) {
1555
+ case "rect":
1556
+ if (shape.cornerRadius && ctx.roundRect) {
1557
+ ctx.roundRect(0, 0, width, height, shape.cornerRadius);
1558
+ } else {
1559
+ ctx.moveTo(0, 0);
1560
+ ctx.lineTo(width, 0);
1561
+ ctx.lineTo(width, height);
1562
+ ctx.lineTo(0, height);
1563
+ ctx.closePath();
1564
+ }
1565
+ break;
1566
+ case "ellipse":
1567
+ ctx.ellipse(width / 2, height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
1568
+ break;
1569
+ case "path":
1570
+ if (shape.points.length >= 2) {
1571
+ ctx.moveTo(shape.points[0].x, shape.points[0].y);
1572
+ for (let i = 1; i < shape.points.length; i++) {
1573
+ const prev = shape.points[i - 1];
1574
+ const curr = shape.points[i];
1575
+ if (prev.out && curr.in) {
1576
+ ctx.bezierCurveTo(
1577
+ prev.x + prev.out.x,
1578
+ prev.y + prev.out.y,
1579
+ curr.x + curr.in.x,
1580
+ curr.y + curr.in.y,
1581
+ curr.x,
1582
+ curr.y
1583
+ );
1584
+ } else {
1585
+ ctx.lineTo(curr.x, curr.y);
1586
+ }
1587
+ }
1588
+ if (shape.closed) ctx.closePath();
1589
+ }
1590
+ break;
1591
+ }
1592
+ ctx.clip();
1593
+ }
1594
+ function blendModeToComposite(mode) {
1595
+ const map = {
1596
+ "multiply": "multiply",
1597
+ "screen": "screen",
1598
+ "overlay": "overlay",
1599
+ "darken": "darken",
1600
+ "lighten": "lighten",
1601
+ "color-dodge": "color-dodge",
1602
+ "color-burn": "color-burn",
1603
+ "hard-light": "hard-light",
1604
+ "soft-light": "soft-light",
1605
+ "difference": "difference",
1606
+ "exclusion": "exclusion",
1607
+ "hue": "hue",
1608
+ "saturation": "saturation",
1609
+ "color": "color",
1610
+ "luminosity": "luminosity"
1611
+ };
1612
+ return map[mode] ?? "source-over";
1613
+ }
1129
1614
  function applyAncestorTransforms(ctx, layerId, effMap, doc) {
1130
1615
  const chain = [];
1131
1616
  const layer = doc.layers.find((l) => l.id === layerId);
@@ -1462,4 +1947,4 @@ export {
1462
1947
  renderDocument,
1463
1948
  renderCommand
1464
1949
  };
1465
- //# sourceMappingURL=chunk-LL2EJ6YE.js.map
1950
+ //# sourceMappingURL=chunk-H4A6HRMY.js.map