La courbe de Bézier tracée par Flutter dessine un petit dauphin

Prenez l'habitude d'écrire ensemble ! C'est le 9ème jour de ma participation au "Nuggets Daily New Plan · April Update Challenge", cliquez pour voir les détails de l'événement .

  • Avant-propos : L'application des courbes de Bézier comble le fossé entre le dessin sur ordinateur et la peinture à la main, et peut mieux exprimer les courbes que les gens veulent dessiner. Afin de mieux comprendre la courbe de Bézier omnipotente, je pense que les dauphins sont le corps de la vie marine. La créature marine avec la courbe la plus parfaite peut nager à une vitesse maximale de 80 km/h dans l'océan ; c'est plus rapide qu'un destroyer. Apprendre à dessiner vient d'apprendre la courbe de Bézier, alors nous utiliserons le dessin de courbe de Bézier aujourd'hui pour voir si nous peut le dessiner Un mignon petit dauphin.
  • Si vous ne comprenez pas le principe des courbes de Bézier, vous pouvez lire cet article par des amis - Principe de Bézier .

D'abord sur les rendus :
image.png

  • pathLa méthode pour tracer une courbe de Bézier pour un chemin est très simple. Il suffit de passer des points de contrôle. Pour le second ordre, on passe 1 point de contrôle et 1 point final. Pour le troisième ordre, on passe 2 points de contrôle et 1 point final, mais vous devez trouver un point de contrôle approprié. Il n'est pas si facile de trouver le point. À ce moment, ce ne serait pas très pratique si nous pouvions utiliser nos doigts pour déboguer en permanence sur l'écran pour trouver le bon point Ensuite, nous allons d'abord implémenter les fonctions suivantes, puis déboguer continuellement les points de contrôle avec nos doigts et créer plusieurs courbes de Bézier à connecter.

1649757651967.gif
On peut voir qu'un Bézier de troisième ordre a besoin d'un point de départ, de points de contrôle 2 et d'un point final. Tout d'abord, nous devons stocker ces points de contrôle via la reconnaissance gestuelle, puis les affecter au composant de dessin pour la mise à jour. Ici, nous devons La classe de gestion d'état ChangeNotifierest utilisée, qui hérite Listenablecar il existe un paramètre repaintacceptant Listenablele type dans la méthode de construction du composant de dessin pour contrôler s'il faut redessiner ou non, et les données seront redessinées lorsque les données changent.

const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
复制代码

Comme CustomPainterles paramètres de la méthode de construction repaintsont responsables de la mise à jour du dessin, nous devons d'abord définir un héritage de classe ChangeNotifierpour stocker ces données.
Code:

class TouchController extends ChangeNotifier {
  List<Offset> _points = []; //点集合
  int _selectIndex = -1;// 选中的点 更新位置用

  int get selectIndex => _selectIndex;

  List<Offset> get points => _points;

  // 选择某一个点 保存index
  set selectIndex(int value) {
    if (_selectIndex == value) return;
    _selectIndex = value;
    notifyListeners();// 通知刷新
  }
   // 选中的点标记
  Offset? get selectPoint => _selectIndex == -1 ? null : _points[_selectIndex];

  // 添加一个点
  void addPoint(Offset point) {
    points.add(point);
    notifyListeners();
  }
   // 手指移动时更新当前点的位置
  void updatePoint(int index, Offset point) {
    points[index] = point;
    notifyListeners();
  }
    // 删除最后一个点 相当于撤回上一步操作
  void removeLast() {
    points.removeLast();
    notifyListeners();
  }

}
复制代码

Une fois que nous avons l'espace pour stocker des données, nous devons obtenir ces points par des gestes, et obtenir la position actuelle pour le stockage et la mise à jour par l'opération de gestes sur la toile.

 GestureDetector(
  child: CustomPaint(
    painter:
        _DolphinPainter(widget.touchController, widget.image),
  ),
  onPanDown: (d) {
    // 按压
    judgeZone(d.localPosition);
  },
  onPanUpdate: (d) {
    // 移动
    if (widget.touchController.selectIndex != -1) {
      widget.touchController.updatePoint(
          widget.touchController.selectIndex, d.localPosition);
    }
  },
)
///判断出是否在某点的半径为r圆范围内
bool judgeCircleArea(Offset src, Offset dst, double r) =>
    (src - dst).distance <= r;
///手指按下触发
void judgeZone(Offset src) {
  /// 循环所有的点
  for (int i = 0; i < widget.touchController.points.length; i++) {
    // 判断手指按的位置有没有按过的点
    if (judgeCircleArea(src, widget.touchController.points[i], 20)) {
      // 有点 不添加更新选中的点
      widget.touchController.selectIndex = i;
      return;
    }
  }
  // 无点 添加新的点 并将选中的点清空
  widget.touchController.addPoint(src);
  widget.touchController.selectIndex = -1;
}
复制代码

À ce stade, nos pressions gestuelles et nos mouvements stockeront les données dans la classe que nous venons de définir, puis nous devrons attribuer ces données au véritable composant de dessin CustomPainter.

class _DolphinPainter extends CustomPainter {
  final TouchController touchController;// 存储数据类
//  final ui.Image image;

  _DolphinPainter(this.touchController, this.image)
    // 这个地方传入需要更新的 Listenable
      : super(repaint: touchController);

  List<Offset>? pos; //存储手势按压的点

