两张图教你使用二三阶贝塞尔曲线

Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线 曲线定义:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生化。 1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名,称为贝塞尔曲线。

线性公式

给定点p0、p1,线性贝塞尔曲线只是一条两点之间的直线,公式如下:

二次方公式

二次方贝塞尔曲线的路径由给定点p0、p1、p2的函数B(t),公式如下:


三次方公式

p0、p1、p2、p3四个点在平面或在三维空间定义了三次贝塞尔曲线。曲线起始于p0走向p1,并从p2的方向来到p3.一般不会经过p1或者p2;这两点只是在那里提供了方向资讯。p0和p1之间的间距,决定了曲线在转而趋进p3之前,走向p2方向的“长度有多长”,公式如下:

上面这段是摘自百度百科,由上面的动态图可以看出,一阶贝塞尔曲线是由两点控制的一条直线,二阶贝塞尔曲线是由一个控制点控制的曲线,三阶贝塞尔曲线是由两个控制点控制的曲线,至于三阶以上的不做研究

下面看一下二阶贝塞尔曲线运行的效果图:


设置二阶贝塞尔曲线的方法如下

moveTo(float x, float y) 其中x、y坐标代表图中曲线靠左边起点的坐标位置

quadTo(float x1, float y1, float x2, float y2) 其中x1、y1坐标代表图中移动点的坐标,也就是我们所说的二阶贝塞尔曲线的控制点坐标;x2、y2坐标代表图中曲线靠右边终点的坐标位置

首先我们要重写view的onTouchEvent的事件,并对该事件进行拦截,也就是返回值为true,代码如下:

@Override  
 public boolean onTouchEvent(MotionEvent event) {  
     switch (event.getAction()) {  
         case MotionEvent.ACTION_DOWN:  
             break;  
         case MotionEvent.ACTION_MOVE:  
             int moveX = (int) (event.getX());  
             int moveY = (int) (event.getY());  
             mControlPoint.x = moveX;  
             mControlPoint.y = moveY;  
             invalidate();  
             break;  
     }  
     return true;  
 }

在move事件中,获取到控制点的坐标,并在onDraw方法中进行路径的绘制,代码如下:

初始化起始点:

mPaint = new Paint();  
mPaint.setStyle(Paint.Style.STROKE);  
  
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();  
mWidth = displayMetrics.widthPixels;  
mHeight = displayMetrics.heightPixels;  
  
mStartPoint.set(100, mHeight / 2);  
mEndPoint.set(mWidth - 100, mHeight / 2);  
mControlPoint.set(mWidth / 2, 100);

进行绘制:

private void drawQuadraticBezier(Canvas canvas) {  
    mPaint.setColor(Color.RED);  
    mPaint.setStrokeWidth(20);  
    mPaint.setStyle(Paint.Style.STROKE);  
    canvas.drawCircle(mControlPoint.x, mControlPoint.y, 10, mPaint);  
  
    mPaint.setStrokeWidth(10);  
    mPaint.setStyle(Paint.Style.FILL);  
    float[] lines = {mStartPoint.x, mStartPoint.y, mControlPoint.x, mControlPoint.y,  
            mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y,  
            mEndPoint.x, mEndPoint.y, mStartPoint.x, mStartPoint.y};  
    canvas.drawLines(lines, mPaint);  
  
    mPaint.setColor(Color.GREEN);  
    mPaint.setStyle(Paint.Style.STROKE);  
    Path path = new Path();  
    path.moveTo(mStartPoint.x, mStartPoint.y);  
    path.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);  
    canvas.drawPath(path, mPaint);  
}

二阶贝塞尔曲线到这里已经介绍完了,接下来介绍下三阶贝塞尔曲线,先看下效果图:

设置二阶贝塞尔曲线的方法如下

moveTo(float x, float y) 其中x、y坐标代表图中在圆周上靠左边起点的坐标位置

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 其中x1、y1坐标代表图中左上角移动点的坐标,x2、y2坐标代表图中右上角移动点的坐标,x1、y1和x2、y2也就是我们所说的三阶贝塞尔曲线的控制点坐标;x3、y3坐标代表图中在圆周上靠右边终点的坐标位置

首先我们要重写view的onTouchEvent的事件,并对该事件进行拦截,也就是返回值为true,代码如下:

