A modern C++20 flood-fill animation library and CLI tool.
Given a PNG image, a seed pixel, and a colour-tolerance threshold, triplefill performs BFS or DFS flood fill, captures animation frames at configurable intervals, and writes the result as a PNG or animated GIF — all with zero external runtime dependencies.
- BFS / DFS flood fill with configurable neighbour ordering (N-E-S-W).
- HSL colour-distance tolerance for natural-looking fill boundaries.
- Pluggable colour pickers: solid, diagonal stripe, quadrant luminance, and border-aware fill.
- Animation capture: frame snapshots every k pixels filled.
- GIF export: built-in LZW encoder with median-cut quantisation — no
ImageMagick, no
system()calls. - Clean library + CLI separation; easy to embed in other projects.
- Cross-platform: builds and tests on Linux, macOS, and Windows via CMake.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure# Solid red fill, BFS, tolerance 0.15
./build/apps/cli/triplefill \
--input photo.png \
--output filled.png \
--seed 100,200 \
--tolerance 0.15 \
--algo bfs \
--picker solid --color 255,0,0,255
# Diagonal stripe fill with animated GIF output
./build/apps/cli/triplefill \
--input photo.png \
--output animation.gif \
--seed 50,50 \
--tolerance 0.2 \
--frame-freq 500 \
--picker stripe --color1 255,128,0,255 --color2 0,128,255,255 \
--stripe-width 8
# Border-aware fill
./build/apps/cli/triplefill \
--input photo.png \
--output bordered.png \
--seed 0,0 \
--tolerance 0.3 \
--picker border --color 0,255,0,255 \
--border-color 255,0,0,255 --border-width 4Run triplefill --help for the full option list.
An interactive in-browser demo powered by WebAssembly (Emscripten). Drag & drop a PNG, click a seed point, choose an algorithm and picker, and watch the fill happen — all computed off-thread in a Web Worker.
- Emscripten SDK — install from
https://emscripten.org/docs/getting_started/downloads.html and source
emsdk_env.shso thatemcmake/emmakeare on PATH. - Node.js ≥ 18
# 1. Build the WASM module
cd web
npm install
npm run build:wasm # configures + compiles triplefill → WASM
# 2. Start the dev server
npm run dev # opens http://localhost:5173A single npm run build produces a fully static web/dist/ directory that
can be deployed to GitHub Pages or any static host.
The repo includes a vercel.json at the root that configures a static Vite
deployment. WASM artifacts are committed to web/public/wasm/ so Vercel does
not need Emscripten installed.
# One-time: install Vercel CLI
npm i -g vercel
# Deploy (from repo root)
VITE_BASE=/ vercel --prodVercel settings (auto-detected from vercel.json):
- Install:
cd web && npm ci - Build:
cd web && npm run build - Output:
web/dist - Base path: Set
VITE_BASE=/as a Vercel environment variable (defaults to./for local/GitHub Pages use).
To rebuild WASM artifacts after C++ changes:
cd web && npm run build:wasm # requires Emscripten SDKThen commit the updated files in web/public/wasm/.
┌──────────────────────────────────────────────────────────┐
│ Browser (main thread) │
│ ┌──────────┐ RGBA buffer ┌──────────────────────┐ │
│ │ Canvas │ ──────────▶ │ Web Worker │ │
│ │ (input) │ │ loads triplefill.wasm │ │
│ └──────────┘ ◀────────── │ calls run_fill() │ │
│ ┌──────────┐ RGBA frames └──────────────────────┘ │
│ │ Canvas │ Transferable ArrayBuffers │
│ │ (output) │ (zero-copy) │
│ └──────────┘ │
└──────────────────────────────────────────────────────────┘
The WASM module exposes a minimal C ABI (run_fill, run_fill_with_frames,
free_buffer). No internal C++ classes are exposed. The JS worker marshals
RGBA buffers in and out and uses Transferable objects to avoid copies.
- Very large images (> 4000 × 4000) may be slow or exhaust WASM memory. The module caps at 1 GB.
- Frame capture with many frames increases memory proportionally;
use
max_framesto cap. - The WASM module is built with
ENVIRONMENT='worker'— it cannot be loaded on the main thread.
include/triplefill/ Public headers (library API)
src/ Library implementation
apps/cli/ Command-line frontend
apps/wasm/ WebAssembly bindings (Emscripten)
web/ Vite + TypeScript web frontend
src/ Application source
worker/ Web Worker (loads WASM, runs fills)
public/wasm/ WASM build artifacts (generated)
scripts/ Build automation
tests/ Catch2 unit + golden-image tests
third_party/ Vendored dependencies (lodepng, Catch2, gif encoder)
cmake/ CMake helpers (sanitisers, toolchains)
legacy/ Original class-project code (not built)
The engine uses a visited bitmap and a work structure:
| Algorithm | Structure | Behaviour |
|---|---|---|
| BFS | std::deque |
FIFO — level-order expansion |
| DFS | std::vector |
LIFO — depth-first expansion |
Neighbour push order is always North → East → South → West. A pixel is marked visited on push and coloured on pop.
Colour distance is computed in HSL space:
d = sqrt(Δh² + Δs² + Δl²)
where Δh uses the shortest arc on the hue circle (normalised to [0, 1]).
- The hot loop avoids heap allocation:
std::dequeandstd::vectorare reused; the visited bitmap is a flatvector<uint8_t>. - Colour pickers use
std::variantdispatch (no virtual-call overhead) viastd::visit; trivial pickers (solid, stripe) are inlined. - The GIF encoder writes directly to
FILE*with sub-block buffering; no temporary in-memory copy of the entire encoded stream. - For very large images, use
--frame-freq 0to skip intermediate frame capture and reduce memory from O(frames × pixels) to O(pixels).
cmake --preset sanitize
cmake --build build-sanitize
ctest --test-dir build-sanitizeMIT — see LICENSE.