scribe_canvas 0.6.1
scribe_canvas: ^0.6.1 copied to clipboard
A high-performance Flutter multi-page canvas library for handwriting, drawing, and annotation with customizable tools and PDF export.
scribe_canvas #
A high-performance Flutter multi-page canvas library for handwriting, drawing, and annotation. Scribe handles multiple A4-sized pages with smooth variable-width strokes, customizable tools, PDF export, and a clean Controller-based API.
Features #
- O(1) High-Performance Rendering — Uses a vector-based picture caching system to ensure 60fps performance even with thousands of strokes.
- Multi-page Canvas — Multiple A4-sized pages in a single scrollable view, with dynamic add/insert/delete support.
- Intelligent Ink — Authentic ink-like strokes with dynamic velocity-based width sampling and Catmull-Rom spline smoothing.
- Smart Stroke Eraser — Object-based erasing with live highlighting. Touch any part of a stroke to remove the entire line instantly.
- Customizable Tools — Cycling pen/eraser sizes, multi-color palette, and an integrated
PageHeadertool bar. - Backgrounds & Templates — Load local or network images, or render a full PDF as per-page background templates.
- Headers & Footers — Persistent banner images applied to every page on export.
- Navigation — Pinch-to-zoom and pan with dynamic constraints. No infinite zoom-out.
- Undo / Redo — Global and per-page undo/redo stacks.
- Serialization — Save and restore canvas state as a compact JSON string (variable-width preserved).
- PDF Export — Export the full multi-page drawing with backgrounds, headers, and footers.
Installation #
dependencies:
scribe_canvas: ^0.6.0
Quick Start #
All interactions with the canvas are done through a ScribeCanvasController. This follows Flutter's standard controller pattern (like TextEditingController or ScrollController).
import 'package:flutter/material.dart';
import 'package:scribe_canvas/scribe_canvas.dart';
class DrawingPage extends StatefulWidget {
const DrawingPage({super.key});
@override
State<DrawingPage> createState() => _DrawingPageState();
}
class _DrawingPageState extends State<DrawingPage> {
final _controller = ScribeCanvasController();
bool _isEraser = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(icon: const Icon(Icons.undo), onPressed: _controller.undo),
IconButton(icon: const Icon(Icons.redo), onPressed: _controller.redo),
IconButton(
icon: Icon(_isEraser ? Icons.edit : Icons.auto_fix_normal),
onPressed: () => setState(() => _isEraser = !_isEraser),
),
IconButton(
icon: const Icon(Icons.picture_as_pdf),
onPressed: _controller.exportToPdf,
),
],
),
body: ScribeCanvas(
controller: _controller, // attach the controller
isEraser: _isEraser,
onToggleEraser: (val) => setState(() => _isEraser = val),
onStrokeEnd: () => setState(() {}),
onUndo: () => setState(() {}),
onRedo: () => setState(() {}),
),
);
}
}
ScribeCanvas Properties #
| Property | Type | Default | Description |
|---|---|---|---|
controller |
ScribeCanvasController? |
— | Required to call any imperative API. |
color |
Color |
Colors.black |
Initial pen color. |
strokeWidth |
double |
4.0 |
Initial pen stroke width. |
strokeSizes |
List<double> |
[2,4,8,16,32] |
Sizes to cycle through in the header. |
isEraser |
bool |
false |
Whether eraser mode is active. |
eraserWidth |
double |
30.0 |
Initial eraser width. |
eraserSizes |
List<double> |
[10,20,30,60,100] |
Eraser sizes to cycle through. |
isPanMode |
bool |
false |
When true, disables drawing and enables panning only. |
multiPage |
bool |
true |
Enables multi-page mode. When false, the canvas is a single fixed page. |
initialColor |
Color |
Colors.black |
Initial color selected in the palette. |
colors |
List<Color> |
(6 colors) | Colors shown in the palette. |
onStrokeStart |
VoidCallback? |
— | Called when a stroke begins. |
onStrokeEnd |
VoidCallback? |
— | Called when a stroke is finalized. Good place to call setState to refresh undo/redo button states. |
onUndo |
VoidCallback? |
— | Called after an undo operation. |
onRedo |
VoidCallback? |
— | Called after a redo operation. |
onStrokeWidthChanged |
ValueChanged<double>? |
— | Called when the user cycles the pen size. |
onEraserWidthChanged |
ValueChanged<double>? |
— | Called when the user cycles the eraser size. |
onToggleEraser |
ValueChanged<bool>? |
— | Called when pen/eraser is toggled from the header. |
onColorChanged |
ValueChanged<Color>? |
— | Called when the user picks a color. |
ScribeCanvasController API #
Undo & Redo #
_controller.undo(); // Undo last global stroke
_controller.redo(); // Redo last global stroke
_controller.undoPage(0); // Undo last stroke on page 0
_controller.redoPage(0); // Redo last stroke on page 0
bool canUndo = _controller.canUndo; // Check before enabling undo button
bool canRedo = _controller.canRedo;
Canvas Operations #
_controller.clear(); // Clear all data (strokes & history)
_controller.clearBackgrounds(); // Remove background images/PDF
_controller.resetView(); // Reset zoom/pan to initial state (on Page 1)
Page Management #
_controller.addPage(); // Append a new page at the end
_controller.insertPage(1); // Insert a blank page at index 1
_controller.deletePage(1); // Delete the page at index 1
Backgrounds & Templates #
// Local image bytes as a background for page 0
final bytes = await rootBundle.load('assets/template.png');
await _controller.setBackgroundImage(0, bytes.buffer.asUint8List(), clearOthers: true);
// Network image as background for page 0
await _controller.setNetworkBackgroundImage(0, 'https://example.com/bg.png');
// PDF rendered as per-page background templates
await _controller.loadPdfBackground(pdfBytes);
// Remove all backgrounds (keeps drawings)
_controller.clearBackgrounds();
Headers & Footers #
Headers and footers are rendered on every page during PDF export.
// From local file
await _controller.setHeaderImage(headerBytes);
await _controller.setFooterImage(footerBytes);
// From network URL
await _controller.setNetworkHeaderImage('https://example.com/logo.png');
await _controller.setNetworkFooterImage('https://example.com/footer.png');
Serialization (Save & Load) #
// Save the current drawing state
final String data = _controller.getEncodedData();
await prefs.setString('canvas', data);
// Restore a previously saved drawing
final String? saved = prefs.getString('canvas');
if (saved != null) _controller.loadEncodedData(saved);
// Legacy format (no variable-width data)
final String legacy = _controller.getLegacyEncodedData();
_controller.loadLegacyEncodedData(legacy);
PDF Export #
// Renders all pages with backgrounds, headers, and footers
// and opens the system share / print dialog.
await _controller.exportToPdf();
Performance #
Scribe is designed for professional-grade note-taking and sketching. It uses a Vector Caching Architecture:
- Active Drawing: Only the single active stroke is processed every frame.
- Committed Drawing: Once a stroke is finished, it is "baked" into a
ui.Picture(a vector snapshot). - Rendering Complexity: Drawing 1 stroke or 10,000 strokes has the exact same cost on the GPU (O(1) blit).
This ensures that even on older hardware, the canvas remains responsive during long writing sessions.
Additional Information #
Scribe uses InteractiveViewer for navigation and CustomPainter for rendering. It leverages the pdf and printing packages for cross-platform PDF generation, and pdfx for rendering PDF background templates.