fluera_canvas 0.10.4 copy "fluera_canvas: ^0.10.4" to clipboard
fluera_canvas: ^0.10.4 copied to clipboard

Document-grade infinite-canvas SDK for Flutter — layered scene graph, selection + transform, lasso, image annotations, text, keyboard shortcuts.

fluera_canvas #

Pure-Dart Flutter SDK for infinite-canvas drawing widgets. Pan, zoom, rotate camera. Layered scene graph. Pressure-aware input. Persistence + PNG export. Selection / lasso / transform / text / stickers / image annotations. 5 k+ strokes at 60 FPS on a mid-tier Android.

Status: 0.10.3 — pre-1.0 but additive only (zero breaking changes from 0.8.0). API stabilises at 1.0.0. Marketing site: engine.fluera.dev

Jump to: Hello canvas · Zero-config · Recipes · Keyboard · Export · Performance · FAQ · Migration · Changelog

Install #

dependencies:
  fluera_canvas: ^0.10.3
import 'package:fluera_canvas/fluera_canvas.dart';

Positioning #

You're building… Use
Notes app, whiteboard, sketching app, design tool, mind-map, diagram editor, infinite-canvas planner fluera_canvas — full scene-graph + persistence + selection / transform
Photo annotation, draw-on-image with strokes that follow when you move / rotate / scale the image fluera_canvas — image annotations are first-class (sticky, split-on-boundary)
Signature pad, e-signature flow signature / hand_signature (more focused on that single use case)
Custom stroke renderer, no widget perfect_freehand (pure rendering primitive)

fluera_canvas deliberately doesn't compete in the signature-pad niche — that's well served. The gap on pub.dev was the document-grade category: infinite surface, thousands of strokes, persistence-friendly, image annotations that ride the image as a rigid block. That's what this SDK is for.

What you get #

  • FlueraCanvas — drop-in drawing widget. Pen, stroke- + pixel-mode eraser, line / rectangle / ellipse shape tools, undo / redo, pressure-aware input, infinite pan / zoom / rotation, PNG export. Headless: drive tool / strokeColor / strokeWidth from any UI.
  • FlueraSketch / FlueraSketchScaffold / FlueraSketchApp — three zero-config widgets at increasing levels of "magic". runApp(const FlueraSketchApp()) literally gives you a complete drawing app.
  • FlueraCanvasToolbar — drop-in Material toolbar. Tool segmented control, color swatches, stroke-width slider, undo / redo / clear, optional layer panel / sticker panel / text tool / image tool.
  • FlueraCanvasColorPickerDialog + showFlueraColorPicker(...) — zero-dependency HSV / hex picker.
  • InfiniteCanvasController — camera with pan, zoom, rotation, spring physics, momentum, multi-phase animation.
  • InfiniteCanvasGestureDetector — multi-touch, stylus, palm rejection, hover tracking. All policies pluggable.
  • Scene graphCanvasStrokeNode, ShapeNode, TextNode, ImageNode, PathNode, GroupNode, LayerNode with visitor pattern.
  • Spatial index + per-stroke ui.Picture cache + multi-layer LayerPictureCache + DirtyRegionTracker + memory-pressure observer — together responsible for the 5 k+ stroke / 60 FPS budget.
  • Input pipeline — One-Euro smoothing, dynamic pressure mapping, palm rejection, stylus prediction, 120 Hz raw processor.
  • Persistence — binary FCV0 v6 round-trips strokes, image bytes, image annotations, text, layer state.

What's new in 0.10.x #

  • FlueraSketchApp zero-config drop-in: runApp(const FlueraSketchApp()) is a complete drawing app. Three presets: notes, whiteboard, signature. Optional onAutoSave / onAutoLoad / onExportPng callbacks for any persistence backend.
  • Lasso UX parity with the Fluera flagship app: tap-on-selection enters transform mode (drag = move, handle = scale / rotate); hit-test stricter (no over-selection from bbox-only).
  • Live stroke now honours active layer's blend mode + opacity — toggling multiply / screen updates the in-flight preview immediately, no jump at pen-up.
  • Smoothing pipeline rebuilt to match the commercial fountain-pen path builder: One-Euro at ingest → arc-length subdivision → two-pass EMA → ghost-tail anchor → Catmull-Rom → cubic bezier (tau = 1/6). Live and committed strokes share identical geometry.
  • Pixel eraser is pressure-aware + velocity-aware + drops micro-survivor fragments < 3 px arc-length.
  • PNG export has 4 bounds modes via FlueraExportBounds: viewport (legacy), allContent, selection, custom. Plus pixelRatio, padding, transparent.
  • TextNode is now selectable + transformable (drag / scale / rotate via the same handles as image / stroke nodes).
  • Edge-case hardening: serializer bounds-checks (DoS protection), renderToImage clamps, single-tap "dot" stroke commit, text editor hot-restart guards.

