← Back to blog

Building a 2D Vector Animation Tool in Zig

Software DevelopmentAnimationProgrammingZigSoftware Engineering

Modern creative tools are incredibly powerful — but most of them are also incredibly heavy.

As a developer and artist, I wanted something different: a fast, focused 2D vector animation tool built with modern low-level tools.

So I started building Splineworks.

Splineworks is a vector drawing and animation program written primarily in Zig, with a custom rendering engine, immediate-mode UI, and timeline animation system.

This article walks through some of the engineering decisions and code behind the project.


Why Zig?

Most creative applications today are written in C++, Rust, or managed languages.

I chose Zig for several reasons:

  • No hidden allocations
  • Predictable memory management
  • C interoperability
  • Extremely fast compile times
  • Great for systems-level graphics work

Zig also makes it easy to integrate with existing graphics libraries like OpenGL.

Example of a typical module setup:

const std = @import("std");
 
pub const Renderer = struct {
    vao: u32,
    vbo: u32,
 
    pub fn init() Renderer {
        return Renderer{
            .vao = 0,
            .vbo = 0,
        };
    }
};

Simple. Explicit. No magic.


Rendering Vector Shapes

Splineworks renders vector paths by converting Bézier curves into GPU-friendly geometry.

Each path is broken into segments, which are tessellated into triangles.

Example structure for a Bézier segment:

pub const BezierSegment = struct {
    p0: Vec2,
    p1: Vec2,
    p2: Vec2,
    p3: Vec2,
};

Sampling the curve looks like this:

fn cubicBezier(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) Vec2 {
    const u = 1.0 - t;
 
    return Vec2{
        .x = u*u*u*p0.x +
             3*u*u*t*p1.x +
             3*u*t*t*p2.x +
             t*t*t*p3.x,
 
        .y = u*u*u*p0.y +
             3*u*u*t*p1.y +
             3*u*t*t*p2.y +
             t*t*t*p3.y,
    };
}

This function evaluates the Bézier curve at any point t between 0 and 1. These points are then used to generate vertices for the GPU.


Immediate-Mode UI

Rather than using a traditional retained UI framework, Splineworks uses an immediate-mode UI architecture. That means the UI is rebuilt every frame.

For example, drawing a button might look like:

pub fn button(ui: *UIContext, label: []const u8) bool {
    const rect = ui.nextWidgetRect();
 
    drawRect(rect, ui.theme.buttonColor);
    drawText(label, rect);
 
    if (ui.mouseClicked(rect)) {
        return true;
    }
 
    return false;
}

Usage:

if (ui.button("Add Keyframe")) {
    timeline.addKeyframe();
}

This approach keeps the UI simple, predictable, and easy to debug.


The Animation Timeline

Splineworks supports per-attribute keyframing, meaning each property is animated independently.

Example animation track:

pub const Keyframe = struct {
    frame: i32,
    value: f32,
};
 
pub const Track = struct {
    keyframes: std.ArrayList(Keyframe),
};

Evaluating a track during playback:

fn evaluate(track: *Track, frame: i32) f32 {
    for (track.keyframes.items) |kf| {
        if (kf.frame == frame) {
            return kf.value;
        }
    }
 
    return 0;
}

This allows Splineworks to animate position, rotation, scale, opacity, and shape morphing — all independently.


Shape Tweening

One feature I'm particularly excited about is shape tweening.

Shape tweening morphs one vector shape into another over time. To make this work, both shapes must have the same number of points and the same segment types.

Interpolation is then straightforward:

fn interpolate(a: Vec2, b: Vec2, t: f32) Vec2 {
    return Vec2{
        .x = a.x + (b.x - a.x) * t,
        .y = a.y + (b.y - a.y) * t,
    };
}

Each point in the path is interpolated independently. The result is a smooth morph between shapes.


GPU Effects Pipeline

Splineworks also supports layer effects like drop shadow, glow, blur, and color overlay.

These are implemented using a framebuffer compositing pipeline:

Shape render → Source FBO
      ↓
Apply effects stack
      ↓
Blend into composition buffer
      ↓
Final screen output

This makes it possible to build an Appearance-style stack similar to Illustrator or Photoshop.


What's Next

Splineworks is still evolving, but the roadmap includes:

  • Advanced gradient fills
  • Motion paths
  • Symbol system
  • GPU accelerated effects
  • Graph editor
  • Export pipeline for animation

My goal is to build a fast, modern vector animation tool built with systems-level engineering principles.


Final Thoughts

Working on Splineworks has been one of the most rewarding engineering projects I've tackled.

Building creative tools forces you to think about rendering pipelines, UI design, animation systems, performance, and usability — all at once.

Zig has proven to be a fantastic language for this kind of work.

If you're interested in graphics programming, creative tooling, or Zig, I highly recommend experimenting with projects like this. You learn a lot very quickly.


If people are interested, I can write future posts about:

  • Building the Splineworks timeline UI
  • GPU compositing pipelines
  • Bézier math
  • Vector boolean operations
  • Building a UI framework in Zig