兩張圖教你使用二三階貝塞爾曲線
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 +
'}';
}
}