@Override  
  public boolean onTouchEvent(MotionEvent event) {  
      switch (event.getAction()) {  
          case MotionEvent.ACTION_DOWN:  
              break;  
          case MotionEvent.ACTION_MOVE:  
              int moveX = (int) (event.getX());  
              int moveY = (int) (event.getY());  
  
              int distanceX = Math.abs(mControlPoint.x - moveX);  
              int distanceY = Math.abs(mControlPoint.y - moveY);  
  
              int distanceX1 = Math.abs(mControlPoint1.x - moveX);  
              int distanceY1 = Math.abs(mControlPoint1.y - moveY);  
              if (distanceX < 50 && distanceY < 50) {  
                  mControlPoint.x = moveX;  
                  mControlPoint.y = moveY;  
              } else if (distanceX1 < 50 && distanceY1 < 50) {  
                  mControlPoint1.x = moveX;  
                  mControlPoint1.y = moveY;  
              }  
              invalidate();  
              break;  
      }  
      return true;  
  }  

在move事件中,判断当前触摸的是哪个控制点,并对该控制点进行赋值,绘制代码如下:初始化数据:

mBloomCenterPoint.set(mWidth / 2, mHeight / 2);  
mStartPoint.set(mWidth / 2, mHeight / 2);  
mEndPoint.set(mWidth / 2, mHeight / 2);  
mControlPoint.set(mWidth / 2 - 200, 100);  
mControlPoint1.set(mWidth / 2 + 200, 100); 

开始绘制:

private void drawCubicBezier(Canvas canvas) {  
        Point topPoint = new Point(mBloomCenterPoint.x, mBloomCenterPoint.y - mRadius);  
        float angle1 = (mBloomCenterPoint.x - mControlPoint.x) * 1.0f / (mBloomCenterPoint.y - mControlPoint.y);  
        float angle2 = (mBloomCenterPoint.x - mControlPoint1.x) * 1.0f / (mBloomCenterPoint.y - mControlPoint1.y);  
  
        boolean isBig1 = false;  
        boolean isBig2 = false;  
        if (mControlPoint.y > mBloomCenterPoint.y) {  
            isBig1 = true;  
        }  
        if (mControlPoint1.y > mBloomCenterPoint.y) {  
            isBig2 = true;  
        }  
        //获取三阶贝塞尔曲线的起始点的值  
        mStartPoint = getFixPoint(topPoint, angle1, isBig1);  
        mEndPoint = getFixPoint(topPoint, angle2, isBig2);  
  
        mPaint.setColor(Color.RED);  
        mPaint.setStrokeWidth(1);  
        mPaint.setStyle(Paint.Style.STROKE);  
        canvas.drawCircle(mControlPoint.x, mControlPoint.y, 10, mPaint);  
        canvas.drawCircle(mControlPoint1.x, mControlPoint1.y, 10, mPaint);  
        canvas.drawCircle(mBloomCenterPoint.x, mBloomCenterPoint.y, mRadius, mPaint);  
  
        mPaint.setStrokeWidth(10);  
        mPaint.setStyle(Paint.Style.FILL);  
        float[] lines = {mStartPoint.x, mStartPoint.y, mControlPoint.x, mControlPoint.y,  
                mControlPoint.x, mControlPoint.y, mControlPoint1.x, mControlPoint1.y,  
                mControlPoint1.x, mControlPoint1.y, mEndPoint.x, mEndPoint.y,  
                mEndPoint.x, mEndPoint.y, mStartPoint.x, mStartPoint.y};  
        canvas.drawLines(lines, mPaint);  
  
        mPaint.setStrokeWidth(10);  
        mPaint.setColor(Color.GREEN);  
        mPaint.setStyle(Paint.Style.FILL);  
        Path path = new Path();  
        path.moveTo(mStartPoint.x, mStartPoint.y);  
        path.cubicTo(mControlPoint.x, mControlPoint.y, mControlPoint1.x, mControlPoint1.y, mEndPoint.x, mEndPoint.y);  
        canvas.drawPath(path, mPaint);  
    }  

private Point getFixPoint(Point topPoint, float angle, boolean isBig) {  
    double radian = Math.atan(angle);  
    if (isBig) {  
        radian += Math.PI;  
    }  
    double sin = Math.sin(radian);  
    double cos = Math.cos(radian);  
    int x = (int) (topPoint.x - mRadius * sin);  
    int y = (int) (topPoint.y + mRadius * (1 - cos));  
  
    Point point = new Point(x, y);  
    return point;  
}  

高级进阶像360安全卫士清理内存的动态效果大家应该都不陌生吧,我们现在用二阶贝塞尔曲线实现这样的效果,先上效果图:

首先我们初始化数据,代码如下:

private void init() {  
    DisplayMetrics displayMetrics = getResources().getDisplayMetrics();  
    mScreenWidth = displayMetrics.widthPixels;  
    mScreenHeight = displayMetrics.heightPixels;  
  
    int height = mScreenHeight * 7 / 10;  
    mStartPoint.set(mScreenWidth / 10, height);  
    mEndPoint.set(mScreenWidth * 9 / 10, height);  
  
    mRadius = 100;  
}  

