1. 程式人生 > >兩張圖教你使用二三階貝塞爾曲線

兩張圖教你使用二三階貝塞爾曲線

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 +  
                '}';  
    }  
}