  @override
  void paint(Canvas canvas, Size size) {
    // 画布原点平移到屏幕中央
    canvas.translate(size.width / 2, size.height / 2);
    // ,因为手势识别的原点是左上角,所以这里将存储的点相对的原点进行偏移到跟画布一致 负值向左上角偏移
    pos = touchController.points
        .map((e) => e.translate(-size.width / 2, -size.height / 2))
        .toList();

// 定义画笔
    var paint = Paint()
      ..strokeWidth = 2
      ..color = Colors.purple
      ..style = PaintingStyle.stroke
      ..isAntiAlias = true;

    // canvas.drawImage(image, Offset(-image.width / 2, -image.height / 2), paint);

    // 如果点小于4个 那么就只绘制点 如果>=4个点 那么就绘制贝塞尔曲线
    if (pos != null && pos!.length >= 4) {
      var path = Path();
      // 设置起点 手指第一个按压的点
      path.moveTo(pos![0].dx, (pos![0].dy));
      // path添加第一个贝塞尔曲线
      path.cubicTo(pos![1].dx,pos![1].dy, pos![2].dx, pos![2].dy, pos![3].dx,
          pos![3].dy);
          //绘制辅助线
      _drawHelpLine(canvas, size, paint, 0);
      // 绘制首个贝塞尔曲线
      canvas.drawPath(path, paint..color = Colors.purple);
      
      // for循环 绘制第2个以后的曲线 以上个终点为下一个的起点
      for (int i = 1; i < (pos!.length - 1) ~/ 3; i++) {
          //之后贝塞尔曲线的起点都是上一个贝塞尔曲线的终点
          // 比如第一个曲线 1,2,3,4.第二个就是4,5,6,7...以此类推,这样我们才能把线连接起来绘制图案
        // 这里把绘制之前的颜色覆盖
      // canvas.drawPath(path, paint..color = Colors.white);
        // 绘制辅助线
        _drawHelpLine(canvas, size, paint, i);
        //绘制贝塞尔曲线
        path.cubicTo(
          pos![i * 3 + 1].dx,
          pos![i * 3 + 1].dy,
          pos![i * 3 + 2].dx,
          pos![i * 3 + 2].dy,
          pos![i * 3 + 3].dx,
          pos![i * 3 + 3].dy,
        );

        if (i == 8) {
          path.close();
        }
        canvas.drawPath(path, paint..color = Colors.purple);
      }

      // 绘制辅助点
      _drawHelpPoint(canvas, paint);
      // 选中点
      _drawHelpSelectPoint(canvas, size, paint);
    } else {
      // 绘制辅助点
      _drawHelpPoint(canvas, paint);
    }


    // 画眼睛 眼睛位于起点的左侧,所以中心点向左偏移
    canvas.drawCircle(
        pos!.first.translate(-50, 5),
        10,
        paint
          ..color = Colors.black87
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2);
    canvas.drawCircle(
        pos!.first.translate(-53, 5),
        7,
        paint
          ..color = Colors.black87
          ..style = PaintingStyle.fill);
  }
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
  return false;
}

void _drawHelpPoint(Canvas canvas, Paint paint) {
  canvas.drawPoints(
      PointMode.points,
      pos ?? [],
      paint
        ..strokeWidth = 10
        ..strokeCap = StrokeCap.round
        ..color = Colors.redAccent);
}

void _drawHelpSelectPoint(Canvas canvas, Size size, Paint paint) {
  Offset? selectPos = touchController.selectPoint;
  selectPos = selectPos?.translate(-size.width / 2, -size.height / 2);
  if (selectPos == null) return;
  canvas.drawCircle(
      selectPos,
      10,
      paint
        ..color = Colors.green
        ..strokeWidth = 2);
}

void _drawHelpLine(Canvas canvas, Size size, Paint paint, int i) {
  canvas.drawLine(
      Offset(pos![i * 3].dx, pos![i * 3].dy),
      Offset(pos![i * 3 + 1].dx, pos![i * 3 + 1].dy),
      paint
        ..color = Colors.redAccent
        ..strokeWidth = 2);

  canvas.drawLine(
      Offset(pos![i * 3 + 1].dx, pos![i * 3 + 1].dy),
      Offset(pos![i * 3 + 2].dx, pos![i * 3 + 2].dy),
      paint
        ..color = Colors.redAccent
        ..strokeWidth = 2);

  canvas.drawLine(
      Offset(pos![i * 3 + 2].dx, pos![i * 3 + 2].dy),
      Offset(pos![i * 3 + 3].dx, pos![i * 3 + 3].dy),
      paint
        ..color = Colors.redAccent
        ..strokeWidth = 2);
}

复制代码
  • À la fin, avec le contrôle de nos doigts et l'aide de lignes auxiliaires, le motif a été lentement dessiné.

image.png

  • Supprimez les lignes et les points auxiliaires.

image.png

  • Ensuite, changez le pinceau pour remplir, et nous avons le mignon petit dauphin avec lequel nous avons commencé.

Résumer

Grâce à ce petit motif de dauphin, nous pouvons mieux comprendre le mécanisme de dessin de la courbe de Bézier.Grâce à votre commande gestuelle, vous pouvez également dessiner n'importe quelle courbe et n'importe quel motif.On peut dire que la courbe de Bézier est l'âme du dessin.La courbe de Sel équivaut à maîtriser tous les composants du dessin, car théoriquement, tous les graphiques en deux dimensions peuvent être dessinés par des courbes de Bézier. Tant que nous pouvons trouver avec précision le point de contrôle, nous pouvons dessiner une infinité de motifs possibles.

Référence : Nuggets Booklet : Flutter Wonderful Flowers

Je suppose que tu aimes

Origine juejin.im/post/7085739301970903047
conseillé
Classement