然后重写onTouchEvent事件,不断的重绘红色的球和绿色的曲线,当只有在球与线接触时,才进行二阶贝塞尔曲线的绘制,touch事件的代码如下:

@Override  
public boolean onTouchEvent(MotionEvent event) {  
    switch (event.getAction()) {  
        case MotionEvent.ACTION_MOVE:  
            int moveX = (int) (event.getX());  
            int moveY = (int) (event.getY());  
  
            mControlPoint.x = moveX;  
            mControlPoint.y = moveY;  
            invalidate();  
            break;  
        case MotionEvent.ACTION_UP:  
            int x = mControlPoint.x;  
            int y = mControlPoint.y;  
            if (y > mStartPoint.y && x > mScreenWidth * 2 / 5  
                    && x < mScreenWidth * 3 / 5) {  
                startAnim();  
            }  
            break;  
    }  
    return true;  
}  

当执行ACTION_UP事件时,判断此时控制点是否进行了二阶变换,如果是,则进行动画的绘制,动画效果的代码如下:

private void startAnim() {  
    ValueAnimator valueAnimator = ValueAnimator.ofInt(mControlPoint.y, -10);  
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
        @Override  
        public void onAnimationUpdate(ValueAnimator animation) {  
            mControlPoint.y = (int) animation.getAnimatedValue();  
            invalidate();  
        }  
    });  
    valueAnimator.setDuration(1000);  
    valueAnimator.start();  
}  

下面看下球跟线接触时,视图是怎么绘制的,代码如下:

@Override  
 protected void onDraw(Canvas canvas) {  
     super.onDraw(canvas);  
  
     Paint paint = new Paint();  
     paint.setStrokeWidth(10);  
     paint.setStyle(Paint.Style.STROKE);  
     paint.setColor(Color.GREEN);  
  
     int x = mControlPoint.x;  
     int y = mControlPoint.y;  
     int height = mStartPoint.y;  
     if (y > mStartPoint.y && x > mScreenWidth * 2 / 5  
             && x < mScreenWidth * 3 / 5) {  
         height = y + y - mStartPoint.y;  
     }  
  
     Path path = new Path();  
     path.moveTo(mStartPoint.x, mStartPoint.y);  
     path.quadTo(mScreenWidth / 2, height, mEndPoint.x, mEndPoint.y);  
     canvas.drawPath(path, paint);  
  
     paint.setStyle(Paint.Style.FILL);  
     paint.setColor(Color.RED);  
     canvas.drawCircle(x, y - mRadius, mRadius, paint);  
 }  

代码中控制点高度的计算,是通过二阶变换公式相减得到的,到目前为止,该过程的绘制代码已全部列出。 在进行三阶贝塞尔曲线变换的时候,绿色部分有点像个花瓣,下面我们用三阶贝塞尔曲线,绘制一朵花,效果图如下:


我们先用进行下数据的初始化操作,定义些常量,代码如下:

public interface BloomOption {  
    //用于控制产生随机花瓣个数范围  
    int minPetalCount = 8;  
    int maxPetalCount = 12;  
    //用于控制产生延长线倍数范围  
    float minPetalStretch = 2f;  
    float maxPetalStretch = 3.5f;  
    //用于控制产生花朵半径随机数范围  
    int minBloomRadius = 100;  
    int maxBloomRadius = 300;  
}  

并进行数据的一些初始化操作:

private void init() {  
    DisplayMetrics displayMetrics = getResources().getDisplayMetrics();  
    int screenWidth = displayMetrics.widthPixels;  
    int screenHeight = displayMetrics.heightPixels;  
    mBloomCenterPoint.set(screenWidth / 2,  screenHeight / 2 - 200);  
    petals = new ArrayList<>();  
    initPetalData();  
}
private void initPetalData() {  
     int petalCount = RandomUtil.randomInt(minPetalCount, maxPetalCount);  
     //每个花瓣应占用的角度  
     float angle = 360f / petalCount;  
     int startAngle = RandomUtil.randomInt(0, 90);  
  
     for (int i = 0; i < petalCount; i++) {  
         //随机产生第一个控制点的拉伸倍数  
         float stretchA = RandomUtil.random(minPetalStretch, maxPetalStretch);  
         //随机产生第二个控制地的拉伸倍数  
         float stretchB = RandomUtil.random(minPetalStretch, maxPetalStretch);  
         //计算每个花瓣的起始角度  
         int beginAngle = startAngle + (int) (i * angle);  
  
         PetalView petal = new PetalView(stretchA, stretchB, beginAngle, angle);  
         petals.add(petal);  
     }  
 }  

下面进行绿色线条的绘制,代码如下:

