Futter CustomPainer, How to erase without having to draw in white?

72 views Asked by At

I am working on a small Flutter application for children, the concept is that there is an image in the background on which users have to draw. I am using Custom Painter for that.

There is a feature to erase what they draw and that is where the problem begins, the easy way to erase is to draw using the white color but when I use that it will also erase the background image which is very important. What is the proper way to erase without having to draw white on the image?

Here is the demo:

enter image description here

Here is the source code:

The main page

Listener(
      onPointerDown: (details) {
        RenderBox? renderBox = context.findRenderObject() as RenderBox?;
        if (renderBox != null) {
          setState(() {
            points.add(
                DrawingPoints(
                    points: renderBox.globalToLocal(details.position),
                    paint: Paint()
                      ..strokeCap = strokeCap
                      ..isAntiAlias = true
                      ..color = widget.selectedColor.withOpacity(1.0)
                      ..strokeWidth = strokeWidth
                )
            );
          });
        }
      },
      onPointerMove: (details) {
        RenderBox? renderBox = context.findRenderObject() as RenderBox?;
        if (renderBox != null) {
          setState(() {
            points.add(DrawingPoints(
                points: renderBox.globalToLocal(details.position),
                paint: Paint()
                  ..strokeCap = strokeCap
                  ..isAntiAlias = true
                  ..color = widget.selectedColor.withOpacity(1.0)
                  ..strokeWidth = strokeWidth));
          });
        }
      },
      onPointerUp: (details) {
        setState(() {
          points.add(null);
        });
      },
      child: CustomPaint(
        size: Size.infinite,
        painter: DrawingPainter(
          pointsList: points,
        ),
      ),
    )

DrawingPainter

class DrawingPainter extends CustomPainter {

  final List<DrawingPoints?> pointsList;
  List<Offset> offsetPoints = [];

  DrawingPainter({required this.pointsList});

  @override
  void paint(Canvas canvas, Size size) {
    for (int i = 0; i < pointsList.length - 1; i++) {

      if (pointsList[i] != null && pointsList[i + 1] != null) {
        canvas.drawLine(pointsList[i]!.points, pointsList[i +1]!.points, pointsList[i]!.paint,);
      }
      else if (pointsList[i] != null && pointsList[i + 1] == null) {
        offsetPoints.clear();
        offsetPoints.add(pointsList[i]!.points);
        offsetPoints.add(Offset(pointsList[i]!.points.dx + 0.1, pointsList[i]!.points.dy + 0.1));
        canvas.drawPoints(PointMode.points, offsetPoints, pointsList[i]!.paint);
      }
    }
  }
  @override
  bool shouldRepaint(DrawingPainter oldDelegate) => true;
}

DrawingPoints

class DrawingPoints {
  final Offset points;
  final Paint paint;

  const DrawingPoints({required this.points, required this.paint});

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;

    return other is DrawingPoints &&
      other.points.dx == points.dy &&
      other.paint.color == paint.color;
  }

  @override
  int get hashCode => points.hashCode ^ paint.hashCode;
}
2

There are 2 answers

0
Loïc Yabili On BEST ANSWER

I was finally been able to solve the issue.

The solution looks very simple, what I did is to put the background image in a Stack as suggested by @iStornZ but I put the image above the custom painter, the image has to be transparent so that even when we paint in white the whole image will still be visible.

Source code

Stack(
    children: [
      CustomPaint(
        size: Size.infinite,
        painter: DrawingPainter(
          pointsList: points,
          erasingPoints: erasingPoints,
          myBackground: myBackground,
        ),
      ),
      Container(
        decoration: const BoxDecoration(
          image: DecorationImage(
            image: AssetImage('assets/puppy-dot.png'),
            fit: BoxFit.fitHeight,
          ),
        ),
      ),
    ],
  )

Demo

enter image description here

1
smita On

To implement erasing functionality without affecting the background image, you can modify your code to handle two separate lists of points - one for drawing and one for erasing. You will also need to add a toggle button or some way for the user to switch between drawing and erasing modes.

Here's an example of how you can do this:

  1. Create separate lists for drawing and erasing points:
