preface

The previous two articles explained how to use Canvas to achieve beautiful dials and wechat red envelope effect in Flutter, respectively. This article will continue to guide you to achieve simple white rabbit effect using Canvas. The core techniques used are quadratic bezier curve and cubic Bezier curve, and the use of Path.

As usual, let’s take a look at the final result:

implementation

Careful observation of the above renderings shows that the little white rabbit with stick pen is actually composed of multiple “3” graphics with different shapes and positions, so the core is how to draw the shape of “3”. Here, two cubic Bezier curves are used to draw.

The whole painting is done in the CustomPainter paint method, so first create a RabbitPainter descendant from CustomPainter and then use it in the Widget with CustomPaint as follows:

class RabbitPainter extends CustomPainter{

  @override
  void paint(Canvas canvas, Size size) async{
    
  }
}

Container(
  color: Colors.white,
  child: Center(
    child: CustomPaint(
      painter: RabbitPainter(),
      size: Size(0.8.sw, 1.sw),
    ),
  ),
);
Copy the code

Draw the “3”

As mentioned above, the cubic Bezier curve is used to draw the shape of “3”, and the cubic Bezier curve requires four points, two endpoints and two curves to control the points of the curve, as shown below:

Create three bezier curve paths using Path as follows:

Path path = Path();
path.moveTo(x, y);
path.cubicTo(x1, y1, x2, y2, x3, y3);
Copy the code

This creates half a “3”, and then adds a cubic Bezier curve to achieve a “3” graphic path, encapsulated as follows:

/// Create a path of shape 3
Path createThreePath(List<Offset> points) {
  Path path = Path();
  path.moveToPoint(points[0]);
  path.cubicToPoints(points.sublist(1.4));
  path.cubicToPoints(points.sublist(4.7));
  return path;
}
Copy the code

The parameters passed in are a set of seven points. Create a Path that first moveTo the first point, and then divide the remaining six points into three points twice by adding two cubic Bezier curves to the Path to form a “3” shape.

MoveToPoint and cubicToPoints are custom Path extension methods, which are easy to use, and their implementation is as follows:

extension PathExt on Path{
  
  void moveToPoint(Offset point){
    moveTo(point.dx, point.dy);
  }
  
  void cubicToPoints(List<Offset> points){
    if(points.length ! =3) {throw "params points length must 3";
    }
    cubicTo(points[0].dx, points[0].dy, points[1].dx, points[1].dy, points[2].dx,  points[2].dy); }}Copy the code

This encapsulates a “3” shape.

The body

First of all, draw the main body of the white rabbit, namely the body Outlines on the left and right sides, and the body Outlines on the left and right sides are composed of a reverse “3” and a forward “3”. Therefore, first of all, we use the method encapsulated above to draw a reverse “3” shape.

List<Offset> createLeftBodyPoints(){
  var position1 = Offset(110.w, 100.w);
  var position2 = Offset(30.w, position1.dy + 20.w);
  var position3 = Offset(40.w, position2.dy + 100.w);
  var position4 = Offset(110.w, position3.dy + 10.w);

  var position5 = Offset(50.w, position4.dy + 20.w);
  var position6 = Offset(60.w, position5.dy + 80.w);
  var position7 = Offset(125.w, position6.dy + 10.w);

  return [
    position1,
    position2,
    position3,
    position4,
    position5,
    position6,
    position7
  ];
}

var leftBodyPoints = createLeftBodyPoints();
var leftBodyPath = createThreePath(leftBodyPoints);
canvas.drawPath(leftBodyPath, _paint);
Copy the code

First create 7 points, the 7 points used to draw the “3” shape, then call the wrapped method to create a Path, and use Canvas.drawPath to draw the graph.

Values such as 110. W are used here as the unit of adaptation. For screen adaptation of Flutter, please refer to:

The implementation effect is as follows:

It is possible to draw the left side of the rabbit’s body by using the same method, but it is easier to flip the left side of the rabbit’s Path, as follows:

