fluera_canvas 0.10.4
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.
Changelog #
0.10.4 (Final FontWeight.index leftover — pana 160/160 — 2026-04-27) #
- One last
FontWeight.indexwas hiding inlib/src/export/binary_canvas_format.dart:286— pana flagged it even after the 0.10.3 sweep that fixed the other two callsites indigital_text_element.dart. Migrated toFontWeight.values.indexOf(...)(returns the same 0..8 enum position, but doesn't go through the deprecated getter, so the on-disk binary layout is byte-for-byte unchanged — old.fcvfiles keep loading verbatim).
0.10.3 (FontWeight migration + README rewrite — pana 160/160 — 2026-04-27) #
-
README rewritten for dev-voice clarity — dropped 3 obsolete "What's new in 0.7.2 / 0.8.0 / 0.9.0" sections (CHANGELOG already carries the per-release detail), removed duplicated "Drop-in toolbar" section (merged into Common recipes), pruned outdated FAQ entries (the 0.3.0 humps regression note, the OnBackInvokedCallback Android warning), fixed three coherence bugs (
simplifyEpsilondefault contradiction, "0.4.0 features" labels, stale pana / test counts inline). Added a compact TOC at the top, replaced the Notability / Goodnotes / Miro / Figma framing with concrete technical claims. README is now ~720 lines (was 876) and reads end-to-end without scrolling past 200 lines of release archaeology. -
Migrated
FontWeight.index→.valueindigital_text_element.dart(DigitalTextSpan + DigitalTextElement encoders). The.indexAPI was deprecated by Flutter in favour of the recommended.value. Pana's analyzer ignores in-line// ignore: deprecated_member_usecomments, so the previous suppression cost 10 pts on Static Analysis. -
Backward-compat preserved via the new
_fontWeightFromJsondecoder helper: it recognises both legacy.index(range 0..8) and modern.value(range 100..900) ranges by sniffing magnitude — the two ranges don't overlap, so the disambiguation is unambiguous. FCV files written by 0.10.x and earlier load identically. -
Pana score: should now reach 160/160 (Static Analysis 50/50 recovered).
0.10.2 (Dartdoc sweep — full coverage for pana — 2026-04-27) #
public_member_api_docsenabled. A 1068-hit sweep added a one-line///stub to every public class / enum / mixin / typedef / field / method / getter / setter / constructor that lacked one. Many stubs are intentionally generic (/// Field \xyz`.`) — they satisfy the dartdoc lint and recover the pana documentation category points without pretending to be hand-written prose. Hand-written docs already present (top-level types, public widgets, recipes) are untouched. Dart format clean, 0 analyzer issues.- Lint stays on: future PRs that add a public API element without
a docstring will fail
flutter analyze, preventing dartdoc regressions.
0.10.1 (Pana score polish — 2026-04-27) #
- Static analysis cleanup: suppressed two
FontWeight.indexdeprecation warnings indigital_text_element.dart(cannot migrate to.valuewithout breaking on-disk JSON round-trip with v0.10.0 files). Recovers the +10 pts pana was deducting. example/is now shipped in the package archive: the previous.pubignoreexcluded it explicitly, so pana flagged "No example found" (-2 pts). The platform host scaffolds insideexample/android|ios|macos|linux|windows|webstay excluded — pub.dev never builds them.- README trimmed: collapsed three commercial upsell sections
(
fluera_canvas_gpu/fluera_engine_pro/ pricing) into a single brief "Beyond the free tier" pointer to the new doc/commercial-add-ons.md. The free package's value proposition reads cleaner above the fold.
0.10.0 (Zero-config drop-in widgets — 2026-04-27) #
-
Lasso UX parity con Fluera flagship (post-0.10.0 polish):
- Tap-on-selection enters transform mode: with
tool: lassoand a non-empty selection, a pen-down inside the selection's bounding rect now enters move mode (and a pen-down on a handle starts resize / rotate) — same priority chain asCanvasTool.select. Pre-fix, every pen-down in lasso tool started a fresh lasso path and silently wiped the selection, forcing the user to manually switch tools to manipulate. Tap OUTSIDE the bbox keeps the previous behaviour: clears the old selection and starts a fresh lasso. Mirrors how the Fluera flagship app handles it (verified againstfluera_engine/.../lasso_tool.dart+_drawing_handlers.dart:448). - Stricter hit-test: dropped the
lassoBounds.overlaps(nodeBounds)catch-all from_lassoHitsNode. Pre-fix a "C"-shaped lasso whose AABB engulfed a stroke sitting in the open mouth of the "C" would mark the stroke selected even though no point of it was ever encircled. Now strict containment: a node is selected only when its centre OR any of its bounds corners falls inside the closed polygon — same convention as Photoshop / Procreate / Figma. The cheap bbox overlap is still used as an early reject to skip the expensivePath.containsray-casting on far-away nodes. - 4 new widget tests cover both behaviours
(
test/lasso_post_selection_test.dart).
- Tap-on-selection enters transform mode: with
-
FlueraSketchApp— fullMaterialAppwrapping a sketch scaffold.void main() => runApp(const FlueraSketchApp());and you have a complete drawing app: AppBar with undo / redo / clear / export-PNG popup, full toolbar with every opt-in flag wired, sensible Material 3 theme. Three preset shapes via the newFlueraSketchPresetenum:notes(Notability-style minimal),whiteboard(full kit with snap-to-grid + smart guides),signature(single pen, no zoom, no shapes — drop-insignaturepackage replacement). -
FlueraSketchScaffold— same scaffold without the wrappingMaterialApp. Drop as a route inside an existing app. OptionalonAutoSave/onAutoLoadcallbacks integrate any persistence backend (path_provider, in-memory, network, secure-storage) via a 2-second-default debounce. OptionalonExportPngsink for the AppBar export menu (falls back to clipboard data URL). -
FlueraSketch— the bare canvas + toolbar, with internaltool/color/strokeWidthstate. Drop into any Scaffold body without writing a single setState. Caller can still pass aGlobalKey<FlueraCanvasState>for advanced control. -
No new dependencies: the package stays pure Dart. Persistence remains consumer-side via callbacks (
path_providercontinues to be a dependency only of the example, not the package). -
9 new widget tests cover all three layers + every preset.
-
Demo gallery: new "Zero-config app (FlueraSketchApp)" demo with 3 tabs showcasing the three magic levels.
Upgrading from 0.5.x? See doc/migration-0.5-to-0.10.0.md for a consolidated, breaking-change-free upgrade path.
0.9.3 (Smoothing pipeline + eraser polish + PNG export overhaul + edge-case hardening — 2026-04-27) #
-
Edge-case hardening pass. Five focused fixes from a triple parallel audit:
- FCV0 reader bounds checks — every uint16 / uint32 length
field (
pointCount,layerCount, imageblobLen, layeridLen/nameLen) is now validated against the remaining buffer before allocating. Hostile or truncated files now throw a cleanFormatExceptioninstead of allocating gigabytes. renderToImageclamps:pixelRatiorejected outside[0.05, 32.0], output dimensions rejected over 16 384 px per side (GPU max). Prevents accidental 2 GB allocations from a mis-typed parameter.- Single-point stroke (pen-down + pen-up without moving) now
commits a visible "dot" stroke instead of silently vanishing.
Triggered only for
draw/ellipsetools — line / rect / shape tools keep their current behaviour. - Text editor hot-restart guard:
_scheduleDisposewraps overlay-entry removal and focus-node disposal in try/catch so a stale_activesurviving a hot restart can't crash the next editor open.
- FCV0 reader bounds checks — every uint16 / uint32 length
field (
-
PNG export overhauled — infinite-canvas-aware.
renderToImagegoes from a single WYSIWYG-of-the-viewport API to four explicit bounds modes via the newFlueraExportBoundsenum. The legacyrenderToImage(width: w, height: h)signature still works unchanged (defaults tobounds: viewport).viewport(default) — what the user sees on screen, baked camera. Same as 0.5.0+.allContent— union of every visible node'sworldBounds. Output dimensions auto-computed from content size ×pixelRatio, so the export always covers everything the canvas holds, even nodes off-screen. Returns a 1×1 sentinel for empty canvases.selection— bounds of the current selection. Sentinel when nothing selected.custom— explicitRectin world coords (passed via the newregion:parameter). Plus:pixelRatio(HiDPI / print export),padding(world-px margin around the bounds),transparent(skip background fill + pattern). NewFlueraCanvasState.contentBoundsWorldandselectionBoundsWorldgetters expose the bounds math standalone for "fit to content" camera animations. Critical bug-fix in the same patch: the legacyrenderToImageiterated_strokesonly and silently dropped image, text, shape and group nodes — a freshly-imported sticker or typed text would not appear in the exported PNG. The new polymorphic walker (mirrored from the on-screen committed painter) honours everyLayerNodechild type.
-
Pixel eraser pressure-aware + velocity-aware + micro cleanup. Three additive UX improvements mutuated from desktop / iPad pen-tablet apps (Procreate, Photoshop, Notability):
- Pressure-aware radius: the eraser circle now scales linearly
between 40 % (precision / soft touch) and 100 % (firm press) of
the nominal
eraserRadius, modulated by the live pen pressure. Pressure 1.0 (the default for finger / non-pressure-aware pointers) preserves the pre-0.9.x behaviour exactly. - Velocity-aware sub-stamp growth: when the cursor jumps far between samples (slow input pipeline / fast drag), intermediate stamps along the swept segment are inflated by up to 1.4× to cover any residual gaps after the half-radius sub-stepping. The terminal stamp (= the actual pointer position) is never inflated, so the visible eraser circle still matches the cursor exactly.
- Micro-survivor cleanup:
splitAroundCirclenow drops post-cut fragments whose total arc length is < 3 world-px. These imperceptible "dot" residues used to litter the scene graph, costing a spatial-index entry and a layer-cache invalidation each.
- Pressure-aware radius: the eraser circle now scales linearly
between 40 % (precision / soft touch) and 100 % (firm press) of
the nominal
-
Pixel eraser repaints in real time during continuous drag.
_onDrawUpdatenow calls_eraseAt(world)BEFORE_commitTick.notify()so the painter reads the freshly-mutated_strokeslist (and the invalidatedLayerPictureCacheentry) on the very next frame. Previously the notify was scheduled against a stale snapshot and on Impeller-Vulkan could be coalesced one stamp behind, making the eraser feel "frozen" mid-drag even though the underlying model was correct. -
Live and committed strokes now share identical geometry.
simplifyEpsilondefault changed from0.5to0— the committed stroke now uses the same raw point list the live painter renders during the drag, so what the user sees while drawing is exactly what gets persisted (no shape change at pen-up).simplifyEpsilon: 0.5remains a valid opt-in for consumers that want the 40-60% point reduction for compression / RAM. -
Stroke smoothing rebuilt to match the fluera_engine pipeline. Four stages, each addressing a different artefact:
- OneEuroFilter at point ingest — every raw stylus sample is
run through an adaptive velocity-aware low-pass filter
(
minCutoff = 1.0,beta = 0.007, the same constants the commercialfluera_engineships with) the moment it lands in the live notifier. Tight smoothing at low speeds kills the sub-pixel tremor every digitiser produces when writing slowly; loose filtering on fast strokes preserves input fidelity. Reinitialised at every pen-down so each stroke starts clean. - Adaptive arc-length subdivision via Catmull-Rom interpolation. Any segment longer than ~4.5 world-px is subdivided into anchors spaced ~3 px apart, with each new anchor computed via the Catmull-Rom parametric form on the four-neighbour window (p[i-1], p[i], p[i+1], p[i+2]) — NOT linear interpolation. Linear inserts would be collinear with the original samples and the EMA pass below would leave them on the line, so the final cubic-bezier still degenerated into the very polyline this stage exists to remove. Spline-interpolated inserts already lie on a curve. Capped at 12 inserts per gap to bound cost on extreme pen-up/down jumps.
- Two-pass EMA pre-smoothing (forward + backward, alpha 0.3, endpoints pinned) over the resampled point list as a final polish before path construction.
- Predicted "ghost" tail anchor. The Catmull-Rom formula
uses
p3to compute the exit tangent at every interior segment. For the terminal segment we used to clampp3to the last real sample, which made the closing tangent flat — the curve would aim at the endpoint along the chord, looking polygonal. We now extrapolate one step ahead via velocity + half-acceleration of the last three samples and use that as a virtualp3. The predicted point is not drawn — the bezier still terminates exactly at the real endpoint — only the tangent direction at the last anchor changes. Materially reduces the perceived latency on platforms (Linux desktop) where the pointer pipeline samples slowly. - Catmull-Rom → Cubic Bezier with tau = 1/6. Control points
are
C1 = P1 + (P2 - P0) / 6andC2 = P2 - (P3 - P1) / 6— the same constant fluera_engine uses for its fountain-pen outline reconstruction. Tighter than the previous tau = 0.5, so the spline hugs the smoothed points instead of flattening into visibly straight segments when input samples are dense. Affects every free-form stroke (live + committed); shapes (line / rect / ellipse) still use straightlineTosegments since their corners must stay sharp.
- OneEuroFilter at point ingest — every raw stylus sample is
run through an adaptive velocity-aware low-pass filter
(
-
Inline text editor: tap-outside catcher is now opaque. A tap on empty canvas while the editor is open commits + closes the overlay WITHOUT propagating the tap down to
FlueraCanvas(which previously treated the same tap as "open a new TextNode here", spawning a fresh empty editor on top of the just-committed one — making the original text appear to "disappear"). -
Keyboard shortcuts no longer hijack text editing. Backspace, Delete, Ctrl/Cmd+A, Ctrl/Cmd+D and the arrow nudges are now short-circuited while
FlueraTextEditor.isEditing == true, so Backspace edits the buffer, arrows move the caret, and Ctrl+A selects within the text — instead of clearing the canvas / nudging a selection / select-all-on-canvas. -
Sticker drops land inside the canvas viewport.
FlueraStickerPanelnow anchors the newImageNodeatstate.viewportCenterWorld(newly-exposed getter onFlueraCanvasState) instead of atMediaQuery.size / 2. On phones / bottom-sheet UI the screen centre often fell underneath the open sticker panel, so the dropped image was off-canvas and looked like the tap did nothing. -
New public API:
FlueraCanvasState.viewportSizeandFlueraCanvasState.viewportCenterWorld. Useful for any consumer overlay that needs to drop something at the centre of the canvas viewport (sticker / image / annotation / chat-bubble) without reaching forMediaQuery. -
Code hygiene: removed
final ctrl = state.controllerdead local inFlueraStickerPanel._commit.
0.9.1 (Bug fix pass post-smoke-test — 2026-04-27) #
- Text editor: Enter now inserts a newline instead of committing.
Removed
onEditingCompletefrom the multilineTextField. Commit still fires on focus-out (tap-outside / IME dismiss) and on Esc. Previously the soft-keyboard "Return" closed the overlay before the user could type a second line. - Lasso hit-test now bounds-aware + live preview rendering.
Selection includes any node whose centre, any bounds corner falls
inside the lasso path, OR whose bounds rect overlaps the lasso's
own bounds — catches large nodes that "span" the lasso. The
in-progress lasso path is now rendered live as a magenta dashed
polyline with translucent fill (Photoshop-style "marching ants")
via
SelectionPainter.lassoPath. - Pixel eraser: sub-stepping for fast drags.
_eraseAtnow interpolates intermediate stamps along the swept segment between two pointer samples (half-radius cadence). Previously, fast drags left "dot trail" gaps because points of the underlying stroke that fell between two pointer samples were never tested against the eraser circle. Stroke-mode and pixel-mode both benefit. - Sticker demo fix.
_TextStickerDemonow useskFlueraDefaultStickers(the package's built-in 8 Material-icon catalogue) instead of aNetworkImageSVG thatNetworkImagecannot decode. The package itself was always correct;sticker_panel_test.dartconfirms it. - Doc clarity. Programmatic + Selection demo subtitles rewritten to spell out the use cases (replay / scripted animation / data-driven art for Programmatic; scattered vs grid selection for Lasso vs Select). README "Lasso free-form selection" recipe gained a "when to use" guidance line.
- Code hygiene. Removed direct
import 'dart:io' show Platformfromfluera_canvas_widget.dart; all three call sites now use the web-safePlatformGuardhelper. Forward-compat with WASM strict mode wheredart:iois unavailable at link time.
0.9.0 (Perf wire-ups + lasso + keyboard + duplicate / Z-order + snap-to-grid + smart guides — 2026-04-26) #
Pre-publish hardening (post-0.9.0 spike, all five "bloccanti tecnici" addressed):
- A —
dart:ioconditional import —fluera_file_export_servicenow usesimport '_file_reader_stub.dart' if (dart.library.io) '_file_reader_io.dart'so web builds compile without the unreachableFile()symbol leaking into the JS module. Asset embedding gracefully falls back to path-only on web. - B — Live stroke chunked
PictureRecordercache —_LiveStrokeNotifiernow bakes every 256 in-progress points into aui.Picturechunk viamaybeChunkAhead. The painter replays onedrawPictureper chunk and only re-tessellates the trailing uncached tail. Long handwriting strokes (5 k+ points) drop from O(N) per-frame work to O(N/256). Chunks are disposed onclear()/setStroke()/ newbeginStroke(). - C — Multi-layer LayerPictureCache consumption — the
multi-layer painter's
paintLayer(c)closure now consultsLayerPictureCache.get(layerId, version)first; on miss it records the entire layer's foreground (children only — opacity / blend / mask wrap it from the outside) into a freshui.Picture, stores, and replays. Every layer-state mutator (setLayerOpacity,setLayerVisible,setLayerLocked,setLayerBlendMode,setLayerMask,setLayerFlueraBlendMode) now bumps the layer version. Cache disabled when_nodesWithTransform > 0(the snapshot would freeze stale world bounds). - D — Default sticker catalogue —
kFlueraDefaultStickersis no longer empty. Eight Material-icon stickers (star, heart, check, close, arrow, idea, flag, pin) ship via the newFlueraIconStickerProvider— anImageProviderthat renders anIconDatato aui.Imageon demand viaTextPainter. Zero asset bundling, zero licensing concerns. Override withstickers: const []to opt out. - E — Backgrounds demo — new
_BackgroundsDemoin the example gallery showcases the four built-inCanvasBackgroundpatterns (solid / grid / dotted / lined) with live toggle from the AppBar.
LayerPictureCache consumption (post-0.9.0 spike): the
single-layer fast path now actually consumes the cache infrastructure
wired in earlier. When _nodesWithTransform == 0 (the >99% case)
the painter records the entire layer's children once into a
ui.Picture, caches it keyed on (layerId, layerVersion), and
replays via a single drawPicture on every subsequent paint until
a mutation bumps the version. Pan / zoom never bumps the version,
so camera moves over a static scene drop from O(N strokes) per
paint to O(1). Multi-layer / non-trivial-blend paths fall back to
the existing per-paint walk. Live-stroke append-only via
PictureRecorder chunks remains deferred — empirical perf shows
the current _paintStrokeSegments single-draw path is already
sub-100µs for typical (<500-pt) strokes; the chunking refactor's
ROI is too small for the implementation cost.
Web / WASM smoke test (post-0.9.0 spike): flutter build web
on the example compiles cleanly + WASM dry-run succeeds. New
test/web_compat_test.dart exercises every public API surface
that a web consumer would touch (commit stroke, persistence
round-trip, selection, transform, group/ungroup, duplicate,
lasso/snap props) without dart:io. Run with
flutter test test/web_compat_test.dart -p chrome for true
in-browser execution. All Platform.* reads in the package are
already gated by kIsWeb || try/catch, and the one dart:io
File() call site (asset embedding in the Pro file-export
service) is wrapped in try/catch so web hosts gracefully fall
back to path-only embedding.
Group / ungroup (post-0.9.0 spike): treat several selected
nodes as one rigid bundle. New API on FlueraCanvasState:
state.groupSelection() → NodeId?— wraps 2+ selected nodes (sharing the same parent layer) inside a fresh [GroupNode], inserted at the position of the front-most member. Selection swings to the new group; the group is now the unit of selection / drag / rotate / scale / delete. Returns the group's id, ornullfor empty / single-node / cross-layer selections.state.ungroupSelection() → int— for every selected [GroupNode], moves its children back to the parent layer at the group's slot (preserving relative Z), then removes the group. Selection swings to the freed children. Returns the count of groups dissolved.- New history ops
_GroupOp+_UngroupBatchOpso a single undo restores the pre-mutation tree exactly (including the original child indices and selection).
_rebuildSelectableIndex updated: GroupNodes register as
selectable units; their children are inert (not in the index)
until ungrouped — matches Figma / Sketch convention. LayerNode
remains a pure container.
Snap-to-grid + smart guides (post-0.9.0 spike): Figma-style
alignment during body-drag move. Two new FlueraCanvas props:
snapToGrid(double, default0) — when > 0, the dragged selection's closest of 9 anchors (4 corners + 4 edge midpoints + centre) snaps to the nearest grid multiple within the tolerance band. Pure-Dart, zero deps.smartGuidesEnabled(bool, defaultfalse) — when true, the drag scans every other visible selectable node and aligns the dragged anchor to the closest matching anchor on a nearby node (corners / edges / centre — 9 candidates per node). Magenta dashed guide lines render on the selection painter while the snap is locked.smartGuidesTolerancePx(double, default6) — logical-px band for both snap modes. Scales with camera zoom so the snap "feels" the same at every level.
Holding the axis-lock modifier (Shift) during the drag bypasses both — a clean override path for pixel-precise positioning.
0.9.0 (Perf wire-ups + lasso + keyboard + duplicate / Z-order — 2026-04-26) #
Free-tier "imbattibile su pub.dev": 0.9.0 cables the performance optimisers that 0.8.x shipped but never called from the main flow, plus closes the remaining feature-completeness gap against design-tool / note-taking competitors. No PRO migration — everything stays free.
Perf wire-ups
PostStrokeOptimizer(Douglas-Peucker) called on every pen-up before commit. NewsimplifyEpsilonprop (default0.5) trims 40-60 % of redundant points without visual difference; pressures stay aligned because the simplifier returns kept indices rather than re-sampled points. SetsimplifyEpsilon = 0to opt out.DirtyRegionTrackerinstance perFlueraCanvasState. Every mutation site (_internalInsertStrokeAt,_internalRemoveStroke,_writeLocalTransform) marks its world bbox dirty. Painter consumption is scheduled for 0.9.1 once the multi-layer composite path (saveLayer + GPU compositor + layer mask) is refactored to clip safely.LayerPictureCacheinstance + per-layerMap<NodeId, int>content version. Bumped on every layer mutation. Public viastate.layerContentVersion(@visibleForTesting) for GPU consumers. Cache consumption inside the painter scheduled for 0.9.1 — same gating as the dirty tracker.WidgetsBindingObserver.didHaveMemoryPressure()wired: dropsLayerPictureCache, resets the dirty tracker, disposes every stroke'sui.Picture. Image-bytes cache preserved (re-decoding would stall the gesture loop).
Feature completeness
state.duplicateSelection({offset})— clones every selected node viacloneInternal(), offsets each byoffset(default 20×20 world px), replaces the selection with the clones. Single undo step.state.bringToFront(id)/state.sendToBack(id)— Z-order tweaks within the parent layer's children list. New_ReorderChildOphistory op preserves the original index for exact undo.CanvasTool.lasso— free-form selection. Drag a path; on pen-up every selectable node whoseworldBounds.centerfalls inside the closed polygon is selected. Concave shapes are honoured (vs the rectangular marquee). Toolbar opt-in viashowLassoTool: true.- Keyboard shortcuts overhaul. Delete/Backspace becomes
selection-aware (clears selection if non-empty, else falls back
to the legacy "clear all"). New shortcuts:
- Esc — clear selection
- Ctrl/Cmd+A — select all on visible+unlocked layers
- Ctrl/Cmd+D — duplicate selection
- Arrow keys — nudge selection by 1 world px (Shift = 10) Existing Ctrl/Cmd+Z / Y / Shift+Z preserved.
Benchmark suite
test/benchmarks/perf_baseline_test.dart— 5 k stroke insert, 10 k hit-test on 5 k-stroke scene, 5 k-point stroke push. Numbers stamped to stdout in[bench] name=usformat for CI scraping.
Migration impact
- 0.8.x → 0.9.0: zero breaking changes.
simplifyEpsilondefault is conservative; opt-out via0. New keyboard shortcuts are opt-in via the existingenableKeyboardShortcuts: trueflag.CanvasTool.lassoappended to the enum (not inserted).
Tests: 274 / 274 green. Pana: 160 / 160. 0 analyze issues. Phases 4 (IncrementalPaintMixin live append-only) and 7 (RTree update on transform) deferred to 0.9.1 — current behaviour is correct via the existing fallback paths.
0.8.0 (Text tool + Sticker panel — 2026-04-26) #
Live text editing on the canvas — tap empty canvas with the text
tool to drop a fresh TextNode and open a Material TextField
overlay caret-positioned over it; tap an existing text node to
re-enter editing. The overlay rides the camera (pan / zoom) while
the user types. Commit on Done / blur / Esc; empty-text commit on
a fresh node rolls back the addition. Single undo per editing
session.
Sticker / emoji / clip-art panel — FlueraStickerPanel widget
that browses a List<FlueraSticker> thumbnail catalogue and
commits the chosen sticker as an ImageNode centered on the
viewport. Reuses the cached ui.Image per sticker id, so the
same sticker dropped twice shares the GPU handle.
Canvas-level text APIs
FlueraCanvasState.addTextNode(node)— appends a TextNode to the active layer with_AddLayerChildOphistory.FlueraCanvasState.updateTextElement(id, element)— in-place swap of the wrappedDigitalTextElement, pushes a single_UpdateTextOpso undo restores the previous text exactly.FlueraCanvasState.findNode(id)— public lookup into the selectable index without going through the test-only debug getter.
FlueraTextEditor
start(state, existing: ..., worldPosition: ...)opens an overlay editor on the target node (or mints a fresh one).commit(state)/cancel(state)close the session. Tap-outside / focus loss / Done all funnel through commit.- Uses
OverlayEntrymounted on the root overlay so it sits above any custom canvas chrome.
CanvasTool.text — new enum value, wires through the gesture
detector: tap empty canvas → fresh text node + edit; tap text node
→ edit existing.
FCV0 v6 — text-node persistence
- New
nodeType=1for text. Layout:nodeIdLen + nodeId + jsonLen + jsonBytes + localTransform. JSON-in-binary so futureDigitalTextElementfield additions don't bump the format version. - Reader: v6+ decodes nodeType=1; v5 readers throw cleanly on it (forward-incompat). v5 / v4 / v3 / v2 / v1 backward-read preserved.
Painter dispatch
- The committed-painter walks
layer.childrenand dispatches per type — strokes via_drawStrokeWithTransform, images viaImageNodePainter.paint, text via the cachedTextPainteronDigitalTextElement.layoutPainter. Z-order is insertion-order (chronological) on both the single-layer fast path and the multi-layer slow path.
Toolbar flags
showTextTool: bool = false— adds aTextsegment to the segmented control.showStickerPanel: bool = false+stickers: List<FlueraSticker>— adds an emoji-emotions IconButton that opens the sticker panel in a Material bottom sheet.
Tests: 252 / 252 green. Pana: 160 / 160.
0.7.2 (Image annotations + OBB selection + perf — 2026-04-26) #
Write directly on top of an image, with strokes that follow the
image when you move / rotate / scale it. New mental model:
ImageNode is now a container for stroke "annotations" stored in
image-local coordinates. A stroke that lands inside the image at
pen-up gets rerouted from the active layer onto the image's
annotations list; a stroke that crosses the boundary is split
segment-by-segment so the inside parts ride the image while the
outside parts stay free children of the layer. Move / rotate / scale
the image and every annotation transforms with it as a rigid block.
Mirror H/V flips both. The split is only active for the freehand
draw tool — line / rectangle / ellipse keep emitting a single
rigid figure regardless of overlap.
Image annotations
ImageNode.annotations: List<CanvasStrokeNode>— strokes parented to the image, stored in its local coord system.ImageNodePainter.paintwalksnode.annotationsafterdrawImageRect, inside the same transform stack the bitmap uses, so annotations inherit the image'slocalTransform+ per-elementposition / rotation / scaleautomatically.- New
_SplitStrokeOphistory op — single undo step for a stroke whose pen-up gesture got fragmented into free + image-parented pieces. - Stroke-mode AND pixel-mode eraser walk
image.annotationstoo: the probe is projected into image-local coords via_ImageHitFrame.worldToLocalso the cut hits the right pixels even on rotated / scaled images. Undo restores the originals.
FCV0 v5 — image annotations persistence
- Bumped binary format from v4 → v5. Image payload gains a trailing
annotationCount: uint32+ array of stroke payloads (in image-local coords). - v4 / v3 / v2 / v1 readers untouched. New writers always emit v5.
OBB selection frame for rotated nodes
CanvasSelectioncarriesframeRect+frameTransform. Single rotated node → frameRect isnode.localBounds, frameTransform isnode.localTransform; the selection painter draws the outline throughcanvas.transform(frameTransform)so it visually rotates with the image. Handles ride the world-transformed corners but stay axis-aligned + 12 px screen-space.- Hit-test handle and body-drag containment project the world pointer into the OBB local frame, so dragging a corner of a rotated image scales along the image's own axes (not the screen's).
- Multi-node selections fall back to
frameTransform = identityandframeRect = bounds— bit-for-bit the pre-OBB behaviour.
Hit-test transform-aware
_transformedNodeIdstracks every node with non-identitylocalTransform. The RTree (which indexes pre-transform bbox) skips them and a transform-aware pass intersects each node'sworldBoundsinstead. Strokes moved with the selection tool now re-select correctly at their new visual position.
Painter Z-order fix
- The committed-painter walks
layer.childrenin insertion order and dispatches per type (stroke vs image) instead of the previous two-pass loop (all strokes, then all images). Strokes drawn after an image are now correctly rendered on top.
Performance optimisations
- A — incremental
_maxZcounter:_registerSelectablestamps Z in O(1) instead of folding over_zOrderIndex.valuesper insert. - B —
_nonStrokeSelectableIdsset: hit-test and onEvicted iterate only the non-stroke subset (typically <100) instead of every selectable node. - F —
_nodesWithTransformcounter:_drawStrokeWithTransformshort-circuits the per-stroke node lookup when no node carries a non-identity transform (>99% common case on notes-app workloads).
Toolbar wiring
- Multi-canvas + autosave demo now exposes the full 0.6.0 / 0.7.0
toolbar feature set:
showSelectionTool,showImageTool,showTransformActions,showLayers.
Pana score: 160 / 160. Tests: 249 / 249 green.
0.7.1 (Image persistence FCV0 v4 — 2026-04-26) #
Imported images now survive save / reload. In 0.7.0 ImageNode
became selectable + transformable but toBytes() skipped them
silently — close the canvas, reopen, the image was gone. 0.7.1 ships
the FCV0 v4 binary format that carries image asset bytes alongside
the scene graph.
Bytes-aware image cache
ImageNodePainter.cacheWithBytes(path, image, bytes)registers the original encoded bytes (PNG / JPG / WebP / …) parallel to the decodedui.Imagehandle.ImageNodePainter.decodeAndCache(path, bytes)runs the codec asynchronously and registers both at completion. Used by the serializer's load path.ImageNodePainter.bytesFor(path)for save-time readback.
FCV0 v4 — image-node payload
- New
nodeType=2on layer-children. Image payload: NodeId, imagePath, position, scale, rotation, opacity, intrinsic size, 16-elementlocalTransformstorage,bytesLen + bytes. - Reader:
DecodeResult.imageBlobs: Map<String, Uint8List>carries the raw bytes; widget kicks offdecodeAndCachefire-and-forget per blob and triggers a repaint when each codec resolves. - ImageNodes whose source bytes were never registered are skipped silently at save time.
Image tool wiring
FlueraImageTool.commitBytesnow callscacheWithBytesso every imported asset round-trips throughtoBytesautomatically.
0.7.0 (Enterprise selection on ImageNode — 2026-04-26) #
Selection / transform / delete generalised across the scene
graph. In 0.6.0 the selection pipeline was hard-coded to
CanvasStrokeNode: tap on an image fell through to clear, marquee
skipped images, transform handles were impossible to invoke,
delete / mirror were no-ops. From 0.7.0 the entire pipeline operates
on CanvasNode — strokes, images, and any future text / shape
addition just plug in.
Phase 1 — _selectableNodes + _zOrderIndex foundation
- New unified index
Map<NodeId, CanvasNode> _selectableNodes— source of truth for every selectable scene-graph node. - DFS-order Z stamp in
Map<NodeId, int> _zOrderIndexfor front-most-wins on stroke / image / mixed hit-test. - Populated by every mutation site:
_internalInsertStrokeAt,_internalRemoveStroke,addImageNode,_AddLayerChildOp,_RemoveLayerOp,_AddLayerOp,removeLayer,_replaceWithLayers(loadFromBytes / initialBytes).@visibleForTestingdebugSelectableIds/debugZOrderIndexgetters for assertions.
Phase 2 — Hit-test unified across stroke + image
_hitTestStrokeNode→_hitTestNode: strokes via the existing RTree spatial-index (fast path), non-stroke nodes via linear scan of_selectableNodes(typically <100 entries — trivial cost)._hitTestIdsInRect(marquee) gets the same treatment, intersecting viaworldBounds.overlaps(rect).select(NodeId?)now validates the id against_selectableNodes(O(1)) so passing anImageNode.idsucceeds — previously returned false. API shape unchanged, semantics extended.
Phase 3 — _selectionFromIds + bounds refresh on CanvasNode
- The bounding-rect accumulator iterates the unified index and reads
node.worldBoundsinstead of the stroke-onlyentry.key.bounds. - Side fix: stroke nodes whose
localTransformwas non-identity used to surface the pre-transform bbox to the painter, drawing the selection frame in the wrong place. Now transform-aware.
Phase 4 — Transform pipeline on CanvasNode
_transformTargetswidened fromList<CanvasStrokeNode>?toList<CanvasNode>?— body was already type-agnostic._beginTransform,mirrorSelection(Axis),_TransformNodesOp._applyall walk the unified index. Mirror H/V on anImageNodeflips itslocalTransformand the result is picked up byImageNodePainter(which already honourednode.localTransformsince 0.6.0).- Drag handles, body-drag move, rotate handle, and mirror buttons now operate on stroke / image / mixed selections.
Phase 5 — _DeleteNodesOp + onNodesDeleted callback
- New op
_DeleteNodesOp(List<_DeletedNodeSnapshot>)replaces_EraseOpfor the publicdeleteSelection()path._EraseOplives on for the pen-eraser flow. - Each snapshot stores
node,layerId,childIndex, and for strokes the originalflatStrokeIndex. Undo restores the originalCanvasStrokeNode(preserving its NodeId), the_strokesmirror at the original Z, the spatial index, and the parent layer's children at the original child index. - New optional callback
FlueraCanvas(onNodesDeleted: void Function(List<CanvasNode>)?)fires for every removed node (mixed types). Backwards-compatonStrokesErasedkeeps firing in parallel with the stroke-only subset; image-only deletions skip it (no strokes were touched).
Phase 6 — Auto-clear selection on draw start
- Notability / Goodnotes-style modal UX: switching to draw / line / rectangle / ellipse and starting a stroke clears the active selection automatically. Consumer can select an image, switch to draw, and write on top without manually clearing first.
- Eraser tools (erase / erasePixel) are exempt — they don't visually overlap the selection frame.
Phase 7 — Version bump
pubspec.yaml0.5.0 → 0.7.0. The 0.6.x series internally rolled layer / selection / transform / image / edge-pan work; 0.7.0 is the first publish of the consolidated set.
Migration
- Public API shape of
select/selectInRect/mirrorSelection/deleteSelection/selection.ids/selectionListenableis unchanged. Behaviour extended: ImageNode (and any future CanvasNode subclass) responds to all of them. - New optional
onNodesDeletedcallback — purely additive. - New
ImageNodeinvariant documented:imageElement.scale / rotation / position= scala iniziale dell'asset;localTransform= mutazioni del transform tool. Sono componibili:worldTransform = localTransform,worldBounds = worldTransform × localBoundsle ingloba entrambe.
Tests
- 22 new across
selectable_nodes_index_test,hit_test_image_test,selection_bounds_image_test,transform_image_test,delete_selection_image_test,draw_clears_selection_test. Suite ~233 → 255 green.flutter analyze --no-pub lib: 0 issues.
Known TODO (Phase G, deferred)
- FCV3 image serialization (
nodeType: 2, bytes embedded vs path-only). Image nodes survive an in-memory undo / redo round-trip but.fluerasave / load drops them. ~2-3 days of work, decision point: bytes-embedded vs asset-path indirection.
0.5.0 (Phase 6 monorepo convergence — 2026-04-26) #
Canvas-core consolidation: ~14 classes absorbed from the private
fluera_engine fork into the public surface. Fully additive — no
breaking changes from 0.4.0; all newly exported types are net-new to
consumers.
This release rolls up the four 0.4.0+1…0.4.0+4 internal markers into
a single semver-bumped publish. The split between free fluera_canvas
and commercial fluera_canvas_gpu is now stable: GPU shader brushes
ship in the latter, vector / Dart-fallback rendering in this package.
Newly exported (Phase 6.A — canvas painters & filters)
IncrementalPaintMixin—CustomPaintermixin that uses aDirtyRegionTrackerto clip canvas paints to dirty bounds (10–100× faster repaints for local mutations).BackgroundPainter,BackgroundImagePainter,FullScreenDarkOverlayPainter— viewport-level infinite-canvas background painters.PointSimplifier— Douglas–Peucker stroke point simplifier for LOD rendering.BallpointBrush,HighlighterBrush,MarkerBrush— base brush engines (advanced brushes such as charcoal, watercolor, fountain pen remain influera_engine/fluera_canvas_gpu).
Newly exported (Phase 6.B — pools, caches, stabilizer)
PathPool,PathPoolStatistics— reusablePathobject pool to reduce per-frame allocations during stroke rendering.StrokePointPool,PoolStatistics—Offsetpool for stroke points.StrokeStabilizer— Procreate / Clip Studio-style 3-stage smoother (string pulling + weighted moving average + corner detection). TheelasticEnabledconstructor parameter lets hosts toggle the velocity-driven string-length and easeInOut catchup curve.StrokeCacheManager— vectorial stroke cache with O(1) undo snapshot ring buffer.LayerPictureCache— LRUPicturecache for per-layer rendering.SnapshotCacheManager— incremental scene-graph snapshot cache.
Newly exported (Phase 6.B finalisation — render interceptors)
RenderInterceptorchain (base class +RenderNexttypedef +DebugBoundsInterceptor+NodeFilterInterceptor).RenderProfilingInterceptorstays influera_enginebecause it depends on the engine's telemetry bus.
Sibling-package change (informational)
- The GPU shader pipeline that previously lived inside the private
fluera_enginehas been extracted to the commercialfluera_canvas_gpupackage. Engine integrators now consumeShaderBrushServicefrompackage:fluera_canvas_gpu/fluera_canvas_gpu.dart. The freefluera_canvascontinues to render brushes via its Dart fallback path. Apps that want the GPU shader brushes (pencil, fountain pen, watercolor, charcoal, oil paint, marker, spray paint, neon glow, ink wash, brush stamp, texture overlay) need bothfluera_canvasandfluera_canvas_gpu.
Tests
- Test suite grows from 49 to 108 green tests with the addition of
test/drawing/object_pools_test.dart,test/rendering/incremental_paint_test.dart,test/rendering/layer_picture_cache_test.dart, andtest/rendering/snapshot_cache_node_test.dart.
Breaking — none. The public API of 0.4.0 is preserved.
0.4.0+4 (Phase 6.B/6.A finalisation — 2026-04-25) #
One additional class extracted from fluera_engine: the RenderInterceptor
chain (base class + RenderNext typedef + DebugBoundsInterceptor +
NodeFilterInterceptor) is now part of canvas. RenderProfilingInterceptor
stays in fluera_engine because it depends on the engine's telemetry bus.
This finishes the immediate block of the monorepo convergence (Phase 6.A
shaders + 6.B optimization + 6.C input/pool). The remaining 9 LEGACY-FORK
files in engine are genuine canvas-core forks that require Enterprise-tier
extraction (Phase 6.D / engine_pro) before they can move; that work is
gated on the first Enterprise lead.
0.4.0+3 (Phase 6.A monorepo convergence — 2026-04-25) #
No public API change in fluera_canvas itself. This release is a marker for
a sibling-package change: the GPU shader pipeline that previously lived inside
the private fluera_engine has been extracted to fluera_canvas_gpu
(commercial). Engine integrators now consume ShaderBrushService from
package:fluera_canvas_gpu/fluera_canvas_gpu.dart.
The free fluera_canvas continues to render brushes via its Dart fallback
path. Apps that want the GPU shader brushes (pencil, fountain pen, watercolor,
charcoal, oil paint, marker, spray paint, neon glow, ink wash, brush stamp,
texture overlay) need both fluera_canvas and fluera_canvas_gpu.
0.4.0+2 (Phase 6 monorepo convergence — 2026-04-25) #
More canvas-core absorbed from fluera_engine fork.
Pure additive: 6 new classes from the monorepo's engine fork are now part of the public canvas surface. No breaking changes.
Newly exported:
PathPool,PathPoolStatistics— reusablePathobject pool to reduce per-frame allocations during stroke rendering.StrokePointPool,PoolStatistics—Offsetpool for stroke points.StrokeStabilizer— Procreate / Clip Studio-style 3-stage smoother (string pulling + weighted moving average + corner detection). TheelasticEnabledconstructor parameter lets hosts toggle the velocity-driven string-length and easeInOut catchup curve.StrokeCacheManager— vectorial stroke cache with O(1) undo snapshot ring buffer.LayerPictureCache— LRUPicturecache for per-layer rendering.SnapshotCacheManager— incremental scene-graph snapshot cache.
Internal: continues monorepo convergence (Phase 6 / Blocco Immediato 6.B+6.C).
Engine lib/src/{drawing/input, drawing/filters, rendering/optimization}
shrank by 6 files. See fluera_engine/docs/MIGRATION_TRACKER.md.
0.4.0+1 (Phase 2 monorepo convergence — 2026-04-25) #
Canvas-core absorbed from fluera_engine fork.
Pure additive: 7 canvas-core classes that previously lived in the private
fluera_engine fork are now part of the public canvas surface. No breaking
changes; all newly exported types are net-new to consumers.
Newly exported:
IncrementalPaintMixin—CustomPaintermixin that uses aDirtyRegionTrackerto clip canvas paints to dirty bounds (10–100× faster repaints for local mutations).BackgroundPainter— viewport-level infinite-canvas background painter.BackgroundImagePainter,FullScreenDarkOverlayPainter— additional painters previously bundled in the enginecanvas_painters.dartbarrel.PointSimplifier— Douglas–Peucker stroke point simplifier for LOD rendering.BallpointBrush,HighlighterBrush,MarkerBrush— base brushes (advanced brushes such as charcoal, watercolor, fountain pen remain engine-side, destined for futurefluera_engine_pro).
Internal: brings the monorepo one step closer to a single source of truth
for canvas / scene-graph / rendering / drawing primitives. See
docs/CANVAS_OWNERSHIP.md in the repo root for the routing rule.
0.4.0 #
Shape tools, pixel-mode eraser, color picker dialog.
Three additive feature blocks that take fluera_canvas from "best
infinite-canvas SDK on pub.dev" to "general-purpose drawing SDK".
Fully backward-compatible with 0.3.0 — no public API was removed
or changed in shape; the new functionality is opt-in.
Shape tools
CanvasTool.line— drag from A to B to commit a straight line.CanvasTool.rectangle— drag corner-to-corner for a 5-point closed rectangle outline.CanvasTool.ellipse— drag for a 32-segment ellipse outline.- All three commit as ordinary
CanvasStrokeinstances, so they participate in undo/redo, persistence (toBytes/loadFromBytes), spatial-index hit-test, and the per-strokeui.Picturecache for free.
Pixel-mode eraser
CanvasTool.erasePixel— instead of removing whole strokes that the eraser circle intersects (which is whatCanvasTool.erasedoes), this mode SPLITS each stroke around the eraser circle and keeps the surviving pieces. Original Z-order preserved.- New internal
_PixelEraseOphistory op:undore-inserts the originals and removes the survivors,redoreverses. - Per-update cost O(k · m) where k = strokes intersecting the eraser circle and m = points per stroke. Combined with the spatial-index viewport cull this stays well below 1 ms per frame for typical scenes.
Color picker dialog
- New
FlueraColorPickerDialogwidget +showFlueraColorPickerimperative helper. HSV saturation/value box + hue slider + alpha slider + hex input. Zero external dependencies. kFlueraDefaultPaletteunchanged — the dialog is opt-in for consumers that want arbitrary colours beyond the 6 presets.
Toolbar opt-in flags
FlueraCanvasToolbar.showShapeTools: false— whentrue, the segmented control gains Line / Rect / Oval buttons.FlueraCanvasToolbar.showPixelEraser: false— whentrue, adds a "Pixel" segment next to "Eraser".FlueraCanvasToolbar.showColorPickerButton: false— whentrue, a sweep-gradient+button appears at the end of the palette row and pops upFlueraColorPickerDialog.- Tool segmented control is now horizontally scrollable so 6 tool buttons + history buttons fit on narrow screens.
Tests
- 9 new tests in
test/shape_tools_test.dartcovering enum ordering, shape-stroke geometry (line / rect / ellipse), pixel eraser tool wiring, color picker dialog open/cancel/return, toolbar opt-in flags. Total 33 tests, all green.
Breaking — none. The CanvasTool enum gains 4 new values; if
your code does an exhaustive switch (tool) without a default,
you'll get analyser warnings until you handle the new cases.
0.3.0 #
Drop-in toolbar, 5k–10k stroke perf, Impeller-Vulkan profile-mode fix.
This release adds a ready-to-use Material toolbar widget, an order-of-magnitude
faster committed-stroke renderer (per-stroke ui.Picture cache + offscreen
compositing layer + smoothed quadratic-bezier paths), and a ticker-driven
workaround for a Flutter pipeline coalescing bug we hit on Android profile
mode (Impeller-Vulkan / Adreno) where mid-gesture frames were dropped.
New: drop-in toolbar
FlueraCanvasToolbar— Material widget that renders pen / eraser segmented control, color swatches, stroke-width slider, and undo / redo / clear buttons. Auto-subscribes to the canvas history listenable so the buttons reflect live state withoutsetStateplumbing.kFlueraDefaultPalette— exported 6-color preset.FlueraCanvasState.historyListenable— read-onlyListenablethat fires on every committed-strokes mutation (commit, erase, clear, undo, redo, load, tool switch, background change).- New example demo: Built-in toolbar — drop-in usage in <30 lines.
Renderer: 5k–10k strokes at 60 FPS
- Each
CanvasStrokenow lazily builds and caches aui.Pictureof its rasterised path. Repainting a committed stroke is onedrawPicturecall, not NdrawLinecalls. - Committed strokes drawn through a
RepaintBoundaryso the rasterizer blits the cached compositing layer instead of re-executing every picture per frame. - Stable painter instances (
_LiveStrokePainter,_CommittedStrokesPainter) created once ininitState— nosuper(repaint:)listener swap on every parent rebuild, which on Impeller-Vulkan was silently dropping notifications. - Strokes now drawn as a single
Pathper stroke with quadratic-bezier smoothing (each interior point is a control, the curve passes through midpoints) — eliminates polyline-kink "humps" on diagonal strokes visible at zoom-in. Stroke width is the average pressure (continuous silhouette, no pressure-band step artefacts). _CommitNotifier(internal) drives committed repaints. Consumers expose this read-only asFlueraCanvasState.historyListenable.
Persistence-friendly initial state
- New
FlueraCanvas(initialBytes: Uint8List?)parameter. The bytes (produced byFlueraCanvasState.toBytes()) are decoded and inserted into the spatial index insideinitState, BEFORE the first build / paint, so the first frame already shows the persisted strokes. This is the right way to restore a saved canvas — callingloadFromByteson 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 appears empty). - Removed the
controller.scale <= 0.5"overview guard" from [InfiniteCanvasGestureDetector] — it was an app-specific carry-over from the original Fluera codebase that prevented drawing at low zoom. The SDK now lets the consumer draw at any zoom; an overview mode can still be implemented app-side by switchingtoolto a non-draw value.
Pipeline workaround (Impeller-Vulkan / Adreno profile mode)
- During an active gesture, a vsync
TickercallssetState({})every frame so the rendering pipeline keeps producing frames. Without it, Flutter coalesces allsetState/markNeedsPaintcalls inside pointer events and only repaints at pen-up — making the live stroke and eraser preview circle invisible mid-gesture. The ticker runs only while a gesture is active (zero idle cost) and is enabled for bothdrawanderasetools, so the eraser preview tracks the pointer in real time.
Eraser preview
- Preview circle now tracks the pointer in real time during the erase gesture on touch (previously frozen until pen-up on Impeller-Vulkan).
Internal
setStatecalls inside mutators of_strokes(commit, erase, clear, push, undo, redo, replace) replaced with explicit_commitTick.notify().widget.backgroundchange indidUpdateWidgetnow triggers a committed repaint via the same notifier.disposereleases all per-strokeui.Pictureresources.
Breaking — none. All public APIs are backward compatible with 0.2.0.
0.2.0 — Unreleased #
Production-ready UX set. Tool mode, undo/redo with shortcuts, spatial index, background patterns, persistence, eraser preview, platform cursor.
CanvasToolenum (draw,erase) passed viaFlueraCanvas(tool: …).- Stroke-mode eraser: pointer removes strokes it touches (O(log n) hit-test
via an
RTree<CanvasStroke>); a whole swipe becomes one undo step. Vector-preserving — erased strokes are restored intact byundo(). eraserRadiusparam (default 24 px screen space).- Undo / redo stack with configurable
historyCapacity(default 100).FlueraCanvasState.undo() / redo() / canUndo / canRedo / historyLength / clearHistory()- Covers: committed draw, erase swipe,
clear(),pushStroke,pushStrokes.
- Spatial index wired into the default painter:
_InfiniteCanvasPainteriterates only strokes returned by a viewport-culled RTree query. Per-frame cost scales with on-screen content, not scene total. Holds 60 FPS up to ~10 000 strokes on a mid-tier Android device. strokeAt(Offset worldPoint, {tolerance})— front-most hit test.strokesInRect(Rect worldRect)— bulk query.onStrokesErasedcallback for eraser-tool observers.- Example app expanded to a 4-demo gallery: drawing tools, stress test (10 k procedural strokes), programmatic push, PNG export.
Persistence
FlueraCanvasState.toBytes()/loadFromBytes(Uint8List)— compact little-endian binary format (magicFCV0, version 1).FlueraCanvasState.toJson()/loadFromJson(String)— human-readable, diff-friendly. ~10× larger than the binary form.- Loading replaces the scene and clears the undo history.
Background patterns
- New
CanvasBackgroundwith factory constructorssolid,grid,dotted,lined. Pattern rendered in world space and anchored to the canvas origin (pan/zoom behaves like moving over real paper). - Breaking:
FlueraCanvas(background:)now takesCanvasBackgroundinstead ofColor. Migrate withbackground: CanvasBackground.solid(myColor).
Eraser preview + desktop cursor
showEraserPreview: true(default) draws a translucent circle under the pointer when [CanvasTool.erase] is active. Radius matches the live hit-test exactly.- Desktop cursor follows the active tool:
SystemMouseCursors.precisefordraw, hidden (replaced by the preview circle) forerase.
Keyboard shortcuts
enableKeyboardShortcuts: true(default) binds:- Ctrl/Cmd + Z → undo
- Ctrl/Cmd + Shift + Z → redo
- Ctrl/Cmd + Y → redo
- Delete / Backspace → clear
Breaking
undoLastStroke()removed — replaced byundo()which is history-aware (also covers erase and clear operations).FlueraCanvas(background:)type changed fromColortoCanvasBackground.
0.1.0 — Unreleased #
First public snapshot of the Fluera Engine SDK split. The following SDK
primitives are now shipped from fluera_canvas:
Canvas + gesture
InfiniteCanvasController(camera physics, dive/flight, rotation lock via injectableRotationLockPersistencehook)LiquidCanvasConfig(opaquereflowslot for consumer add-ons)InfiniteCanvasGestureDetectorwith injectablePalmRejectionPolicyStylusHoverTrackerhooks (no-op defaults)
StylusDetector,PlatformGuard,InputPredictor,RawInputProcessor120Hz
Drawing models + filters
ProDrawingPoint,PressureCurve,VelocityCurve,BrushPreset,ProBrushSettingsOneEuroFilter,AdvancedOneEuroFilter,DynamicPressureMapper,OrganicNoise,PhysicsInkSimulator,PostStrokeOptimizer,PredictiveRenderer
Scene graph
CanvasNode,CanvasNodeFactory(withexternalFactoryhook for consumer-registered node types),NodeVisitor(7 base visit methods +visitOtherfallback for engine-only types),DefaultNodeVisitor- 7 base node classes:
GroupNode,LayerNode,ShapeNode,StrokeNode,TextNode,ImageNode,PathNode FrozenNodeView,PaintStackMixin,SceneGraphInterceptor,TransformBridge,NodeConstraint,ContentOrigin,NodeId,InvalidationGraph- Event infrastructure:
EngineEvent+EngineEventBus,SceneGraphEvent+SceneGraphObserver
Effects + models
NodeEffect,ShaderEffect,ShaderEffectWrapper,MeshGradient,GradientFill,PaintStack,FillLayer,StrokeLayerDigitalTextElement,ImageElement,ExportSettings,CanvasLayer,TextOverlay,ToneCurve,ColorAdjustments,GradientFilter,PerspectiveSettings,VectorPathShapeType,AccessibilityInfo/Action/TreeNode/TreeBuilder
Rendering
RTree,SpatialIndexManager,ViewportCuller,StrokeOptimizer,OptimizedPathBuilder,PaintPool,DirtyRegionTracker,LodConfig,MemoryPressureLevelPathRenderer,BatchRendererShapePainter,DigitalTextPainter,OriginIndicatorPainter,PaperPatternPainter,PaperGrainPainterBrushTexture
GPU live-stroke bridge (Dart side)
NativeStrokeOverlay+NativeStrokeOverlayControllerfaçadeVulkanStrokeOverlayService,WebGpuStrokeOverlayService,WebGpuOverlayView,NativeStrokeFfi
Export + import
RasterImageEncoder,RasterEncoderChannel,BinaryCanvasFormat,ExportPreset,ExportConfig(rich preset variant withpageFormat,background,copyWith, PDF fields),ExportFormat(png/jpeg)FlueraFileHeader/TOC/Writer/Reader/ExportServiceSvgImporter,TimelapseExportConfigPdfBookmark,PdfWatermark,WatermarkPosition,PdfPageLabel,PdfTextField/Checkbox/Dropdown,PdfImageXObject,PdfLinkAnnotation
Core utilities
EngineError,ErrorSeverity,ErrorDomain,EngineTelemetry+ counters/gauges/histograms,SchemaVersionException,generateUid
Native GPU live-stroke pipeline (full cross-platform)
- Android: Vulkan (
libfluera_vk_stroke.so, KotlinFlueraCanvasPlugin) - iOS: Metal (
MetalStrokeOverlayPlugin+ CADisplayLink 120 Hz sync) - macOS: Metal (
MetalStrokeOverlayPlugin+StrokeShaders.metal) - Linux: OpenGL (GTK+EGL/GL,
FlueraCanvasPlugin) - Windows: D3D11 (
FlueraCanvasPluginCApi) - Web: WebGPU (
WebGpuStrokeOverlayService+WebGpuOverlayView, gated onnavigator.gpuavailability, Dart fallback otherwise)
Single MethodChannel name on all native platforms:
fluera_canvas/native_stroke. Web uses a direct JS interop bridge.
Known limitations #
- The
SceneGraphclass, transaction / snapshot / integrity helpers, and thedocument_nodeare resident in the privatefluera_enginepackage — they depend onEngineScope, the design-variables / animation-timeline systems, and the spatial-index wrapper that holdCanvasNodereferences. Consumers who need the full scene graph should depend onfluera_enginedirectly. - Tools (
PenTool,EraserTool,UnifiedShapeTool,DigitalTextTool), navigation widgets (CanvasMinimap,ZoomLevelIndicator,ContentBoundsTracker,CameraActions), and concrete brush engines (BallpointBrush,PencilBrush,HighlighterBrush) remain engine- resident — their transitive dependencies cross intoFlueraLayerController,SceneGraph, and the GPU shader services.
Note #
API is pre-1.0 and may break between minor versions until the surface
stabilises.