[转载]贝塞尔曲线

效果图

效果图中我们实现了一个简单的随手指滑动的二阶贝塞尔曲线,还有一个复杂点的,穿越所有已知点的贝塞尔曲线。学会使用贝塞尔曲线后可以实现例如QQ红点滑动删除啦,360动态球啦,bulabulabula~

什么是贝塞尔曲线?

贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。

读完上述贝塞尔曲线简介我还是一头雾水,来个示例呗。

示例

线性贝塞尔曲线

给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:

二次方贝塞尔曲线

二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

三次方贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;公式如下:

N次方贝塞尔曲线

身为三维生物超出三维我很方,这里只给示例图。想具体了解的同学请左转度娘。

就当没看过上面

Android在API=1的时候就提供了贝塞尔曲线的画法,只是隐藏在Path#quadTo()和Path#cubicTo()方法中,一个是二阶贝塞尔曲线,一个是三阶贝塞尔曲线。当然,如果你想自己写个方法,依照上面贝塞尔的表达式也是可以的。不过一般没有必要,因为Android已经在native层为我们封装好了二阶和三阶的函数。

从一个二阶贝塞尔开始

自定义一个BezierView

初始化各个参数,花3s扫一下即可。

    private Paint mPaint;
    private Path mPath;
    private Point startPoint;
    private Point endPoint;
    // 辅助点
    private Point assistPoint;
        public BezierView(Context context) {
        this(context, null);
    }

    public BezierView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BezierView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mPaint = new Paint();
        mPath = new Path();
        startPoint = new Point(300, 600);
        endPoint = new Point(900, 600);
        assistPoint = new Point(600, 900);
        // 抗锯齿
        mPaint.setAntiAlias(true);
        // 防抖动
        mPaint.setDither(true);
    }

在onDraw中画二阶贝塞尔

        // 画笔颜色
        mPaint.setColor(Color.BLACK);
        // 笔宽
        mPaint.setStrokeWidth(POINTWIDTH);
        // 空心
        mPaint.setStyle(Paint.Style.STROKE);
        // 重置路径
        mPath.reset();
        // 起点
        mPath.moveTo(startPoint.x, startPoint.y);
        // 重要的就是这句
        mPath.quadTo(assistPoint.x, assistPoint.y, endPoint.x, endPoint.y);
        // 画路径
        canvas.drawPath(mPath, mPaint);
        // 画辅助点
        canvas.drawPoint(assistPoint.x, assistPoint.y, mPaint);

上面注释很清晰就不赘述了。示例中贝塞尔是可以跟着手指的滑动而变化,我一拍榴莲,肯定是复写了onTouchEvent()!

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                assistPoint.x = (int) event.getX();
                assistPoint.y = (int) event.getY();
                Log.i(TAG, "assistPoint.x = " + assistPoint.x);
                Log.i(TAG, "assistPoint.Y = " + assistPoint.y);
                invalidate();
                break;
        }
        return true;
    }

最后将我们自定义的BezierView添加到布局文件中。至此一个简单的二阶贝塞尔曲线就完成了。假设一下,在向下拉动的过程中,在曲线上增加一个“小超人”,360动态清理是不是就出来了呢?有兴趣的可以自己拓展下。

以一个三阶贝塞尔结束

天气预报曲线图示例

(图一)

(图二)

概述

要想得到上图的效果,需要二阶贝塞尔和三阶贝塞尔配合。具体表现为,第一段和最后一段曲线为二阶贝塞尔,中间N段都为三阶贝塞尔曲线。

思路

先根据相邻点(P1,P2, P3)计算出相邻点的中点(P4, P5),然后再计算相邻中点的中点(P6)。然后将(P4,P6, P5)组成的线段平移到经过P2的直线(P8,P2,P7)上。接着根据(P4,P6,P5,P2)的坐标计算出(P7,P8)的坐标。最后根据P7,P8等控制点画出三阶贝塞尔曲线。

点和线的解释

黑色点:要经过的点,例如温度 蓝色点:两个黑色点构成线段的中点 黄色点:两个蓝色点构成线段的中点 灰色点:贝塞尔曲线的控制点 红色线:黑色点的折线图 黑色线:黑色点的贝塞尔曲线,也是我们最终想要的效果

声明

为了方便讲解以及读者的理解。本篇以图一效果为例进行讲解。BezierView坐标都是根据屏幕动态生成的,想要图二的效果只需修改初始坐标,不用对代码做很大的修改即可实现。

