perfect_freehand 2.2.0 perfect_freehand: ^2.2.0 copied to clipboard
Draw perfect pressure-sensitive freehand lines. A set of functions for turning input points into a polygon of outline points.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:perfect_freehand/perfect_freehand.dart';
import 'package:perfect_freehand_example/toolbar.dart';
void main() {
runApp(
MaterialApp(
title: 'Drawing App',
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.system,
theme: ThemeData(
brightness: Brightness.light,
colorSchemeSeed: Colors.blue,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
colorSchemeSeed: Colors.blue,
),
home: const DemoDrawingApp(),
),
);
}
class DemoDrawingApp extends StatefulWidget {
const DemoDrawingApp({super.key});
@override
State<DemoDrawingApp> createState() => _DemoDrawingAppState();
}
class _DemoDrawingAppState extends State<DemoDrawingApp> {
StrokeOptions options = StrokeOptions(
size: 16,
thinning: 0.7,
smoothing: 0.5,
streamline: 0.5,
start: StrokeEndOptions.start(
taperEnabled: true,
customTaper: 0.0,
cap: true,
),
end: StrokeEndOptions.end(
taperEnabled: true,
customTaper: 0.0,
cap: true,
),
simulatePressure: true,
isComplete: false,
);
/// Previous lines drawn.
final lines = ValueNotifier(<Stroke>[]);
/// The current line being drawn.
final line = ValueNotifier<Stroke?>(null);
void clear() => setState(() {
lines.value = [];
line.value = null;
});
void onPointerDown(PointerDownEvent details) {
final supportsPressure = details.kind == PointerDeviceKind.stylus;
options = options.copyWith(simulatePressure: !supportsPressure);
final localPosition = details.localPosition;
final point = PointVector(
localPosition.dx,
localPosition.dy,
supportsPressure ? details.pressure : null,
);
line.value = Stroke([point]);
}
void onPointerMove(PointerMoveEvent details) {
final supportsPressure = details.pressureMin < 1;
final localPosition = details.localPosition;
final point = PointVector(
localPosition.dx,
localPosition.dy,
supportsPressure ? details.pressure : null,
);
line.value = Stroke([...line.value!.points, point]);
}
void onPointerUp(PointerUpEvent details) {
lines.value = [...lines.value, line.value!];
line.value = null;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
body: Listener(
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
child: Stack(
children: [
Positioned.fill(
child: ValueListenableBuilder(
valueListenable: lines,
builder: (context, lines, _) {
return CustomPaint(
painter: StrokePainter(
color: colorScheme.onSurface,
lines: lines,
options: options,
),
);
},
),
),
Positioned.fill(
child: ValueListenableBuilder(
valueListenable: line,
builder: (context, line, _) {
return CustomPaint(
painter: StrokePainter(
color: colorScheme.onSurface,
lines: line == null ? [] : [line],
options: options,
),
);
},
),
),
Toolbar(
options: options,
updateOptions: setState,
clear: clear,
),
],
),
),
);
}
@override
void dispose() {
lines.dispose();
line.dispose();
super.dispose();
}
}
class StrokePainter extends CustomPainter {
const StrokePainter({
required this.color,
required this.lines,
required this.options,
});
final Color color;
final List<Stroke> lines;
final StrokeOptions options;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
for (final line in lines) {
final outlinePoints = getStroke(line.points, options: options);
if (outlinePoints.isEmpty) {
continue;
} else if (outlinePoints.length < 2) {
// If the path only has one point, draw a dot.
canvas.drawCircle(
outlinePoints.first,
options.size / 2,
paint,
);
} else {
final path = Path();
path.moveTo(outlinePoints.first.dx, outlinePoints.first.dy);
for (int i = 0; i < outlinePoints.length - 1; ++i) {
final p0 = outlinePoints[i];
final p1 = outlinePoints[i + 1];
path.quadraticBezierTo(
p0.dx,
p0.dy,
(p0.dx + p1.dx) / 2,
(p0.dy + p1.dy) / 2,
);
}
// You'll see performance improvements if you cache this Path
// instead of creating a new one every paint.
canvas.drawPath(path, paint);
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class Stroke {
final List<PointVector> points;
const Stroke(this.points);
}