private void drawStem(Canvas canvas) {  
    Paint paint = new Paint();  
    paint.setStrokeWidth(10);  
    paint.setColor(Color.GREEN);  
    paint.setStyle(Paint.Style.STROKE);  
  
    Path path = new Path();  
    path.moveTo(mBloomCenterPoint.x, mBloomCenterPoint.y);  
    path.quadTo(mBloomCenterPoint.x + 50, mBloomCenterPoint.y + 200, mBloomCenterPoint.x - 50, mBloomCenterPoint.y + 600);  
    canvas.drawPath(path, paint);  
}  

下面进行花的绘制,代码如下:onDraw方法:

int radius = RandomUtil.randomInt(minBloomRadius, maxBloomRadius);  
int size = petals.size();  
MyPoint point = new MyPoint(mBloomCenterPoint.x, mBloomCenterPoint.y);  
for (int i = 0; i < size; i++) {  
    PetalView petal = petals.get(i);  
    if (petal != null) {  
        petal.render(point, radius, canvas);  
    }  
}  

PetalView.java:

public class PetalView {  
    private static final String TAG = "PetalView";  
  
    private float stretchA;//第一个控制点延长线倍数  
    private float stretchB;//第二个控制点延长线倍数  
    private float startAngle;//起始旋转角,用于确定第一个端点  
    private float angle;//两条线之间夹角,由起始旋转角和夹角可以确定第二个端点  
    private int radius = 100;//花芯的半径  
    private Path path = new Path();//用于保存三次贝塞尔曲线  
    private Paint paint = new Paint();  
  
    public PetalView(float stretchA, float stretchB, float startAngle, float angle) {  
        this.stretchA = stretchA;  
        this.stretchB = stretchB;  
        this.startAngle = startAngle;  
        this.angle = angle;  
        paint.setColor(Color.RED);  
    }  
  
    public void render(MyPoint p, int radius, Canvas canvas) {  
        if (this.radius <= radius) {  
            this.radius += 25;  
        }  
        draw(p, canvas);  
    }  
  
    private void draw(MyPoint p, Canvas canvas) {  
        path = new Path();  
        //将向量(0,radius)旋转起始角度,第一个控制点根据这个旋转后的向量计算  
        MyPoint t = new MyPoint(0, this.radius).rotate(RandomUtil.degrad(this.startAngle));  
        //第一个端点,为了保证圆心不会随着radius增大而变大这里固定为3  
        MyPoint v1 = new MyPoint(0, 3).rotate(RandomUtil.degrad(this.startAngle));  
        //第二个端点  
        MyPoint v2 = t.clone().rotate(RandomUtil.degrad(this.angle));  
        //延长线,分别确定两个控制点  
        MyPoint v3 = t.clone().mult(this.stretchA);  
        MyPoint v4 = v2.clone().mult(this.stretchB);  
        //由于圆心在p点,因此,每个点要加圆心坐标点  
        v1.add(p);  
        v2.add(p);  
        v3.add(p);  
        v4.add(p);  
  
        path.moveTo(v1.x, v1.y);  
        //参数分别是:第一个控制点,第二个控制点,终点  
        path.cubicTo(v3.x, v3.y, v4.x, v4.y, v2.x, v2.y);  
        canvas.drawPath(path, paint);  
    }  
}  

MyPoint.java:

public class MyPoint {  
    public int x;  
    public int y;  
  
    public MyPoint() {  
    }  
  
    public MyPoint(int x, int y) {  
        this.x = x;  
        this.y = y;  
    }  
  
    //旋转  
    public MyPoint rotate(float theta) {  
        int x = this.x;  
        int y = this.y;  
        this.x = (int) (Math.cos(theta) * x - Math.sin(theta) * y);  
        this.y = (int) (Math.sin(theta) * x + Math.cos(theta) * y);  
        return this;  
    }  
  
    //乘以一个常数  
    public MyPoint mult(float f) {  
        this.x *= f;  
        this.y *= f;  
        return this;  
    }  
  
    //复制  
    public MyPoint clone() {  
        return new MyPoint(this.x, this.y);  
    }  
  
    //向量相减  
    public MyPoint subtract(MyPoint p) {  
        this.x -= p.x;  
        this.y -= p.y;  
        return this;  
    }  
  
    //向量相加  
    public MyPoint add(MyPoint p) {  
        this.x += p.x;  
        this.y += p.y;  
        return this;  
    }  
  
    public MyPoint set(int x, int y) {  
        this.x = x;  
        this.y = y;  
        return this;  
    }  
  
    @Override  
    public String toString() {  
        return "MyPoint{" +  
                "x=" + x +  
                ", y=" + y +  
                '}';  
    }  
}  

github地址
参考文章:http://www.html5tricks.com/demo/jiaoben1892/index.html
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值