那么,开始吧!

初始化参数

    private static final String TAG = "BIZIER";
    private static final int LINEWIDTH = 5;
    private static final int POINTWIDTH = 10;

    private Context mContext;
    /** 即将要穿越的点集合 */
    private List mPoints = new ArrayList<>();
    /** 中点集合 */
    private List mMidPoints = new ArrayList<>();
    /** 中点的中点集合 */
    private List mMidMidPoints = new ArrayList<>();
    /** 移动后的点集合(控制点) */
    private List mControlPoints = new ArrayList<>();

    private int mScreenWidth;
    private int mScreenHeight;
    private void init(Context context) {
        mPaint = new Paint();
        mPath = new Path();
        // 抗锯齿
        mPaint.setAntiAlias(true);
        // 防抖动
        mPaint.setDither(true);

        mContext = context;
        getScreenParams();
        initPoints();
        initMidPoints(this.mPoints);
        initMidMidPoints(this.mMidPoints);
        initControlPoints(this.mPoints, this.mMidPoints , this.mMidMidPoints);

    }

第一个函数获取屏幕宽高就不说了。紧接着初始化了初始点、中点、中点的中点、控制点。我们一个个的跟进。首先是初始点。

    /** 添加即将要穿越的点 */
    private void initPoints() {
        int pointWidthSpace = mScreenWidth / 5;
        int pointHeightSpace = 100;
        for (int i = 0; i < 5; i++) {
            Point point;
            // 一高一低五个点
            if (i%2 != 0) {
                point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeight/2 - pointHeightSpace);
            } else {
                point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeight/2);
            }
            mPoints.add(point);
        }
    }

这里循环创建了一高一低五个点,并添加到List mPoints中。上文说道图一到图二只需修改这里的初始点即可。

    /** 初始化中点集合 */
    private void initMidPoints(List points) {
        for (int i = 0; i < points.size(); i++) {
            Point midPoint = null;
            if (i == points.size()-1){
                return;
            }else {
                midPoint = new Point((points.get(i).x + points.get(i + 1).x)/2, (points.get(i).y + points.get(i + 1).y)/2);
            }
            mMidPoints.add(midPoint);
        }
    }

    /** 初始化中点的中点集合 */
    private void initMidMidPoints(List midPoints){
        for (int i = 0; i < midPoints.size(); i++) {
            Point midMidPoint = null;
            if (i == midPoints.size()-1){
                return;
            }else {
                midMidPoint = new Point((midPoints.get(i).x + midPoints.get(i + 1).x)/2, (midPoints.get(i).y + midPoints.get(i + 1).y)/2);
            }
            mMidMidPoints.add(midMidPoint);
        }
    }

这里算出中点集合以及中点的中点集合,小学数学题没什么好说的。唯一需要注意的是他们数量的差别。

    /** 初始化控制点集合 */
    private void initControlPoints(List points, List midPoints, List midMidPoints){
        for (int i = 0; i < points.size(); i ++){
            if (i ==0 || i == points.size()-1){
                continue;
            }else{
                Point before = new Point();
                Point after = new Point();
                before.x = points.get(i).x - midMidPoints.get(i - 1).x + midPoints.get(i - 1).x;
                before.y = points.get(i).y - midMidPoints.get(i - 1).y + midPoints.get(i - 1).y;
                after.x = points.get(i).x - midMidPoints.get(i - 1).x + midPoints.get(i).x;
                after.y = points.get(i).y - midMidPoints.get(i - 1).y + midPoints.get(i).y;
                mControlPoints.add(before);
                mControlPoints.add(after);
            }
        }
    }

大家需要注意下这个方法的计算过程。以图一(P2,P4, P6,P8)为例。现在P2、P4、P6的坐标是已知的。根据由于(P8, P2)线段由(P4, P6)线段平移而来,所以可得如下结论:P2 - P6 = P8 - P4 。即P8 = P2 - P6 + P4。其余同理。

画辅助点以及对比折线图

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // ***********************************************************
        // ************* 贝塞尔进阶--曲滑穿越已知点 **********************
        // ***********************************************************

        // 画原始点
        drawPoints(canvas);
        // 画穿越原始点的折线
        drawCrossPointsBrokenLine(canvas);
        // 画中间点
        drawMidPoints(canvas);
        // 画中间点的中间点
        drawMidMidPoints(canvas);
        // 画控制点
        drawControlPoints(canvas);
        // 画贝塞尔曲线
        drawBezier(canvas);

    }