var matrix4 = Matrix4.translationValues(0.8.sw, 0.0);
matrix4.rotateY(2*pi/2);
var rightBodyPath = leftBodyPath.transform(matrix4.storage);
canvas.drawPath(rightBodyPath, _paint);
Copy the code

Use Matrix4 to pan and rotate the Path. The reason for the pan is that the width of the canvas is set to 0.8.sw, which means x is shifted to the far right of the canvas, and then rotated 180 degrees on the Y axis, which means the graph is flipped over.

The left and right contours of the body are thus achieved.

ear

The ear is actually a “3” shape, but it is placed upside down and stretched up to make the two “3” convex shapes more protruding

var leftFirstPosition = leftBodyPath.getPositionFromPercent(0);
var rightFirstPosition = rightBodyPath.getPositionFromPercent(0);

var centerWidth = rightFirstPosition.dx - leftFirstPosition.dx;

var position1 = Offset(leftFirstPosition.dx, leftFirstPosition.dy);
var position2 = Offset(leftFirstPosition.dx  - 50.w, - 20.w);
var position3 = Offset(leftFirstPosition.dx  + centerWidth/2.- 20.w);
var position4 = Offset(leftFirstPosition.dx  + centerWidth/2, leftFirstPosition.dy);
var position5 = Offset(leftFirstPosition.dx  + centerWidth/2 + 5.w, - 12.w);
var position6 = Offset(rightFirstPosition.dx  + 55.w, - 12.w);
var position7 = Offset(rightFirstPosition.dx, rightFirstPosition.dy);

var points = [position1, position2, position3, position4, position5, position6, position7];

var earPath = createThreePath(points);

canvas.drawPath(earPath, _paint);
Copy the code

The two ends of the ear are respectively the starting point of the left body and the starting point of the right body, so calculate the first point of the figure on the left and right, and calculate the distance between the two points. GetPositionFromPercent is also used to customize the extension Path method, which is used to obtain the corresponding points on the Path by percentage, as follows:

extension PathExt on Path{
	Offset getPositionFromPercent(double percent){
    var pms = computeMetrics();
    var pm = pms.first;
    varposition = pm.getTangentForOffset(pm.length * percent)? .position ?? Offset.zero;returnposition; }}Copy the code

Then create seven points for building the “3” figure, and finally achieve the effect of the ear:

Hands and feet

Rabbit hands and feet are also composed of two “3” graphics, one hand and one foot is a “3”, first draw the left hand and feet, the code is as follows:

var handsFeetPosition1 = Offset(leftBodyPoints[3].dx + 10.w, leftBodyPoints[3].dy + 10.w);
var handsFeetPosition2 = Offset(handsFeetPosition1.dx + 20.w, handsFeetPosition1.dy + 5.w);
var handsFeetPosition3 = Offset(handsFeetPosition2.dx + 20.w, handsFeetPosition2.dy + 40.w);
var handsFeetPosition4 = Offset(handsFeetPosition1.dx, handsFeetPosition3.dy + 15.w);

var handsFeetPosition5 = Offset(handsFeetPosition4.dx + 20.w, handsFeetPosition4.dy + 10.w);
var handsFeetPosition6 = Offset(handsFeetPosition5.dx + 10.w, handsFeetPosition5.dy + 20.w);
var handsFeetPosition7 = Offset(leftBodyPoints.last.dx, leftBodyPoints.last.dy);
var leftHandsFeetPoints = [
  handsFeetPosition1,
  handsFeetPosition2,
  handsFeetPosition3,
  handsFeetPosition4,
  handsFeetPosition5,
  handsFeetPosition6,
  handsFeetPosition7,
];

var leftHandsFeetPath = createThreePath(leftHandsFeetPoints);
canvas.drawPath(leftHandsFeetPath, _paint);
Copy the code

Also create 7 points for drawing “3”. Here, the starting point is offset from the central end point on the left side of the rabbit body. The drawing effect is as follows:

Do the same for the hands and feet on the right:

