贝塞尔曲线简介
由上图可以看出:A,C依据控制点B不断的取点使得AD:AB=BE:BC=DF:DE,构成一个二阶贝塞尔曲线。AD:AB的变化取值范围便是[0,1],
所以要在范围t[0,1]内描述下图
![]
直线上所有的值,便可推出公式:
接下来就可以把X看成矢量坐标上的点P,转换一下便可得出一阶贝塞尔曲线公式。
一阶贝塞尔曲线
公式
二阶贝塞尔曲线
对于二阶贝塞尔曲线,其实你可以理解为:在上利用一阶公式求出点然后在上利用一阶公式求出点最后在上再利用一阶公式就可以求出最终贝塞尔曲线上的点具体推导过程如下:先求出线段上的控制点。 推导得出:
我们常用的画图工具中的曲线,如画图,PS等都是运用贝塞尔曲线计算实现的。
三阶贝赛尔曲线
公式
Flutter中的贝塞尔曲线
/// Adds a quadratic bezier segment that curves from the current
/// point to the given point (x2,y2), using the control point
/// (x1,y1) 二阶贝塞尔曲线,控制点为(x1,y1),终点为(x2,y2),起点为当前path所在的点
void quadraticBezierTo(double x1, double y1, double x2, double y2) native 'Path_quadraticBezierTo';
/// Adds a cubic bezier segment that curves from the current point
/// to the given point (x3,y3), using the control points (x1,y1) and
/// (x2,y2) 三阶贝塞尔曲线,控制点为(x1,y1),(x2,y2),终点为(x3,y3),起点为当前path所在的点
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) native 'Path_cubicTo';
贝塞尔曲线演示
bezierShow.gif
贝塞尔曲线绘制正余弦函数
需求
在屏幕中间画整段的正余弦波。
效果
bezierShow2.gif
分析
- 首先算出屏幕中间左侧边和右侧边的两点。
- 在屏幕中线上距离屏幕两侧等间距的位置确定起始点。
- 在波峰和波谷找到两个控制点
- 把点带入方法绘制,即可得到正余弦波
实现
关键代码
///屏幕左上脚的坐标顶点对应着(0,0)点
//屏幕中左侧点
Offset offset1 = Offset(0, Screen.screenHeightDp / 2);
_path.moveTo(offset1.dx, offset1.dy);
///假设,整个波长=屏幕宽度=M,画的是一条正弦
///绘制波峰
///波峰控制点就在(M/4,centerY-波纹高度),终点在屏幕中点
_path.quadraticBezierTo(
Screen.screenWidthDp / 4,
Screen.screenHeightDp / 2 - this.height,
Screen.screenWidthDp / 2,
Screen.screenHeightDp / 2);
///绘制波谷,此时画笔的起点已经在屏幕中心
///波谷控制点就在(3M/4,centerY+波纹高度),终点在屏幕中线终点
_path.quadraticBezierTo(
Screen.screenWidthDp / 4 * 3,
Screen.screenHeightDp / 2 + this.height,
Screen.screenWidthDp,
Screen.screenHeightDp / 2);
///绘制,可以把style = PaintingStyle.fill换成stoke看看效果
canvas.drawPath(_path, _paint);
完整代码
Screen类的代码在前面时钟绘制一篇有,要在程序启动的时候调用init初始化。
正余弦代码
///贝塞尔曲线示例一
class CustomBezierWidget extends StatefulWidget {
@override
_BezierWidgetState createState() {
// TODO: implement createState
return _BezierWidgetState();
}
}
class _BezierWidgetState extends State {
Timer timer;
int height = 100;
bool flag = true;
@override
void initState() {
// TODO: implement initState
super.initState();
timer = Timer.periodic(Duration(microseconds: 5000), (timer) {
setState(() {
if (flag) {
height = height - 1;
} else {
height = height + 1;
}
if (height == -100) {
flag = false;
}
if (height == 100) flag = true;
});
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
timer.cancel();
}
@override
Widget build(BuildContext context) {
return CustomPaint(painter: BezierPainter(height));
}
}
class BezierPainter extends CustomPainter {
final int height; //波的高度
BezierPainter(this.height);
//路径画笔
Paint _paint = Paint()
..color = Colors.deepOrange
..style = PaintingStyle.fill
..isAntiAlias = true
..strokeWidth = 10;
//点画笔
Paint _pointPaint = Paint()
..color = Colors.teal
..strokeWidth = 10
..isAntiAlias = true
..style = PaintingStyle.fill;
//曲线路径
Path _path = Path();
///屏幕左上脚的坐标顶点对应着(0,0)点
//屏幕中左侧点
Offset offset1 = Offset(0, Screen.screenHeightDp / 2);
//屏幕终点
Offset offset2 = Offset(Screen.screenWidthDp / 2, Screen.screenHeightDp / 2);
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
print('Size.width==>${size.width} Size.height==>${size.height}');
print(
'Screen.width==>${Screen.screenWidthDp} Screen.height==>${Screen.screenHeightDp}');
_path.moveTo(offset1.dx, offset1.dy);
///假设,整个波长=屏幕宽度=M,画的是一条正弦
///绘制波峰
///波峰控制点就在(M/4,centerY-波纹高度),终点在屏幕中点
_path.quadraticBezierTo(
Screen.screenWidthDp / 4,
Screen.screenHeightDp / 2 - this.height,
Screen.screenWidthDp / 2,
Screen.screenHeightDp / 2);
///绘制波谷,此时画笔的起点已经在屏幕中心
///波谷控制点就在(3M/4,centerY+波纹高度),终点在屏幕中线终点
_path.quadraticBezierTo(
Screen.screenWidthDp / 4 * 3,
Screen.screenHeightDp / 2 + this.height,
Screen.screenWidthDp,
Screen.screenHeightDp / 2);
///绘制,可以把style = PaintingStyle.fill换成stoke看看效果
canvas.drawPath(_path, _paint);
//描绘辅助控制点
canvas.drawPoints(
PointMode.points,
[
Offset(0, Screen.screenHeightDp / 2),
Offset(Screen.screenWidthDp / 4,
Screen.screenHeightDp / 2 - this.height),
Offset(Screen.screenWidthDp / 2, Screen.screenHeightDp / 2)
],
_pointPaint);
canvas.drawPoints(
PointMode.points,
[
Offset(Screen.screenWidthDp / 2, Screen.screenHeightDp / 2),
Offset(Screen.screenWidthDp / 4 * 3,
Screen.screenHeightDp / 2 + this.height),
Offset(Screen.screenWidthDp, Screen.screenHeightDp / 2)
],
_pointPaint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
贝塞尔曲线演示代码
写的比较粗糙,类稍微有点多
import 'package:flutter/material.dart';
import 'package:flutter_weekly/common/utils/screen.dart';
import 'dart:math' as math;
class BezierGestureWidget extends StatefulWidget {
@override
_BezierGestureWidgetState createState() => _BezierGestureWidgetState();
}
class _BezierGestureWidgetState extends State {
int _bezierType = 3;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('贝塞尔曲线演示视图'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RaisedButton(
child: Text("一阶贝塞尔"),
onPressed: () {
setState(() {
_bezierType = 1;
});
},
),
RaisedButton(
child: Text("二阶贝塞尔"),
onPressed: () {
setState(() {
_bezierType = 2;
});
},
),
RaisedButton(
child: Text("三阶贝塞尔"),
onPressed: () {
setState(() {
_bezierType = 3;
});
},
),
],
),
Expanded(
child: Container(
child: GestureWidget(_bezierType),
),
flex: 1,
)
],
),
);
}
}
///演示控件
class GestureWidget extends StatefulWidget {
final int _type;
GestureWidget(this._type);
@override
_GestureWidgetState createState() => _GestureWidgetState();
}
class _GestureWidgetState extends State {
Offset moveOffset=Offset(0, 0);
///贝塞尔曲线控制点,这里只演示三阶曲线
Offset _ctrlOffset0;//控制点1
Offset _ctrlOffset1;
_GestureWidgetState(){
if(_ctrlOffset0==null){
print("_ctrlOffset0==null");
_ctrlOffset0= Offset(90, Screen.screenHeightDp/3-60);
}
if(_ctrlOffset1==null){
print("_ctrlOffset1==null");
_ctrlOffset1=Offset(Screen.screenWidthDp-90, Screen.screenHeightDp/3-60);
}
} //控制点2
@override
Widget build(BuildContext context) {
return Stack(
children: [
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
moveOffset =
new Offset(details.globalPosition.dx, details.globalPosition.dy);
///这里可以看做是初始化的两个控制点
//计算控制点距离控制点1,2的距离,以此判断手势想要移动哪个控制点
double ctrl0Length=math.sqrt((math.pow((moveOffset.dx-_ctrlOffset0.dx).abs(), 2)+(math.pow((moveOffset.dy-_ctrlOffset0.dy).abs(), 2))));//勾股定理
double ctrl1Length=math.sqrt((math.pow((moveOffset.dx-_ctrlOffset1.dx).abs(), 2)+(math.pow((moveOffset.dy-_ctrlOffset1.dy).abs(), 2))));//勾股定理
if(ctrl0Length>ctrl1Length){
///控制的是 _ctrlOffset1 这个点
_ctrlOffset1=moveOffset;
}else{
_ctrlOffset0=moveOffset;
}
});
},
onTapDown: (TapDownDetails details){
setState(() {
moveOffset =
new Offset(details.globalPosition.dx, details.globalPosition.dy);
//计算控制点距离控制点1,2的距离,以此判断手势想要移动哪个控制点
double ctrl0Length=math.sqrt((math.pow((moveOffset.dx-_ctrlOffset0.dx).abs(), 2)+(math.pow((moveOffset.dy-_ctrlOffset0.dy).abs(), 2))));//勾股定理
double ctrl1Length=math.sqrt((math.pow((moveOffset.dx-_ctrlOffset1.dx).abs(), 2)+(math.pow((moveOffset.dy-_ctrlOffset1.dy).abs(), 2))));//勾股定理
if(ctrl0Length>ctrl1Length){
///控制的是 _ctrlOffset1 这个点
_ctrlOffset1=moveOffset;
}else{
_ctrlOffset0=moveOffset;
}
});
},
),
BezierWidget(this.widget._type, moveOffset,_ctrlOffset0,_ctrlOffset1),
],
);
}
}
class BezierWidget extends StatefulWidget {
final int _type;
final Offset _moveOffset;
final Offset _ctrlOffset0;//控制点1
final Offset _ctrlOffset1;//控制点2
BezierWidget(this._type, this._moveOffset, this._ctrlOffset0,
this._ctrlOffset1);
@override
_BezierWidgetState createState() => _BezierWidgetState();
}
class _BezierWidgetState extends State {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: BezierExamplePainter(this.widget._type, this.widget._moveOffset,this.widget._ctrlOffset0,this.widget._ctrlOffset1),
);
}
}
class BezierExamplePainter extends CustomPainter {
final int _type;
final Offset _moveOffset;
final Offset _ctrlOffset0;//控制点1
final Offset _ctrlOffset1;//控制点2
BezierExamplePainter(this._type, this._moveOffset, this._ctrlOffset0,
this._ctrlOffset1);
double _r=5;//点半径
///起始点和终止点设置不变
Offset _startOffset;//起始点
Offset _endOffset;//结束点
///设置画笔
Paint _pathPaint=Paint()..isAntiAlias=true..strokeWidth=4..color=Colors.deepOrange..style=PaintingStyle.stroke;
Paint _pointPaint=Paint()..isAntiAlias=true..strokeWidth=4..style=PaintingStyle.fill..color=Colors.green;
Paint _ctrPaint=Paint()..isAntiAlias=true..strokeWidth=4..style=PaintingStyle.fill..color=Colors.blue;
//路径
Path _linePath=Path();
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
_startOffset=Offset(size.width+60, Screen.screenHeightDp/3);
_endOffset=Offset(Screen.screenWidthDp-60, Screen.screenHeightDp/3);
print("_startOffset: ${_startOffset.toString()}");
canvas.drawCircle(_startOffset, _r, _pointPaint);
canvas.save();
canvas.restore();
_linePath.reset();
switch(_type){
case 1:{
canvas.drawLine(_startOffset, this._moveOffset, _pathPaint);
canvas.drawCircle(_moveOffset, _r, _pointPaint);
break;
}
case 2:{
_linePath.moveTo(_startOffset.dx, _startOffset.dy);
_linePath.quadraticBezierTo(this._moveOffset.dx, this._moveOffset.dy, _endOffset.dx, _endOffset.dy);
canvas.drawCircle(_moveOffset, _r, _ctrPaint);
canvas.drawCircle(_endOffset, _r, _pointPaint);
canvas.drawPath(_linePath, _pathPaint);
break;
}
case 3:
{
_linePath.moveTo(_startOffset.dx, _startOffset.dy);
_linePath.cubicTo(_ctrlOffset0.dx, _ctrlOffset0.dy, _ctrlOffset1.dx, _ctrlOffset1.dy, _endOffset.dx, _endOffset.dy);
canvas.drawCircle(_endOffset, _r, _pointPaint);
canvas.drawCircle(_ctrlOffset0, _r, _ctrPaint);
canvas.drawCircle(_ctrlOffset1, _r, _ctrPaint);
canvas.drawPath(_linePath, _pathPaint);
break;
}
default:
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}