Full per-release detail in CHANGELOG.md. Upgrading from 0.5.x? See doc/migration-0.5-to-0.10.0.md — zero breaking changes, plus a checklist of opt-in flags to surface the new selection / lasso / text / sticker / snap-to-grid features.

Hello canvas #

The headless variant — wire tool / color / width from your own UI. Use this when you want full control over the drawing chrome.

import 'package:flutter/material.dart';
import 'package:fluera_canvas/fluera_canvas.dart';

class MyPage extends StatefulWidget {
  const MyPage({super.key});
  @override
  State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  final _canvasKey = GlobalKey<FlueraCanvasState>();
  CanvasTool _tool = CanvasTool.draw;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Notes'),
        actions: [
          IconButton(
            icon: const Icon(Icons.undo),
            onPressed: () => _canvasKey.currentState?.undo(),
          ),
          IconButton(
            icon: const Icon(Icons.redo),
            onPressed: () => _canvasKey.currentState?.redo(),
          ),
        ],
      ),
      body: FlueraCanvas(
        key: _canvasKey,
        tool: _tool,
        strokeColor: Colors.black87,
        strokeWidth: 2.5,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _tool =
            _tool == CanvasTool.draw ? CanvasTool.erase : CanvasTool.draw),
        child: Icon(_tool == CanvasTool.draw
            ? Icons.edit
            : Icons.cleaning_services),
      ),
    );
  }
}

Want pan / zoom / rotation driven from outside (a "reset view" button, programmatic fly-to)? Pass controller: InfiniteCanvasController().

Zero-config drop-in #

Pick the level of "magic":

Whole app, one line #

void main() => runApp(const FlueraSketchApp());

You get a Material 3 MaterialApp + AppBar with undo / redo / clear / export-PNG popup, the full toolbar with every opt-in flag wired (pen / shapes / pixel-eraser / lasso / select / text / sticker / image / layers / transform / color picker), dotted background. Three presets:

const FlueraSketchApp(preset: FlueraSketchPreset.notes);      // minimal
const FlueraSketchApp(preset: FlueraSketchPreset.whiteboard); // default
const FlueraSketchApp(preset: FlueraSketchPreset.signature);  // single pen, no zoom

Embed a sketch page in an existing app #

MaterialPageRoute(builder: (_) => FlueraSketchScaffold(
  title: 'My note',
  preset: FlueraSketchPreset.notes,
  // Optional persistence hooks — wire any backend.
  onAutoSave: (bytes) => File('$dir/note.fcv').writeAsBytes(bytes),
  onAutoLoad: () async => File('$dir/note.fcv').readAsBytes(),
));

The scaffold debounces autosave at 2 s (autoSaveDebounce:). The export-PNG popup falls back to a clipboard data-URL if you don't pass onExportPng.

Just the canvas + toolbar (no Scaffold) #

Scaffold(
  appBar: AppBar(title: Text('My drawing')),
  body: const FlueraSketch(),  // canvas + toolbar self-contained
);

FlueraSketch owns the tool / color / strokeWidth state internally — no setState plumbing. Pass canvasKey: GlobalKey<FlueraCanvasState>() if you want to drive save / load / undo from outside.

The three widgets stack: FlueraSketchApp → wraps → FlueraSketchScaffold → wraps → FlueraSketch. Each is usable standalone. When you outgrow the presets, drop down to FlueraCanvas — same scene graph, every API still available.

Tools #

Switch via the tool: parameter (or via FlueraCanvasToolbar opt-in flags):

Tool What it does
CanvasTool.draw Free-form pressure-aware stroke.
CanvasTool.erase Stroke-mode eraser — removes whole strokes the eraser circle touches. Vector-preserving (undo brings them back intact).
CanvasTool.erasePixel Pixel-mode eraser — splits intersected strokes around the eraser circle and keeps the surviving pieces.
CanvasTool.line Drag from A to B for a straight line.
CanvasTool.rectangle Drag corner-to-corner for a rectangle outline.
CanvasTool.ellipse Drag for an ellipse outline (32-segment polyline).
CanvasTool.select Tap a node to select; drag to move; corners / edges / rotate handle for transform.
CanvasTool.lasso Drag a closed path; nodes whose centre or corners fall inside are selected. Concave shapes work.
CanvasTool.text Tap to drop a TextNode; inline Material TextField opens for editing.
CanvasTool.image (via FlueraImageTool.pickAndCommit) — file-picker import.

