likes
comments
collection
share

【 Flutter 绘制 】点集的贝塞尔曲线拟合

作者站长头像
站长
· 阅读数 56
1. 问题描述

现在有一批如下的点,很容易通过 canvas.drawPoints 绘制出如下的折线。

【 Flutter 绘制 】点集的贝塞尔曲线拟合

---->[ 点集 ]----
List<Offset> points1 = [
  Offset(0, 20),
  Offset(40, 40) ,
  Offset(80, -20),
  Offset(120, -40),
  Offset(160, -80),
  Offset(200, -20),
  Offset(240, -40),
];

但很多时候,我们希望用一个曲线 来展示数据,而非生硬的折线。

【 Flutter 绘制 】点集的贝塞尔曲线拟合

所以本文就来探讨一下 如何使用贝塞尔曲线对点集进行拟合

【 Flutter 绘制 】点集的贝塞尔曲线拟合


2. 绘制点与折线

程序入口文件 main.dart , 此处横屏全屏显示。

---->[p14_bezier/s05_bezier_line/main.dart]----
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'paper.dart';

void main() {
  // 确定初始化
  WidgetsFlutterBinding.ensureInitialized();
  //横屏
  SystemChrome.setPreferredOrientations(
      [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
  //全屏显示
  SystemChrome.setEnabledSystemUIOverlays([]);

  runApp(Paper());
}

显示组件 Paper ,使用 PaperPainter 画板。

---->[p14_bezier/s05_bezier_line/paper.dart]----
class Paper extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: CustomPaint( painter: PaperPainter() ),
    );
  }
}

通过简单的几步,如下的折线图便跃然纸上。其中 Coordinate 是我写的一个坐标系绘制辅助类,来方便查看点的位置,从而帮助理解。详见源码,不想用的话也不影响,删掉即可。

【 Flutter 绘制 】点集的贝塞尔曲线拟合

---->[p14_bezier/s05_bezier_line/paper.dart]----
class PaperPainter extends CustomPainter {
  final Coordinate coordinate = Coordinate();
  List<Offset> points1 = [
    Offset(0, 20),
    Offset(40, 40) ,
    Offset(80, -20),
    Offset(120, -40),
    Offset(160, -80),
    Offset(200, -20),
    Offset(240, -40),
  ];

  Paint _helpPaint = Paint();
  Paint _mainPaint = Paint();
  Path _linePath = Path();
  
  @override
  void paint(Canvas canvas, Size size) {
    coordinate.paint(canvas, size);
    // 画布原点 移到 屏幕中心
    canvas.translate(size.width / 2, size.height / 2);
    // 绘制辅助点线 
    _drawHelp(canvas);
  }

  void _drawHelp(Canvas canvas) {
    _helpPaint..style = PaintingStyle.stroke;
    // 绘制点
    points1.forEach((element) {
      canvas.drawCircle(element, 2, 
                        _helpPaint..strokeWidth=1..color=Colors.orange);
    });
    // 绘制折线
    canvas.drawPoints(PointMode.polygon, points1, 
                        _helpPaint..strokeWidth=0.5..color=Colors.red);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

3. 贝塞尔曲线拟合

在下面方法中,传入一个 List<Offset> 类型的点集 points 。其中首尾两段线使用二阶贝塞尔曲线,中间的使用三阶贝塞尔曲线。起止点和控制点通过 current 当前点和 next 下一点来控制。

【 Flutter 绘制 】点集的贝塞尔曲线拟合

void addBezierPathWithPoints(Path path, List<Offset> points) {
  for (int i = 0; i < points.length - 1; i++) {
    Offset current = points[i];
    Offset next = points[i+1];
    if (i == 0) {
      path.moveTo(current.dx, current.dy);
      // 控制点
      double ctrlX = current.dx + (next.dx - current.dx) / 2;
      double ctrlY = next.dy;
      path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy);
    } else if (i < points.length - 2) {
      // 控制点 1
      double ctrl1X = current.dx + (next.dx - current.dx) / 2;
      double ctrl1Y = current.dy;
      // 控制点 2
      double ctrl2X = ctrl1X;
      double ctrl2Y = next.dy;
      path.cubicTo(ctrl1X,ctrl1Y,ctrl2X,ctrl2Y,next.dx,next.dy);
    }else{
      path.moveTo(current.dx, current.dy);
      // 控制点
      double ctrlX = current.dx + (next.dx - current.dx) / 2;
      double ctrlY = current.dy;
      path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy);
    }
  }
}

首先来看第一段曲线 (0, 20) 是起点 current (40, 40) 是下一点 next,对于二阶贝塞尔曲线来说,只要确定控制点就完事了。这里 控制点 x 取两点的中点横坐标,y 取 next 的纵坐标,即下面的 (10,40) 点。

【 Flutter 绘制 】点集的贝塞尔曲线拟合

if (i == 0) {
  path.moveTo(current.dx, current.dy);
  // 控制点
  double ctrlX = current.dx + (next.dx - current.dx) / 2;
  double ctrlY = next.dy;
  path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy);
} 

再看最后一段曲线 ,和第一段类似,三点的位置如下,注意这里使用的是相对于倒数第二个点的添加 relativeQuadraticBezierTo,来保证曲线的连贯性

【 Flutter 绘制 】点集的贝塞尔曲线拟合

// 控制点
double ctrlX = (next.dx - current.dx) / 2;
double ctrlY = 0;
path.relativeQuadraticBezierTo(ctrlX, ctrlY, next.dx-current.dx, next.dy-current.dy);

第二段曲线使用 三阶贝塞尔,控制点如下所示。

【 Flutter 绘制 】点集的贝塞尔曲线拟合

  // 控制点 1
  double ctrl1X = current.dx + (next.dx - current.dx) / 2;
  double ctrl1Y = current.dy;
  // 控制点 2
  double ctrl2X = ctrl1X;
  double ctrl2Y = next.dy;
  path.cubicTo(ctrl1X,ctrl1Y,ctrl2X,ctrl2Y,next.dx,next.dy);

同样后面的几条线段都是类似,控制点如下,这样就生成了连续的曲线。这里通过 addBezierPathWithPoints 方法就可以实现将一个点集编程一个曲线路径添加到指定 Path 中。

【 Flutter 绘制 】点集的贝塞尔曲线拟合

这样使用多个点集也就会形成多个曲线。

【 Flutter 绘制 】点集的贝塞尔曲线拟合


4. 在统计图中使用

这样在后面 16 章实现的折线统计图就可以使用曲线来替换折线,代码见 p16_chart.s03_line_plus

【 Flutter 绘制 】点集的贝塞尔曲线拟合

本篇到此结束,不止是 Flutter 中的贝塞尔曲线,其他平台、框架中的贝塞尔曲线也是类似的,所以这个知识点虽然比较很小,但很重要。很好地理解它,能提升你对贝塞尔曲线的认识,一把利器握在手里,你是要驾驭它,而不是畏惧它。

转载自:https://juejin.cn/post/6904453408477380621
评论
请登录