var rightHandsFeetPosition1 = Offset(leftBodyPoints[3].dx + 80.w, leftBodyPoints[3].dy + 15.w);
var rightHandsFeetPosition2 = Offset(rightHandsFeetPosition1.dx - 20.w, rightHandsFeetPosition1.dy + 5.w);
var rightHandsFeetPosition3 = Offset(rightHandsFeetPosition2.dx - 15.w, rightHandsFeetPosition2.dy + 30.w);
var rightHandsFeetPosition4 = Offset(rightHandsFeetPosition1.dx - 15.w, rightHandsFeetPosition3.dy + 15.w);

var rightHandsFeetPosition5 = Offset(rightHandsFeetPosition4.dx - 15.w, rightHandsFeetPosition4.dy + 10.w);
var rightHandsFeetPosition6 = Offset(rightHandsFeetPosition5.dx - 5.w, rightHandsFeetPosition5.dy + 20.w);

var rightLastPosition = rightPath.getPositionFromPercent(1);
var rightHandsFeetPosition7 = Offset(rightLastPosition.dx, rightLastPosition.dy);
var rightHandsFeetPoints = [
  rightHandsFeetPosition1,
  rightHandsFeetPosition2,
  rightHandsFeetPosition3,
  rightHandsFeetPosition4,
  rightHandsFeetPosition5,
  rightHandsFeetPosition6,
  rightHandsFeetPosition7,
];
var rightHandsFeetPath = createThreePath(rightHandsFeetPoints);

canvas.drawPath(rightHandsFeetPath, _paint);
Copy the code

The effect is as follows:

Carrot leaves

The turnip leaf also consists of a “3”, the code is as follows:

var point1 = Offset(leftHandsFeetPoints.first.dx + 20.w, leftHandsFeetPoints.first.dy - 5.w);
var point2 = Offset(leftHandsFeetPoints.first.dx - 5.w, leftHandsFeetPoints.first.dy - 45.w);
var point3 = Offset(leftHandsFeetPoints.first.dx  + 45.w, leftHandsFeetPoints.first.dy- 45.w);
var point4 = Offset(leftHandsFeetPoints.first.dx + 35.w, leftHandsFeetPoints.first.dy - 10.w);
var point5 = Offset(leftHandsFeetPoints.first.dx + 40.w, leftHandsFeetPoints.first.dy - 35.w);
var point6 = Offset(rightFirstPosition.dx  + 0.w, leftHandsFeetPoints.first.dy- 35.w);
var point7 = Offset(leftHandsFeetPoints.first.dx + 50.w, leftHandsFeetPoints.first.dy - 5.w);

var points = [point1, point2,  point3, point4, point5, point6, point7];
Path radishLeafPath = createThreePath(points);
canvas.drawPath(radishLeafPath, _paint)
Copy the code

The “3” of radish leaves takes the starting point of the path of hands and feet on the left and right sides as a reference point for a certain unit of cheap, and the drawing effect is as follows:

mouth

The rabbit’s mouth also consists of an upside-down “3” with the following code:

var radishHeadMinYPosition = radishLeafPath.getMinYPosition();
var mouthPosition1 = Offset(radishHeadMinYPosition.dx - 10.w, radishHeadMinYPosition.dy - 20.w);
var mouthPosition2 = Offset(mouthPosition1.dx - 2.w, mouthPosition1.dy + 10.w);
var mouthPosition3 = Offset(mouthPosition2.dx + 18.w, mouthPosition2.dy + 5.w);
var mouthPosition4 = Offset(mouthPosition3.dx + 2.w, mouthPosition1.dy + 2.w);

var mouthPosition5 = Offset(mouthPosition4.dx , mouthPosition4.dy + 10.w);
var mouthPosition6 = Offset(mouthPosition5.dx + 18.w, mouthPosition5.dy + 2.w);
var mouthPosition7 = Offset(mouthPosition6.dx + 2.w, mouthPosition1.dy);
var mouthPoints = [
  mouthPosition1,
  mouthPosition2,
  mouthPosition3,
  mouthPosition4,
  mouthPosition5,
  mouthPosition6,
  mouthPosition7,
];
var mouthPath = createThreePath(mouthPoints);

canvas.drawPath(mouthPath, _paint);
Copy the code