Shape tools commit as ordinary CanvasStroke instances, so they participate in undo / redo, persistence, hit-test, and the per-stroke ui.Picture cache for free.

Common recipes #

All snippets assume final canvasKey = GlobalKey<FlueraCanvasState>(); and a CanvasTool _tool field driven by your widget state. The canvas is intentionally headless — wire _tool from a SegmentedButton, a FlueraCanvasToolbar, your own UI, anything.

Select + drag + delete #

FlueraCanvas(
  key: canvasKey,
  tool: CanvasTool.select,        // tap to pick a node, drag to move
  snapToGrid: 16,                  // optional: snap drag to 16-px grid
  smartGuidesEnabled: true,        // optional: magenta align guides
);

// Programmatic delete (Delete key is auto-wired):
canvasKey.currentState?.deleteSelection();

Selection is observable via canvasKey.currentState!.selectionListenable. See state.duplicateSelection(), state.bringToFront(id), state.sendToBack(id), state.groupSelection(), state.ungroupSelection().

Lasso free-form selection #

FlueraCanvas(key: canvasKey, tool: CanvasTool.lasso);
// User drags any closed path; every node whose bounds the lasso
// encloses or crosses is added to the selection. Live magenta dashed
// preview during the drag. After pen-up, tapping inside the bbox
// drags the selection (no need to switch to `select` first); tapping
// outside starts a fresh lasso.

Use lasso for scattered or non-rectangular selections (strokes around an imported image, an artistic cluster, hand-drawn diagrams). Use CanvasTool.select marquee for grid layouts where an axis-aligned rectangle does the job faster.

Snap-to-grid + smart guides #

FlueraCanvas(
  key: canvasKey,
  tool: CanvasTool.select,
  snapToGrid: 8,                   // 0 disables; 8 / 16 / 24 are typical
  smartGuidesEnabled: true,        // align with neighbours on drag
  smartGuidesTolerancePx: 6,       // screen-space tolerance
);
// Hold Shift while dragging to bypass both snap and guides.

Live text tool #

FlueraCanvas(key: canvasKey, tool: CanvasTool.text);
// Tap empty canvas: a Material TextField caret opens inline. Type;
// commit on Done / blur / Esc. Tap an existing TextNode to re-enter
// edit mode. Use CanvasTool.select afterwards to drag / scale / rotate.

Sticker panel #

FlueraCanvasToolbar(
  canvasKey: canvasKey,
  tool: _tool,
  onToolChanged: (t) => setState(() => _tool = t),
  showStickerPanel: true,
  // stickers: const [], // omit or set to [] to opt out of defaults
);

// Defaults: kFlueraDefaultStickers ships 8 Material-icon stickers via
// FlueraIconStickerProvider — zero asset bundling.

// Custom catalogue:
final myStickers = <FlueraSticker>[
  FlueraSticker.fromIcon(id: 'rocket', label: 'Rocket', icon: Icons.rocket_launch),
  FlueraSticker.fromIcon(id: 'bolt',   label: 'Bolt',   icon: Icons.bolt),
];

Image annotations (sticky strokes) #

// 1. Import an image:
await FlueraImageTool.pickAndCommit(context: context, state: canvasKey.currentState!);

// 2. Switch to a draw tool and stroke on top of the image. Strokes
//    that fall inside the image bounds are auto-attached as image
//    annotations (in image-local coords) and ride every move /
//    rotate / scale / mirror as a rigid block. Strokes that cross the
//    boundary are split: inside parts attach, outside parts stay free.

Custom toolbar layout #

When the built-in FlueraCanvasToolbar doesn't fit your design (Cupertino, sidebar, floating, custom segmented button) — skip it and drive the canvas directly:

class _DemoState extends State<Demo> {
  final _canvasKey = GlobalKey<FlueraCanvasState>();
  CanvasTool _tool = CanvasTool.draw;
  Color _color = Colors.black;
  double _width = 2.5;

  @override
  Widget build(BuildContext context) => Column(children: [
    Expanded(child: FlueraCanvas(
      key: _canvasKey,
      tool: _tool,
      strokeColor: _color,
      strokeWidth: _width,
    )),
    // Your own toolbar widgets here — anything that mutates
    // _tool / _color / _width via setState.
  ]);
}

