@pixerate/editor 0.29.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.
@@ -25,35 +25,38 @@ import {
25
25
  resolveToken
26
26
  } from "../chunk-D6AVOTSD.js";
27
27
  import {
28
- ANIMATION_EASING_LABELS,
29
- ANIMATION_EASING_PRESETS,
30
28
  ANIMATION_TIME_EPSILON,
31
29
  AnimationPlayer,
32
- CUBIC_BEZIER_PRESETS,
33
30
  ManualClock,
34
31
  createAnimationPlayer,
35
- cubicBezierAt,
36
- easingFromInfluence,
37
- easingLabel,
38
- easingToCss,
39
- evaluateEasing,
40
32
  evaluateTrack,
41
33
  formatOklab,
42
34
  interpolateAnimationValue,
43
- isAnimationEasing,
44
35
  isAnimationKeyframe,
45
36
  mixColorsOklab,
46
37
  normalizeKeyframes,
47
38
  oklabToRgb,
48
39
  pageHidden,
49
- parseCssEasing,
50
40
  parseOklab,
51
41
  realClock,
52
- resolveEasing,
53
42
  rgbToOklab,
54
- springAt,
55
43
  wrapAnimationTime
56
- } from "../chunk-PWJARCI4.js";
44
+ } from "../chunk-W7BOBWZD.js";
45
+ import {
46
+ ANIMATION_EASING_LABELS,
47
+ ANIMATION_EASING_PRESETS,
48
+ CUBIC_BEZIER_PRESETS,
49
+ cubicBezierAt,
50
+ easingFromInfluence,
51
+ easingLabel,
52
+ easingToCss,
53
+ elasticAt,
54
+ evaluateEasing,
55
+ isAnimationEasing,
56
+ parseCssEasing,
57
+ resolveEasing,
58
+ springAt
59
+ } from "../chunk-UB2OY7FD.js";
57
60
  import {
58
61
  normalizeTokenColor
59
62
  } from "../chunk-TGD52FEZ.js";
@@ -2449,6 +2452,233 @@ function fitFreehandPath(points, options = {}) {
2449
2452
  return fitPathData(points, { tolerance: 1, ...options, closed: false });
2450
2453
  }
2451
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
+
2452
2682
  // src/canvas/shapeGeometry.ts
2453
2683
  var SHAPE_KINDS = ["rect", "ellipse", "polygon", "star", "path"];
2454
2684
  function isShapeDescription(value) {
@@ -2858,23 +3088,54 @@ function getShapeSvg(shape, size) {
2858
3088
  }
2859
3089
  return transforms.length > 0 ? { d, transform: transforms.join(" "), fillRule } : { d, fillRule };
2860
3090
  }
2861
- function getShapeRenderData(shape, size = {}) {
3091
+ function getShapeRenderData(shape, size = {}, options = {}) {
2862
3092
  const vb = shape.kind === "path" ? validViewBox(shape.viewBox) : null;
2863
3093
  const width = size.width && size.width > 0 ? size.width : vb ? vb[2] : 100;
2864
3094
  const height = size.height && size.height > 0 ? size.height : vb ? vb[3] : 100;
2865
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
+ }
2866
3123
  const data = {
2867
3124
  viewBox: `0 0 ${formatNumber2(width, 3)} ${formatNumber2(height, 3)}`,
2868
3125
  width,
2869
3126
  height,
2870
3127
  d: svg.d,
2871
3128
  fillRule: svg.fillRule,
2872
- fill: shape.fill ?? "currentColor",
2873
- stroke: shape.stroke ?? "none",
3129
+ fill: fillStr,
3130
+ stroke: strokeStr,
2874
3131
  strokeWidth: shape.strokeWidth ?? 1
2875
3132
  };
2876
3133
  if (svg.transform) data.transform = svg.transform;
2877
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;
2878
3139
  return data;
2879
3140
  }
2880
3141
  function clampRatio(value) {
@@ -5310,12 +5571,18 @@ function revertShapeCombination(nodes, change, previous) {
5310
5571
  }
5311
5572
  var INVISIBLE_PAINT = /* @__PURE__ */ new Set(["none", "transparent"]);
5312
5573
  function hasVisibleStroke(shape) {
5313
- 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;
5314
5578
  const width = shape.strokeWidth ?? 1;
5315
5579
  return Number.isFinite(width) && width > 0;
5316
5580
  }
5317
5581
  function hasVisibleFill(shape) {
5318
- 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());
5319
5586
  }