The position of the mouth is referred to by the turnip leaf. In a certain position above the turnip leaf, getMinYPosition is used to calculate the minimum point of Y of the Path of the turnip leaf. This method is also a custom extension method from Path, which is implemented as follows:

extension PathExt on Path{
  Offset getMinYPosition(){
    var pms = computeMetrics();
    var pm = pms.first;
    var minPosition = pm.getTangentForOffset(0)? .position;for(int i = 0; i< pm.length; i++){
      varposition = pm.getTangentForOffset(i.toDouble())? .position;if(minPosition == null|| (position ! =null&& position.dy < minPosition.dy)){ minPosition = position; }}returnminPosition ?? Offset.zero; }}Copy the code

By calculating all points on the Path, cycle all points to take out the point with the smallest Y value, which is the fixed point of the turnip leaf here, and then adjust the offset by a certain unit, and finally draw the figure of the mouth, the effect is as follows:

eyes

Glasses are finally not “3”, just use the quadratic Bezier curve, the code is as follows:

var point1 = Offset(leftBodyPoints.first.dx - 5.w, leftBodyPoints.first.dy + 50.w);
var point2 = Offset(point1.dx + 10.w, point1.dy - 13.w);
var point3 = Offset(point1.dx + 20.w, point1.dy);

Path leftEyesPath = Path();
leftEyesPath.moveToPoint(point1);
leftEyesPath.quadraticBezierToPoints([point2, point3]);

canvas.drawPath(leftEyesPath, _paint);

var rightFirstPosition = rightBodyPath.getPositionFromPercent(0);
var point1 = Offset(rightFirstPosition.dx - 15.w, rightFirstPosition.dy + 50.w);
var point2 = Offset(point1.dx + 10.w, point1.dy - 13.w);
var point3 = Offset(point1.dx + 20.w, point1.dy);

Path rightEyesPath = Path();
rightEyesPath.moveToPoint(point1);
rightEyesPath.quadraticBezierToPoints([point2, point3]);

canvas.drawPath(rightEyesPath, _paint);
Copy the code

The eye is drawn using a quadratic Bezier curve, requiring only three points, i.e. two endpoints and a point that controls the amplitude of the curve. Here, the eyes are drawn with the starting points on the left and right of the body respectively as reference points. Since the Path of the contour on the right of the body is obtained by rotation on the left, getPositionFromPercent is used to obtain the first point of the Path on the right, and then offset by a certain unit. The final results are as follows:

carrot

In front of the carrot leaf, the next is to draw the main body of the carrot, the main body of the carrot is to connect the rabbit’s left and right hands and feet with curves in the middle to form a closed figure is the main body of the carrot.

Take a look at the top half of the curve drawing:

var radishTopPath = Path();
var radishTopPosition1 = leftHandsFeetPath.getPositionFromPercent(0.07);
var radishTopPosition2 = radishLeafPath.getPositionFromPercent(0).translate(0.- 6.w);
var radishTopPosition3 = radishLeafPath.getPositionFromPercent(1).translate(0.- 9.w);
var radishTopPosition4 = rightHandsFeetPath.getPositionFromPercent(0.07);

radishTopPath.moveToPoint(radishTopPosition1);
radishTopPath.cubicToPoints([radishTopPosition2, radishTopPosition3, radishTopPosition4]);

canvas.drawPath(radishTopPath, _paint);
Copy the code

The top curve of carrot is also drawn using cubic Bezier curves. The starting point is 0.07 of the path of the left hand and foot, and the specific point coordinates are obtained by getPositionFromPercent. The end point is 0.07 of the path of the right hand and foot, which is symmetric with the left. The two control points of the curve have been used as the starting point and ending point of the carrot leaf for a certain unit of deviation, and the final result is as follows:

Next look at the bottom of the curve drawing, implementation approach and the top of the curve is consistent, but not used at the bottom of a three Bessel curve, but a quadratic bezier curve, with left and right hands and feet on the path to the specified point 0.9 (path location) as a curve at the bottom of the start and end point, curve control points to the starting point coordinates for a certain unit of migration, code implementation is as follows:

