draw_your_image
A Flutter package for creating customizable drawing canvases with a declarative API.

Core Concept
Fully declarative, fully customizable
- No controllers required - Manage stroke data in your widget state
- Customize everything - Control stroke behavior through simple callbacks
- Bring your own features - Implement undo/redo, zoom/pan, image export as your app needs
This package focuses on providing a flexible drawing widget, leaving app-specific features to you.
Features
โจ Device-aware drawing - Distinguish between stylus, finger, and mouse input
๐จ Flexible stroke handling - Customize behavior per input device or any other criteria
๐๏ธ Built-in smoothing - Catmull-Rom spline interpolation included
โ๏ธ Fully customizable - Colors, widths, smoothing algorithms
๐งน Multiple erasing modes - Pixel-level and stroke-level erasing
๐ Intersection detection - Customizable stroke overlap detection
Quick Start
class MyDrawingPage extends StatefulWidget {
@override
_MyDrawingPageState createState() => _MyDrawingPageState();
}
class _MyDrawingPageState extends State<MyDrawingPage> {
/// Store all the strokes on app side as state
List<Stroke> _strokes = [];
@override
Widget build(BuildContext context) {
return Draw(
strokes: _strokes, // pass strokes via
onStrokeDrawn: (stroke) {
// store new drawn stroke and rebuild
setState(() {
_strokes = [..._strokes, stroke];
});
},
);
}
}
That's it! The canvas accepts any input and draws with default settings.
Device-Aware Drawing with onStrokeStarted
The onStrokeStarted callback lets you control stroke behavior based on input devices or any other criteria.
Stroke? Function(Stroke newStroke, Stroke? currentStroke)
Parameters:
newStroke- The stroke about to be startedcurrentStroke- The stroke currently being drawn (null if none)
Return:
- The stroke to draw (can be
newStroke,currentStroke, or a modified version) nullto reject the stroke
Example: Stylus draws, finger erases
If you want to draw lines with stylus while erase them with a finger, the function can be implemented like below:
extension on PointerDeviceKind {
bool get isStylus =>
this ||
this == PointerDeviceKind.invertedStylus;
}
Stroke? customHandler(Stroke newStroke, Stroke? currentStroke) {
// if we have an ongoing stroke, just continue.
if (currentStroke != null) {
return currentStroke;
}
if (newStroke.deviceKind == PointerDeviceKind.stylus) {
// if stylus, draw black line
return newStroke.copyWith(color: Colors.black);
} else {
// if finger, erasor mode
return newStroke.copyWith(isErasing: true, width: 20.0);
}
}
Draw(
strokes: _strokes,
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
onStrokeStarted: customHandler,
)
Example: Stylus-only drawing
If pre-defined utility functions fit to your needs, you can pick one of them.
Draw(
strokes: _strokes,
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
onStrokeStarted: stylusOnlyHandler, // Pre-defined utility
)
Pre-defined Utilities
stylusOnlyHandler- Accept only stylus inputstylusPriorHandler- Prioritize stylus when drawing (palm rejection)
API Reference
Draw Widget Properties
| Property | Type | Required | Description |
|---|---|---|---|
strokes |
List<Stroke> |
โ | List of strokes to display |
onStrokeDrawn |
void Function(Stroke) |
โ | Called when a stroke is complete |
onStrokeStarted |
Stroke? Function(Stroke, Stroke?) |
Control stroke behavior based on input | |
onStrokeUpdated |
Stroke? Function(Stroke) |
Modify stroke in real-time as points are added | |
onStrokesRemoved |
void Function(List<Stroke>) |
Called when strokes are removed by erasing | |
strokeColor |
Color |
Default stroke color | |
strokeWidth |
double |
Default stroke width | |
backgroundColor |
Color |
Canvas background color | |
erasingBehavior |
ErasingBehavior |
Erasing mode (none, pixel, stroke) |
|
smoothingFunc |
Path Function(Stroke) |
Custom smoothing function | |
strokePainter |
List<Paint> Function(Stroke) |
Custom stroke painting function | |
intersectionDetector |
IntersectionDetector |
Custom intersection detection function | |
shouldAbsorb |
bool Function(PointerDownEvent) |
Control whether to absorb pointer events |
Stroke Properties
class Stroke {
PointerDeviceKind deviceKind; // Input device type
List<StrokePoint> points; // Stroke points with pressure/tilt data
Color color; // Stroke color
double width; // Stroke width
ErasingBehavior erasingBehavior; // Erasing mode
}
StrokePoint
Each point in a stroke contains rich input data:
class StrokePoint {
final Offset position; // Position of the point
final double pressure; // Raw pressure (0.0 to 1.0+)
final double pressureMin; // Minimum pressure for this device
final double pressureMax; // Maximum pressure for this device
final double tilt; // Stylus tilt angle (0 to ฯ/2 radians)
final double orientation; // Stylus orientation (-ฯ to ฯ radians)
// Normalized pressure getter (0.0 to 1.0)
double get normalizedPressure;
}
Note that all the parameters are originated in Flutter's PointerEvent. See documentation of PointerEvent for detailed information.
Key features:
tiltandorientationenable calligraphy-style effectsnormalizedPressureautomatically adjusts for device-specific pressure ranges- Works seamlessly with all input devices (stylus, touch, mouse)
ErasingBehavior
enum ErasingBehavior {
none, // Normal drawing (default)
pixel, // Pixel-level erasing (BlendMode.clear)
stroke, // Stroke-level erasing (removes entire strokes)
}
Smoothing Modes
Smoothing algorithm is also customizable. You can choose pre-defined functions below or make your own function.
SmoothingMode.catmullRom.converter // Smooth curves (default)
SmoothingMode.none.converter // No smoothing (straight lines)
Pressure-Sensitive Drawing
Create variable-width strokes that respond to stylus pressure using the built-in generatePressureSensitivePath function:
Draw(
strokes: _strokes,
strokeWidth: 8.0,
smoothingFunc: generatePressureSensitivePath,
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
)
Tilt-Based Effects
For advanced calligraphy effects using stylus tilt and orientation:
Path calligraphyPath(Stroke stroke) {
for (final point in stroke.points) {
// More tilt (flat stylus) = wider stroke
final tiltFactor = 1.0 + (point.tilt / (math.pi / 2)) * 1.5;
final width = baseWidth * tiltFactor * point.normalizedPressure;
// Use orientation to rotate brush angle
final angle = point.orientation;
// ... create path with rotated brush shape
}
}
Draw(
smoothingFunc: calligraphyPath,
// ...
)
See example/lib/pages/tilt_demo_page.dart for a complete implementation.
Custom Stroke Painting with strokePainter
The strokePainter callback allows you to fully customize how strokes are rendered. You can create advanced visual effects like gradients, glows, shadows, and shader effects by returning multiple Paint objects.
Draw(
strokePainter: (Stroke stroke) {
final List<Paint> paint = _buildYourPaint(stroke);
return paint;
},
)
Parameters:
stroke- The stroke to be painted
Return:
- A list of
Paintobjects to be applied to the stroke (in order)
Example: Gradient Effect
LayoutBuilder(
builder: (context, constraints) {
final canvasSize = Size(constraints.maxWidth, constraints.maxHeight);
return Draw(
strokes: _strokes,
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
strokePainter: (stroke) {
final gradient = ui.Gradient.linear(
Offset.zero,
Offset(canvasSize.width, canvasSize.height),
[Colors.blue, Colors.purple, Colors.pink],
);
return [
Paint()
..shader = gradient
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
},
);
},
)
Example: Multi-Layer Effect (Glow)
strokePainter: (stroke) {
return [
// Outer glow
paintWithOverride(stroke, strokeWidth: stroke.width + 8, strokeColor: Colors.cyan)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8),
// Inner glow
paintWithOverride(stroke, strokeWidth: stroke.width + 4, strokeColor: Colors.blue)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4),
// Core stroke
paintWithDefault(stroke),
];
}
Helper Functions
The package provides utility functions for creating Paint objects:
paintWithDefault- Creates a paint with default propertiespaintWithOverride- Creates a paint with overridden properties
Using with InteractiveViewer
The shouldAbsorb callback allows you to control which pointer events are handled by the Draw widget versus parent widgets like InteractiveViewer. This enables powerful combinations like:
- Touch for pan/zoom, stylus for drawing
- Device-specific gesture handling
- Conditional pointer event absorption
InteractiveViewer(
child: Draw(
strokes: _strokes,
shouldAbsorb: (event) {
// Absorb stylus events for drawing, let touch events pass through for pan/zoom
return event.kind == PointerDeviceKind.stylus ||
event.kind == PointerDeviceKind.invertedStylus;
},
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
onStrokeStarted: (newStroke, currentStroke) {
if (currentStroke != null) return currentStroke;
// Only draw with stylus (touch is for pan/zoom)
return newStroke.deviceKind == PointerDeviceKind.stylus ? newStroke : null;
},
),
)
See the example app for a complete implementation of touch-for-pan/stylus-for-draw functionality.
Working with AI Assistants
This package is designed to work well with AI coding assistants. If you want accurate response from AI agents, refer AI_GUIDE.md before asking.