@pixerate/editor-svelte 0.4.0 → 0.5.1

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.
@@ -0,0 +1,188 @@
1
+ import { cubicOut } from 'svelte/easing';
2
+ import type { XYPosition } from '@xyflow/svelte';
3
+ import type {
4
+ CanvasNode,
5
+ FanOutTrajectoryOptions,
6
+ NodeTransition,
7
+ TrajectoryFunction,
8
+ TrajectoryPoint
9
+ } from './types.js';
10
+
11
+ /**
12
+ * Standard linear trajectory: straight line interpolation with linear scale & opacity progression.
13
+ */
14
+ export const linearTrajectory: TrajectoryFunction = (
15
+ origin: XYPosition,
16
+ target: XYPosition,
17
+ progress: number
18
+ ): TrajectoryPoint => {
19
+ return {
20
+ position: {
21
+ x: origin.x + (target.x - origin.x) * progress,
22
+ y: origin.y + (target.y - origin.y) * progress
23
+ },
24
+ scale: progress,
25
+ opacity: Math.min(1, progress * 1.5)
26
+ };
27
+ };
28
+
29
+ /**
30
+ * Creates a fan-out trajectory where nodes arc outward from origin to target.
31
+ * Matches the curved distribution shown in multi-node branching diagrams.
32
+ */
33
+ export function createFanOutTrajectory(options: FanOutTrajectoryOptions = {}): TrajectoryFunction {
34
+ const {
35
+ curvature = 0.4,
36
+ startScale = 0,
37
+ endScale = 1,
38
+ startOpacity = 0,
39
+ endOpacity = 1
40
+ } = options;
41
+
42
+ return (
43
+ origin: XYPosition,
44
+ target: XYPosition,
45
+ progress: number,
46
+ index: number,
47
+ total: number
48
+ ): TrajectoryPoint => {
49
+ const dx = target.x - origin.x;
50
+ const dy = target.y - origin.y;
51
+
52
+ // Normalized index from -1 (top) to +1 (bottom)
53
+ const normIndex = total > 1 ? (index - (total - 1) / 2) / ((total - 1) / 2) : 0;
54
+
55
+ // Perpendicular control point offset to create smooth fan arc
56
+ const midX = origin.x + dx * 0.4;
57
+ const midY = origin.y + dy * 0.1 - normIndex * Math.abs(dx) * 0.15 * curvature;
58
+
59
+ // Quadratic Bezier interpolation: B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
60
+ const t = progress;
61
+ const oneMinusT = 1 - t;
62
+
63
+ const x = oneMinusT * oneMinusT * origin.x + 2 * oneMinusT * t * midX + t * t * target.x;
64
+ const y = oneMinusT * oneMinusT * origin.y + 2 * oneMinusT * t * midY + t * t * target.y;
65
+
66
+ const scale = startScale + (endScale - startScale) * progress;
67
+ const opacity = startOpacity + (endOpacity - startOpacity) * progress;
68
+
69
+ return {
70
+ position: { x, y },
71
+ scale,
72
+ opacity
73
+ };
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Creates a custom quadratic Bezier trajectory with a specified or dynamically calculated control point.
79
+ */
80
+ export function createBezierTrajectory(
81
+ getControlPoint: (origin: XYPosition, target: XYPosition) => XYPosition
82
+ ): TrajectoryFunction {
83
+ return (origin: XYPosition, target: XYPosition, progress: number): TrajectoryPoint => {
84
+ const cp = getControlPoint(origin, target);
85
+ const t = progress;
86
+ const oneMinusT = 1 - t;
87
+
88
+ const x = oneMinusT * oneMinusT * origin.x + 2 * oneMinusT * t * cp.x + t * t * target.x;
89
+ const y = oneMinusT * oneMinusT * origin.y + 2 * oneMinusT * t * cp.y + t * t * target.y;
90
+
91
+ return {
92
+ position: { x, y },
93
+ scale: progress,
94
+ opacity: progress
95
+ };
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Coordinates and animates multiple node transitions simultaneously with zero per-frame array allocations.
101
+ * Directly mutates node positions and data (scale, opacity) in-place during the animation loop.
102
+ */
103
+ export function runMultiNodeTransition<TNode extends CanvasNode = CanvasNode>(
104
+ transitions: NodeTransition<TNode>[],
105
+ options: {
106
+ defaultDuration?: number;
107
+ defaultEasing?: (t: number) => number;
108
+ onUpdate?: () => void;
109
+ onComplete?: () => void;
110
+ } = {}
111
+ ): () => void {
112
+ if (transitions.length === 0) {
113
+ options.onComplete?.();
114
+ return () => {};
115
+ }
116
+
117
+ const { defaultDuration = 400, defaultEasing = cubicOut, onUpdate, onComplete } = options;
118
+
119
+ let isCancelled = false;
120
+ const startTime = Date.now();
121
+
122
+ const items = transitions.map((tr, i) => ({
123
+ ...tr,
124
+ index: tr.index ?? i,
125
+ total: tr.total ?? transitions.length,
126
+ duration: tr.duration ?? defaultDuration,
127
+ easing: tr.easing ?? defaultEasing,
128
+ delay: tr.delay ?? 0,
129
+ trajectory: tr.trajectory ?? linearTrajectory
130
+ }));
131
+
132
+ function frame() {
133
+ if (isCancelled) return;
134
+
135
+ const now = Date.now();
136
+ let allCompleted = true;
137
+
138
+ for (let i = 0; i < items.length; i++) {
139
+ const item = items[i];
140
+ const elapsed = now - (startTime + item.delay);
141
+
142
+ if (elapsed < 0) {
143
+ allCompleted = false;
144
+ continue;
145
+ }
146
+
147
+ const rawProgress = item.duration <= 0 ? 1 : Math.min(elapsed / item.duration, 1);
148
+ const progress = item.duration <= 0 ? 1 : item.easing(rawProgress);
149
+
150
+ const point = item.trajectory(item.from, item.to, progress, item.index, item.total);
151
+
152
+ // Direct in-place mutation avoids array reconstruction during RAF
153
+ item.node.position = { ...point.position };
154
+
155
+ if (point.scale !== undefined || point.opacity !== undefined) {
156
+ if (!item.node.data) {
157
+ item.node.data = {} as any;
158
+ }
159
+ if (point.scale !== undefined) {
160
+ (item.node.data as any).scale = point.scale;
161
+ }
162
+ if (point.opacity !== undefined) {
163
+ (item.node.data as any).opacity = point.opacity;
164
+ }
165
+ }
166
+
167
+ if (rawProgress < 1) {
168
+ allCompleted = false;
169
+ }
170
+ }
171
+
172
+ onUpdate?.();
173
+
174
+ if (!allCompleted) {
175
+ if (typeof requestAnimationFrame !== 'undefined') {
176
+ requestAnimationFrame(frame);
177
+ }
178
+ } else {
179
+ onComplete?.();
180
+ }
181
+ }
182
+
183
+ frame();
184
+
185
+ return () => {
186
+ isCancelled = true;
187
+ };
188
+ }
@@ -95,6 +95,123 @@ export interface CanvasInteractionPresetConfig {
95
95
  maxZoom?: number;
96
96
  }
97
97
 
98
+ /**
99
+ * Parametric point returned by a trajectory evaluation.
100
+ */
101
+ export interface TrajectoryPoint {
102
+ position: XYPosition;
103
+ scale?: number;
104
+ opacity?: number;
105
+ }
106
+
107
+ /**
108
+ * Trajectory evaluation function determining path, scale, and opacity over time t in [0, 1].
109
+ */
110
+ export type TrajectoryFunction = (
111
+ origin: XYPosition,
112
+ target: XYPosition,
113
+ progress: number,
114
+ index: number,
115
+ total: number
116
+ ) => TrajectoryPoint;
117
+
118
+ /**
119
+ * Options for fan-out trajectory generator.
120
+ */
121
+ export interface FanOutTrajectoryOptions {
122
+ curvature?: number;
123
+ spreadAngle?: number;
124
+ startScale?: number;
125
+ endScale?: number;
126
+ startOpacity?: number;
127
+ endOpacity?: number;
128
+ }
129
+
130
+ /**
131
+ * Single node animation transition definition.
132
+ */
133
+ export interface NodeTransition<TNode extends CanvasNode = CanvasNode> {
134
+ node: TNode;
135
+ from: XYPosition;
136
+ to: XYPosition;
137
+ trajectory?: TrajectoryFunction;
138
+ delay?: number;
139
+ duration?: number;
140
+ easing?: (t: number) => number;
141
+ index?: number;
142
+ total?: number;
143
+ }
144
+
145
+ /**
146
+ * Displacement axis / direction.
147
+ */
148
+ export type DisplacementDirection = 'right' | 'left' | 'down' | 'up' | 'vertical';
149
+
150
+ /**
151
+ * Options for directional displacement calculation.
152
+ */
153
+ export interface DirectionalDisplacementOptions {
154
+ direction?: DisplacementDirection;
155
+ gap?: number;
156
+ spreadSiblingLanes?: boolean;
157
+ siblingTolerance?: number;
158
+ minY?: number;
159
+ }
160
+
161
+ /**
162
+ * Options for Dagre reflow displacement calculation.
163
+ */
164
+ export interface ReflowDisplacementOptions extends CanvasLayoutOptions {
165
+ anchorNodeId: string;
166
+ }
167
+
168
+ /**
169
+ * Snapshot of canvas graph state prior to node explosion, enabling exact reversibility.
170
+ */
171
+ export interface ExplosionSnapshot<
172
+ TNode extends CanvasNode = CanvasNode,
173
+ TEdge extends CanvasEdge = CanvasEdge
174
+ > {
175
+ parentId: string;
176
+ childNodeIds: string[];
177
+ childEdgeIds: string[];
178
+ displacedNodes: { id: string; originalPosition: XYPosition }[];
179
+ timestamp: number;
180
+ }
181
+
182
+ /**
183
+ * Options for exploding a node.
184
+ */
185
+ export interface ExplodeNodeOptions<
186
+ TNode extends CanvasNode = CanvasNode,
187
+ TEdge extends CanvasEdge = CanvasEdge
188
+ > {
189
+ childNodes: TNode[];
190
+ childEdges?: TEdge[];
191
+ connectParentToChildren?: boolean | 'first' | 'all';
192
+ connectChildrenToDownstream?: boolean;
193
+ displacementStrategy?: 'directional' | 'dagre' | 'none';
194
+ directionalOptions?: DirectionalDisplacementOptions;
195
+ reflowOptions?: Partial<ReflowDisplacementOptions>;
196
+ trajectory?: TrajectoryFunction;
197
+ duration?: number;
198
+ staggerDelay?: number;
199
+ easing?: (t: number) => number;
200
+ onComplete?: () => void;
201
+ }
202
+
203
+ /**
204
+ * Options for collapsing an exploded node.
205
+ */
206
+ export interface CollapseNodeOptions {
207
+ duration?: number;
208
+ staggerDelay?: number;
209
+ easing?: (t: number) => number;
210
+ trajectory?: TrajectoryFunction;
211
+ onComplete?: () => void;
212
+ }
213
+
214
+
98
215
  /**
99
216
  * Pure calculation of graph differences between a current state and a baseline state.
100
217
  */
@@ -343,3 +460,114 @@ export declare const miroCompatiblePreset: CanvasInteractionPresetConfig;
343
460
  * Helper to ensure panels restore pointer events after selection rectangle releases.
344
461
  */
345
462
  export declare function restorePanelPointerEvents(panelSelector?: string): void;
463
+
464
+ /**
465
+ * Standard linear trajectory: straight line interpolation with linear scale & opacity progression.
466
+ */
467
+ export declare const linearTrajectory: TrajectoryFunction;
468
+
469
+ /**
470
+ * Creates a fan-out trajectory where nodes arc outward from origin to target.
471
+ */
472
+ export declare function createFanOutTrajectory(options?: FanOutTrajectoryOptions): TrajectoryFunction;
473
+
474
+ /**
475
+ * Creates a custom quadratic Bezier trajectory with a specified or dynamically calculated control point.
476
+ */
477
+ export declare function createBezierTrajectory(
478
+ getControlPoint: (origin: XYPosition, target: XYPosition) => XYPosition
479
+ ): TrajectoryFunction;
480
+
481
+ /**
482
+ * Coordinates and animates multiple node transitions simultaneously with zero per-frame array allocations.
483
+ */
484
+ export declare function runMultiNodeTransition<TNode extends CanvasNode = CanvasNode>(
485
+ transitions: NodeTransition<TNode>[],
486
+ options?: {
487
+ defaultDuration?: number;
488
+ defaultEasing?: (t: number) => number;
489
+ onUpdate?: () => void;
490
+ onComplete?: () => void;
491
+ }
492
+ ): () => void;
493
+
494
+ /**
495
+ * Calculates a bounding rectangle for a set of nodes.
496
+ */
497
+ export declare function computeNodesBoundingBox<TNode extends CanvasNode = CanvasNode>(
498
+ nodes: TNode[],
499
+ defaultWidth?: number,
500
+ defaultHeight?: number
501
+ ): Rect;
502
+
503
+ /**
504
+ * Pure directional displacement calculation.
505
+ */
506
+ export declare function calculateDirectionalDisplacement<TNode extends CanvasNode = CanvasNode>(params: {
507
+ existingNodes: TNode[];
508
+ originNode: TNode;
509
+ childNodes: TNode[];
510
+ getNodesBounds?: (nodes: TNode[]) => Rect;
511
+ options?: DirectionalDisplacementOptions;
512
+ }): Map<string, XYPosition>;
513
+
514
+ /**
515
+ * Pure hierarchical Dagre reflow displacement calculation.
516
+ */
517
+ export declare function calculateReflowDisplacement<
518
+ TNode extends CanvasNode = CanvasNode,
519
+ TEdge extends CanvasEdge = CanvasEdge
520
+ >(params: {
521
+ existingNodes: TNode[];
522
+ existingEdges: TEdge[];
523
+ originNode: TNode;
524
+ childNodes: TNode[];
525
+ childEdges?: TEdge[];
526
+ getNodesBounds?: (nodes: TNode[]) => Rect;
527
+ options?: CanvasLayoutOptions;
528
+ }): Map<string, XYPosition>;
529
+
530
+ export interface CanvasGraphTarget<
531
+ TNode extends CanvasNode = CanvasNode,
532
+ TEdge extends CanvasEdge = CanvasEdge
533
+ > {
534
+ nodes: TNode[];
535
+ edges: TEdge[];
536
+ setNodes: (nodes: TNode[], triggerCallback?: boolean) => void;
537
+ setEdges: (edges: TEdge[], triggerCallback?: boolean) => void;
538
+ addNodeOrNodes: (nodes: TNode | TNode[], animateFrom?: XYPosition, save?: boolean) => void;
539
+ addEdges: (edges: TEdge[], save?: boolean) => void;
540
+ removeNodes: (nodes: TNode[], save?: boolean) => void;
541
+ removeEdges: (edges: TEdge[], save?: boolean) => void;
542
+ createEdge?: (sourceId: string, targetId: string, options?: any) => TEdge;
543
+ onSave?: () => void;
544
+ }
545
+
546
+ export interface CreateCanvasExplosionOptions<TNode extends CanvasNode = CanvasNode> {
547
+ getNodesBounds?: (nodes: TNode[]) => Rect;
548
+ onSave?: () => void;
549
+ }
550
+
551
+ /**
552
+ * Headless Svelte 5 rune managing canvas node explosion, spatial layout displacement,
553
+ * coordinated multi-node animation paths, and reversible collapsing.
554
+ */
555
+ export declare function createCanvasExplosion<
556
+ TNode extends CanvasNode = CanvasNode,
557
+ TEdge extends CanvasEdge = CanvasEdge
558
+ >(
559
+ graph: CanvasGraphTarget<TNode, TEdge>,
560
+ options?: CreateCanvasExplosionOptions<TNode>
561
+ ): {
562
+ readonly explodedNodeIds: string[];
563
+ isExploded(nodeId: string): boolean;
564
+ explodeNode(parentId: string, explodeOptions: ExplodeNodeOptions<TNode, TEdge>): boolean;
565
+ collapseNode(parentId: string, collapseOptions?: CollapseNodeOptions): boolean;
566
+ toggleExplode(
567
+ parentId: string,
568
+ explodeOptions: ExplodeNodeOptions<TNode, TEdge>,
569
+ collapseOptions?: CollapseNodeOptions
570
+ ): boolean;
571
+ getSnapshot(parentId: string): ExplosionSnapshot<TNode, TEdge> | undefined;
572
+ };
573
+
@@ -44,3 +44,25 @@ export {
44
44
  restorePanelPointerEvents,
45
45
  type CreateCanvasInteractionsOptions
46
46
  } from './createCanvasInteractions.svelte.js';
47
+
48
+ // Trajectory calculation & multi-node transitions
49
+ export {
50
+ linearTrajectory,
51
+ createFanOutTrajectory,
52
+ createBezierTrajectory,
53
+ runMultiNodeTransition
54
+ } from './createCanvasTrajectory.js';
55
+
56
+ // Spatial layout displacement
57
+ export {
58
+ computeNodesBoundingBox,
59
+ calculateDirectionalDisplacement,
60
+ calculateReflowDisplacement
61
+ } from './createCanvasDisplacement.js';
62
+
63
+ // Node explosion & collapse rune
64
+ export {
65
+ createCanvasExplosion,
66
+ type CanvasGraphTarget,
67
+ type CreateCanvasExplosionOptions
68
+ } from './createCanvasExplosion.svelte.js';
@@ -94,3 +94,120 @@ export interface CanvasInteractionPresetConfig {
94
94
  minZoom?: number;
95
95
  maxZoom?: number;
96
96
  }
97
+
98
+ /**
99
+ * Parametric point returned by a trajectory evaluation.
100
+ */
101
+ export interface TrajectoryPoint {
102
+ position: XYPosition;
103
+ scale?: number;
104
+ opacity?: number;
105
+ }
106
+
107
+ /**
108
+ * Trajectory evaluation function determining path, scale, and opacity over time t in [0, 1].
109
+ */
110
+ export type TrajectoryFunction = (
111
+ origin: XYPosition,
112
+ target: XYPosition,
113
+ progress: number,
114
+ index: number,
115
+ total: number
116
+ ) => TrajectoryPoint;
117
+
118
+ /**
119
+ * Options for fan-out trajectory generator.
120
+ */
121
+ export interface FanOutTrajectoryOptions {
122
+ curvature?: number;
123
+ spreadAngle?: number;
124
+ startScale?: number;
125
+ endScale?: number;
126
+ startOpacity?: number;
127
+ endOpacity?: number;
128
+ }
129
+
130
+ /**
131
+ * Single node animation transition definition.
132
+ */
133
+ export interface NodeTransition<TNode extends CanvasNode = CanvasNode> {
134
+ node: TNode;
135
+ from: XYPosition;
136
+ to: XYPosition;
137
+ trajectory?: TrajectoryFunction;
138
+ delay?: number;
139
+ duration?: number;
140
+ easing?: (t: number) => number;
141
+ index?: number;
142
+ total?: number;
143
+ }
144
+
145
+ /**
146
+ * Displacement axis / direction.
147
+ */
148
+ export type DisplacementDirection = 'right' | 'left' | 'down' | 'up' | 'vertical';
149
+
150
+ /**
151
+ * Options for directional displacement calculation.
152
+ */
153
+ export interface DirectionalDisplacementOptions {
154
+ direction?: DisplacementDirection;
155
+ gap?: number;
156
+ spreadSiblingLanes?: boolean;
157
+ siblingTolerance?: number;
158
+ minY?: number;
159
+ }
160
+
161
+ /**
162
+ * Options for Dagre reflow displacement calculation.
163
+ */
164
+ export interface ReflowDisplacementOptions extends CanvasLayoutOptions {
165
+ anchorNodeId: string;
166
+ }
167
+
168
+ /**
169
+ * Snapshot of canvas graph state prior to node explosion, enabling exact reversibility.
170
+ */
171
+ export interface ExplosionSnapshot<
172
+ TNode extends CanvasNode = CanvasNode,
173
+ TEdge extends CanvasEdge = CanvasEdge
174
+ > {
175
+ parentId: string;
176
+ childNodeIds: string[];
177
+ childEdgeIds: string[];
178
+ displacedNodes: { id: string; originalPosition: XYPosition }[];
179
+ timestamp: number;
180
+ }
181
+
182
+ /**
183
+ * Options for exploding a node.
184
+ */
185
+ export interface ExplodeNodeOptions<
186
+ TNode extends CanvasNode = CanvasNode,
187
+ TEdge extends CanvasEdge = CanvasEdge
188
+ > {
189
+ childNodes: TNode[];
190
+ childEdges?: TEdge[];
191
+ connectParentToChildren?: boolean | 'first' | 'all';
192
+ connectChildrenToDownstream?: boolean;
193
+ displacementStrategy?: 'directional' | 'dagre' | 'none';
194
+ directionalOptions?: DirectionalDisplacementOptions;
195
+ reflowOptions?: Partial<ReflowDisplacementOptions>;
196
+ trajectory?: TrajectoryFunction;
197
+ duration?: number;
198
+ staggerDelay?: number;
199
+ easing?: (t: number) => number;
200
+ onComplete?: () => void;
201
+ }
202
+
203
+ /**
204
+ * Options for collapsing an exploded node.
205
+ */
206
+ export interface CollapseNodeOptions {
207
+ duration?: number;
208
+ staggerDelay?: number;
209
+ easing?: (t: number) => number;
210
+ trajectory?: TrajectoryFunction;
211
+ onComplete?: () => void;
212
+ }
213
+