List<DrawingPoints?> drawingPoints = [];
List<DrawingPoints?> erasingPoints = [];
  1. Modify your onPointerDown, onPointerMove, and onPointerUp callbacks to add points to the appropriate list based on the current mode (drawing or erasing):
onPointerDown: (details) {
  RenderBox? renderBox = context.findRenderObject() as RenderBox?;
  if (renderBox != null) {
    setState(() {
      if (isErasing) {
        erasingPoints.add(
          DrawingPoints(
            points: renderBox.globalToLocal(details.position),
            paint: Paint()
              ..color = Colors.transparent  // Set erasing color to transparent
              ..blendMode = BlendMode.clear // Set blend mode to clear
              ..isAntiAlias = true
              ..style = PaintingStyle.stroke
              ..strokeWidth = strokeWidth,
          ),
        );
      } else {
        drawingPoints.add(
          DrawingPoints(
            points: renderBox.globalToLocal(details.position),
            paint: Paint()
              ..strokeCap = strokeCap
              ..isAntiAlias = true
              ..color = widget.selectedColor.withOpacity(1.0)
              ..strokeWidth = strokeWidth,
          ),
        );
      }
    });
  }
},
onPointerMove: (details) {
  RenderBox? renderBox = context.findRenderObject() as RenderBox?;
  if (renderBox != null) {
    setState(() {
      if (isErasing) {
        erasingPoints.add(
          DrawingPoints(
            points: renderBox.globalToLocal(details.position),
            paint: Paint()
              ..color = Colors.transparent  // Set erasing color to transparent
              ..blendMode = BlendMode.clear // Set blend mode to clear
              ..isAntiAlias = true
              ..style = PaintingStyle.stroke
              ..strokeWidth = strokeWidth,
          ),
        );
      } else {
        drawingPoints.add(
          DrawingPoints(
            points: renderBox.globalToLocal(details.position),
            paint: Paint()
              ..strokeCap = strokeCap
              ..isAntiAlias = true
              ..color = widget.selectedColor.withOpacity(1.0)
              ..strokeWidth = strokeWidth,
          ),
        );
      }
    });
  }
},
onPointerUp: (details) {
  setState(() {
    if (isErasing) {
      erasingPoints.add(null);
    } else {
      drawingPoints.add(null);
    }
  });
},
  1. Modify your DrawingPainter to handle both drawing and erasing points:
class DrawingPainter extends CustomPainter {
  final List<DrawingPoints?> drawingPoints;
  final List<DrawingPoints?> erasingPoints;

  DrawingPainter({required this.drawingPoints, required this.erasingPoints});

  @override
  void paint(Canvas canvas, Size size) {
    for (int i = 0; i < drawingPoints.length - 1; i++) {
      if (drawingPoints[i] != null && drawingPoints[i + 1] != null) {
        canvas.drawLine(
          drawingPoints[i]!.points,
          drawingPoints[i + 1]!.points,
          drawingPoints[i]!.paint,
        );
      }
    }

    for (int i = 0; i < erasingPoints.length - 1; i++) {
      if (erasingPoints[i] != null && erasingPoints[i + 1] != null) {
        canvas.drawLine(
          erasingPoints[i]!.points,
          erasingPoints[i + 1]!.points,
          erasingPoints[i]!.paint,
        );
      }
    }
  }

  @override
  bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
  1. Finally, add a variable to track the current mode (drawing or erasing), and a toggle button to switch between modes:
bool isErasing = false;

// Toggle button callback
void toggleMode() {
  setState(() {
    isErasing = !isErasing;
  });
}

// Use isErasing to determine which list of points to pass to the DrawingPainter
CustomPaint(
  size: Size.infinite,
  painter: DrawingPainter(
    drawingPoints: drawingPoints,
    erasingPoints: erasingPoints,
  ),
),

// Add a toggle button to switch between drawing and erasing modes
ElevatedButton(
  onPressed: toggleMode,
  child: Text(isErasing ? 'Switch to Drawing' : 'Switch to Erasing'),
),

With these changes, you should be able to draw in drawing mode and erase in erasing mode without affecting the background image. The erasing points will use a transparent color and the BlendMode.clear blend mode to effectively erase the drawn lines.