Flutterによって描かれたベジェ曲線は小さなイルカを描きます

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して9日目です。クリックしてイベントの詳細をご覧ください

  • はじめに:ベジェ曲線を適用すると、コンピューターでの描画と手描きのギャップが埋められ、人々が描きたい曲線をより適切に表現できます。全能のベジェ曲線をよりよく理解するために、イルカは海洋生物の体だと思います。最も完璧な曲線を持つ海の生き物は、海で最高速度80km / hで泳ぐことができます。これは、駆逐艦よりも高速です。描画を学ぶと、ベジェ曲線を学習したばかりです。次に、今日はベジェ曲線描画を使用して、それを描くことができますかわいい小さなイルカ。
  • ベジェ曲線の原理を理解していない場合は、友人がこの記事を読むことができます-ベジェ原理

最初のレンダリング:
image.png

  • pathパスのベジェ曲線を描く方法は非常に簡単です。コントロールポイントを渡すだけです。2次の場合は1つのコントロールポイントと1つのエンドポイントを渡します。3次の場合は2つのコントロールポイントと1を渡します。終点ですが、適切な制御点を見つける必要があります。点を見つけるのはそれほど簡単ではありません。現時点では、画面上で指を使って継続的にデバッグして適切な点を見つけることができれば、あまり便利ではありません。次に、最初に次の機能を実装し、指でコントロールポイントを継続的にデバッグし、複数のベジェ曲線を作成して接続します。

1649757651967.gif
3次ベジェには、開始点が1つ、制御点が2つ、終了点が1つ必要であることがわかります。まず、これらの制御点をジェスチャ認識で保存してから、描画コンポーネントに割り当てて更新する必要があります。状態管理ChangeNotifierクラスを使用します。これは、描画コンポーネントの構築メソッドにListenableパラメータrepaint受け入れListenableタイプがあり、再描画するかどうかを制御するために継承され、データが変更されるとデータが再描画されます。

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

CustomPainter構築メソッドのパラメータは図面の更新を担当するため、最初にこれらのデータを格納repaintするためのクラス継承を定義する必要があります。コード:ChangeNotifier

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();
  }

}
复制代码

データを保存するスペースができたら、ジェスチャを使用してこれらのポイントを取得し、キャンバス上でジェスチャを操作して保存および更新するための現在の位置を取得する必要があります。

 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;
}
复制代码

この時点で、ジェスチャを押して移動すると、定義したクラスにデータが保存されます。次に、このデータを実際の描画コンポーネントに割り当てる必要があります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);
}

复制代码
  • 結局、私たちの指のコントロールと補助線の助けを借りて、パターンはゆっくりと描かれました。

image.png

  • 補助線と点を削除します。

image.png

  • 次に、ブラシを塗りつぶすように変更すると、最初にかわいい小さなイルカができました。

要約する

この小さなイルカのパターンを通して、ベジェ曲線の描画メカニズムをよりよく理解できます。ジェスチャコントロールを使用して、任意の曲線やパターンを描画することもできます。ベジェ曲線は描画の魂であると言えます。セル曲線理論的には、すべての2次元グラフィックスはベジェ曲線で描画できるため、すべての描画コンポーネントを習得することと同じです。制御点を正確に見つけることができれば、可能な限り無限のパターンを描画できます。

参考:ナゲッツ小冊子:フラッターワンダフルフラワーズ

おすすめ

転載: juejin.im/post/7085739301970903047
おすすめ