FlueraCanvas is intentionally headless. The state is observable via historyListenable / selectionListenable so a custom toolbar can gate undo / redo / contextual buttons without polling.

Save / load (mobile + desktop) #

// Persist to disk:
final bytes = canvasKey.currentState!.toBytes();
await File('$dir/scene.fcv').writeAsBytes(bytes);

// Restore inside initState (the safest path — see FAQ #1):
FlueraCanvas(key: canvasKey, initialBytes: await loadBytes());

// Or reload after first frame:
canvasKey.currentState?.loadFromBytes(bytes);

FCV0 v6 round-trips strokes, image bytes, image annotations, text nodes, layer state. Backward-read v1 → v5 unchanged.

Save / load (web) #

dart:io File doesn't exist in the browser sandbox. state.toBytes() still returns a Uint8List — wire it to a download instead:

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:web/web.dart' as web;            // web only
import 'dart:js_interop';

void downloadFcv(Uint8List bytes, String filename) {
  final blob = web.Blob(
    [bytes.toJS].toJS,
    web.BlobPropertyBag(type: 'application/octet-stream'),
  );
  final url = web.URL.createObjectURL(blob);
  web.HTMLAnchorElement()
    ..href = url
    ..download = filename
    ..click();
  web.URL.revokeObjectURL(url);
}

if (kIsWeb) {
  downloadFcv(canvasKey.currentState!.toBytes(), 'scene.fcv');
} else {
  await File('$dir/scene.fcv').writeAsBytes(canvasKey.currentState!.toBytes());
}

To load on web, use file_selector (already a transitive dep of fluera_canvas):

import 'package:file_selector/file_selector.dart';

final file = await openFile(acceptedTypeGroups: [
  XTypeGroup(label: 'fluera', extensions: ['fcv']),
]);
if (file != null) {
  canvasKey.currentState?.loadFromBytes(await file.readAsBytes());
}

This snippet works on every platform — no kIsWeb branch needed.

Keyboard shortcuts #

FlueraCanvas wraps its widget tree in Shortcuts + Actions and ships 15 default keybindings. The Ctrl/Cmd modifier auto-resolves per platform (Cmd on macOS / iOS, Ctrl elsewhere) — no Platform branching required.

Shortcut (Win / Linux / Web) Shortcut (macOS / iOS) Action
Ctrl+Z Cmd+Z Undo last op
Ctrl+Y or Ctrl+Shift+Z Cmd+Y or Cmd+Shift+Z Redo
Ctrl+A Cmd+A Select all selectable nodes in active layer
Ctrl+D Cmd+D Duplicate current selection (offset 20 × 20 px)
Delete or Backspace Delete or Backspace Delete selection — falls back to clear canvas when nothing is selected
Esc Esc Clear selection / cancel inline text editor
same Nudge selection 1 px (world-space)
Shift + ← → ↑ ↓ same Nudge selection 10 px

Hold Shift while body-dragging a selection to bypass snapToGrid and smartGuidesEnabled for that single drag.

Add your own shortcuts #

Wrap FlueraCanvas in a Shortcuts + Actions of your own — the outer wrapper is evaluated first, so your bindings win for the activators they claim and the built-in 15 still fire for everything else. No package change required:

class _SaveIntent extends Intent { const _SaveIntent(); }

Shortcuts(
  shortcuts: <ShortcutActivator, Intent>{
    const SingleActivator(LogicalKeyboardKey.keyS, control: true):
        const _SaveIntent(),
  },
  child: Actions(
    actions: <Type, Action<Intent>>{
      _SaveIntent: CallbackAction<_SaveIntent>(onInvoke: (_) {
        _persist(canvasKey.currentState!.toBytes());
        return null;
      }),
    },
    child: FlueraCanvas(key: canvasKey, /* … */),
  ),
);

To override one of the built-ins, declare the same activator with your own Intent + Action; the package's binding is shadowed.

Export #

fluera_canvas ships PNG raster export with four infinite-canvas-aware bounds modes via FlueraExportBounds.

What you see (legacy / WYSIWYG) #

final image = await state.renderToImage(width: 1920, height: 1080);
final png = await image.toByteData(format: ui.ImageByteFormat.png);

Everything (even off-screen) #

The infinite-canvas killer feature: rasterise the union of every visible node's worldBounds, regardless of where the camera is pointing. Output dimensions auto-computed.

