@pixerate/editor-svelte 0.3.12 → 0.5.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.
@@ -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
+ }