var radishBottomPath = Path();
var radishBottomPosition1 = leftHandsFeetPath.getPositionFromPercent(0.9);
var radishBottomPosition2 = Offset(radishBottomPosition1.dx + 18.w, radishBottomPosition1.dy+40.w);
var radishBottomPosition3 = rightHandsFeetPath.getPositionFromPercent( 0.9);

radishBottomPath.moveToPoint(radishBottomPosition1);
radishBottomPath.quadraticBezierToPoints([radishBottomPosition2, radishBottomPosition3]);

canvas.drawPath(radishBottomPath, _paint);
Copy the code

The effect is as follows:

It looks like a carrot, but it doesn’t feel like much, so I’m going to add some embellishments to make it look more like a carrot. Draw three short curves inside the carrot using quadratic Bezier curves:

var radishBodyPath1 = Path();
var radishBodyPosition1 = leftHandsFeetPath.getPositionFromPercent( 0.3);
var radishBodyPosition2 = Offset(radishBodyPosition1.dx + 5.w, radishBodyPosition1.dy- 3.w);
var radishBodyPosition3 = Offset(radishBodyPosition2.dx + 10.w, radishBodyPosition1.dy+3.w);
radishBodyPath1.moveToPoint(radishBodyPosition1);
radishBodyPath1.quadraticBezierToPoints([radishBodyPosition2, radishBodyPosition3]);

var radishBodyPath2 = Path();
var radishBodyPosition4 = rightHandsFeetPath.getPositionFromPercent( 0.7);
var radishBodyPosition5 = Offset(radishBodyPosition4.dx - 5.w, radishBodyPosition4.dy- 3.w);
var radishBodyPosition6 = Offset(radishBodyPosition5.dx - 10.w, radishBodyPosition5.dy+3.w);
radishBodyPath2.moveToPoint(radishBodyPosition4);
radishBodyPath2.quadraticBezierToPoints([radishBodyPosition5, radishBodyPosition6]);

var radishBodyPath3 = Path();
var radishBodyPosition7 = rightHandsFeetPath.getPositionFromPercent( 0.78);
var radishBodyPosition8 = Offset(radishBodyPosition7.dx - 3.w, radishBodyPosition7.dy- 3.w);
var radishBodyPosition9 = Offset(radishBodyPosition8.dx - 5.w, radishBodyPosition8.dy+3.w);
radishBodyPath3.moveToPoint(radishBodyPosition7);
radishBodyPath3.quadraticBezierToPoints([radishBodyPosition8, radishBodyPosition9]);

canvas.drawPath(radishBodyPath1, _paint);
canvas.drawPath(radishBodyPath2, _paint);
canvas.drawPath(radishBodyPath3, _paint);
Copy the code

Take 0.3 of the left hand and foot paths and 0.7 and 0.78 of the right hand and foot paths as the starting points of the curve, and then offset by a certain unit, finally draw three internal curves of the carrot for interplanting, and the final effect is as follows:

The tail

After the above drawing, the rabbit looks like that, but with a tail missing. After all, rabbits can’t live without a tail. The tail is also a third-order Bezier curve, and the code is as follows:

var tailPath = Path();
var tailPosition1 = rightBodyPath.getPositionFromPercent(0.8);
var tailPosition2 = Offset(tailPosition1.dx + 35.w, tailPosition1.dy - 30.w);
var tailPosition3 = Offset(tailPosition1.dx + 35.w, tailPosition1.dy + 40.w);
var tailPosition4 = rightBodyPath.getPositionFromPercent(0.9);

tailPath.moveToPoint(tailPosition1);
tailPath.cubicToPoints([tailPosition2, tailPosition3, tailPosition4]);

canvas.drawPath(tailPath, _paint);
Copy the code

Take 0.8 position of the path of the right body as the starting point, 0.9 position as the end point, and add two control points with a certain unit offset from the starting point in the middle to finally achieve the effect of the tail, as shown in the figure below:

Overall color fill

After the graph is drawn, the next step is to fill the color. First, fill the whole with white. The method adopted here is to merge the outermost Path of the small white rabbit into a Path and fill it with white. The code is as follows:

