From 4c54265f89c89efcd0359d9aa8853e90d8a3bf83 Mon Sep 17 00:00:00 2001 From: lukas-heiligenbrunner Date: Sat, 29 Oct 2022 17:39:25 +0200 Subject: [PATCH] outsource line drawing in paint controller --- lib/canvas/drawing_page.dart | 197 ++++++------------------------- lib/canvas/my_painter.dart | 26 ++-- lib/canvas/paint_controller.dart | 116 ++++++++++++++++++ lib/tool_bar.dart | 86 ++++++++++++++ 4 files changed, 251 insertions(+), 174 deletions(-) create mode 100644 lib/canvas/paint_controller.dart create mode 100644 lib/tool_bar.dart diff --git a/lib/canvas/drawing_page.dart b/lib/canvas/drawing_page.dart index 0980b2a..5d22948 100644 --- a/lib/canvas/drawing_page.dart +++ b/lib/canvas/drawing_page.dart @@ -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 { - List _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 { // 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 { 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 { ); } - 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 { 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 { 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, ), ), diff --git a/lib/canvas/my_painter.dart b/lib/canvas/my_painter.dart index cc94d60..09d6d36 100644 --- a/lib/canvas/my_painter.dart +++ b/lib/canvas/my_painter.dart @@ -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 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; } } diff --git a/lib/canvas/paint_controller.dart b/lib/canvas/paint_controller.dart new file mode 100644 index 0000000..e466b38 --- /dev/null +++ b/lib/canvas/paint_controller.dart @@ -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 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(); + } + } +} diff --git a/lib/tool_bar.dart b/lib/tool_bar.dart new file mode 100644 index 0000000..345fd35 --- /dev/null +++ b/lib/tool_bar.dart @@ -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 createState() => _ToolBarState(); +} + +class _ToolBarState extends State { + 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, + ), + ], + ), + ); + } +}