Flutter之 贝塞尔曲线(一)

贝塞尔曲线简介

bezier1.png

由上图可以看出:A,C依据控制点B不断的取点使得AD:AB=BE:BC=DF:DE,构成一个二阶贝塞尔曲线。AD:AB的变化取值范围便是[0,1],
所以要在范围t[0,1]内描述下图
![]
bezier1_1.png


直线上所有的值,便可推出公式:

接下来就可以把X看成矢量坐标上的点P,转换一下便可得出一阶贝塞尔曲线公式。

一阶贝塞尔曲线

一阶贝塞尔曲线.gif

公式

B_{1}(t) = (1 - t)P_0 + tP_1,t\in[0,1]

二阶贝塞尔曲线

二阶贝塞尔曲线.gif

对于二阶贝塞尔曲线,其实你可以理解为:在P_0P_1上利用一阶公式求出点P_0^{'}然后在P_1P_2上利用一阶公式求出点P_1^{'}最后在P_0^{'}P_1^{'}上再利用一阶公式就可以求出最终贝塞尔曲线上的点P_0{''}具体推导过程如下:先求出线段上的控制点。P_0^{'} = (1 - t)P_0 +tP_1 P_1^{'} = (1 - t)P_1 + tP_2推导得出:B_{2}(t) = (1 - t)P_0^{'} + tP_1^{'} = (1 - t)((1 - t)P_0 + tP_1) + t((1 - t)P_1 + tP_2) = (1 - t)^2P_0 + 2t(1 - t)P_1 + t^2P_2
我们常用的画图工具中的曲线,如画图,PS等都是运用贝塞尔曲线计算实现的。

三阶贝赛尔曲线

三阶贝塞尔曲线.gif

公式

B_{3}(t) = (1 - t)^3P_0 + 3t(1 - t)^2P_1 + 3t^2(1 - t)P_2 + t^3P_3 , t\in[0, 1]

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<CustomBezierWidget> {
  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<BezierGestureWidget> {
  int _bezierType = 3;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('贝塞尔曲线演示视图'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              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<GestureWidget> {
  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: <Widget>[
        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<BezierWidget> {
  @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;
  }
}


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容