Skip to content

[Feat] CSS Keyframe Animation Builder #116

@FumingPower3925

Description

@FumingPower3925

Description

Visual timeline editor for CSS @keyframes animations. Add keyframes on a draggable timeline, set properties per keyframe, and see a live preview on a customizable element. Exports clean @keyframes CSS. The visual approach makes animation authoring intuitive — no more guessing percentages.

Features

Timeline Editor

  • Visual timeline — Horizontal bar representing 0%–100% of the animation
  • Keyframe markers — Draggable diamond markers on the timeline, click to select
  • Add keyframe — Click on the timeline to add a new keyframe at that percentage
  • Remove keyframe — Right-click or delete key to remove a keyframe
  • Snap to grid — Keyframes snap to 5% increments (toggle off for freeform)
  • Multi-select — Shift+click to select multiple keyframes, move together
  • Copy keyframe — Duplicate a keyframe's properties to another position

Properties Panel

Per-keyframe CSS properties with visual controls:

  • Transform:
    • Translate X/Y/Z (sliders, px or %)
    • Scale X/Y (slider, 0–3)
    • Rotate (angle dial, 0–360°)
    • Skew X/Y (slider, -45° to 45°)
  • Opacity — Slider 0–1
  • Colors:
  • Dimensions:
    • width / height (px, %, auto)
    • border-radius (px or %)
    • padding / margin
  • Filter:
    • blur() — slider 0–20px
    • brightness() — slider 0–200%
    • saturate() — slider 0–200%
    • hue-rotate() — angle dial 0–360°
    • grayscale() — slider 0–100%
  • Clip-path — Select from presets (circle, polygon, inset)
  • Custom CSS — Freeform CSS input per keyframe for properties not covered above

Animation Controls

  • Name — Custom animation name (default: my-animation)
  • Duration — 0.1s–10s slider
  • Timing functionlinear, ease, ease-in, ease-out, ease-in-out, cubic-bezier() with visual curve editor
  • Iteration count — 1, 2, 3, ..., infinite
  • Directionnormal, reverse, alternate, alternate-reverse
  • Fill modenone, forwards, backwards, both
  • Play state — Play/pause the preview animation
  • Playback speed — 0.25x, 0.5x, 1x, 2x for slow-motion debugging

Preview

  • Live element — Customizable preview target:
    • Shape: square, circle, rectangle, text, custom HTML
    • Size, color, border, background
    • The animation plays on this element in real-time
  • Ghost frames — Toggle to show all keyframe states simultaneously (onion skinning)
  • Background — Checkered (transparent), white, dark, custom color
  • Scrub — Drag a playhead across the timeline to manually scrub through the animation

Easing Curve Editor

  • Cubic bezier — Draggable control points on a curve graph
  • Preset curves — Quick-select common easing functions with visual preview
  • Per-keyframe easing — Different easing between each keyframe pair (via animation-timing-function within keyframes)

Implementation

Dependencies

Zero — native CSS animations + DOM manipulation.

Core Logic

interface KeyframeData {
    id: string;
    offset: number;             // 0–1 (0% to 100%)
    properties: CSSProperties;  // Typed subset of CSS properties
    easing?: string;            // Easing to this keyframe
}

interface AnimationConfig {
    name: string;
    keyframes: KeyframeData[];
    duration: number;           // seconds
    timingFunction: string;
    iterationCount: number | 'infinite';
    direction: string;
    fillMode: string;
}