5320
5587
  function shapeTargets(nodes, ids) {
5321
5588
  const idSet = new Set(ids);
@@ -11397,6 +11664,310 @@ function getCanvasComponentDragId(dataTransfer) {
11397
11664
  const id = dataTransfer?.getData(CANVAS_COMPONENT_DRAG_TYPE);
11398
11665
  return id ? id : void 0;
11399
11666
  }
11667
+
11668
+ // src/canvas/freehand.ts
11669
+ function createDefaultStrokeStyle(tool) {
11670
+ if (tool === "highlighter") {
11671
+ return {
11672
+ color: "#facc15",
11673
+ width: 24,
11674
+ opacity: 0.35,
11675
+ cap: "round",
11676
+ join: "round"
11677
+ };
11678
+ }
11679
+ return {
11680
+ color: "#3b82f6",
11681
+ width: 3,
11682
+ opacity: 1,
11683
+ cap: "round",
11684
+ join: "round"
11685
+ };
11686
+ }
11687
+ function streamlinePoints(points, factor = 0.35) {
11688
+ if (points.length < 3 || factor <= 0) return [...points];
11689
+ const clampedFactor = Math.min(Math.max(factor, 0), 0.95);
11690
+ const smoothed = [points[0]];
11691
+ for (let i = 1; i < points.length - 1; i++) {
11692
+ const prev = smoothed[i - 1];
11693
+ const curr = points[i];
11694
+ const x = prev[0] * clampedFactor + curr[0] * (1 - clampedFactor);
11695
+ const y = prev[1] * clampedFactor + curr[1] * (1 - clampedFactor);
11696
+ smoothed.push([x, y]);
11697
+ }
11698
+ smoothed.push(points[points.length - 1]);
11699
+ return smoothed;
11700
+ }
11701
+ function strokeBoundingBox(points, margin = 0) {
11702
+ if (points.length === 0) {
11703
+ return { x: 0, y: 0, width: 0, height: 0 };
11704
+ }
11705
+ let minX = Infinity;
11706
+ let minY = Infinity;
11707
+ let maxX = -Infinity;
11708
+ let maxY = -Infinity;
11709
+ for (const [x, y] of points) {
11710
+ if (x < minX) minX = x;
11711
+ if (y < minY) minY = y;
11712
+ if (x > maxX) maxX = x;
11713
+ if (y > maxY) maxY = y;
11714
+ }
11715
+ const w = Math.max(maxX - minX, 0);
11716
+ const h = Math.max(maxY - minY, 0);
11717
+ return {
11718
+ x: minX - margin,
11719
+ y: minY - margin,
11720
+ width: Math.max(w + margin * 2, 1),
11721
+ height: Math.max(h + margin * 2, 1)
11722
+ };
11723
+ }
11724
+ function strokePointsToPathData(points, options = {}) {
11725
+ if (points.length === 0) return "";
11726
+ if (points.length === 1) {
11727
+ const [x, y] = points[0];
11728
+ return `M ${x} ${y} L ${x + 0.01} ${y}`;
11729
+ }
11730
+ if (points.length === 2) {
11731
+ const [p0, p1] = points;
11732
+ return `M ${p0[0]} ${p0[1]} L ${p1[0]} ${p1[1]}`;
11733
+ }
11734
+ const fitted = fitFreehandPath(points, options);
11735
+ return fitted.length > 0 ? fitted : `M ${points[0][0]} ${points[0][1]}`;
11736
+ }
11737
+ function strokeToShapeNode(stroke, options = {}) {
11738
+ const points = stroke.points;
11739
+ const strokeWidth = stroke.style.width || 3;
11740
+ const bounds = strokeBoundingBox(points, 0);
11741
+ const localPoints = points.map(
11742
+ ([x, y]) => [x - bounds.x, y - bounds.y]
11743
+ );
11744
+ const d = strokePointsToPathData(localPoints, {
11745
+ tolerance: options.tolerance ?? 1,
11746
+ cornerAngleDeg: options.cornerAngleDeg ?? 40
11747
+ });
11748
+ const width = Math.max(bounds.width, 1);
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);
11752
+ return {
11753
+ id: options.id ?? stroke.id,
11754
+ type: "shape",
11755
+ position: { x: bounds.x, y: bounds.y },
11756
+ width,
11757
+ height,
11758
+ style: {
11759
+ width,
11760
+ height
11761
+ },
11762
+ data: {
11763
+ shape: {
11764
+ kind: "path",
11765
+ d,
11766
+ viewBox: [0, 0, width, height],
11767
+ stroke: strokePaint,
11768
+ strokeWidth,
11769
+ opacity: stroke.style.opacity,
11770
+ strokeLinecap: stroke.style.cap ?? "round",
11771
+ strokeLinejoin: stroke.style.join ?? "round",
11772
+ ...strokeGradient ? { strokeGradient } : {},
11773
+ fill: "none"
11774
+ },
11775
+ tool: stroke.tool
11776
+ }
11777
+ };
11778
+ }
11779
+ function findStrokesAtPoint(point, nodes, radius = 12) {
11780
+ const matches2 = [];
11781
+ for (const node of nodes) {
11782
+ const shape = getNodeShape(node);
11783
+ if (!shape || shape.kind !== "path") continue;
11784
+ const rect = getNodeRect(node);
11785
+ const strokeWidth = shape.strokeWidth ?? 2;
11786
+ const effectiveRadius = radius + strokeWidth / 2;
11787
+ if (point[0] < rect.x - effectiveRadius || point[0] > rect.x + rect.width + effectiveRadius || point[1] < rect.y - effectiveRadius || point[1] > rect.y + rect.height + effectiveRadius) {
11788
+ continue;
11789
+ }
11790
+ const localPoint = [point[0] - rect.x, point[1] - rect.y];
11791
+ try {
11792
+ const dist3 = distanceToBezierPath(shape.d, localPoint);
11793
+ if (dist3 <= effectiveRadius) {
11794
+ matches2.push(node);
11795
+ }
11796
+ } catch {
11797
+ if (point[0] >= rect.x && point[0] <= rect.x + rect.width && point[1] >= rect.y && point[1] <= rect.y + rect.height) {
11798
+ matches2.push(node);
11799
+ }
11800
+ }
11801
+ }
11802
+ return matches2;
11803
+ }
11804
+
11805
+ // src/canvas/drawController.ts
11806
+ function createCanvasDrawController(options) {
11807
+ let currentTool = options.tool ?? "select";
11808
+ let currentStyle = {
11809
+ ...createDefaultStrokeStyle(currentTool),
11810
+ ...options.style
11811
+ };
11812
+ let activeStroke = null;
11813
+ const erasedInDrag = /* @__PURE__ */ new Map();
11814
+ let dragStartNodes = null;
11815
+ function setTool(tool) {
11816
+ if (currentTool === tool) return;
11817
+ cancelStroke();
11818
+ currentTool = tool;
11819
+ if (tool === "pen" || tool === "highlighter") {
11820
+ currentStyle = {
11821
+ ...createDefaultStrokeStyle(tool),
11822
+ ...options.style
11823
+ };
11824
+ options.onStyleChange?.(currentStyle);
11825
+ }
11826
+ options.onToolChange?.(tool);
11827
+ }
11828
+ function setStyle(style) {
11829
+ currentStyle = { ...currentStyle, ...style };
11830
+ options.onStyleChange?.(currentStyle);
11831
+ }
11832
+ function startStroke(point) {
11833
+ if (currentTool === "select") return;
11834
+ if (currentTool === "eraser") {
11835
+ erasedInDrag.clear();
11836
+ dragStartNodes = [...options.getNodes()];
11837
+ eraseAt(point);
11838
+ return;
11839
+ }
11840
+ const id = options.generateNodeId?.() ?? `stroke-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
11841
+ activeStroke = {
11842
+ id,
11843
+ tool: currentTool,
11844
+ points: [point],
11845
+ style: { ...currentStyle },
11846
+ startTime: Date.now()
11847
+ };
11848
+ options.onActiveStrokeChange?.(activeStroke);
11849
+ }
11850
+ function continueStroke(point) {
11851
+ if (currentTool === "eraser") {
11852
+ eraseAt(point);
11853
+ return;
11854
+ }
11855
+ if (!activeStroke) return;
11856
+ const points = activeStroke.points;
11857
+ const last = points[points.length - 1];
11858
+ if (Math.hypot(point[0] - last[0], point[1] - last[1]) < 0.25) return;
11859
+ points.push(point);
11860
+ options.onActiveStrokeChange?.({ ...activeStroke, points: [...points] });
11861
+ }
11862
+ function endStroke() {
11863
+ if (currentTool === "eraser") {
11864
+ const startSnapshot = dragStartNodes;
11865
+ dragStartNodes = null;
11866
+ if (erasedInDrag.size > 0 && options.history && startSnapshot) {
11867
+ const erasedIds = new Set(Array.from(erasedInDrag.keys()));
11868
+ options.history.execute({
11869
+ name: "Erase strokes",
11870
+ execute: () => {
11871
+ const remaining = options.getNodes().filter((n) => !erasedIds.has(n.id));
11872
+ options.setNodes(remaining);
11873
+ },
11874
+ undo: () => {
11875
+ options.setNodes(startSnapshot);
11876
+ }
11877
+ });
11878
+ }
11879
+ erasedInDrag.clear();
11880
+ return null;
11881
+ }
11882
+ if (!activeStroke) return null;
11883
+ const stroke = activeStroke;
11884
+ activeStroke = null;
11885
+ options.onActiveStrokeChange?.(null);
11886
+ if (stroke.points.length === 0) return null;
11887
+ const drawOpts = options.drawOptions ?? {};
11888
+ const smoothed = streamlinePoints(stroke.points, drawOpts.streamline ?? 0.35);
11889
+ const node = strokeToShapeNode(
11890
+ { ...stroke, points: smoothed },
11891
+ {
11892
+ tolerance: drawOpts.tolerance ?? 1,
11893
+ cornerAngleDeg: drawOpts.cornerAngleDeg ?? 40
11894
+ }
11895
+ );
11896
+ if (options.history) {
11897
+ options.history.execute({
11898
+ name: `Draw ${stroke.tool}`,
11899
+ execute: () => {
11900
+ options.setNodes([...options.getNodes(), node]);
11901
+ },
11902
+ undo: () => {
11903
+ options.setNodes(options.getNodes().filter((n) => n.id !== node.id));
11904
+ }
11905
+ });
11906
+ } else {
11907
+ options.setNodes([...options.getNodes(), node]);
11908
+ }
11909
+ options.onStrokeComplete?.(node);
11910
+ return node;
11911
+ }
11912
+ function cancelStroke() {
11913
+ if (activeStroke) {
11914
+ activeStroke = null;
11915
+ options.onActiveStrokeChange?.(null);
11916
+ }
11917
+ if (erasedInDrag.size > 0) {
11918
+ const entries = Array.from(erasedInDrag.values());
11919
+ const current = [...options.getNodes()];
11920
+ const sorted = [...entries].sort((a, b) => a.index - b.index);
11921
+ for (const { node, index } of sorted) {
11922
+ const insertAt = Math.min(index, current.length);
11923
+ current.splice(insertAt, 0, node);
11924
+ }
11925
+ options.setNodes(current);
11926
+ erasedInDrag.clear();
11927
+ }
11928
+ }
11929
+ function eraseAt(point, radius = 12) {
11930
+ const nodes = options.getNodes();
11931
+ const candidates = findStrokesAtPoint(point, nodes, radius);
11932
+ const newlyErased = [];
11933
+ for (const candidate of candidates) {
11934
+ if (erasedInDrag.has(candidate.id) || isLocked(candidate)) continue;
11935
+ const index = nodes.findIndex((n) => n.id === candidate.id);
11936
+ if (index !== -1) {
11937
+ erasedInDrag.set(candidate.id, { node: candidate, index });
11938
+ newlyErased.push(candidate);
11939
+ }
11940
+ }
11941
+ if (newlyErased.length > 0) {
11942
+ const erasedSet = new Set(newlyErased.map((n) => n.id));
11943
+ const nextNodes = nodes.filter((n) => !erasedSet.has(n.id));
11944
+ options.setNodes(nextNodes);
11945
+ options.onNodesErase?.(newlyErased);
11946
+ }
11947
+ return newlyErased;
11948
+ }
11949
+ return {
11950
+ get tool() {
11951
+ return currentTool;
11952
+ },
11953
+ setTool,
11954
+ get style() {
11955
+ return currentStyle;
11956
+ },
11957
+ setStyle,
11958
+ get activeStroke() {
11959
+ return activeStroke;
11960
+ },
11961
+ get isDrawing() {
11962
+ return activeStroke !== null;
11963
+ },
11964
+ startStroke,
11965
+ continueStroke,
11966
+ endStroke,
11967
+ cancelStroke,
11968
+ eraseAt
11969
+ };
11970
+ }
11400
11971
  export {
11401
11972
  ANIMATABLE_PROPERTIES,
11402
11973
  ANIMATION_DATA_KEY,
@@ -11407,6 +11978,7 @@ export {
11407
11978
  AnimationEditError,
11408
11979
  AnimationPlayer,
11409
11980
  CANVAS_COMPONENT_DRAG_TYPE,
11981
+ CANVAS_GRADIENT_PRESETS,
11410
11982
  CANVAS_TOKEN_PROPERTIES,
11411
11983
  CHILD_LAYOUT_DATA_KEY,
11412
11984
  COMPONENT_DATA_KEY,
@@ -11439,6 +12011,7 @@ export {
11439
12011
  addCanvasVariant,
11440
12012
  addKeyframesAtTime,
11441
12013
  alignNodes,
12014
+ angleToSvgLinearCoords,
11442
12015
  animateNode,
11443
12016
  applyAnimation,
11444
12017
  applyAnimationOverrides,
@@ -11485,8 +12058,10 @@ export {
11485
12058
  createCanvasAutoLayoutController,
11486
12059
  createCanvasComponent,
11487
12060
  createCanvasComponentsController,
12061
+ createCanvasDrawController,
11488
12062
  createCanvasInstance,
11489
12063
  createCanvasScenesStore,
12064
+ createDefaultStrokeStyle,
11490
12065
  createFanOutTrajectory,
11491
12066
  createGuideIndex,
11492
12067
  createNodeGuideIndex,
@@ -11507,6 +12082,7 @@ export {
11507
12082
  easingFromInfluence,
11508
12083
  easingLabel,
11509
12084
  easingToCss,
12085
+ elasticAt,
11510
12086
  ensureLayout,
11511
12087
  estimateCanvasHeight,
11512
12088
  estimateNodeSize,
@@ -11516,6 +12092,7 @@ export {
11516
12092
  excludePaths,
11517
12093
  exportCssAnimation,
11518
12094
  findConnectorSnap,
12095
+ findStrokesAtPoint,
11519
12096
  findVariant,
11520
12097
  fitCubicRun,
11521
12098
  fitCubics,
@@ -11582,6 +12159,7 @@ export {
11582
12159
  getVariantOptions,
11583
12160
  getVariantSet,
11584
12161
  getVariantValues,
12162
+ gradientToSvgDef,
11585
12163
  handlePoint,
11586
12164
  hasVisibleStroke,
11587
12165
  inferResizeHandle,
@@ -11596,6 +12174,7 @@ export {
11596
12174
  isAutoLayoutFrame,
11597
12175
  isCanvasComponent,
11598
12176
  isCanvasComponentSet,
12177
+ isCanvasGradient,
11599
12178
  isCanvasInstance,
11600
12179
  isCssExportExact,
11601
12180
  isEventFromTextInput,
@@ -11647,6 +12226,7 @@ export {
11647
12226
  parseCanvasClipboardData,
11648
12227
  parseCanvasScenes,
11649
12228
  parseCssEasing,
12229
+ parseCssGradient,
11650
12230
  parseKeyframeClipboard,
11651
12231
  parseNodeStyleDeclarations,
11652
12232
  parseOklab,
@@ -11682,6 +12262,7 @@ export {
11682
12262
  repositionNodesTo,
11683
12263
  resetCanvasInstanceOverrides,
11684
12264
  resizeHandleAxes,
12265
+ resolveCanvasGradient,
11685
12266
  resolveCanvasTokens,
11686
12267
  resolveEasing,
11687
12268
  resolvePadding,
@@ -11724,7 +12305,11 @@ export {
11724
12305
  snapToGrid,
11725
12306
  splitSegment,
11726
12307
  springAt,
12308
+ streamlinePoints,
12309
+ strokeBoundingBox,
11727
12310
  strokeOutlineRings,
12311
+ strokePointsToPathData,
12312
+ strokeToShapeNode,
11728
12313
  subSegment,
11729
12314
  subtractPaths,
11730
12315
  subtractPolygons,