preface

For some interesting drawing skills and knowledge, I will add them to the book “Flutter Drawing Guide – Wonderful Ideas” through “chapters”, on the one hand, to keep the book “up-to-date” and “dynamic”. On the other hand, it is to put some important knowledge to a good home. Ordinary writing is like a flash in the pan, no matter how beautiful, will eventually be devoid of time.

In addition, the paper is completely open and free, and will be published in the ordinary article at the same time, and the paper will be published in the small volume three days after the ordinary article, so that it is easy to expose mistakes and collect suggestions and feedback. This article, as one of the extras, mainly discusses the knowledge of angles and coordinates.


The Angle between one and two points

Have you ever wondered how to calculate the Angle between two points. For example, the Angle between p0 and P1, which is the slope between those two points. If you’ve been to junior high school, you can just use the inverse trigonometric function. Which pit points to pay attention to, on the one hand to learn knowledge, on the other hand to practice painting skills, painting together!


1. Draw the line information

So let’s first draw the following effect, point p0(0,0); Points p1 (60, 60).

To facilitate data management, the start and end points are encapsulated in the Line class. The Line body of the black part is assumed by the Line class, which can reduce the drawing logic of the artboard.

class Line {
  Line({
    this.start = Offset.zero,
    this.end = Offset.zero,
  });

  Offset start;
  Offset end;

  finalPaint pointPaint = Paint() .. style = PaintingStyle.stroke .. strokeWidth =1;

  void paint(Canvas canvas){
    canvas.drawLine(Offset.zero, end, pointPaint);
    drawAnchor(canvas,start);
    drawAnchor(canvas,end);
  }

  void drawAnchor(Canvas canvas, Offset offset) {
    canvas.drawCircle(offset, 4, pointPaint.. style = PaintingStyle.stroke); canvas.drawCircle(offset,2, pointPaint..style = PaintingStyle.fill);
  }
}
Copy the code

The artboard is AnglePainter, where dashed lines are drawn through my dash_painter library. After defining the line object, I use line.paint(canvas) in paint. Can draw the black part of the line body, blue auxiliary information is drawn by drawHelp. In this way, line body drawing can be changed by changing the point position of line object. The drawing performance corresponding to the change of P1 point is as follows:

P1 (60, 60) P1 (60-80) p1(-60,-80) P1 (60 reached)
class AnglePainter extends CustomPainter {
  // Draw a dotted line
  final DashPainter dashPainter = const DashPainter(span: 4, step: 4);

  finalPaint helpPaint = Paint() .. style = PaintingStyle.stroke.. color = Colors.lightBlue.. strokeWidth =1;

  final TextPainter textPainter = TextPainter(
    textAlign: TextAlign.center,
    textDirection: TextDirection.ltr,
  );

  Line line = Line(start: Offset.zero, end: const Offset(60.60));

  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    drawHelp(canvas, size);
    line.paint(canvas);
  }

  voiddrawHelp(Canvas canvas, Size size) { Path helpPath = Path() .. moveTo(-size.width /2.0)
      ..relativeLineTo(size.width, 0);
    dashPainter.paint(canvas, helpPath, helpPaint);
    drawHelpText('0 °', canvas, Offset(size.width / 2 - 20.0));
    drawHelpText('p0', canvas, line.start.translate(- 20.0));
    drawHelpText('p1', canvas, line.end.translate(- 20.0));
  }

  void drawHelpText(  String text, Canvas canvas, Offset offset, {
    Color color = Colors.lightBlue 
  }) {
    textPainter.text = TextSpan(
      text: text,
      style: TextStyle(fontSize: 12, color: color),
    );
    textPainter.layout(maxWidth: 200);
    textPainter.paint(canvas, offset);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false; }}Copy the code

2. Angle calculation

The Offset object in a Flutter has the direction property, which is calculated using the atan2 arctangent function. Let’s look at the Angle characteristics obtained from the direction attribute.

class Line {
  / / the same...
  