var bodyBorderPath = Path();
var positionFromPathPercent = earPath.getPositionFromPercent(0); bodyBorderPath .. moveTo(positionFromPathPercent.dx, positionFromPathPercent.dy) .. addPointsFromPath(earPath) .. addPointsFromPath(rightBodyPath) .. addPointsFromPath(rightHandsFeetPath.getPathFromPercent(0.9.1), isReverse: true)
  ..addPointsFromPath(radishBottomPath, isReverse: true)
  ..addPointsFromPath(leftHandsFeetPath.getPathFromPercent(0.9.1))
  ..addPointsFromPath(leftBodyPath, isReverse: true).. addPath(tailPath, Offset.zero) .. close(); _paint.style = PaintingStyle.fill; _paint.color = Colors.white; canvas.drawPath(bodyBorderPath, _paint);Copy the code

Create bodyBorderPath and get the first point of earPath, move bodyBorderPath to this point, Then call addPointsFromPath to add the other paths in turn to bodyBorderPath. AddPointsFromPath is a custom method that extends Path as follows:

void addPointsFromPath(Path copyPath, {bool isReverse = false{})var pms = copyPath.computeMetrics();
  var pm = pms.first;
  if(isReverse){
    for(double i = pm.length; i > 0; i--){
      varposition = pm.getTangentForOffset(i.toDouble())? .position;if(position ! =null){ lineTo(position.dx, position.dy); }}}else{
    for(int i = 0; i< pm.length; i++){
      varposition = pm.getTangentForOffset(i.toDouble())? .position;if(position ! =null){ lineTo(position.dx, position.dy); }}}}Copy the code

The first parameter is the Path to be added, and the second parameter isReverse indicates whether to reverse the Path to be added to the current Path. This is done by first calculating the points of the Path to be added, and then iterating through each point using lineTo to add each point to the current Path.

For rightHandsFeetPath and leftHandsFeetPath, getPathFromPercent is used to cut part of the Path and add it to bodyBorderPath. GetPathFromPercent is also a custom extension of Path.

Path getPathFromPercent( double startPercent, double endPercent){
  var pms = computeMetrics();
  var pm = pms.first;
  var resultPath = pm.extractPath(pm.length * startPercent, pm.length * endPercent);
  return resultPath;
}
Copy the code

The code is simple, calculating the point of Path and then using the extractPath method to get the Path from the specified start location to the specified end location.

The final result is as follows:

The ear fill

After filling the whole with white, fill the ear with pink, not the whole ear with pink, but the part. The code implementation is as follows:

_paint.color = Color(0xFFE79EC3);

var leftFirstPosition = leftBodyPath.getPositionFromPercent(0);
var rightFirstPosition = rightBodyPath.getPositionFromPercent(0);
/// The left ear fill
canvas.save();
var leftEarRect = Rect.fromLTWH(leftFirstPosition.dx - 3.w, 25.w, 30.w, (leftFirstPosition.dy - 30.w));
canvas.translate(leftEarRect.center.dx,  leftEarRect.center.dy);
Path leftEarPath = Path();
leftEarPath.addOval(Rect.fromLTWH(- leftEarRect.width /2, -leftEarRect.height/2, leftEarRect.width, leftEarRect.height));

leftEarPath  = leftEarPath.transform(Matrix4.rotationZ(-pi/15).storage);
canvas.drawPath(leftEarPath, _paint);
canvas.restore();

/// The right ear fill
canvas.save();
var rightEarRect = Rect.fromLTWH(rightFirstPosition.dx - 23.w, 25.w, 30.w, (rightFirstPosition.dy - 30.w));
canvas.translate(rightEarRect.center.dx,  rightEarRect.center.dy);

Path rightEarPath = Path();
rightEarPath.addOval(Rect.fromLTWH(- rightEarRect.width / 2, - rightEarRect.height / 2, rightEarRect.width, rightEarRect.height));
rightEarPath  = rightEarPath.transform(Matrix4.rotationZ(pi/10).storage);
canvas.drawPath(rightEarPath, _paint);
canvas.restore();
Copy the code

