How I built a 'Magic Move' animation engine for Excalidraw from scratch published

Published: (January 12, 2026 at 08:11 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

The Goal

I love Excalidraw for sketching system architectures. But sketches are static. When I want to show how a packet moves through a load balancer, or how a database shard splits, I have to wave my hands frantically or create 10 different slides.

I wanted the ability to “Sketch Logic, Export Motion.”

I didn’t want a timeline editor (like After Effects). That’s too much work for a simple diagram.

I wanted “Keyless Animation”:

  • Draw Frame 1 (the start state).
  • Clone it to Frame 2.
  • Move elements to their new positions.
  • The engine automatically figures out the transition.

I built this engine using Next.js, Excalidraw, and Framer Motion. Below is a technical deep dive into how I implemented the logic.

1. The Core Logic: Diffing States

The hardest part isn’t the animation loop; it’s the diffing. When we move from Frame A to Frame B, we identify elements by their stable IDs and categorize them into one of three buckets:

  • Stable: The element exists in both frames (needs to morph/move).
  • Entering: Exists in B but not A (needs to fade in).
  • Exiting: Exists in A but not B (needs to fade out).

I wrote a categorizeTransition utility that maps elements efficiently:

// Simplified logic from src/utils/editor/transition-logic.ts
export function categorizeTransition(prevElements, currElements) {
    const stable = [];
    const morphed = [];
    const entering = [];
    const exiting = [];

    const prevMap = new Map(prevElements.map(e => [e.id, e]));
    const currMap = new Map(currElements.map(e => [e.id, e]));

    // 1. Find Morphs (Stable) & Entering
    currElements.forEach(curr => {
        if (prevMap.has(curr.id)) {
            const prev = prevMap.get(curr.id);
            // Separate "Stable" (identical) from "Morphed" (changed)
            // to optimize the render loop
            if (areVisuallyIdentical(prev, curr)) {
                stable.push({ key: curr.id, element: curr });
            } else {
                morphed.push({ key: curr.id, start: prev, end: curr });
            }
        } else {
            entering.push({ key: curr.id, end: curr });
        }
    });

    // 2. Find Exiting
    prevElements.forEach(prev => {
        if (!currMap.has(prev.id)) {
            exiting.push({ key: prev.id, start: prev });
        }
    });

    return { stable, morphed, entering, exiting };
}

2. Interpolating Properties

For the Morphed elements we need to calculate the intermediate state at any given progress (0.0 → 1.0).

  • Numbers (x, y, width): Linear interpolation works fine.
  • Colors (strokeColor): Convert Hex to RGBA, interpolate each channel, then convert back.
  • Angles: Use “shortest path” interpolation.

If an object rotates from 10° to 350°, linear interpolation would go the long way around. The following helper finds the shortest direction:

// src/utils/smart-animation.ts
const angleProgress = (oldAngle, newAngle, progress) => {
    let diff = newAngle - oldAngle;

    // Normalize to -π to +π to find shortest direction
    while (diff > Math.PI) diff -= 2 * Math.PI;
    while (diff  {
    return 1 - Math.pow(1 - t, 4);
};

This ensures that big architecture blocks “thud” into place with weight, rather than sliding around like ghosts.

What’s Next?

I’m currently working on:

  • Sub‑step animations: Allowing you to click through bullet points within a single frame.
  • Export to MP4: Recording the canvas stream directly to a video file.

The project is live, and I built it to help developers communicate better.

Try it here:
Free Stripe Promotion Code: postara

Let me know what you think of the approach!

Back to Blog

Related posts

Read more »

Hello, Newbie Here.

Hi! I'm falling back into the realm of S.T.E.M. I enjoy learning about energy systems, science, technology, engineering, and math as well. One of the projects I...