Hokusai
A pure Rust brush engine inspired by libmypaint, designed for WebAssembly and native targets.
The full pipeline — .myb brush load → stroke engine → dab blend on tiles — is implemented and can render real libmypaint brushes (charcoal, calligraphy, marker_fat, …). Pixel-level parity with libmypaint is the goal; the gap is tracked in TODO.
Goals
- Pure Rust, no
unsafe— clean WASM (wasm32-unknown-unknown) story. - libmypaint
.mybJSON compatibility — brushes authored for MyPaint / Krita load and round-trip without translation. - Pixel-level parity with libmypaint — same fix15 math, same tile layout, same stroke math. Compatibility is the design priority; the "Hokusai" name does not imply behavioural divergence.
- Pluggable surfaces via the
TiledSurfacetrait. Backends are split into feature-gated crates. - Tile-based infinite canvas — 64×64 RGBA fix15 tiles, matching libmypaint exactly so dab traversal and rounding stay bit-identical.
Workspace layout
hokusai/
├── crates/
│ ├── hokusai-core/ # Brush types, stroke engine, fix15, tiles, brushmodes
│ ├── hokusai-brush/ # libmypaint `.myb` JSON read / write
│ ├── hokusai-tile-mem/ # Reference in-memory TiledSurface
│ └── hokusai-compat/ # Snapshot regression harness (libmypaint parity track)
└── hokusai/ # Umbrella crate that re-exports the above via features
└── examples/ # stroke_to_png, myb_to_png (+ vendored .myb fixtures)
Planned: hokusai-tiny-skia (raster output), hokusai-wasm (wasm-bindgen glue).
Quick look
use ;
use myb;
use MemSurface;
let json = read_to_string?;
let brush: Brush = from_str?;
let mut state = default;
let mut surface = new;
// First call seeds position only; subsequent calls emit dabs.
brush.stroke_to;
brush.stroke_to;
# Ok::
Run the bundled examples to render to PNG:
Features
Brush data
- All ~50 libmypaint settings as a strongly-typed enum with libmypaint-canonical string keys
- All inputs (
pressure,speed1/2,random,stroke,direction,tilt, …) .mybv3 JSON parse / serialize, round-trip safe for known keys
Stroke engine
- Per-event setting evaluation (
base_value + Σ curve(input)) slow_trackingsmoothing of the cursor path- Distance + time based dab spacing (
dabs_per_actual_radius,dabs_per_basic_radius,dabs_per_second) - Per-dab HSV drift via
change_color_h/change_color_v/change_color_hsv_s - Smudge bucket sampling and mixing
- Fresh-stroke / long-pause detection
Pixel blending (draw_dab)
- Normal + Eraser blend in linear sRGB fix15 (premultiplied alpha)
- Two-segment hardness falloff
- Elliptical dabs (
aspect_ratio,angle) anti_aliasingedge featheringlock_alphamaskingposterizeper-pixel quantization
Infrastructure
- Tile-aware traversal across arbitrary canvas extents
- Deterministic MT19937 PRNG
- Snapshot regression harness (
hokusai-compat) withHOKUSAI_UPDATE_GOLDENS=1 - CI: fmt, clippy
-Dwarnings, test on Linux/macOS/Windows, wasm32 build check, MSRV 1.75
TODO
Stroke engine
-
tracking_noise— random jitter added to the smoothed pointer position -
attackinput — initial pressure ramp at stroke start -
stroke_holdtime+stroke_duration_logarithmicfor theStrokeinput - Speed slowness low-pass (
speed1_slowness,speed2_slowness) — currently uses raw event speed -
offset_by_random/offset_by_speeddab position jitter -
stroke_threshold— suppress dabs below a pressure floor - Tilt-derived inputs (
tilt_declination,tilt_ascension) — currently0.0 - Custom input — recursive evaluation through
custom_input/custom_input_slowness - Slow tracking per-dab (
slow_tracking_per_dab) for radius smoothing - Gridmap inputs (
gridmap_x,gridmap_y) - Offset settings (
offset_x,offset_y,offset_angle*,offset_multiplier)
Pixel blending
- Colorize — HSL hue/saturation replacement at the destination pixel
- Spectral
paintmode — modern MyPaint pigment mixing -
change_color_hsl_s/change_color_l— HSL-space colour drift - Direct
tile_lookup-freeget_colorpath for backends that can't expose tiles
Compatibility
- libmypaint-sourced golden snapshots — replace the self-generated PNGs under
crates/hokusai-compat/fixtures/with output from upstream libmypaint to make the harness actually verify parity (seecrates/hokusai-compat/src/lib.rsdocs) - GRand-compatible PRNG output mapping — current PRNG is MT19937 but the
g_rand_double/g_rand_int_rangewrappers haven't been bit-matched yet - Lossless round-trip for unknown
.mybsettings (preserve, don't drop) - Compatibility tests against the full libmypaint brush pack
Backends
-
hokusai-tiny-skia— flatten tiles into atiny-skiaPixmap -
hokusai-wasm—wasm-bindgenJS bindings + browser demo
Cargo features (umbrella hokusai crate)
| Feature | Default | What it enables |
|---|---|---|
myb-json |
✅ | .myb JSON parser / serializer |
tile-mem |
✅ | Reference HashMap-backed TiledSurface |
tiny-skia |
— | (planned) tiny-skia Pixmap backend |
wasm |
— | (planned) wasm-bindgen JS bindings |
Contributing
See CONTRIBUTING.md for build / test / snapshot workflow, fixture conventions, and commit-message style.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Vendored brush fixtures under hokusai/examples/fixtures/ are unmodified copies from mypaint-brushes (CC0 1.0).