@pixerate/editor 0.30.0 → 0.31.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.
@@ -2452,6 +2452,233 @@ function fitFreehandPath(points, options = {}) {
2452
2452
  return fitPathData(points, { tolerance: 1, ...options, closed: false });
2453
2453
  }
2454
2454
 
2455
+ // src/canvas/gradient.ts
2456
+ var CANVAS_GRADIENT_PRESETS = {
2457
+ sunset: {
2458
+ type: "linear",
2459
+ angle: 90,
2460
+ stops: [
2461
+ { offset: 0, color: "#f97316" },
2462
+ { offset: 1, color: "#ec4899" }
2463
+ ]
2464
+ },
2465
+ ocean: {
2466
+ type: "linear",
2467
+ angle: 90,
2468
+ stops: [
2469
+ { offset: 0, color: "#06b6d4" },
2470
+ { offset: 1, color: "#3b82f6" }
2471
+ ]
2472
+ },
2473
+ aurora: {
2474
+ type: "linear",
2475
+ angle: 90,
2476
+ stops: [
2477
+ { offset: 0, color: "#10b981" },
2478
+ { offset: 1, color: "#6366f1" }
2479
+ ]
2480
+ },
2481
+ neon: {
2482
+ type: "linear",
2483
+ angle: 90,
2484
+ stops: [
2485
+ { offset: 0, color: "#a855f7" },
2486
+ { offset: 1, color: "#ec4899" }
2487
+ ]
2488
+ },
2489
+ amber: {
2490
+ type: "linear",
2491
+ angle: 90,
2492
+ stops: [
2493
+ { offset: 0, color: "#f59e0b" },
2494
+ { offset: 1, color: "#ef4444" }
2495
+ ]
2496
+ },
2497
+ cosmic: {
2498
+ type: "linear",
2499
+ angle: 90,
2500
+ stops: [
2501
+ { offset: 0, color: "#3b82f6" },
2502
+ { offset: 0.5, color: "#8b5cf6" },
2503
+ { offset: 1, color: "#ec4899" }
2504
+ ]
2505
+ }
2506
+ };
2507
+ function isCanvasGradient(value) {
2508
+ if (!value || typeof value !== "object") return false;
2509
+ const val = value;
2510
+ if (val.type !== "linear" && val.type !== "radial") return false;
2511
+ if (!Array.isArray(val.stops)) return false;
2512
+ return true;
2513
+ }
2514
+ function splitCssArgs(str) {
2515
+ const parts = [];
2516
+ let depth = 0;
2517
+ let current = "";
2518
+ for (let i = 0; i < str.length; i++) {
2519
+ const char = str[i];
2520
+ if (char === "(") depth++;
2521
+ else if (char === ")") depth = Math.max(0, depth - 1);
2522
+ if (char === "," && depth === 0) {
2523
+ if (current.trim()) parts.push(current.trim());
2524
+ current = "";
2525
+ } else {
2526
+ current += char;
2527
+ }
2528
+ }
2529
+ if (current.trim()) parts.push(current.trim());
2530
+ return parts;
2531
+ }
2532
+ function parseCssAngle(val) {
2533
+ const trimmed = val.trim().toLowerCase();
2534
+ if (trimmed.endsWith("deg")) {
2535
+ return parseFloat(trimmed);
2536
+ }
2537
+ if (trimmed.endsWith("turn")) {
2538
+ return parseFloat(trimmed) * 360;
2539
+ }
2540
+ if (trimmed.endsWith("rad")) {
2541
+ return parseFloat(trimmed) * 180 / Math.PI;
2542
+ }
2543
+ if (trimmed.startsWith("to ")) {
2544
+ const dir = trimmed.slice(3).trim();
2545
+ if (dir === "bottom") return 180;
2546
+ if (dir === "top") return 0;
2547
+ if (dir === "right") return 90;
2548
+ if (dir === "left") return 270;
2549
+ if (dir === "bottom right" || dir === "right bottom") return 135;
2550
+ if (dir === "bottom left" || dir === "left bottom") return 225;
2551
+ if (dir === "top right" || dir === "right top") return 45;
2552
+ if (dir === "top left" || dir === "left top") return 315;
2553
+ }
2554
+ const n = parseFloat(trimmed);
2555
+ return Number.isFinite(n) ? n : 180;
2556
+ }
2557
+ function parseCssGradient(css) {
2558
+ const trimmed = css.trim();
2559
+ const linearMatch = trimmed.match(/^linear-gradient\s*\((.*)\)$/is);
2560
+ if (linearMatch) {
2561
+ const rawArgs = splitCssArgs(linearMatch[1]);
2562
+ if (rawArgs.length === 0) return null;
2563
+ let angle = 180;
2564
+ let stopStartIndex = 0;
2565
+ const first = rawArgs[0].trim().toLowerCase();
2566
+ if (first.endsWith("deg") || first.endsWith("turn") || first.endsWith("rad") || first.startsWith("to ")) {
2567
+ angle = parseCssAngle(first);
2568
+ stopStartIndex = 1;
2569
+ }
2570
+ const stopStrings = rawArgs.slice(stopStartIndex);
2571
+ if (stopStrings.length === 0) return null;
2572
+ const stops = parseStopStrings(stopStrings);
2573
+ if (stops.length === 0) return null;
2574
+ return {
2575
+ type: "linear",
2576
+ angle,
2577
+ stops
2578
+ };
2579
+ }
2580
+ const radialMatch = trimmed.match(/^radial-gradient\s*\((.*)\)$/is);
2581
+ if (radialMatch) {
2582
+ const rawArgs = splitCssArgs(radialMatch[1]);
2583
+ if (rawArgs.length === 0) return null;
2584
+ let stopStartIndex = 0;
2585
+ const first = rawArgs[0].trim().toLowerCase();
2586
+ if (first.startsWith("circle") || first.startsWith("ellipse") || first.includes("at ")) {
2587
+ stopStartIndex = 1;
2588
+ }
2589
+ const stopStrings = rawArgs.slice(stopStartIndex);
2590
+ if (stopStrings.length === 0) return null;
2591
+ const stops = parseStopStrings(stopStrings);
2592
+ if (stops.length === 0) return null;
2593
+ return {
2594
+ type: "radial",
2595
+ cx: 0.5,
2596
+ cy: 0.5,
2597
+ r: 0.5,
2598
+ stops
2599
+ };
2600
+ }
2601
+ return null;
2602
+ }
2603
+ function parseStopStrings(stopStrings) {
2604
+ const count = stopStrings.length;
2605
+ return stopStrings.map((str, idx) => {
2606
+ const parts = str.trim().split(/\s+(?=[0-9%])/);
2607
+ const color = parts[0];
2608
+ let offset = count === 1 ? 0 : idx / (count - 1);
2609
+ if (parts.length > 1) {
2610
+ const posStr = parts[1].trim();
2611
+ if (posStr.endsWith("%")) {
2612
+ offset = parseFloat(posStr) / 100;
2613
+ } else {
2614
+ const val = parseFloat(posStr);
2615
+ if (Number.isFinite(val)) offset = val;
2616
+ }
2617
+ }
2618
+ return {
2619
+ offset: Math.min(1, Math.max(0, offset)),
2620
+ color
2621
+ };
2622
+ });
2623
+ }
2624
+ function resolveCanvasGradient(value) {
2625
+ if (!value) return null;
2626
+ if (isCanvasGradient(value)) return value;
2627
+ if (typeof value === "string") {
2628
+ const lower = value.trim().toLowerCase();
2629
+ if (CANVAS_GRADIENT_PRESETS[lower]) {
2630
+ return CANVAS_GRADIENT_PRESETS[lower];
2631
+ }
2632
+ if (lower.startsWith("linear-gradient") || lower.startsWith("radial-gradient")) {
2633
+ return parseCssGradient(value);
2634
+ }
2635
+ }
2636
+ return null;
2637
+ }
2638
+ function angleToSvgLinearCoords(angleDeg = 180) {
2639
+ const rad = (90 - angleDeg) * Math.PI / 180;
2640
+ const dx = Math.cos(rad);
2641
+ const dy = -Math.sin(rad);
2642
+ const half = 0.5;
2643
+ return {
2644
+ x1: Math.round((half - dx * half) * 1e3) / 1e3,
2645
+ y1: Math.round((half - dy * half) * 1e3) / 1e3,
2646
+ x2: Math.round((half + dx * half) * 1e3) / 1e3,
2647
+ y2: Math.round((half + dy * half) * 1e3) / 1e3
2648
+ };
2649
+ }
2650
+ function gradientToSvgDef(gradient, id) {
2651
+ const stops = (gradient.stops ?? []).map((s) => ({
2652
+ offset: `${Math.round(s.offset * 1e3) / 10}%`,
2653
+ stopColor: s.color,
2654
+ stopOpacity: s.opacity
2655
+ }));
2656
+ if (gradient.type === "radial") {
2657
+ return {
2658
+ id,
2659
+ type: "radial",
2660
+ attrs: {
2661
+ cx: gradient.cx !== void 0 ? `${gradient.cx * 100}%` : "50%",
2662
+ cy: gradient.cy !== void 0 ? `${gradient.cy * 100}%` : "50%",
2663
+ r: gradient.r !== void 0 ? `${gradient.r * 100}%` : "50%"
2664
+ },
2665
+ stops
2666
+ };
2667
+ }
2668
+ const coords = angleToSvgLinearCoords(gradient.angle ?? 180);
2669
+ return {
2670
+ id,
2671
+ type: "linear",
2672
+ attrs: {
2673
+ x1: `${coords.x1 * 100}%`,
2674
+ y1: `${coords.y1 * 100}%`,
2675
+ x2: `${coords.x2 * 100}%`,
2676
+ y2: `${coords.y2 * 100}%`
2677
+ },
2678
+ stops
2679
+ };
2680
+ }
2681
+
2455
2682
  // src/canvas/shapeGeometry.ts