First call canvas.save() to save the canvas, create a Rect based on the starting point of the ear, then move the canvas to the center of the Rect, create a Path and add an ellipse. The Rect of the ellipse is the Rect created above. Then rotate the Path z-axis so that it is aligned with the shape of the ear, draw the Path, and call canvas.restore() to restore the canvas. The filling of the right ear is the same, and the final result is as follows:

Cheek is red

Blush drawing is very simple, is to draw a pink circle on the left and right side of the face, code as follows:

_paint.color = Color(0xFFE79EC3);

var pointion1 = leftBodyPoints.first;
Path leftFacePath = Path();
Rect leftFaceRect = Rect.fromLTWH(position1.dx - 25.w - 15.w, position1.dy + 80.w - 15.w, 30.w, 30.w);
leftFacePath.addOval(leftFaceRect);
canvas.drawPath(leftFacePath, _paint);


var rightFirstPosition = rightBodyPath.getPositionFromPercent(0);
Path rightFacePath = Path();
Rect rightFaceRect = Rect.fromLTWH(rightFirstPosition.dx + 25.w - 15.w, rightFirstPosition.dy + 80.w - 15.w, 30.w, 30.w);
rightFacePath.addOval(rightFaceRect);
canvas.drawPath(rightFacePath, _paint);
Copy the code

Create a 30. W square Rect in the position of the face, then add an ellipse to the Path and draw it as follows:

carrot

The filling of radish is divided into two steps: the radish leaf and the radish body. The filling of radish leaf is relatively simple. You only need to use the filling mode to draw the Path of radish leaf, and the code is as follows:

_paint.style = PaintingStyle.fill;
_paint.color = Colors.green;
canvas.drawPath(radishLeafPath, _paint);
Copy the code

The effect is as follows:

To fill the radish body, the Path of the radish body should be constructed first. When drawing the radish body before, multiple paths should be combined to fill the radish body. To fill the radish body, the multiple paths should be combined into one Path first, and the code is as follows:

Path radishBorderPath = Path();
var radishFistPosition = radishTopPath.getPositionFromPercent(0); radishBorderPath .. moveTo(radishFistPosition.dx, radishFistPosition.dy) .. addPointsFromPath(radishTopPath) .. addPointsFromPath(rightHandsFeetPath) .. addPointsFromPath(radishBottomPath, isReverse:true)
  ..addPointsFromPath(leftHandsFeetPath, isReverse: true)
  ..close();
Copy the code

The addPointsFromPath method is also adopted. Merge the top and bottom curves as well as the lines of left and right hands and feet into a Path, and then fill and draw the Path to get the effect of carrot. The drawing code is as follows:

_paint.style = PaintingStyle.fill;
_paint.color = Colors.orange;
canvas.drawPath(radishBorderPath, _paint);
Copy the code

The effect is as follows:

Such a lovely carrot holding the small white rabbit effect is completed.

animation

After the graphics are drawn, the animation effect is added. The animation effect is divided into two parts: line drawing animation and color filling animation. Animation is drawn using AnimationController in conjunction with CustomPainter. Different lengths of Path are drawn by different progress of Animation to achieve Animation effect.

Line drawing animation

To achieve the animation effect of lines, namely the dynamic drawing of lines, it is necessary to calculate the pathpoints of the Path, and then dynamically draw the length of the Path according to the progress of the animation. Because there are many paths here, it would be too troublesome to use a separate animation to control each one. So here we put all the paths of drawing lines into one set and control them with an animation.

First create an animation in the Widget that uses RabbitPainter:

var bodyAniamtion = AnimationController(vsync: this, upperBound: 15.0)
  ..duration = const Duration(seconds: 10);
Copy the code

Create an AnimationController and set the maximum animation size to 15. Why 15? Since there are 15 paths, set the maximum animation to 15, and then set the animation duration to 10 seconds.

Used in RabbitPainter:

