Cross-platform handwriting engine written in Rust. Delivers consistent stroke quality across iOS and Android.
┌─────────────────────────────────────────────────────┐
│ Native App (Swift / Kotlin) │
│ Touch input → Consume render commands │
└────────────────────┬────────────────────────────────┘
│ FFI
┌────────────────────▼────────────────────────────────┐
│ melan-ffi UniFFI 0.28 bindings │
│ RwLock<DrawEngine> → Swift/Kotlin type conversion │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────┐
│ melan-core Pure Rust engine │
│ Coordinates · Bézier · Variable width · Undo/Redo │
└─────────────────────────────────────────────────────┘
Poll-based rendering — Feed input, get render commands back.
engine.begin_stroke(screen, pressure, timestamp) → Vec<RenderCommand>
engine.add_point(screen, pressure, timestamp) → Vec<RenderCommand> // incremental
engine.end_stroke() → Vec<RenderCommand> // full re-renderThe app just consumes commands via switch and draws. Pressure calculation, Bézier interpolation, coordinate conversion, and undo history are all handled by the engine.
| Command | Description |
|---|---|
Clear |
Fill canvas with background color |
SaveState / RestoreState |
Save/restore graphics context |
SetTransform |
Apply zoom & pan transform |
DrawVariableWidthPath |
Draw variable-width Bézier path |
crates/
├── melan-core/ # Pure Rust engine (minimal dependencies)
│ ├── canvas.rs # DrawEngine — main API
│ ├── brush.rs # Brush config (Pen, Highlighter, Eraser)
│ ├── coordinate/ # Document ↔ Screen coordinate conversion
│ ├── geometry.rs # Catmull-Rom → cubic Bézier conversion
│ ├── render.rs # RenderCommand generation
│ ├── format/ # JSON / Protobuf serialization
│ ├── history.rs # Undo / Redo stack
│ ├── layer.rs # Layer management
│ └── stroke.rs # Stroke builder
│
├── melan-ffi/ # UniFFI bindings (shared by iOS & Android)
│ ├── engine.rs # MelanEngine wrapper (RwLock)
│ └── types.rs # FFI type definitions + From conversions
│
└── uniffi-bindgen/ # Swift / Kotlin code generation CLI
MelanEngine::new(canvas_size) Create with custom size
MelanEngine::new_a4() Create A4 (595×842pt)
set_brush(config) Set brush
begin_stroke(x, y, pressure, timestamp) Start stroke
add_point(x, y, pressure, timestamp) Add point (incremental render)
end_stroke() End stroke (full re-render)
undo() / redo() Undo / Redo
clear_all() Clear all strokes
zoom(factor, focal_x, focal_y) Pinch zoom (focal point fixed)
pan(dx, dy) Pan
reset_viewport() Reset zoom & pan
full_render() Full scene render commands
get_state() Query engine state (stroke_count, can_undo, scale...)
save(format) Serialize to JSON/Protobuf → Vec<u8>
load(data) Restore engine state from bytes
cargo test --workspace # Run 44 tests
cargo build -p melan-ffi # Build FFI libraryrustup target add aarch64-apple-ios aarch64-apple-ios-sim
./scripts/build-ios.shOutput:
MelanCoreFFI.xcframework(device + simulator)Sources/MelanCore/MelanCoreFFI.swift(Swift bindings)
Build artifacts are distributed via SPM from melan-swift.
// Xcode → Add Package → https://github.com/gomminjae/melan-swift.git
import MelanCore
let engine = MelanEngine.newA4()# Install prerequisites
cargo install cargo-ndk
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/<version>
./scripts/build-android.shOutput:
android/jniLibs/<abi>/libmelan_ffi.so(arm64-v8a, armeabi-v7a, x86_64, x86)android/kotlin/(Kotlin bindings)
Copy android/jniLibs/ and android/kotlin/ into your Android project, then:
import com.melan.core.*
val engine = MelanEngine.newA4()Document coordinates (engine internal)
- Origin: top-left (0, 0)
- Unit: 1pt = 1/72 inch (PDF points)
- Y-axis: increases downward
Screen coordinates (platform input)
- Pass touch coordinates as-is
- Viewport converts automatically
- screen = (document + offset) × scale
MIT License - see LICENSE