Why build this?
I started this project because I wanted something better than existing GIF animation tools. The concept was great, but I ran into performance issues and wanted more control over how it worked and looked. So I rebuilt it from scratch in Rust with a focus on stability through multi-process architecture and keeping the codebase open for anyone to contribute to or learn from.
Multi-Process Architecture
The core insight: a bad animation shouldn't crash your entire session. Gif-Engine runs each animation in its own isolated process, managed by a central "manager" process that handles the UI and configuration.
Process Isolation Benefits
- Memory isolation prevents one animation from corrupting others
- A crashing GIF only kills its own window, not the manager
- Independent CPU scheduling per animation
- Clean shutdown: manager can terminate stuck animations
The tradeoff is higher memory usage per animation, but for desktop use with 2-5 concurrent animations, the stability gain is worth it.
Transparent Window Rendering
The real challenge was getting true per-pixel alpha transparency working on Windows. Most "transparent" windows are actually keying out a single color, which produces harsh edges and doesn't blend with the desktop.
WS_EX_LAYERED Implementation
// Window creation flags for true alpha blending
WS_EX_LAYERED // Enable per-pixel alpha
WS_EX_TOOLWINDOW // Hide from taskbar
WS_EX_TRANSPARENT // Click-through by default
The rendering pipeline: decode frames via the image crate,
composite to handle GIF disposal methods correctly (restore to background,
do not dispose, etc.), then present via UpdateLayeredWindow
with a 32-bit DIB section containing premultiplied alpha.
Frame Composition Challenges
GIF disposal methods are notoriously tricky. Each frame can specify:
- None: Draw over previous frame
- Do Not Dispose: Leave pixels as-is
- Restore to Background: Clear frame rectangle to transparent
- Restore to Previous: Restore to state before this frame
Getting this right required maintaining a full framebuffer and tracking the "previous" state for each frame, not just the current one.
Window Anchoring ("Pet Mode")
The most requested feature: attach animations to specific windows so they follow movement and stay positioned relative to the window they're attached to.
Implementation Details
// Anchor tracking loop
target_hwnd = FindWindowEx(...);
GetWindowRect(target_hwnd, &rect);
SetWindowPos(
anim_hwnd,
HWND_TOPMOST,
rect.left + offset_x,
rect.top + offset_y,
width, height,
SWP_NOACTIVATE
);
The manager enumerates running processes, finds their main window handles, and polls window position at 60Hz. When the target window moves, the animation repositions to maintain the relative offset. Edge cases handled:
- Target window minimized: hide animation
- Target window closed: detach and restore to screen position
- Z-order changes: keep animation just above target window
Click-Through and Interaction
By default, animations are "click-through" so you can work behind them. But users need to drag and reposition sometimes. The solution: modifier keys.
// Hit testing based on modifier key state
if (GetAsyncKeyState(VK_CONTROL) & 0x8000) {
// Enable interaction mode
SetWindowLong(hwnd, GWL_EXSTYLE,
ex_style & ~WS_EX_TRANSPARENT);
} else {
// Click-through mode
SetWindowLong(hwnd, GWL_EXSTYLE,
ex_style | WS_EX_TRANSPARENT);
}
State Persistence
Everything lives in %APPDATA%\gif-engine\:
- store.json: Library entries, tags, settings
- running.json: Active animation process tracking
- gifs/: Managed copies of animation files
The app copies imported animations to its own directory, so reorganizing
your original files doesn't break the library. serde handles
JSON serialization with pretty-printing for human-readable diffs.
Lessons Learned
Process Management Is Harder Than It Looks
On Windows, cleanly terminating a child process requires care. Initially
I used TerminateProcess which leaves resources dangling. Switched
to a graceful shutdown protocol: send "quit" command, wait for exit with
timeout, then hard-terminate if needed.
Window Z-Order Is Surprisingly Complex
Getting animations to stay above specific windows but not steal focus
required deep dives into WS_EX_NOACTIVATE, SetWindowPos
flags, and the nuances of HWND_TOPMOST vs relative positioning.
Rust's Windows API Bindings Are Good
The windows crate provides raw FFI bindings. For higher-level
abstractions, winit handles window creation but I needed raw
Win32 for the layered window flags. Mixing raw and managed code worked
fine with proper unsafe blocks.