final image = await state.renderToImage(
  bounds: FlueraExportBounds.allContent,
  pixelRatio: 2.0,    // optional — HiDPI / print export
  padding: 24,        // optional — world-px margin around the bounds
);

Selection only #

final image = await state.renderToImage(
  bounds: FlueraExportBounds.selection,
  pixelRatio: 2.0,
);
// Returns a 1×1 sentinel image when nothing is selected.

A specific region #

final image = await state.renderToImage(
  bounds: FlueraExportBounds.custom,
  region: const Rect.fromLTWH(0, 0, 800, 600),  // world coords
);

Transparent background #

Pair any non-viewport mode with transparent: true to skip the background fill + grid / dotted / lined pattern, producing a PNG with a real alpha channel:

final image = await state.renderToImage(
  bounds: FlueraExportBounds.allContent,
  pixelRatio: 2.0,
  transparent: true,
);

Guards: pixelRatio is clamped to [0.05, 32.0]; output size is rejected over 16 384 px per side (GPU max texture). Helpful sibling getters: state.contentBoundsWorld, state.selectionBoundsWorld, state.viewportCenterWorld, state.viewportSize.

Platforms #

fluera_canvas is pure Dart. No platform-channel code, no native plugin, no shaders — runs anywhere Flutter runs.

Platform Status
Android
iOS
macOS
Linux
Windows
Web (CanvasKit / WASM)

Only runtime deps: meta, vector_math, file_selector (latter only pulled when the image / sticker tools are wired). Zero native footprint, no AndroidManifest tweaks, no Podfile changes.

Per-platform notes #

  • Keyboard shortcuts auto-adjust: Ctrl+… on Windows / Linux / Web; Cmd+… on macOS / iOS. No consumer wiring required.
  • Trackpad pinch-zoom and two-finger pan work out of the box on macOS / Windows / Linux / ChromeOS via PointerScrollEvent and PointerPanZoom* events.
  • Stylus / Apple Pencil / S Pen / Surface Pen / Wacom are detected automatically via PointerDeviceKind.stylus; pressure + tilt are forwarded to the brush pipeline.
  • Web file save: state.toBytes() returns a Uint8List — trigger a download via the snippet in Save / load (web) above.
  • Image import (FlueraImageTool.pickAndCommit) uses file_selector and works on every platform Flutter supports, including web (renders a hidden <input type="file">).
  • Right-click context menu and clipboard copy/paste are not bundled — they're consumer-side decisions. Wire them via Listener.onPointerDown (button == 2) and Clipboard.setData / Clipboard.getData if your UX needs them.

Performance #

Every optimisation is wired by default. The knobs below let power users tune for their workload.

  • Stroke smoothing pipeline — five-stage chain mirrored from the commercial fluera_engine fountain-pen path builder: One-Euro filter at point ingest (minCutoff = 1.0, beta = 0.007) → adaptive arc-length subdivision via Catmull-Rom interpolation on long gaps → two-pass EMA pre-smoothing (forward + backward, alpha 0.3, endpoints pinned) → predicted "ghost" tail anchor (velocity + half-acceleration extrapolation, never drawn) → Catmull-Rom → cubic bezier with tau = 1/6. Live and committed strokes share identical geometry — what the user sees while drawing is exactly what gets persisted.
  • Stroke simplificationsimplifyEpsilon: 0 default (raw fidelity). Set 0.5 to opt into Douglas-Peucker compression (~40-60 % point reduction with sub-pixel visual difference).
  • LayerPictureCache — pan / zoom on a 10 k-stroke static scene paints in O(1): one cached ui.Picture per layer, replayed.
  • DirtyRegionTracker — mutating one stroke in the bottom-right doesn't repaint the full screen.
  • MemoryPressure observer — drops picture caches on didHaveMemoryPressure() so the OS doesn't reap your app on mobile.
  • RTree-backed hit test — O(log n + k) on visible-stroke queries.
  • Live stroke chunked PictureRecorder — handwriting strokes with 5 k+ points cache every 256-point range; the painter replays cached chunks and only re-tessellates the trailing tail.
  • Pixel eraser polish — pressure-aware radius (linear ramp 40-100 %), velocity-aware sub-stamp growth (≤ 1.4× to absorb fast-drag gaps), micro-survivor cleanup (drops < 3 px arc-length fragments after a cut).

Numbers + benchmark recipes: doc/performance.md.

Beyond the free tier #

