How I Built an Engine That Turns Architecture Sketches Into Animations

Written by behruamm | Published 2026/01/22
Tech Story Tags: animation | javascript | reac | nextjs | opensource | excalidraw | web-development | magic-move

TLDRStatic architecture diagrams are hard to explain. I built a tool for "Keyless Animation" that automatically calculates transitions between Excalidraw frames. This post breaks down the technical logic behind state diffing, shortest-path rotation, and physics-based easing for fluid motion.via the TL;DR App

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".

The Goal

I didn't want a timeline editor (like After Effects). That's too much work for a simple diagram.
I wanted"Keyless Animation":

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

I built this engine using Next.jsExcalidraw, and Framer Motion. Here 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:

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

I wrote a categorizeTransitionutility 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);
            // We 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 to 1.0).

You can't just use simple linear interpolation for everything.

  • Numbers (x, y, width): Linear works fine.
  • Colors (strokeColor): You must convert Hex to RGBA, interpolate each channel, and convert back.
  • Angles: You need "shortest path" interpolation.

If an object is at 10 degrees and rotates to 350 degrees, linear interpolation goes the long way around. We want it to just rotate -20 degrees.

// src/utils/smart-animation.ts

const angleProgress = (oldAngle, newAngle, progress) => {
    let diff = newAngle - oldAngle;

    // Normalize to -PI to +PI to find shortest direction
    while (diff > Math.PI) diff -= 2 * Math.PI;
    while (diff < -Math.PI) diff += 2 * Math.PI;

    return oldAngle + diff * progress;
};

3. The Render Loop & Overlapping Phases

Instead of CSS transitions (which are hard to sync for complex canvas repaints), I used a requestAnimationFrame loop in a React hook called useTransitionAnimation.

A key "secret sauce" to making animations feel professional is overlap.
If you play animations sequentially (Exit -> Move -> Enter), it feels robotic.
I overlapped the phases so the scene feels alive:

// Timeline Logic
const exitEnd = hasExit ? 300 : 0;
const morphStart = exitEnd; 
const morphEnd = morphStart + 500;

// [MAGIC TRICK] Start entering elements BEFORE the morph ends
// This creates that "Apple Keynote" feel where things arrive 
// just as others are settling into place.
const overlapDuration = 200; 
const enterStart = Math.max(morphStart, morphEnd - overlapDuration);

4. Making it feel "Physical"

Linear movement (progress = time / duration) is boring.
I implemented spring-based easing functions. Even though I'm manually calculating specific frames, I apply aneasing curve to the progressvalue before feeding it into the interpolator.

// Quartic Ease-Out Approximation for a "Heavy" feel
const springEasing = (t) => {
    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 here: https://postara.io/

Free Stripe Promotion Code: postara

Let me know what you think of the approach!


Written by behruamm | An AI Native Solo Founder.
Published by HackerNoon on 2026/01/22