Problem
The Hiro League Flutter app is open source. Community contributors build it for all platforms (Windows, macOS, Android, iOS, web) via GitHub Actions. The app includes a character face renderer that displays animated dot-matrix faces driven by text expressions. The core IP is the expression-to-visual algorithm — the logic that interprets text (e.g."happy", "surprised") and produces an animation plan: which dots move, how far, with what easing and timing. This logic must remain proprietary even though the app repo is public.
The rendering layer itself (drawing dots on screen) is standard Flutter and is fine to be open source.
Constraints
- The app is open source; contributors must be able to build without accessing the algorithm source
- All five platforms must be supported from a single codebase
- The face occupies roughly 1/3 of the screen at ~9×9 real pixels per dot (~40–70 dots wide, ~40–89 dots tall)
- Style is dot-matrix; resolution is intentionally low
- Target framerate: 24 fps
Locked decisions
| # | Decision |
|---|---|
| 1 | The expression-to-animation algorithm lives in a native library (C or Rust), not in Dart. |
| 2 | The native library is compiled per-platform: .dll/.so/.dylib/.xcframework for native targets, .wasm for Flutter web. |
| 3 | The native source is in a private repository. Only compiled binaries are committed to the public app repo. |
| 4 | The Dart bridge uses dart:ffi on native platforms and JS interop on web, selected via conditional imports. |
| 5 | The Flutter renderer is pure Dart (CustomPainter) and is fully open source. |
| 6 | The native library is called once per expression change, not once per frame. It returns an AnimationDescriptor. Flutter’s own animation system handles per-frame interpolation. |
| 7 | flutter_rust_bridge is the preferred option if Rust is chosen (auto-generates Dart FFI bindings from Rust). ffigen is the fallback if C is chosen. |
Architecture
Cross-platform compilation targets
| Platform | Binary format | Bridge |
|---|---|---|
| Android | .so (arm64, arm, x86_64) | dart:ffi |
| iOS | .xcframework | dart:ffi |
| macOS | .dylib | dart:ffi |
| Windows | .dll | dart:ffi |
| Linux | .so | dart:ffi |
| Flutter web | .wasm | dart:js_interop |
Conditional import pattern
AnimationDescriptor
The native library returns a structured descriptor, not raw pixel data. Flutter drives the animation from this descriptor using its own interpolation.- FFI is only called on expression transitions — not in the render loop
- Flutter renders at whatever framerate the device supports (not capped at 24 fps)
- The algorithm’s output is data, not pixels — harder to reverse-engineer the intent
Performance notes
- Dot grid at 1/3 screen with 9×9 grouping: ~3,000–5,000 dots
- FFI call overhead: ~1–5 µs per call; frame budget at 24 fps is ~41 ms (overhead is ~0.01%)
- If pixel buffer transfer is needed: 64×64 RGBA = 16 KB/frame, 384 KB/s — trivial
- Zero-copy sharing is possible: allocate a shared
callocbuffer once; native writes, Dart reads viaasTypedList
Per-frame FFI calls are not needed and should be avoided. The native library is called once per expression change. The AnimationDescriptor drives all subsequent frames entirely in Dart/Flutter.
Repository layout
Build flow for contributors
- Contributor clones the public app repo
native/face_engine/contains pre-compiled binaries — no source- Local dev builds use the stub (
face_engine_stub.dart) — face shows a placeholder - CI (GitHub Actions) has a deploy key for the private engine repo; full builds use real binaries
- Release builds are produced only in CI — contributors never need the engine source
Open questions
C or Rust?
C or Rust?
Both work. Rust is preferred because:
flutter_rust_bridgeauto-generates the Dart FFI bindings — no manual binding work- Memory safety without a garbage collector
- Cross-compilation to all targets including WASM is well-supported via
cargo
ffigen handles binding generation.Shader usage
Shader usage
Fragment shaders in Flutter are GLSL text files — readable source. The rendering aesthetic (dot-matrix look, bloom, scanlines) can be achieved with shaders, but the shader source would be visible in the public repo.Since the rendering layer is intentionally open, this is acceptable. The IP lives in the
AnimationDescriptor that feeds the renderer, not in the shaders themselves.If rendering shaders also need to be private, they can be compiled to SPIR-V and embedded in the native binary, then passed to Flutter via a texture. This adds complexity and is deferred until needed.Flutter web shader limitations
Flutter web shader limitations
Flutter web (CanvasKit/Skia) has partial fragment shader support. For the dot-matrix renderer,
CustomPainter with canvas drawing operations (circles, rounded rectangles) is more portable than shaders and avoids web shader compatibility issues.