function generateCSS(config: AnimationConfig): string {
    const sorted = [...config.keyframes].sort((a, b) => a.offset - b.offset);
    
    let css = `@keyframes ${config.name} {\n`;
    for (const kf of sorted) {
        const percent = kf.offset === 0 ? 'from' : kf.offset === 1 ? 'to' : `${Math.round(kf.offset * 100)}%`;
        css += `  ${percent} {\n`;
        for (const [prop, value] of Object.entries(kf.properties)) {
            if (value !== undefined && value !== '') {
                css += `    ${camelToKebab(prop)}: ${value};\n`;
            }
        }
        if (kf.easing) css += `    animation-timing-function: ${kf.easing};\n`;
        css += `  }\n`;
    }
    css += `}\n\n`;
    
    css += `.animated-element {\n`;
    css += `  animation: ${config.name} ${config.duration}s ${config.timingFunction} ${config.iterationCount === 'infinite' ? 'infinite' : config.iterationCount} ${config.direction} ${config.fillMode};\n`;
    css += `}\n`;
    
    return css;
}

// Apply animation to preview element via CSSOM
function applyAnimation(element: HTMLElement, config: AnimationConfig): Animation {
    const keyframes = config.keyframes
        .sort((a, b) => a.offset - b.offset)
        .map(kf => ({ offset: kf.offset, ...kf.properties, easing: kf.easing }));
    
    return element.animate(keyframes, {
        duration: config.duration * 1000,
        iterations: config.iterationCount === 'infinite' ? Infinity : config.iterationCount,
        direction: config.direction as PlaybackDirection,
        fill: config.fillMode as FillMode,
        easing: config.timingFunction,
    });
}

// Timeline scrubbing via Web Animations API
function scrubTo(animation: Animation, progress: number): void {
    animation.pause();
    animation.currentTime = progress * animation.effect!.getComputedTiming().duration! as number;
}

// Cubic bezier curve editor
function cubicBezierToCSS(x1: number, y1: number, x2: number, y2: number): string {
    return `cubic-bezier(${x1.toFixed(2)}, ${y1.toFixed(2)}, ${x2.toFixed(2)}, ${y2.toFixed(2)})`;
}

Ghost Frames (Onion Skinning)

function renderGhostFrames(container: HTMLElement, element: HTMLElement, keyframes: KeyframeData[]): void {
    container.innerHTML = '';
    for (const kf of keyframes) {
        const ghost = element.cloneNode(true) as HTMLElement;
        ghost.style.opacity = '0.15';
        ghost.style.position = 'absolute';
        Object.assign(ghost.style, kf.properties);
        // Label with percentage
        const label = document.createElement('span');
        label.textContent = `${Math.round(kf.offset * 100)}%`;
        label.style.cssText = 'position:absolute;top:-18px;font-size:10px;color:#888;';
        ghost.appendChild(label);
        container.appendChild(ghost);
    }
}

Privacy

Pure CSS generation. No data leaves the browser. Animation presets stored in localStorage.

UI

  • Timeline (top): Horizontal bar with draggable keyframe markers, playhead scrubber
  • Preview (center): Live animated element on configurable background, ghost frames toggle
  • Properties panel (right): Per-keyframe property controls, organized by category (Transform, Color, Filter, etc.)
  • Animation controls (bottom-left): Duration, easing, iteration, direction, fill mode, play/pause, speed
  • Easing curve (bottom-right): Visual cubic-bezier editor with preset buttons
  • CSS output (bottom): Live-updating @keyframes CSS with copy button
  • Presets: Save/load animation presets (bounce, fade, slide, spin, pulse, shake, flip)
  • Responsive layout

Files to Create/Modify

File Purpose
tools/animation-builder/package.json Workspace package
tools/animation-builder/src/tool.ts CSS generation, Web Animations API integration
tools/animation-builder/src/timeline.ts Timeline UI component, drag handling, scrubbing
tools/animation-builder/src/easing.ts Cubic bezier editor, preset curves
tools/animation-builder/src/page.ts Full editor layout, preview, property panel
tools/animation-builder/src/meta.ts ToolMeta
tools/animation-builder/src/index.ts Exports
tools/animation-builder/tests/tool.unit.test.ts CSS generation, keyframe sorting, easing output
tools/animation-builder/tests/page.unit.test.ts DOM rendering tests
src/index.ts Register route
src/index.html Add to tool grid

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions