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),
)
)
Related
enter image description here
enter image description here
It looks like this. The center is supposed to be filled with white, but it's transparent.
How can I fill this marker?
I used this code. It's from this link.
I wanted to set the marker's color to black.
So I used this code.
`
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MarkerGenerator {
final _markerSize;
double _circleStrokeWidth = 0;
double _circleOffset = 0;
double _outlineCircleWidth = 0;
double _fillCircleWidth = 0;
double _iconSize = 0;
double _iconOffset = 0;
MarkerGenerator(this._markerSize) {
// calculate marker dimensions
_circleStrokeWidth = _markerSize / 10.0;
_circleOffset = _markerSize / 2;
_outlineCircleWidth = _circleOffset - (_circleStrokeWidth / 2);
_fillCircleWidth = _markerSize / 3;
final outlineCircleInnerWidth = _markerSize - (2 * _circleStrokeWidth);
_iconSize = sqrt(pow(outlineCircleInnerWidth, 2) / 2);
final rectDiagonal = sqrt(2 * pow(_markerSize, 2));
final circleDistanceToCorners =
(rectDiagonal - outlineCircleInnerWidth) / 2;
_iconOffset = sqrt(pow(circleDistanceToCorners, 2) / 2);
}
/// Creates a BitmapDescriptor from an IconData
Future<BitmapDescriptor> createBitmapDescriptorFromIconData(IconData iconData,
Color iconColor, Color circleColor, Color backgroundColor) async {
final pictureRecorder = PictureRecorder();
final canvas = Canvas(pictureRecorder);
// _paintCircleFill(canvas, backgroundColor);
// _paintCircleStroke(canvas, circleColor);
_paintIcon(canvas, iconColor, iconData);
final picture = pictureRecorder.endRecording();
final image =
await picture.toImage(_markerSize.round(), _markerSize.round());
final bytes = await image.toByteData(format: ImageByteFormat.png);
return BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
}
/// Paints the icon background
void _paintCircleFill(Canvas canvas, Color color) {
final paint = Paint()
..style = PaintingStyle.fill
..color = color;
canvas.drawCircle(
Offset(_circleOffset, _circleOffset), _fillCircleWidth, paint);
}
/// Paints a circle around the icon
void _paintCircleStroke(Canvas canvas, Color color) {
final paint = Paint()
..style = PaintingStyle.stroke
..color = color
..strokeWidth = _circleStrokeWidth;
canvas.drawCircle(
Offset(_circleOffset, _circleOffset), _outlineCircleWidth, paint);
}
/// Paints the icon
void _paintIcon(Canvas canvas, Color color, IconData iconData) {
final textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text = TextSpan(
text: String.fromCharCode(iconData.codePoint),
style: TextStyle(
letterSpacing: 0.0,
fontSize: _markerSize,
fontFamily: iconData.fontFamily,
color: color,
));
textPainter.layout();
// textPainter.paint(canvas, Offset(_iconOffset, _iconOffset));
textPainter.paint(canvas, Offset.zero);
}
}
`
I would like to fill in the marker.
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;
}
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?
I'm trying to draw this kind of shape with flutter:
Expected result
So far I can draw an arc using drawArc():
class CurvePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.green;
paint.style = PaintingStyle.fill;
paint.strokeWidth = 5;
final rect = Rect.fromLTRB(50, 100, 130, 200);
final startAngle = -pi;
final sweepAngle = pi;
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
But is that the right way to do it or should I use quadraticBezierTo and drawPath?
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 ;)