Skip to content

Commit 4682075

Browse files
MilanMilan
authored andcommitted
Improve fly-in animation: start from cursor position, faster timing, fix flicker
1 parent a7a4549 commit 4682075

4 files changed

Lines changed: 85 additions & 31 deletions

File tree

src/lib/animation/flyInAnimation.ts

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// ============================================================================
1717

1818
const CONFIG = {
19-
duration: 500, // Animation duration (ms) - matches assembly animation
19+
duration: 300, // Animation duration (ms)
2020
flyDistanceMargin: 100, // Extra margin beyond viewport edge (px in flow coords)
2121
domReadyDelay: 50 // Wait for DOM to render after node creation (ms)
2222
};
@@ -38,48 +38,85 @@ export interface ViewportInfo {
3838
// ============================================================================
3939

4040
/**
41-
* Animate a single node flying in from the left edge of the viewport
41+
* Animate a single node flying in from the cursor position
4242
*
4343
* @param nodeId - The ID of the node to animate
4444
* @param targetPosition - The node's final position in flow coordinates
4545
* @param getViewport - Function to get current viewport info
46+
* @param cursorScreen - Cursor position in screen coordinates (optional)
47+
* @param screenToFlow - Function to convert screen to flow coordinates (optional)
4648
*/
4749
export function runFlyInAnimation(
4850
nodeId: string,
4951
targetPosition: { x: number; y: number },
50-
getViewport: () => ViewportInfo
52+
getViewport: () => ViewportInfo,
53+
cursorScreen?: { x: number; y: number } | null,
54+
screenToFlow?: (pos: { x: number; y: number }) => { x: number; y: number }
5155
): void {
52-
// Wait for DOM element to exist
53-
setTimeout(() => {
54-
const nodeEl = document.querySelector(`[data-id="${nodeId}"]`) as HTMLElement;
55-
if (!nodeEl) return;
56+
let flyFromX: number;
57+
let flyFromY: number;
5658

59+
if (cursorScreen && screenToFlow) {
60+
// Convert cursor screen position to flow coordinates
61+
const cursorFlow = screenToFlow(cursorScreen);
62+
// Calculate offset from target position (fly-from is relative to final position)
63+
flyFromX = cursorFlow.x - targetPosition.x;
64+
flyFromY = cursorFlow.y - targetPosition.y;
65+
} else {
66+
// Fallback: fly from left edge of viewport
5767
const viewport = getViewport();
58-
59-
// Calculate left edge of viewport in flow coordinates
6068
const leftEdgeX = -viewport.x / viewport.zoom;
69+
flyFromX = leftEdgeX - CONFIG.flyDistanceMargin - targetPosition.x;
70+
flyFromY = 0;
71+
}
72+
73+
// Poll for DOM element (typically appears within 1-2 frames)
74+
let attempts = 0;
75+
const maxAttempts = 20;
76+
77+
function tryAnimate() {
78+
const nodeEl = document.querySelector(`[data-id="${nodeId}"]`) as HTMLElement;
6179

62-
// Fly from left edge (with margin) to target position
63-
// The fly-from value is relative to the node's final position
64-
const flyFromX = leftEdgeX - CONFIG.flyDistanceMargin - targetPosition.x;
65-
const flyFromY = 0; // Keep vertical position (fly horizontally)
80+
if (!nodeEl) {
81+
attempts++;
82+
if (attempts < maxAttempts) {
83+
requestAnimationFrame(tryAnimate);
84+
}
85+
return;
86+
}
87+
88+
// Immediately set initial position to prevent flicker
89+
// This positions the node at the start of the animation before CSS takes over
90+
nodeEl.style.translate = `${flyFromX}px ${flyFromY}px`;
91+
nodeEl.style.scale = '0.8';
92+
nodeEl.style.opacity = '0';
6693

6794
// Set CSS variables for the animation
6895
nodeEl.style.setProperty('--fly-from-x', `${flyFromX}px`);
6996
nodeEl.style.setProperty('--fly-from-y', `${flyFromY}px`);
7097
nodeEl.style.setProperty('--assembly-duration', `${CONFIG.duration}ms`);
7198
nodeEl.style.setProperty('--assembly-delay', '0ms');
7299

73-
// Add the assembling class to trigger the CSS animation
74-
nodeEl.classList.add('assembling');
75-
76-
// Cleanup after animation completes
77-
setTimeout(() => {
78-
nodeEl.classList.remove('assembling');
79-
nodeEl.style.removeProperty('--fly-from-x');
80-
nodeEl.style.removeProperty('--fly-from-y');
81-
nodeEl.style.removeProperty('--assembly-duration');
82-
nodeEl.style.removeProperty('--assembly-delay');
83-
}, CONFIG.duration + 50);
84-
}, CONFIG.domReadyDelay);
100+
// Start animation in next frame (after initial styles are applied)
101+
requestAnimationFrame(() => {
102+
// Remove inline styles and let animation take over
103+
nodeEl.style.removeProperty('translate');
104+
nodeEl.style.removeProperty('scale');
105+
nodeEl.style.removeProperty('opacity');
106+
107+
// Add the assembling class to trigger the CSS animation
108+
nodeEl.classList.add('assembling');
109+
110+
// Cleanup after animation completes
111+
setTimeout(() => {
112+
nodeEl.classList.remove('assembling');
113+
nodeEl.style.removeProperty('--fly-from-x');
114+
nodeEl.style.removeProperty('--fly-from-y');
115+
nodeEl.style.removeProperty('--assembly-duration');
116+
nodeEl.style.removeProperty('--assembly-delay');
117+
}, CONFIG.duration + 50);
118+
});
119+
}
120+
121+
requestAnimationFrame(tryAnimate);
85122
}

src/lib/components/FlowUpdater.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,9 @@
316316
width: canvas?.clientWidth ?? window.innerWidth,
317317
height: canvas?.clientHeight ?? window.innerHeight
318318
};
319-
}
319+
},
320+
value.cursorScreen,
321+
screenToFlowPosition
320322
);
321323
}
322324
});

