【 Flutter 绘制 】点集的贝塞尔曲线拟合
1. 问题描述
现在有一批如下的点,很容易通过
canvas.drawPoints
绘制出如下的折线。
---->[ 点集 ]----
List<Offset> points1 = [
Offset(0, 20),
Offset(40, 40) ,
Offset(80, -20),
Offset(120, -40),
Offset(160, -80),
Offset(200, -20),
Offset(240, -40),
];
但很多时候,我们希望用一个
曲线
来展示数据,而非生硬的折线。
所以本文就来探讨一下
如何使用贝塞尔曲线对点集进行拟合
。
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
是我写的一个坐标系绘制辅助类,来方便查看点的位置,从而帮助理解。详见源码,不想用的话也不影响,删掉即可。
---->[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
下一点来控制。
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)
点。
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,来保证曲线的连贯性
。
// 控制点
double ctrlX = (next.dx - current.dx) / 2;
double ctrlY = 0;
path.relativeQuadraticBezierTo(ctrlX, ctrlY, next.dx-current.dx, next.dy-current.dy);
第二段曲线使用
三阶贝塞尔
,控制点如下所示。
// 控制点 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
中。
这样使用多个点集也就会形成多个曲线。
4. 在统计图中使用
这样在后面 16 章实现的折线统计图就可以使用曲线来替换折线,代码见
p16_chart.s03_line_plus
本篇到此结束,不止是 Flutter 中的贝塞尔曲线,其他平台、框架中的贝塞尔曲线也是类似的,所以这个知识点虽然比较很小,但很重要。很好地理解它,能提升你对贝塞尔曲线的认识,一把利器握在手里,你是要驾驭它,而不是畏惧它。
转载自:https://juejin.cn/post/6904453408477380621