2456
2683
  var SHAPE_KINDS = ["rect", "ellipse", "polygon", "star", "path"];
2457
2684
  function isShapeDescription(value) {
@@ -2861,23 +3088,54 @@ function getShapeSvg(shape, size) {
2861
3088
  }
2862
3089
  return transforms.length > 0 ? { d, transform: transforms.join(" "), fillRule } : { d, fillRule };
2863
3090
  }
2864
- function getShapeRenderData(shape, size = {}) {
3091
+ function getShapeRenderData(shape, size = {}, options = {}) {
2865
3092
  const vb = shape.kind === "path" ? validViewBox(shape.viewBox) : null;
2866
3093
  const width = size.width && size.width > 0 ? size.width : vb ? vb[2] : 100;
2867
3094
  const height = size.height && size.height > 0 ? size.height : vb ? vb[3] : 100;
2868
3095
  const svg = getShapeSvg(shape, { width, height });
3096
+ const baseId = options.id || Math.random().toString(36).slice(2, 9);
3097
+ let fillStr = "currentColor";
3098
+ let fillGradientDef;
3099
+ const rawFill = shape.fillGradient ?? shape.fill;
3100
+ const resolvedFillGradient = resolveCanvasGradient(rawFill);
3101
+ if (resolvedFillGradient) {
3102
+ const gradId = `shape-fill-${baseId}`;
3103
+ fillGradientDef = gradientToSvgDef(resolvedFillGradient, gradId);
3104
+ fillStr = `url(#${gradId})`;
3105
+ } else if (typeof shape.fill === "string") {
3106
+ fillStr = shape.fill;
3107
+ } else if (shape.fill === void 0) {
3108
+ fillStr = "currentColor";
3109
+ }
3110
+ let strokeStr = "none";
3111
+ let strokeGradientDef;
3112
+ const rawStroke = shape.strokeGradient ?? shape.stroke;
3113
+ const resolvedStrokeGradient = resolveCanvasGradient(rawStroke);
3114
+ if (resolvedStrokeGradient) {
3115
+ const gradId = `shape-stroke-${baseId}`;
3116
+ strokeGradientDef = gradientToSvgDef(resolvedStrokeGradient, gradId);
3117
+ strokeStr = `url(#${gradId})`;
3118
+ } else if (typeof shape.stroke === "string") {
3119
+ strokeStr = shape.stroke;
3120
+ } else if (shape.stroke === void 0) {
3121
+ strokeStr = "none";
3122
+ }
2869
3123
  const data = {
2870
3124
  viewBox: `0 0 ${formatNumber2(width, 3)} ${formatNumber2(height, 3)}`,
2871
3125
  width,
2872
3126
  height,
2873
3127
  d: svg.d,
2874
3128
  fillRule: svg.fillRule,
2875
- fill: shape.fill ?? "currentColor",
2876
- stroke: shape.stroke ?? "none",
3129
+ fill: fillStr,
3130
+ stroke: strokeStr,
2877
3131
  strokeWidth: shape.strokeWidth ?? 1
2878
3132
  };
2879
3133
  if (svg.transform) data.transform = svg.transform;
2880
3134
  if (shape.opacity !== void 0) data.opacity = shape.opacity;
3135
+ if (shape.strokeLinecap) data.strokeLinecap = shape.strokeLinecap;
3136
+ if (shape.strokeLinejoin) data.strokeLinejoin = shape.strokeLinejoin;
3137
+ if (fillGradientDef) data.fillGradient = fillGradientDef;
3138
+ if (strokeGradientDef) data.strokeGradient = strokeGradientDef;
2881
3139
  return data;
2882
3140
  }
2883
3141
  function clampRatio(value) {
@@ -5313,12 +5571,18 @@ function revertShapeCombination(nodes, change, previous) {
5313
5571
  }
5314
5572
  var INVISIBLE_PAINT = /* @__PURE__ */ new Set(["none", "transparent"]);
5315
5573
  function hasVisibleStroke(shape) {
5316
- if (!shape || typeof shape.stroke !== "string" || INVISIBLE_PAINT.has(shape.stroke.trim().toLowerCase())) return false;
5574
+ if (!shape) return false;
5575
+ const stroke = shape.strokeGradient ?? shape.stroke;
5576
+ if (stroke === void 0 || stroke === null) return false;
5577
+ if (typeof stroke === "string" && INVISIBLE_PAINT.has(stroke.trim().toLowerCase())) return false;
5317
5578
  const width = shape.strokeWidth ?? 1;
5318
5579
  return Number.isFinite(width) && width > 0;
5319
5580
  }
5320
5581
  function hasVisibleFill(shape) {
5321
- return shape.fill === void 0 || !INVISIBLE_PAINT.has(String(shape.fill).trim().toLowerCase());
5582
+ if (shape.fillGradient) return true;
5583
+ if (shape.fill === void 0) return true;
5584
+ if (typeof shape.fill === "object" && shape.fill !== null) return true;
5585
+ return !INVISIBLE_PAINT.has(String(shape.fill).trim().toLowerCase());
5322
5586
  }
5323
5587
  function shapeTargets(nodes, ids) {
5324
5588
  const idSet = new Set(ids);
@@ -11483,6 +11747,8 @@ function strokeToShapeNode(stroke, options = {}) {
11483
11747
  });
11484
11748
  const width = Math.max(bounds.width, 1);
11485
11749
  const height = Math.max(bounds.height, 1);
11750
+ const strokePaint = stroke.style.gradient ?? stroke.style.color;
11751
+ const strokeGradient = stroke.style.gradient ?? (typeof stroke.style.color === "object" && stroke.style.color ? stroke.style.color : void 0);
11486
11752
  return {
11487
11753
  id: options.id ?? stroke.id,
11488
11754
  type: "shape",
@@ -11498,9 +11764,12 @@ function strokeToShapeNode(stroke, options = {}) {
11498
11764
  kind: "path",
11499
11765
  d,
11500
11766
  viewBox: [0, 0, width, height],
11501
- stroke: stroke.style.color,
11767
+ stroke: strokePaint,
11502
11768
  strokeWidth,
11503
11769
  opacity: stroke.style.opacity,
11770
+ strokeLinecap: stroke.style.cap ?? "round",
11771
+ strokeLinejoin: stroke.style.join ?? "round",
11772
+ ...strokeGradient ? { strokeGradient } : {},
11504
11773
  fill: "none"
11505
11774
  },
11506
11775
  tool: stroke.tool
@@ -11709,6 +11978,7 @@ export {
11709
11978
  AnimationEditError,
11710
11979
  AnimationPlayer,
11711
11980
  CANVAS_COMPONENT_DRAG_TYPE,
11981
+ CANVAS_GRADIENT_PRESETS,
11712
11982
  CANVAS_TOKEN_PROPERTIES,
11713
11983
  CHILD_LAYOUT_DATA_KEY,
11714
11984
  COMPONENT_DATA_KEY,
@@ -11741,6 +12011,7 @@ export {
11741
12011
  addCanvasVariant,
11742
12012
  addKeyframesAtTime,
11743
12013
  alignNodes,
12014
+ angleToSvgLinearCoords,
11744
12015
  animateNode,
11745
12016
  applyAnimation,
11746
12017
  applyAnimationOverrides,
@@ -11888,6 +12159,7 @@ export {
11888
12159
  getVariantOptions,
11889
12160
  getVariantSet,
11890
12161
  getVariantValues,
12162
+ gradientToSvgDef,
11891
12163
  handlePoint,
11892
12164
  hasVisibleStroke,
11893
12165
  inferResizeHandle,
@@ -11902,6 +12174,7 @@ export {
11902
12174
  isAutoLayoutFrame,
11903
12175
  isCanvasComponent,
11904
12176
  isCanvasComponentSet,
12177
+ isCanvasGradient,
11905
12178
  isCanvasInstance,
11906
12179
  isCssExportExact,
11907
12180
  isEventFromTextInput,
@@ -11953,6 +12226,7 @@ export {
11953
12226
  parseCanvasClipboardData,
11954
12227
  parseCanvasScenes,
11955
12228
  parseCssEasing,
12229
+ parseCssGradient,
11956
12230
  parseKeyframeClipboard,
11957
12231
  parseNodeStyleDeclarations,
11958
12232
  parseOklab,
@@ -11988,6 +12262,7 @@ export {
11988
12262
  repositionNodesTo,
11989
12263
  resetCanvasInstanceOverrides,
11990
12264
  resizeHandleAxes,
12265
+ resolveCanvasGradient,
11991
12266
  resolveCanvasTokens,
11992
12267
  resolveEasing,
11993
12268
  resolvePadding,