src/lib/stores/viewTriggers.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,27 @@ export function triggerEditAnnotation(annotationId: string): void {
9494
}
9595

9696
// Fly-in animation trigger - triggers fly-in animation for a newly placed node
97-
export const flyInAnimationTrigger = writable<{ nodeId: string; position: { x: number; y: number }; id: number }>({
97+
export const flyInAnimationTrigger = writable<{
98+
nodeId: string;
99+
position: { x: number; y: number };
100+
cursorScreen: { x: number; y: number } | null;
101+
id: number
102+
}>({
98103
nodeId: '',
99104
position: { x: 0, y: 0 },
105+
cursorScreen: null,
100106
id: 0
101107
});
102108

103-
export function triggerFlyInAnimation(nodeId: string, position: { x: number; y: number }): void {
104-
flyInAnimationTrigger.update((current) => ({ nodeId, position, id: current.id + 1 }));
109+
export function triggerFlyInAnimation(
110+
nodeId: string,
111+
position: { x: number; y: number },
112+
cursorScreen?: { x: number; y: number }
113+
): void {
114+
flyInAnimationTrigger.update((current) => ({
115+
nodeId,
116+
position,
117+
cursorScreen: cursorScreen ?? null,
118+
id: current.id + 1
119+
}));
105120
}

src/routes/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -935,9 +935,9 @@
935935
// Subsystem creation auto-creates Interface block inside
936936
const newNode = historyStore.mutate(() => graphStore.addNode(type, position));
937937
938-
// Trigger fly-in animation for the new node
938+
// Trigger fly-in animation for the new node (from cursor position)
939939
if (newNode) {
940-
triggerFlyInAnimation(newNode.id, position);
940+
triggerFlyInAnimation(newNode.id, position, mousePosition);
941941
}
942942
}
943943
</script>

0 commit comments

Comments
 (0)