可以看到,在画贝塞尔曲线之前我们画了一系列的辅助点,还有和贝塞尔曲线作对比的折线图。效果如图一。辅助点的坐标全都得到了,基本的画画就比较简单了。有能力的可跳过下面这段,直接进入drawBezier(canvas)方法。基本的画画这里只贴代码,如有疑问可评论或者私信。

    /** 画原始点 */
    private void drawPoints(Canvas canvas) {
        mPaint.setStrokeWidth(POINTWIDTH);
        for (int i = 0; i < mPoints.size(); i++) {
            canvas.drawPoint(mPoints.get(i).x, mPoints.get(i).y, mPaint);
        }
    }

    /** 画穿越原始点的折线 */
    private void drawCrossPointsBrokenLine(Canvas canvas) {
        mPaint.setStrokeWidth(LINEWIDTH);
        mPaint.setColor(Color.RED);
        // 重置路径
        mPath.reset();
        // 画穿越原始点的折线
        mPath.moveTo(mPoints.get(0).x, mPoints.get(0).y);
        for (int i = 0; i < mPoints.size(); i++) {
            mPath.lineTo(mPoints.get(i).x, mPoints.get(i).y);
        }
        canvas.drawPath(mPath, mPaint);
    }

    /** 画中间点 */
    private void drawMidPoints(Canvas canvas) {
        mPaint.setStrokeWidth(POINTWIDTH);
        mPaint.setColor(Color.BLUE);
        for (int i = 0; i < mMidPoints.size(); i++) {
            canvas.drawPoint(mMidPoints.get(i).x, mMidPoints.get(i).y, mPaint);
        }
    }

    /** 画中间点的中间点 */
    private void drawMidMidPoints(Canvas canvas) {
        mPaint.setColor(Color.YELLOW);
        for (int i = 0; i < mMidMidPoints.size(); i++) {
            canvas.drawPoint(mMidMidPoints.get(i).x, mMidMidPoints.get(i).y, mPaint);
        }

    }

    /** 画控制点 */
    private void drawControlPoints(Canvas canvas) {
        mPaint.setColor(Color.GRAY);
        // 画控制点
        for (int i = 0; i < mControlPoints.size(); i++) {
            canvas.drawPoint(mControlPoints.get(i).x, mControlPoints.get(i).y, mPaint);
        }
    }

画贝塞尔曲线

    /** 画贝塞尔曲线 */
    private void drawBezier(Canvas canvas) {
        mPaint.setStrokeWidth(LINEWIDTH);
        mPaint.setColor(Color.BLACK);
        // 重置路径
        mPath.reset();
        for (int i = 0; i < mPoints.size(); i++){
            if (i == 0){// 第一条为二阶贝塞尔
                mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y);// 起点
                mPath.quadTo(mControlPoints.get(i).x, mControlPoints.get(i).y,// 控制点
                        mPoints.get(i + 1).x,mPoints.get(i + 1).y);
            }else if(i < mPoints.size() - 2){// 三阶贝塞尔
                mPath.cubicTo(mControlPoints.get(2*i-1).x,mControlPoints.get(2*i-1).y,// 控制点
                        mControlPoints.get(2*i).x,mControlPoints.get(2*i).y,// 控制点
                        mPoints.get(i+1).x,mPoints.get(i+1).y);// 终点
            }else if(i == mPoints.size() - 2){// 最后一条为二阶贝塞尔
                mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y);// 起点
                mPath.quadTo(mControlPoints.get(mControlPoints.size()-1).x,mControlPoints.get(mControlPoints.size()-1).y,
                        mPoints.get(i+1).x,mPoints.get(i+1).y);// 终点
            }
        }
        canvas.drawPath(mPath,mPaint);
    }

注释太详细,都没什么好写的了。不过这里需要注意判断里面的条件,对起点和终点的判断一定要理解。要不然很可能会送你一个ArrayIndexOutOfBoundsException。

结束

贝塞尔曲线可以实现很多绚丽的效果,难的不是贝塞尔,而是good idea。

参考:http://www.2cto.com/kf/201604/497130.html

时间: 2024-08-27 23:06:23

[转载]贝塞尔曲线的相关文章

贝塞尔曲线原理(转载)