  double getrad => (end-start).direction; } ---->[source: Offset#direction]----double get direction => math.atan2(dy, dx);
Copy the code

So let’s convert the radians that we calculated into the Angle values that we put in the upper left corner. In the coordinate system where x axis is positive right and y axis is positive down, the offset Angle is radian offset clockwise from x axis, within the range of [-pi, PI]. In other words, the Angle of the upper part of the X-axis is negative, as shown in figures 3 and 4 below.

P1 (60, 60) P1 (60 reached) p1(-60,-60) P1 (60-80)
drawHelpText(
  'point of view:${(line.rad * 180 / pi).toStringAsFixed(2)}° ',
  canvas,
  Offset(
    -size.width / 2 + 10,
    -size.height / 2 + 10,),);Copy the code

Here the Angle is between [-pi, PI], so can we make it between 0,2 PI? This is more consistent with the normal cognition of 0~360°. It’s actually very simple. If it’s negative, add 2 PI, as positiveRad does.

---->[Line]----
double get rad => (end - start).direction;
 
double get positiveRad => rad < 0 ? 2 * pi + rad : rad;
Copy the code

3. Use of angles

Here’s a quick example: Use the Angle between two points to determine the Angle of rotation of the rectangle, and animate p1 point in a circle around P0. Because of the Angle change of the two points, the rectangle will also be accompanied by rotation.

In order to notify the artboard of changes to Line, we make it inherit from ChangeNotifier as a listener. And give the Rotate method, which passes in angles to update the coordinates. For convenience, start with 0,0, and only change the end coordinate. Given that p1 moves in a circle, the distance between the two points is unchanged, and the rotation Angle is known, then when p1 rotates rad, the coordinates of p1 can be easily obtained:

class Line with ChangeNotifier {
  / / the same...
  
  double get length => (end - start).distance;
  
  void rotate(doublerad) { end = Offset(length * cos(rad), length * sin(rad)); notifyListeners(); }}Copy the code

The angular motion of the ellipse is realized above, so how can you dynamically draw the following line and the horizontal positive arc?

In fact, it is very simple, we already know the Angle value, using Canvas. DrawArc can draw the arc according to the first Angle.

---->[AnglePainter#drawHelp]----
canvas.drawArc(
  Rect.fromCenter(center: Offset.zero, width: 20, height: 20),
  0,
  line.positiveRad,
  false,
  helpPaint,
);
Copy the code

4. A point rotates arbitrarily around a point

In fact, this circular motion is a very special case, where the line starts at the origin, and the initial Angle is 0. In this way, the effect of initial Angle need not be considered in coordinate calculation. But for general cases, the above calculation can be wrong. So how do you make p zero arbitrary? This is actually moving to simple junior high school math:

Given: p0(a,b), p1(c,d), find p1 rotated clockwise about P0 θ radians to obtain p1'points. O: p1 'The coordinates of the points.Copy the code

And it’s actually pretty easy to figure out, you know, if you rotate it by theta radians you get p1 prime. With p0 as the origin, the coordinates of P1 prime are obvious.

If the Angle between two points is rad and the distance between two points is length, then p1': (length*cos(rad+θ),length*sin(rad+θ)) given p0 is start, then (0,0) is used as the coordinate system, then p1': (length * cos + theta (rad), length * sin (rad + theta)) + startCopy the code

Since the rotate argument is the total rotation Angle, and the rotate method updates the end coordinate every time it is triggered, rad is constantly updated. What we need to deal with is the rotation Angle between each animation trigger, which is the detaRotate below. See rad_rotate for the complete source code of this case

double detaRotate = 0;
void rotate(double rotate) {
  detaRotate = rotate - detaRotate;
  end = Offset(
        length * cos(rad + detaRotate),
        length * sin(rad + detaRotate),
      ) +
      start;
  detaRotate = rotate;
  notifyListeners();
}
Copy the code

Why should your point be a point

You might think of these as just operations on points, but I think of them as constraint bindings, because the operations themselves are constraint rules. Two points of data form a structure, a skeleton, so why should the point you see be a point?


1. Draw arrows

Here is an example of drawing an arrow: what is displayed on the screen is the Line#paint method. Just draw the arrow using the information provided by the two points. The logic is to draw a horizontal arrow, and then rotate it around p0, depending on the Angle of rotation.

voidpaint(Canvas canvas) { canvas.save(); canvas.translate(start.dx, start.dy); canvas.rotate(positiveRad); Path arrowPath = Path(); arrowPath .. relativeLineTo(length -10.3)
    ..relativeLineTo(0.2)
    ..lineTo(length, 0)
    ..relativeLineTo(- 10.- 5)
    ..relativeLineTo(0.2).. close(); canvas.drawPath(arrowPath,pointPaint); canvas.restore(); }Copy the code

In this way, the change of point data can also drive the change of drawing. See the complete source code of this case: arrow


2. Draw pictures

Here is a picture, now through PS to obtain the arm area data: 0, 93, 104, 212. The top left and bottom left points form a straight line, so what if we draw a picture based on the position of the points?

To store image and region information, we define the ImageZone object, passing in the image image and region RECt in the construct. In addition, through image and RECt, we can calculate the line object formed by the corresponding coordinates of the upper left corner and lower left corner with the center of the picture as the origin.

import 'dart:ui';
import 'line.dart';

class ImageZone {
  final Image image;
  final Rect rect;

  Line? _line;

  ImageZone({required this.image, this.rect = Rect.zero});

  Line get line {
    if(_line ! =null) {
      return_line! ; } Offset start = Offset( -(image.width /2 - rect.right), -(image.height / 2 - rect.bottom));
    Offset end = start.translate(-rect.width, -rect.height);
    _line = Line(start: start, end: end);
    return _line!;
  }
}
Copy the code

Define a paint method in ImageZone that draws images using canvas and line. In this way, it is convenient to draw pictures in Line class and simplify the drawing logic of Line.

---->[ImageZone]----
void paint(Canvas canvas, Line line) {
    canvas.save();
    canvas.translate(line.start.dx, line.start.dy);
    canvas.rotate(line.positiveRad - this.line.positiveRad);
    canvas.translate(-line.start.dx, -line.start.dy);
    canvas.drawImageRect(
      image,
      rect,
      rect.translate(-image.width / 2, -image.height / 2),
      imagePaint,
    );
    canvas.restore();
 }
Copy the code

Add a attachImage method to the Line class to attach the ImageZone object to the Line object. In Paint, only the _zone object is used to draw.

---->[Line]----
class Line with ChangeNotifier {
  / / the same...
  
  ImageZone? _zone;

  void attachImage(ImageZone zone) {
    _zone = zone;
    start = zone.line.start;
    end = zone.line.end;
    notifyListeners();
  }
  
  void paint(Canvas canvas) {
  // Draw the arrow slightly...._zone? .paint(canvas,this);
 }
Copy the code

This allows us to enchant a rectangular region of the image onto a line segment. The image of the hand is loaded via _loadImage, and the line object is enchanted with attachImage.

void _loadImage() async {
  ByteData data = await rootBundle.load('assets/images/hand.png');
  List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
  _image = await decodeImageFromList(Uint8List.fromList(bytes));
  line.attachImage(ImageZone(
      rect: const Rect.fromLTRB(0.93.104.212), image: _image! )); }Copy the code

Similarly, a line segment can be rotated around its starting point, as follows.

void _updateLine() {
  line.rotate(ctrl.value * 2* pi/50);
}
Copy the code

Draw the background image to get a complete effect. See the complete source code for this case: body


3. The line rotates around any point

Now let’s figure out how to rotate a given line segment around a point, and the problem is equivalent to:

Given the coordinates of points P0, P1, and p2, segments P0 and P1 rotate clockwise about p2 by θ radians to P0 'and P1'. Find: P0 ', P1 'coordinates.Copy the code


1. Problem analysis

Since two points define a straight line, the rotation of segment P0 and p1 about p2 is equivalent to the rotation of segment P0 and p1 about p2. The schematic diagram is as follows:

In the rotate method, we pass in a coordinate centre, and based on that coordinate and the rotation Angle, we process points P0 and P1 to get a new point.

void rotate(double rotate,{Offset? centre}) {
		//TODO
}
Copy the code

2. Solution and code handling

Having dealt with the logic of rotation around the starting point, here we can use a very clever scheme:

We can construct line P2, P0, which performs rotation logic and whose end coordinate is P0 '. Find the coordinates of P1 ', we can construct p2, P1 line segment, let the line segment perform rotation logic, its end coordinate is p1 '.Copy the code

With the idea, let’s look at the implementation of the code. The rotation around the starting point implemented earlier is encapsulated in the _rotateByStart method.

---->[Line]----
void _rotateByStart(double rotate) {
  end = Offset(
        length * cos(rad + rotate),
        length * sin(rad + rotate),
      ) +
      start;
}
Copy the code

The rotate method, which is callable from the outside, can be passed in to the centre point and, if empty, to the starting point. Tag1 and TAG2 construct line segments p2P0 and p2P1 respectively. Then the two lines are rotated to get the desired p0 ‘and P1’ coordinates.

double detaRotate = 0;

void rotate(double rotate, {Offset? centre}) {
  detaRotate = rotate - detaRotate;
  centre = centre ?? start;
  Line p2p0 = Line(start: centre, end: start); // tag1
  Line p2p1 = Line(start: centre, end: end); // tag2
  p2p0._rotateByStart(detaRotate);
  p2p1._rotateByStart(detaRotate);
  start = p2p0.end;
  end = p2p1.end;
  detaRotate = rotate;
  notifyListeners();
}
Copy the code

3. Line segmentation value coordinates

Now we have a requirement to compute the coordinates of the point at the percent fraction of the line segment. For example, 0.5 is the coordinate in the middle of the segment, and 0.4 is the coordinate 40% of the length of the vertex. The effect is as follows:

0.2 0.5 0.8

In fact, the idea is very simple, since the point is on the line, then the slope is the same, but the length changes, according to the slope and length can be calculated coordinate value, the code implementation is as follows:

Offset percent(double percent){
  return Offset(
      length*percent*cos(rad),
      length*percent*sin(rad),
  )+start;
}
Copy the code

I said the line rotates around the point. Now that we know the coordinates of the indexing value, we can easily rotate the line around the indexing anchor point. See rotate_by_point for the complete source code of this case


The point line operation in this paper is to modify the data of the coordinate itself. For example, in rotation, the Angle value corresponding to the line is real. This data-driven approach based on logical operation can carry out some interesting operations and make it easier for data to be linked. In addition, this paper is only a simple study of two points forming a line. The combination of lines, constraints, may open the door to a new world. After the relevant opportunity to further study, share with you.

The content that this article wants to introduce here is almost, thank you for watching, bye ~


See this article for a look at the “Gold Digging Gifts” campaign, and feel free to discuss it in the comments and leave your thoughts and insights. Finally, two of the best review users will be selected and each will receive a gold digging badge. The deadline is 12 noon on September 13. Welcome everyone to actively discuss, avoid unnecessary comments, thank you for your support ~