Vector export (SVG / PDF), native sub-frame live-stroke latency, advanced brush engines, real-time collaboration, PDF annotation, LaTeX OCR, SQLCipher storage and timeline playback are intentionally not in this package. They're available as optional commercial add-ons (fluera_canvas_gpu, fluera_engine_pro) that plug into the same scene graph via the public GpuStrokeBackend hook — no fork, no rewrite. See doc/commercial-add-ons.md for the full list and pricing pointer. The free fluera_canvas package on its own is production-ready.

FAQ / Troubleshooting #

My persisted canvas appears empty when I reopen it. Use FlueraCanvas(initialBytes: bytesFromDisk) to restore — it decodes inside initState, before the first paint. Calling loadFromBytes on the State after the first frame can leave the RepaintBoundary cached layer stale on Impeller-Vulkan / Adreno (the second paint is silently coalesced and the canvas looks empty). Full write-up: doc/troubleshooting-impeller.md.

My live stroke is invisible until I lift my finger on Android. Flutter pipeline coalescing quirk on Impeller-Vulkan / Adreno in profile mode — mid-gesture setState / markNeedsPaint calls inside pointer-event handlers get folded together until pen-up. FlueraCanvas ships a vsync Ticker workaround that calls setState({}) from a frame callback while a draw / erase / shape gesture is active — the live stroke and eraser preview circle track the pointer in real time. Zero idle cost. Verified on Adreno 660 / Impeller-Vulkan, profile mode.

What's the difference between the stroke eraser and the pixel eraser? The stroke eraser (CanvasTool.erase) removes whole strokes whose bounds touch the eraser circle. Vector-preserving — undo brings them back intact. The pixel eraser (CanvasTool.erasePixel) splits each touched stroke around the circle and keeps the surviving pieces; on undo the original is restored. Use stroke-mode for sketching apps where strokes are atoms; use pixel-mode for fine corrections in note-taking apps.

Can I add a custom shape tool (polygon, arrow, …)? The built-in line / rectangle / ellipse cover the common cases. For anything else, build the polyline yourself and call canvasState.pushStroke(CanvasStroke(points: [...], ...)). It rides the same undo / redo / persistence / hit-test infrastructure for free.

How do I open the color picker without the toolbar?

final picked = await showFlueraColorPicker(
  context: context,
  initial: currentColor,
  enableAlpha: true, // optional, default true
);
if (picked != null) setState(() => currentColor = picked);

Returns null on cancel.

Why is the API so big? I see hundreds of exported symbols. Most are scene-graph primitives, brush models, filters, and input pipeline pieces inherited from the larger commercial fluera_engine. They're free to use but not required by FlueraCanvas itself. Stick to the symbols documented in this README and you'll have everything you need for typical drawing-app use cases.

How many strokes can it handle at 60 FPS? ~5 000–10 000 strokes in a typical viewport on a mid-tier Android (Adreno 660) thanks to per-stroke ui.Picture cache + spatial-index viewport culling + cached committed RepaintBoundary. Beyond that you'll start to see frame drops during camera animation; idle and during drawing it's still fluid. See doc/performance.md for the breakdown.

How do I build a multi-canvas / autosave app? That pattern is consumer-side (we don't ship path_provider / sqflite / encrypted storage as SDK deps). The example app's "Multi-canvas + autosave" demo shows the full pattern in ~150 lines using path_provider — copy and adapt.

Does fluera_canvas work on Web? Yes. CanvasKit and WASM compilation both work out of the box — the package has zero web-specific code, just Dart through Flutter's standard web toolchain.

Can I use fluera_canvas without the commercial GPU plugin? Yes — that's the default. The package is pure Dart: no platform code, no MethodChannel, no shaders. The optional native GPU plugin (fluera_canvas_gpu, separate package) plugs into the public GpuStrokeBackend hook to buy sub-frame latency on the live path, but is strictly optional.

More guides: doc/architecture.md, doc/performance.md, doc/troubleshooting-impeller.md.

Contributing #

Issues and PRs welcome at github.com/Lorencoshametaj/fluera_canvas. Please open an issue before starting a large change so we can align on API shape — we're converging on 1.0 and want to avoid breaking churn.

License #

MIT — see LICENSE.

2
likes
150
points
26
downloads

Documentation

API reference

Publisher

verified publisherfluera.dev

Weekly Downloads

Document-grade infinite-canvas SDK for Flutter — layered scene graph, selection + transform, lasso, image annotations, text, keyboard shortcuts.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#canvas #whiteboard #infinite-canvas #notes #graphics

License

MIT (license)

Dependencies

file_selector, flutter, meta, vector_math

More

Packages that depend on fluera_canvas