最近在做图形学的实验,关于贝塞尔曲线在网上看到一个非常好的资料: 以下内容转载自:https://www.cnblogs.com/hnfxs/p/3148483.html 原理和简单推导(以三阶为例): 设P0.P02.P2是一条抛物线上顺序三个不同的点.过P0和P2点的两切线交于P1点,在P02点的切线交P0P1和P2P1于P01和P11,则如下比例成立: 这是所谓抛物线的三切线定理. 当P0,P2固定,引入参数t,令上述比值为t:(1-t),即有: t从0变到1,第一.二式就分别表示控制二边

UIBezierPath 贝塞尔曲线

1. UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(30, 30, 100, 100) cornerRadius:0]; CAShapeLayer * layer = [CAShapeLayer layer];    layer.path = path.CGPath;    layer.fillColor = [[UIColor blackColor]CGColor]; layer.strokeC

【开源项目解析】QQ“一键下班”功能实现解析——学习Path及贝塞尔曲线的基本使用

早在很久很久以前,QQ就实现了"一键下班"功能.何为"一键下班"?当你QQ有信息时,下部会有信息数量提示红点,点击拖动之后,就会出现"一键下班"效果.本文将结合github上关于此功能的一个简单实现,介绍这个功能的基本实现思路. 项目地址 https://github.com/chenupt/BezierDemo 最终实现效果 实现原理解析 我个人感觉,这个效果实现的很漂亮啊!那么咱们就来看看实现原理是什么~ 注:下面内容请参照项目源码观看. 其

贝塞尔曲线算法

前言 在google找到一篇个人认为最通俗的贝塞尔曲线实现算法博文. 我转载了该博文图片和公式. 出处: http://blog.csdn.net/jimi36/article/details/7792103 图片 一次贝塞尔. 二次贝塞尔. 三次贝塞尔. 贝塞尔曲线算法,布布扣,bubuko.com

贝塞尔曲线(UIBezierPath)属性、方法汇总

UIBezierPath主要用来绘制矢量图形,它是基于Core Graphics对CGPathRef数据类型和path绘图属性的一个封装,所以是需要图形上下文的(CGContextRef),所以一般UIBezierPath在drawRect中使用. UIBezierPath的属性介绍: 1.CGPath:将UIBezierPath类转换成CGPath,类似于UIColor的CGColor 2.empty:只读类型,路径上是否有有效的元素 3.bounds:和view的bounds是不一样的,它获

贝塞尔曲线 &amp; CAShapeLayer &amp; Stroke 动画 浅谈

转载自:http://46aae4d1e2371e4aa769798941cef698.devproxy.yunshipei.com/qiaoqiaoqiao2014/article/details/46928787 1.CAShapeLayer简介  1.1CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值:    1.2CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)    1.3使用CAShapeLayer(属于CoreAnimati

iOS开发之画图板(贝塞尔曲线)

贝塞尔曲线,听着挺牛气一词,不过下面我们在做画图板的时候就用到贝塞尔绘直线,没用到绘制曲线的功能.如果会点PS的小伙伴会对贝塞尔曲线有更直观的理解.这篇博文的重点不在于如何用使用贝塞尔曲线,而是利用贝塞尔划线的功能来封装一个画图板. 画图板的截图如下,上面的白板就是我们的画图板,是自己封装好的一个UIView,下面会详细的介绍如何封装这个画图板,下面的控件用来控制我们画图板的属性以及Undo,Redo和保存功能.点击保存时会把绘制的图片保存到手机的相册中.下面是具体的实现方案. 一.封装画图板

iOS之贝塞尔曲线

这段时间一直在探索一些新奇的事物,博客一直没有更新.今天我们来学一些好玩的东西.比如利用facebook的pop,做一个会动的贝塞尔曲线.废话不多说,来看看吧. #import "Beisaier.h" #import <pop/POP.h> @interface Beisaier () { CAShapeLayer *layer1; UIBezierPath *aPath1; } @end @implementation Beisaier - (void)viewDidL

贝塞尔曲线实现购物车飞入效果

代码地址如下:http://www.demodashi.com/demo/12618.html 前言 做了一个模仿添加物品飞入购物车效果的例子,下面来讲讲它的简单使用 将涉及到以下内容: 工具类的使用 项目结构图与效果图 程序设计与实现 一. 工具类设计 工具类比较多代码,这里就不每个都贴出来了,如下截图为工具类 二. 工具类的使用 飞入购物车效果我封装成了一个工具类FlyAnimtor,下面讲讲它的使用. 如果你想使用飞入效果,你可以这样写: FlyAnimtor.getInstance().