outsource line drawing in paint controller
This commit is contained in:
parent
be3b54f258
commit
4c54265f89
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
116
lib/canvas/paint_controller.dart
Normal file
116
lib/canvas/paint_controller.dart
Normal 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
86
lib/tool_bar.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user