Flutter) How can I select shapes created with CustomPaint() - flutter

I'm making a whiteboard app like the image below.
After drawing the shape, I want to move the drawn shape and line.
The rectangle and circles can be used with containers() to determine if the shape contains the points currently being touched.
But sometimes it doesn't work out. (Clicking inside the shape does not confirm that the point being touched is included)
And in the case of lines, I don't know how to make sure that the points being touched are included.(I only use two points when drawing a line, and I don't know how to check if the touched point is between these two points.)
The following is part of the code for this.
List<Rectangle>? _rects;
List<Circle>? _circles;
List<Line>? _lines;
List<ArrowLine>? _arrowLines;
int _selectedRectIndex = -1;
int _selectedCircleIndex = -1;
int _selectedLineIndex = -1;
int _selectedArrowLineIndex = -1;
// ===================
return GestureDetector(
onScaleStart: (details) {
Tool tool = context.read<DrawProvider>().tool;
RenderBox box = context.findRenderObject() as RenderBox;
Offset point = box.globalToLocal(details.focalPoint);
// pointer
switch (tool) {
case Tool.pointer:
_rects = [];
_circles = [];
_lines = [];
// save shape list
for (shape in shapes) {
if (shape is Rectangle) {
_rects!.add(shape as Rectangle);
} else if (shape is Circle) {
_circles!.add(shape as Circle);
} else if (shape is Line) {
_lines!.add(shape as Line);
}
}
// check seleted
final selectedRectIndex =
_rects!.lastIndexWhere((rect) => rect.rect!.contains(point));
final selectedCircleIndex = _circles!
.lastIndexWhere((rect) => rect.rect!.contains(point));
// TODO check line selete
// rectangle
if (selectedRectIndex != -1) {
onRectangleSelected(selectedRectIndex);
_seletedType = SeletedType.rectangle;
}
// circle
else if (selectedCircleIndex != -1) {
onCircleSelected(selectedCircleIndex);
_seletedType = SeletedType.circle;
} else {
_seletedType = SeletedType.none;
logger.d("shapeIndex: $_selectedCircleIndex");
}
break;
default:
}
}
},
onScaleUpdate: (details) {
Tool tool = context.read<DrawProvider>().tool;
RenderBox box = context.findRenderObject() as RenderBox;
Offset point = box.globalToLocal(details.focalPoint);
switch (tool) {
case Tool.pointer:
// move shapes
// rectangle
if (_seletedType == SeletedType.rectangle) {
final newLeft = point.dx;
final newTop = point.dy;
final newRight = point.dx + _seletedRectWidth!;
final newBottom = point.dy + _seletedRectHeight!;
_rects![_selectedRectIndex].rect =
Rect.fromLTRB(newLeft, newTop, newRight, newBottom);
}
// circle
else if (_seletedType == SeletedType.circle) {
final newLeft = point.dx;
final newTop = point.dy;
final newRight = point.dx + _seletedCircleWidth!;
final newBottom = point.dy + _seletedCircleHeight!;
// 이동은 되지만 이상하게 보임!
_circles![_selectedCircleIndex].rect =
Rect.fromLTRB(newLeft, newTop, newRight, newBottom);
}
// TODO line
else {
return;
}
break;
},
},
// ==============
void onRectangleSelected(int index) {
_selectedRectIndex = index;
}
void onCircleSelected(int index) {
_selectedCircleIndex = index;
}
void onLineSelected(int index) {
_selectedLineIndex = index;
}
void onArrowLineSelected(int index) {
_selectedArrowLineIndex = index;
}
shape and customPainter
class Shape {} // Origin class for all the shapes
// Rectangle
class Rectangle extends Shape {
Rect? rect;
double? left, top;
/* Because of the implementation left and top values
have to be saved inside of a Rectangle */
final paint = Paint();
Rectangle(Color color, double width) {
this.paint
..color = color
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeWidth = width
..style = PaintingStyle.stroke; // no fill
}
}
// A straight section defined by 2 points
class Line extends Shape {
Offset? p1, p2;
final paint = Paint();
Line(Color color, double width) {
this.paint
..color = color
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeWidth = width
..style = PaintingStyle.stroke; // no fill
}
}
class ArrowLine extends Shape {
// sketche에서 화살표 추가
Offset? p1, p2;
final paint = Paint();
ArrowLine(Color color, double width) {
this.paint
..color = color
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeWidth = width;
}
}
class Circle extends Shape {
Rect? rect;
double? left, top;
Size? size; // test
final paint = Paint();
Circle(Color color, double width, Size size) {
this.paint
..color = color
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeWidth = width
..style = PaintingStyle.stroke; // no fill
}
}
// ===================
class Sketcher extends CustomPainter {
final List<Shape> shapes;
Sketcher(this.shapes) : super();
#override
void paint(Canvas canvas, Size size) {
for (var shape in shapes) {
if (shape is Rectangle) {
canvas.drawRect(shape.rect!, shape.paint);
} else if (shape is Circle) {
canvas.drawOval(shape.rect!, shape.paint);
} else if (shape is Line) {
canvas.drawLine(shape.p1!, shape.p2!, shape.paint);
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
May I know why the shape is sometimes not selected and how to check if the point is between two points on the line?

Related

How To Make A Curved Chart with Custom Painter Which Include Custom Tool-tip with Moving Cursor?

I'm Learning Custom Paint and I'm Confused About Them i referred Few Documentation but Still i'm Lacking on it.
Referrence:Curved Chart,
Paths
Some of the Code I have Mentioned Here ..Pls Help Me out of This....
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class ChartPainter extends CustomPainter {
final List<String> x;
final List<double> y;
final double min, max;
ChartPainter(this.x, this.y, this.min, this.max);
final yLabelStyle = const TextStyle(color: Colors.white, fontSize: 14);
final xLabelStyle = const TextStyle(
color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold);
static double border = 10.0;
static double radius = 5.0;
final dotPaintFill = Paint()
..color = Colors.black
..style = PaintingStyle.fill
..strokeWidth = 2;
final linePaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
#override
void paint(Canvas canvas, Size size) {
final clipRect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.clipRect(clipRect);
canvas.drawPaint(Paint()..color = Colors.black);
final drawableHeight = size.height / 10.0 * border;
final drawableWidth = size.width / 15 * border;
final leftPadding = size.width / 6;
final rightPadding = size.width / 1000;
final hd = drawableHeight / 5.0;
final wd = drawableWidth / x.length.toDouble();
final height = hd * 3.0;
final width = drawableWidth;
if (height <= 0.0 || width <= 0.00) return;
if (max - min < 1.0e-6) return;
final hr = height / (max - min);
final left = border;
final top = border;
final c = Offset(left + wd / 2.0, top + height / 2.0);
_drawOutline(canvas, c, wd, height);
final points = _computePoints(c, wd, height, hr);
final fillpath = _computePath(
points, drawableHeight, width, true, leftPadding, rightPadding);
final labels = _computeLabels();
canvas.drawPath(fillpath, linePaint);
// drawvertical(canvas, c, wd, height, hr, wd);
_drawDataPoints(canvas, points, dotPaintFill, linePaint);
_drawYLabels(canvas, labels, points, wd, top);
// final c1 = Offset(c.dx, top + 4 * hd);
// _drawXLabels(canvas, c1, wd);
}
void _drawXLabels(Canvas canvas, Offset c, double wd) {
x.forEach((xp) {
drawTextCentered(canvas, c, xp, xLabelStyle, wd);
c += Offset(wd, 0);
});
}
void _drawYLabels(Canvas canvas, List<String> labels, List<Offset> points,
double wd, double top) {
var i = 0;
labels.forEach((label) {
final dp = points[i];
final dy = (dp.dy - 15.0) < top ? 15.0 : -15.0;
final ly = dp + Offset(0, dy);
drawTextCentered(canvas, ly, label, yLabelStyle, wd);
i++;
});
}
void _drawDataPoints(
Canvas canvas, List<Offset> points, Paint dotPaintFill, Paint linePaint) {
points.forEach((dp) {
canvas.drawCircle(dp, radius, dotPaintFill);
canvas.drawCircle(dp, radius, linePaint);
});
}
Path _computePath(List<Offset> points, double h, double w, bool cp,
double leftPadding, double rightPadding) {
Path path = Path();
for (var i = 0; i < points.length; i++) {
final p = points[i];
if (i == 0) {
path.moveTo(p.dx, p.dy);
path.lineTo(p.dx, p.dy);
} else {
path.lineTo(p.dx, p.dy);
}
}
return path;
}
List<Offset> _computePoints(
Offset c, double width, double height, double hr) {
List<Offset> points = [];
y.forEach((yp) {
final yy = height - (yp - min) * hr;
final dp = Offset(c.dx, c.dy - height / 2.0 + yy);
points.add(dp);
c += Offset(width, 0);
});
return points;
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
final Paint outlinePaint = Paint()
..strokeWidth = 1.0
..style = PaintingStyle.stroke
..color = Colors.white;
void _drawOutline(Canvas canvas, Offset c, double width, double height) {
y.forEach((p) {
final rect = Rect.fromCenter(center: c, width: width, height: height);
canvas.drawRect(rect, outlinePaint);
c += Offset(width, 0);
});
}
List<String> _computeLabels() {
return y.map((yp) => yp.toStringAsFixed(1)).toList();
}
TextPainter measureText(
String s, TextStyle style, double maxwidth, TextAlign align) {
final span = TextSpan(text: s, style: style);
final tp = TextPainter(
text: span, textAlign: align, textDirection: TextDirection.ltr);
tp.layout(minWidth: 0, maxWidth: maxwidth);
return tp;
}
Size drawTextCentered(
Canvas canvas, Offset c, String text, TextStyle style, double maxwidth) {
final tp = measureText(text, style, maxwidth, TextAlign.center);
final offset = c + Offset(-tp.width / 2.0, -tp.height / 2.0);
tp.paint(canvas, offset);
return tp.size;
}
}
In cubicTO() Method we assign 3 points one for control points to draw a curve . and One for Starting point and another for end point I'm confused about points and math

How to have several overlapping paths with filling in all of them?

I am using CustomPainter to display some figures, however there are some that go on top of others, the problem I have is that I want them to be displayed like this :
And the results is like this :
I don't know why, but to show them i have the code like this :
class _ZonePaint extends CustomPainter {
_ZonePaint({
[...]
});
[...]
#override
void paint(Canvas canvas, Size size) {
/// defines the look of the strokes
final _pencilPaint = Paint()
..strokeCap = StrokeCap.round
..color = secondaryMaterialColor.withOpacity(0.2)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
/// defines the look of the filling
Paint _fillPaint = Paint()
..color = secondaryMaterialColor.withOpacity(0.2)
..style = PaintingStyle.fill;
final Path path = Path();
for (int i = 0; i < list.length; i++) {
final pointList = list[i].pointList;
path.moveTo(
pointList[0].dx,
pointList[0].dy,
);
for (int f = 1; f < pointList.length; f++) {
path.lineTo(pointList[f].dx, pointList[f].dy);
}
path.close();
}
/// draw the zone
canvas.drawPath(path, _pencilPaint);
canvas.drawPath(path, _fillPaint);
}
#override
bool shouldRepaint(_ZonePaint oldDelegate) => false;
}

Detect tap on PositionComponent with FixedResolutionViewport

When using a FixedResolutionViewport the tap event on a PositionComponent is relative to the screen size and not to the resized viewport as expected.
If I remove FixedResolutionViewport the tap is registered correctly.
Am I missing something? What can I do to tap correctly on my Component (Circle)?
This is my code.
I'm using flame 1.1.0.
Vector2 calculateVector(
double x, double y, double fieldW, double fieldH, Vector2 size) {
var calcY = (y / fieldH) * size.y;
var calcX = (y / fieldW) * size.x;
return Vector2(calcX, calcY);
}
class TestGame extends FlameGame with HasTappableComponents, ScaleDetector {
TestGame();
#override
bool debugMode = true;
late double startZoom;
static double fieldW = 1080;
static double fieldH = 1717;
#override
backgroundColor() => const Color.fromARGB(255, 65, 129, 77);
#override
Future<void> onLoad() async {
double maxSide = max(size.x, size.y);
var side = maxSide * (fieldW / fieldH);
camera.viewport =
FixedResolutionViewport(Vector2(side, maxSide), clip: false);
final fieldSprite = await loadSprite('field_checkered.png');
final field = SpriteComponent(
sprite: fieldSprite,
size: Vector2(side, maxSide),
);
var ply = Circle(
position: calculateVector(500, 500, fieldH, fieldH, size),
size: Vector2(50, 50));
await add(field);
await add(ply);
}
}
class Circle extends PositionComponent with TapCallbacks, HasGameRef<TestGame> {
var isSelected = false;
var radius = 0.0;
var paint = Paint()..color = const Color(0xFF80C080);
Circle({required position, required size}) {
super.position = Vector2(
position.x,
position.y,
);
super.size = size;
super.positionType = PositionType.viewport;
radius = size.x / 2;
}
#override
void render(Canvas canvas) {
gameRef.camera.viewport.apply(canvas);
canvas.drawCircle(Offset(radius, radius), radius, paint);
super.render(canvas);
}
#override
void onTapDown(TapDownEvent event) {
isSelected = !isSelected;
if (isSelected) {
paint = Paint()..color = Color.fromARGB(255, 75, 0, 76);
priority = 2;
} else {
paint = Paint()..color = Color.fromARGB(255, 253, 147, 255);
priority = 1;
}
}
}
You should be able to use the Tappable and HasTappables mixins instead. The HasTappableComponents mixin is still in the experimental package and is to be used with the new CameraComponent and not the viewport+camera on the FlameGame.

Flutter) I want to know how to select and delete canvas shapes

I am developing an online whiteboard app.
I can draw different shapes on the screen using the GestureDetector() and CustomPaint(). And now I'm want to create a function to delete the drew shapes.
The way to delete a shape that I am trying to implement is to delete it from the Shapes List when I tap it, rather than simply overwhite it with a background color.
I think it would be good if I could get a Shape data in the tapped position
Can I get a hint about this?
The following are some of the drawing codes I used
// sketcher.dart
class Sketcher extends CustomPainter {
final List<Shape> shapes;
Sketcher(this.shapes) : super();
#override
void paint(Canvas canvas, Size size) {
for (var shape in shapes) {
// shape type
if (shape is Pen) {
canvas.drawPoints(PointMode.polygon, shape.points, shape.paint);
} else if (shape is Square) {
canvas.drawRect(shape.rect!, shape.paint);
} else if (shape is Line) {
canvas.drawLine(shape.p1!, shape.p2!, shape.paint);
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
// ==================================
// shape.dart
class Shape {} // Origin class for all the shapes
// A curved line consisting of points
class Pen extends Shape {
final List<Offset> points = [];
final paint = Paint();
Pen(Color color, double width) {
// width 추가
this.paint
..color = color
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeWidth = width;
}
void add(Offset point) => points.add(point);
}
// Square
class Square extends Shape {
Rect? rect;
double? left, top;
final paint = Paint();
Square(Color color, double width) {
this.paint
..color = color
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeWidth = width;
}
}
class Line extends Shape {
Offset? p1, p2;
final paint = Paint();
Line(Color color, double width) {
this.paint
..color = color
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeWidth = width;
}
}
// ==================================
// draw screen code
extension LastElement<T> on List<T> {
T lastElement() {
return this[this.length - 1];
}
}
List<Shape> _shapes = [];
GestureDetector(
onScaleStart: (details) {
Tool tool = context.read<DrawProvider>().tool;
RenderBox box = context.findRenderObject() as RenderBox;
Offset point = box.globalToLocal(details.focalPoint);
switch (tool) {
// pen
case Tool.pen:
// get seleted tool data
double seletedPenWidth =
context.read<DrawProvider>().seletedPenWidth;
Color seletedPenColor =
context.read<DrawProvider>().seletedPenColor;
shapes.add(Pen(seletedPenColor, seletedPenWidth));
(shapes.lastElement() as Pen).add(point);
break;
// shape
case Tool.square:
double seletedShapeWidth = 5.0; // TODO
Color seletedShapeColor =
context.read<DrawProvider>().seletedShapeColor;
shapes.add(Square(seletedShapeColor, seletedShapeWidth));
double left = point.dx;
double top = point.dy;
(shapes.lastElement() as Square).left = left;
(shapes.lastElement() as Square).top = top;
break;
}
},
onScaleUpdate: (details) {
Tool tool = context.read<DrawProvider>().tool;
RenderBox box = context.findRenderObject() as RenderBox;
Offset point = box.globalToLocal(details.focalPoint);
switch (tool) {
// pen
case Tool.pen:
(shapes.lastElement() as Pen).add(point);
currentLineStreamController.add(shapes.lastElement() as Pen);
break;
// shape
case Tool.shape:
double right = point.dx;
double bottom = point.dy;
// Rect Shape
var currentSquare = shapes.lastElement() as Square;
currentSquare.rect = Rect.fromLTRB(
currentSquare.left!, currentSquare.top!, right, bottom);
currentLineStreamController.add(currentSquare);
break;
}
},
onScaleEnd: (details) {},
child: CustomPaint(
painter: Sketcher(shapes),
)
)

When i try to load base64 image using flame engine at that time i got below error. i used flame: ^0.27.0

when I used aync await method at that time it works properly but when i try to load image in flame's component class I got error:
I have created a Background class that extends flame engine's component class. Now I am tring to load a base64 image using the then function, but I get an error but when I use the async await method for image loading it works properly.
class Background extends Component with Resizable {
static final Paint _paint = Paint();
Size imageSize = Size(411.42857142857144, 822.8571428571429);
#override
void render(Canvas c) {
Rect myRect = const Offset(0.0, 0.0) & Size(size.width, size.height);
Flame.images.fromBase64('demo', imageBase).then((value) {
paintImage(canvas: c, rect: myRect, image: value);
});
}
#override
void update(double t) {
// TODO: implement update
}
void paintImage({
#required Canvas canvas,
#required Rect rect,
#required image.Image image,
String debugImageLabel,
double scale = 1.0,
ColorFilter colorFilter,
BoxFit fit,
Alignment alignment = Alignment.center,
Rect centerSlice,
ImageRepeat repeat = ImageRepeat.noRepeat,
bool flipHorizontally = false,
bool invertColors = false,
FilterQuality filterQuality = FilterQuality.low,
bool isAntiAlias = false,
}) {
if (rect.isEmpty) return;
Size outputSize = rect.size;
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
Offset sliceBorder;
if (centerSlice != null) {
sliceBorder = Offset(
centerSlice.left + inputSize.width - centerSlice.right,
centerSlice.top + inputSize.height - centerSlice.bottom,
);
outputSize = outputSize - sliceBorder as Size;
inputSize = inputSize - sliceBorder as Size;
}
fit ??= centerSlice == null ? BoxFit.scaleDown : BoxFit.fill;
assert(centerSlice == null || (fit != BoxFit.none && fit != BoxFit.cover));
final FittedSizes fittedSizes =
applyBoxFit(fit, inputSize / scale, outputSize);
final Size sourceSize = fittedSizes.source * scale;
Size destinationSize = fittedSizes.destination;
if (centerSlice != null) {
outputSize += sliceBorder;
destinationSize += sliceBorder;
}
// Output size is fully calculated.
if (repeat != ImageRepeat.noRepeat && destinationSize == outputSize) {
repeat = ImageRepeat.noRepeat;
}
final Paint paint = Paint()..isAntiAlias = isAntiAlias;
if (colorFilter != null) paint.colorFilter = colorFilter;
if (sourceSize != destinationSize) {
paint.filterQuality = filterQuality;
}
paint.invertColors = invertColors;
final double halfWidthDelta =
(outputSize.width - destinationSize.width) / 2.0;
final double halfHeightDelta =
(outputSize.height - destinationSize.height) / 2.0;
final double dx = halfWidthDelta +
(flipHorizontally ? -alignment.x : alignment.x) * halfWidthDelta;
final double dy = halfHeightDelta + alignment.y * halfHeightDelta;
final Offset destinationPosition = rect.topLeft.translate(dx, dy);
final Rect destinationRect = destinationPosition & destinationSize;
final bool needSave = repeat != ImageRepeat.noRepeat || flipHorizontally;
if (needSave) canvas.save();
if (repeat != ImageRepeat.noRepeat) canvas.clipRect(rect);
if (flipHorizontally) {
final double dx = -(rect.left + rect.width / 2.0);
canvas.translate(-dx, 0.0);
canvas.scale(-1.0, 1.0);
canvas.translate(dx, 0.0);
}
if (centerSlice == null) {
final Rect sourceRect = alignment.inscribe(
sourceSize,
Offset.zero & inputSize,
);
if (repeat == ImageRepeat.noRepeat) {
canvas.drawImageRect(image, sourceRect, destinationRect, paint);
} else {
print("no repet else");
}
}
//if (needSave) canvas.restore();
}
}
This is totally not acceptable:
#override
void render(Canvas c) {
Rect myRect = const Offset(0.0, 0.0) & Size(size.width, size.height);
Flame.images.fromBase64('demo', imageBase).then((value) {
paintImage(canvas: c, rect: myRect, image: value);
});
}
The render method must be sync. It takes a canvas object to be rendered right now. You cannot make async operations on here! Of course the canvas will be disposed, it only lives for one frame. The render method is called every frame and must be quick and short lived. When the image actually loads, you no longer will have access to canvas because the whole render cycle will be done. It was already rendered on the screen! You cannot change the past! That doesn't make sense.
What you need to do is to load the image elsewhere and render conditionally if it's loaded. Move the loading to your constructor:
Flame.images.fromBase64('demo', imageBase).then((value) {
this.image = value;
});
And then on the render method, render conditionally:
#override
void render(Canvas c) {
Rect myRect = const Offset(0.0, 0.0) & Size(size.width, size.height);
if (this.image != null) {
paintImage(canvas: c, rect: myRect, image: this.image);
}
}
By creating an image field on your component. Also consider using SpriteComponent instead that does all that for you in the correct way. And never make the render or update methods async ;)