outsource line drawing in paint controller

This commit is contained in:
lukas-heiligenbrunner 2022-10-29 17:39:25 +02:00
parent be3b54f258
commit 4c54265f89
4 changed files with 251 additions and 174 deletions

View File

@ -1,17 +1,15 @@
import 'dart:math';
import 'dart:ui';
import 'package:adwaita_icons/adwaita_icons.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:iconify_flutter/iconify_flutter.dart';
import 'package:iconify_flutter/icons/emojione_monotone.dart';
import 'package:iconify_flutter/icons/jam.dart';
import 'package:notes/canvas/my_painter.dart';
import 'package:notes/canvas/screen_document_mapping.dart';
import 'my_painter.dart';
import 'paint_controller.dart';
import 'screen_document_mapping.dart';
import '../icon_material_button.dart';
import 'document_types.dart';
import '../tool_bar.dart';
/// Handles input events and draws canvas element
class DrawingPage extends StatefulWidget {
const DrawingPage({Key? key}) : super(key: key);
@ -20,15 +18,11 @@ class DrawingPage extends StatefulWidget {
}
class _DrawingPageState extends State<DrawingPage> {
List<Stroke> _strokes = [];
bool allowDrawWithFinger = false;
double zoom = .75;
double basezoom = 1.0;
Offset offset = const Offset(.0, .0);
// todo better pen system
bool eraseractive = false;
PaintController controller = PaintController();
@override
void initState() {
@ -37,7 +31,7 @@ class _DrawingPageState extends State<DrawingPage> {
// todo might be weird behaviour if used with short side
final screenWidth =
(window.physicalSize.longestSide / window.devicePixelRatio);
_calcNewPageOffset(const Offset(.0, .0), screenWidth);
_calcNewPageOffset(const Offset(.0, .0), screenWidth - 45);
}
@override
@ -47,68 +41,39 @@ class _DrawingPageState extends State<DrawingPage> {
IconMaterialButton(
icon: const Icon(FluentIcons.book_open_48_filled),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {},
onPressed: () {
// todo implement
},
),
IconMaterialButton(
icon: const Icon(FluentIcons.document_one_page_24_regular),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {},
onPressed: () {
// todo implement
},
),
IconMaterialButton(
icon: const Icon(Icons.attachment_outlined),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {},
onPressed: () {
// todo implement
},
rotation: -pi / 4,
),
IconMaterialButton(
icon: const Icon(Icons.more_vert),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {},
onPressed: () {
// todo implement
},
),
]),
body: Row(
children: [
Container(
color: const Color(0xff3f3f3f),
width: 45,
child: Column(
children: [
const SizedBox(
height: 10,
),
IconMaterialButton(
icon: const Iconify(EmojioneMonotone.fountain_pen, color: Color.fromRGBO(255, 255, 255, .85),),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () => setState(() => eraseractive = false),
selected: !eraseractive,
iconSize: 24,
),
IconMaterialButton(
icon: const Iconify(Jam.highlighter, color: Color.fromRGBO(255, 255, 255, .85),),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () => setState(() => eraseractive = false),
selected: false,
iconSize: 24,
),
IconMaterialButton(
icon: Transform.translate(
offset: const Offset(-2.0, .0),
child: const AdwaitaIcon(AdwaitaIcons.eraser2),
),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () => setState(() => eraseractive = true),
iconSize: 24,
selected: eraseractive,
),
IconMaterialButton(
icon: const Icon(FluentIcons.select_object_24_regular),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () => setState(() => eraseractive = false),
selected: false,
iconSize: 24,
),
],
),
ToolBar(
onPenChange: (pen) {
controller.changePen(pen);
},
),
Expanded(child: RepaintBoundary(child: _buildCanvas())),
],
@ -116,28 +81,6 @@ class _DrawingPageState extends State<DrawingPage> {
);
}
double _calcTiltedWidth(double baseWidth, double tilt) {
if (tilt == .0) return baseWidth;
return baseWidth * tilt;
}
double _calcAngleDependentWidth(Point pt1, Point pt2, double basetickness) {
double dx = pt2.point.dx - pt1.point.dx;
double dy = pt2.point.dy - pt1.point.dy;
// todo those deltas to small to get an accurate direction!
double alpha = atan(dx / dy);
// alpha has range from 0 - 2pi
// we want 0.5 -1;
alpha /= (2 * pi * 2);
alpha += .5;
double thickness = basetickness * alpha;
return thickness;
}
// calculate new page offset from mousepointer delta
void _calcNewPageOffset(Offset delta, double canvasWidth) {
if (zoom > 1.0) {
@ -175,96 +118,28 @@ class _DrawingPageState extends State<DrawingPage> {
return;
}
// todo outsource this eraser pen
if (eraseractive) {
// todo dynamic eraser size
final eraserrect = Rect.fromCircle(center: pos, radius: 3);
for (final stroke in _strokes) {
// check if delete action was within bounding rect of stroke
if (stroke.getBoundingRect().contains(pos)) {
// check if eraser hit an point within its range
for (final pt in stroke.points) {
if (eraserrect.contains(pt.point)) {
setState(() {
_strokes = List.from(_strokes)..remove(stroke);
});
return;
}
}
}
}
return;
}
controller.pointMoveEvent(pos, event.kind, event.tilt);
if (allowDrawWithFinger || event.kind != PointerDeviceKind.touch) {
final pts = _strokes.last.points;
if (pts.last.point == pos) return;
double newWidth = _calcTiltedWidth(5.0, event.tilt);
if (_strokes.last.points.length > 1) {
newWidth =
_calcAngleDependentWidth(pts.last, pts[pts.length - 2], newWidth);
}
setState(() {
_strokes = List.from(_strokes, growable: false)
..last.addPoint(Point(pos, newWidth));
});
} else {
if (event.kind == PointerDeviceKind.touch) {
_calcNewPageOffset(event.delta, size.width);
}
}
Widget _buildCanvas() {
final size = MediaQuery.of(context).size;
final canvasSize = Size(size.width - 45, size.height);
return Listener(
behavior: HitTestBehavior.opaque,
onPointerMove: (e) => _onPointerMove(e, size),
onPointerSignal: (event) {
print('Button: ${event.buttons}');
},
onPointerMove: (e) => _onPointerMove(e, canvasSize),
onPointerDown: (event) {
print('Button: ${event.buttons}');
if (allowDrawWithFinger || event.kind != PointerDeviceKind.touch) {
Offset pos = event.localPosition;
final scale = calcPageDependentScale(zoom, a4Page, size);
pos = translateScreenToDocumentPoint(pos, scale, offset);
// todo line drawn on edge where line left page
if (!a4Page.contains(pos)) return;
if (eraseractive) return;
setState(() {
_strokes = List.from(_strokes)
..add(Stroke.fromPoints(
[Point(pos, _calcTiltedWidth(3.0, event.tilt))]));
});
}
Offset pos = event.localPosition;
final scale = calcPageDependentScale(zoom, a4Page, canvasSize);
pos = translateScreenToDocumentPoint(pos, scale, offset);
controller.pointDownEvent(pos, event.kind, event.tilt);
},
onPointerUp: (event) {
if (eraseractive) return;
if (allowDrawWithFinger || event.kind != PointerDeviceKind.touch) {
if (_strokes.last.points.length <= 1) {
// if the line consists only of one point (point) add endpoint as the same to allow drawing a line
// todo maybe solve this in custompainter in future
setState(() {
_strokes = List.from(_strokes, growable: false)
..last.points.add(_strokes.last.points.last);
});
} else {
setState(() {});
}
print(_strokes.length);
print(_strokes.last.points.length);
}
controller.pointUpEvent(event.kind);
},
child: GestureDetector(
onScaleUpdate: (details) {
@ -277,18 +152,12 @@ class _DrawingPageState extends State<DrawingPage> {
onScaleEnd: (details) {
basezoom = zoom;
},
onSecondaryTap: () {
print('secctab');
},
onTertiaryTapDown: (details) {
print('tertiary button');
},
child: CustomPaint(
painter: MyPainter(
strokes: _strokes,
offset: offset,
zoom: zoom,
canvasSize: canvasSize),
canvasSize: canvasSize,
controller: controller),
size: canvasSize,
),
),

View File

@ -1,24 +1,32 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:notes/canvas/paint_controller.dart';
import 'package:notes/canvas/screen_document_mapping.dart';
import 'document_types.dart';
final Rect a4Page =
Rect.fromPoints(const Offset(.0, .0), const Offset(210, 210 * sqrt2));
class MyPainter extends CustomPainter {
List<Stroke> strokes;
double zoom;
Offset offset;
Size canvasSize;
PaintController controller;
late Pen activePen;
MyPainter(
{required this.strokes,
required this.zoom,
{required this.zoom,
required this.offset,
required this.canvasSize});
required this.canvasSize,
required this.controller})
: super(repaint: controller) {
activePen = controller.activePen;
controller.addListener(() {
activePen = controller.activePen;
});
}
Offset _translatept(Offset pt, Size canvasSize) {
final scale = calcPageDependentScale(zoom, a4Page, canvasSize);
@ -42,7 +50,7 @@ class MyPainter extends CustomPainter {
_translatept(a4Page.bottomRight, size)),
backgroundPaint);
for (final stroke in strokes) {
for (final stroke in controller.strokes) {
for (int i = 0; i < stroke.points.length - 1; i++) {
Offset pt1 = stroke.points[i].point;
pt1 = _translatept(pt1, size);
@ -58,8 +66,6 @@ class MyPainter extends CustomPainter {
@override
bool shouldRepaint(MyPainter oldDelegate) {
return oldDelegate.strokes != strokes ||
oldDelegate.zoom != zoom ||
oldDelegate.offset != offset;
return oldDelegate.zoom != zoom || oldDelegate.offset != offset;
}
}

View File

@ -0,0 +1,116 @@
import 'dart:math';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'document_types.dart';
import 'my_painter.dart';
enum Pen { eraser, pen, highlighter, selector }
class PaintController extends ChangeNotifier {
Pen activePen = Pen.pen;
List<Stroke> strokes = [];
final bool _allowDrawWithFinger = false;
void changePen(Pen pen) {
activePen = pen;
notifyListeners();
}
double _calcTiltedWidth(double baseWidth, double tilt) {
if (tilt == .0) return baseWidth;
return baseWidth * tilt;
}
double _calcAngleDependentWidth(Point pt1, Point pt2, double basetickness) {
double dx = pt2.point.dx - pt1.point.dx;
double dy = pt2.point.dy - pt1.point.dy;
// todo those deltas to small to get an accurate direction!
double alpha = atan(dx / dy);
// alpha has range from 0 - 2pi
// we want 0.5 -1;
alpha /= (2 * pi * 2);
alpha += .5;
double thickness = basetickness * alpha;
return thickness;
}
void pointDownEvent(Offset offset, PointerDeviceKind pointer, double tilt) {
if (_allowDrawWithFinger || pointer != PointerDeviceKind.touch) {
// todo line drawn on edge where line left page
if (!a4Page.contains(offset)) return;
// todo handle other pens
if (activePen != Pen.pen) return;
strokes
.add(Stroke.fromPoints([Point(offset, _calcTiltedWidth(3.0, tilt))]));
notifyListeners();
}
}
void pointUpEvent(PointerDeviceKind pointer) {
if (activePen == Pen.eraser) return;
if (_allowDrawWithFinger || pointer != PointerDeviceKind.touch) {
if (strokes.last.points.length <= 1) {
// if the line consists only of one point (point) add endpoint as the same to allow drawing a line
// todo maybe solve this in custompainter in future
strokes.last.points.add(strokes.last.points.last);
notifyListeners();
}
}
}
void pointMoveEvent(Offset offset, PointerDeviceKind pointer, double tilt) {
if (!a4Page.contains(offset)) {
return;
}
if (_allowDrawWithFinger || pointer != PointerDeviceKind.touch) {
switch (activePen) {
case Pen.eraser:
// todo dynamic eraser size
final eraserrect = Rect.fromCircle(center: offset, radius: 3);
for (final stroke in strokes) {
// check if delete action was within bounding rect of stroke
if (stroke.getBoundingRect().contains(offset)) {
// check if eraser hit an point within its range
for (final pt in stroke.points) {
if (eraserrect.contains(pt.point)) {
strokes.remove(stroke);
notifyListeners();
return;
}
}
}
}
break;
case Pen.pen:
final pts = strokes.last.points;
if (pts.last.point == offset) return;
double newWidth = _calcTiltedWidth(5.0, tilt);
if (strokes.last.points.length > 1) {
newWidth = _calcAngleDependentWidth(
pts.last, pts[pts.length - 2], newWidth);
}
strokes.last.addPoint(Point(offset, newWidth));
break;
case Pen.highlighter:
// TODO: Handle this case.
break;
case Pen.selector:
// TODO: Handle this case.
break;
}
notifyListeners();
}
}
}

86
lib/tool_bar.dart Normal file
View File

@ -0,0 +1,86 @@
import 'package:adwaita_icons/adwaita_icons.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:iconify_flutter/iconify_flutter.dart';
import 'package:iconify_flutter/icons/emojione_monotone.dart';
import 'package:iconify_flutter/icons/jam.dart';
import 'canvas/paint_controller.dart';
import 'icon_material_button.dart';
class ToolBar extends StatefulWidget {
const ToolBar({Key? key, required this.onPenChange}) : super(key: key);
final void Function(Pen pen) onPenChange;
@override
State<ToolBar> createState() => _ToolBarState();
}
class _ToolBarState extends State<ToolBar> {
Pen activepen = Pen.pen;
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xff3f3f3f),
width: 45,
child: Column(
children: [
const SizedBox(
height: 10,
),
IconMaterialButton(
icon: const Iconify(
EmojioneMonotone.fountain_pen,
color: Color.fromRGBO(255, 255, 255, .85),
),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {
setState(() => activepen = Pen.pen);
widget.onPenChange(Pen.pen);
},
selected: activepen == Pen.pen,
iconSize: 24,
),
IconMaterialButton(
icon: const Iconify(
Jam.highlighter,
color: Color.fromRGBO(255, 255, 255, .85),
),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {
setState(() => activepen = Pen.highlighter);
widget.onPenChange(Pen.highlighter);
},
selected: activepen == Pen.highlighter,
iconSize: 24,
),
IconMaterialButton(
icon: Transform.translate(
offset: const Offset(-2.0, .0),
child: const AdwaitaIcon(AdwaitaIcons.eraser2),
),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {
setState(() => activepen = Pen.eraser);
widget.onPenChange(Pen.eraser);
},
iconSize: 24,
selected: activepen == Pen.eraser,
),
IconMaterialButton(
icon: const Icon(FluentIcons.select_object_24_regular),
color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {
setState(() => activepen = Pen.selector);
widget.onPenChange(Pen.selector);
},
selected: activepen == Pen.selector,
iconSize: 24,
),
],
),
);
}
}