/// Put all line paths into the collection for unified drawing
var list = [
  leftBodyPath,
  rightBodyPath,
  earPath,
  leftHandsFeetPath,
  rightHandsFeetPath,
  radishLeafPath,
  mouthPath,
  leftEyesPath,
  rightEyesPath,
  radishTopPath,
  radishBottomPath,
  radishBodyPath1,
  radishBodyPath2,
  radishBodyPath3,
  tailPath,
];


///Draws an overall border animation
void drawBorder(Canvas canvas, List<Path> list){
  int index = (bodyAnimation.value as double) ~ /1 ;
  double progress = bodyAnimation.value % 1;
  for(int i = 0 ; i < index; i++){
    var path = list[i];
    canvas.drawPath(path, _paint);
  }
  if(index >= list.length){
    return;
  }
  var path = list[index];
  var pms = path.computeMetrics();
  var pm = pms.first;
  canvas.drawPath(pm.extractPath(0, progress * pm.length), _paint);
}
Copy the code

In drawBorder, we first get the value of the animation and round it by dividing the value of the animation by 1. That is, we get the Path at which the animation was executed until it was drawn. Then we divide the value of the animation by 1 to get the remainder. Get the progress of the current Path drawing. After obtaining these two values, draw the completed Path by calling Canvas. drawPath completely, then take out the Path currently being drawn, calculate the pathpoint of Path, and then use extractPath to obtain the length of the current drawing according to the animation progress, draw it out, The result of dynamic drawing is as follows:

Color fill animation

The fill animation does not work well if it is drawn according to line animation logic. Here, the alternative is to get the Bounds of the fill Path, the range of the Path, from the getBounds of the Path, and the value is a Rect, a rectangle, Then, the canvas is clipped. First, the Path Path of the canvas is clipped, and then the Rect rectangle is filled. At this point, the Rect height can be changed according to the progress to achieve the dynamic filling effect.

As shown above, to draw a fill of a circle, draw the border of the circle, then draw the fill of the rectangle of the circle’s range, removing the excess by clipping the canvas.

According to the above principle, to realize the overall white filling Animation, first create the Animation of the filling Animation:

var fillBodyAnimation = AnimationController(vsync: this)
          ..duration = const Duration(seconds: 1);
Copy the code

Then modify the padding code:

/// Draw overall white fill
canvas.save();
canvas.clipPath(bodyBorderPath);
var bodyRect = bodyBorderPath.getBounds();
canvas.drawRect(bodyRect.clone(height: bodyRect.height * fillAnimation.value), _paint);
canvas.restore();
Copy the code

Because the canvas needs to be clipped so as not to affect other drawings, we call Canvas.save () to save the canvas and then clipPath to clipping the canvas. In this case, only the Path area is kept. Call Path’s getBounds method to get the Rect of the Path region, call drawRect to fill it, calculate the height of the Rect based on the animation progress, and call Canvas.restore () to restore the canvas.

Here, the Clone method of Rect is used, which is a custom method to extend Rect. The code is as follows:

extension RectExt on Rect{

  Rect clone({
    double? left,
    double? top,
    double? right,
    double? bottom,
    double? width,
    double? height}){

    if(right ! =null|| bottom ! =null) {return Rect.fromLTRB(left ?? this.left, top ?? this.top, right ?? this.right, bottom ?? this.bottom);
    }

    if(width ! =null|| height ! =null) {return Rect.fromLTWH(left ?? this.left, top ?? this.top, width ?? this.width, height ?? this.height);
    }

    return Rect.fromLTRB(this.left, this.top, this.right, this.bottom); }}Copy the code

The main function is to clone Rect and modify one of its attributes.

Other ears, blush, radish filling effect principle is the same as above, not one paste code, take a look at the final filling animation effect picture:

conclusion

The effect of the whole white rabbit is mainly combined by drawing 7 shapes of “3”. The techniques involved are mainly the use of Path and Canvas, including using The Bezier curve of Path to draw the shape of “3” and using the calculation of Path Path to obtain the specified point or segment on Path. Through the calculation of Path to achieve dynamic drawing animation and canvas cutting and shifting. The flexible use of Path and Canvas is used to achieve the desired effect.

Source code address: flutter_rabbit

The article has been simultaneously published to the official wechat account loongwind