貝塞爾曲線開發(fā)的藝術(shù)
一句話概括貝塞爾曲線:將任意一條曲線轉(zhuǎn)化為精確的數(shù)學(xué)公式蛇受。
很多繪圖工具中的鋼筆工具,就是典型的貝塞爾曲線的應(yīng)用丹墨,這里的一個(gè)網(wǎng)站可以在線模擬鋼筆工具的使用:
貝塞爾曲線中有一些比較關(guān)鍵的名詞廊遍,解釋如下:
- 數(shù)據(jù)點(diǎn):通常指一條路徑的起始點(diǎn)和終止點(diǎn)
- 控制點(diǎn):控制點(diǎn)決定了一條路徑的彎曲軌跡,根據(jù)控制點(diǎn)的個(gè)數(shù)贩挣,貝塞爾曲線被分為一階貝塞爾曲線(0個(gè)控制點(diǎn))喉前、二階貝塞爾曲線(1個(gè)控制點(diǎn))、三階貝塞爾曲線(2個(gè)控制點(diǎn))等等王财。
要想對(duì)貝塞爾曲線有一個(gè)比較好的認(rèn)識(shí)卵迂,可以參考WIKI上的鏈接:
https://en.wikipedia.org/wiki/B%C3%A9zier_curve
貝塞爾曲線模擬
在Android中,一般來說绒净,開發(fā)者只考慮二階貝塞爾曲線和三階貝塞爾曲線见咒,SDK也只提供了二階和三階的API調(diào)用。對(duì)于再高階的貝塞爾曲線挂疆,通掣睦溃可以將曲線拆分成多個(gè)低階的貝塞爾曲線,也就是所謂的降階操作缤言。下面將通過代碼來模擬二階和三階的貝塞爾曲線是如何繪制和控制的宝当。
貝塞爾曲線的一個(gè)比較好的動(dòng)態(tài)演示如下所示:
http://myst729.github.io/bezier-curve/
二階模擬
二階貝塞爾曲線在Android中的API為:quadTo()和rQuadTo(),這兩個(gè)API在原理上是可以互相轉(zhuǎn)換的——quadTo是基于絕對(duì)坐標(biāo)胆萧,而rQuadTo是基于相對(duì)坐標(biāo)庆揩,所以后面我都只以其中一個(gè)來進(jìn)行講解。
先來看下最終的效果:
從前面的介紹可以知道鸳碧,二階貝塞爾曲線有兩個(gè)數(shù)據(jù)點(diǎn)和一個(gè)控制點(diǎn)盾鳞,只需要在代碼中繪制出這些輔助點(diǎn)和輔助線即可,同時(shí)瞻离,控制點(diǎn)可以通過onTouchEvent來進(jìn)行傳遞腾仅。
package com.xys.animationart.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/**
* 二階貝塞爾曲線
* <p/>
* Created by xuyisheng on 16/7/11.
*/
public class SecondOrderBezier extends View {
private Paint mPaintBezier;
private Paint mPaintAuxiliary;
private Paint mPaintAuxiliaryText;
private float mAuxiliaryX;
private float mAuxiliaryY;
private float mStartPointX;
private float mStartPointY;
private float mEndPointX;
private float mEndPointY;
private Path mPath = new Path();
public SecondOrderBezier(Context context) {
super(context);
}
public SecondOrderBezier(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBezier.setStyle(Paint.Style.STROKE);
mPaintBezier.setStrokeWidth(8);
mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintAuxiliary.setStyle(Paint.Style.STROKE);
mPaintAuxiliary.setStrokeWidth(2);
mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintAuxiliaryText.setStyle(Paint.Style.STROKE);
mPaintAuxiliaryText.setTextSize(20);
}
public SecondOrderBezier(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mStartPointX = w / 4;
mStartPointY = h / 2 - 200;
mEndPointX = w / 4 * 3;
mEndPointY = h / 2 - 200;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.moveTo(mStartPointX, mStartPointY);
// 輔助點(diǎn)
canvas.drawPoint(mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);
canvas.drawText("控制點(diǎn)", mAuxiliaryX, mAuxiliaryY, mPaintAuxiliaryText);
canvas.drawText("起始點(diǎn)", mStartPointX, mStartPointY, mPaintAuxiliaryText);
canvas.drawText("終止點(diǎn)", mEndPointX, mEndPointY, mPaintAuxiliaryText);
// 輔助線
canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);
canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);
// 二階貝塞爾曲線
mPath.quadTo(mAuxiliaryX, mAuxiliaryY, mEndPointX, mEndPointY);
canvas.drawPath(mPath, mPaintBezier);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mAuxiliaryX = event.getX();
mAuxiliaryY = event.getY();
invalidate();
}
return true;
}
}
三階模擬
三階貝塞爾曲線在Android中的API為:cubicTo()和rCubicTo(),這兩個(gè)API在原理上是可以互相轉(zhuǎn)換的——quadTo是基于絕對(duì)坐標(biāo)套利,而rCubicTo是基于相對(duì)坐標(biāo)推励,所以后面我都只以其中一個(gè)來進(jìn)行講解。
有了二階的基礎(chǔ)肉迫,再來模擬三階就非常簡(jiǎn)單了验辞,無非是增加了一個(gè)控制點(diǎn)而已,先看下效果圖:
代碼只需要在二階的基礎(chǔ)上添加一些輔助點(diǎn)即可喊衫,下面只給出一些關(guān)鍵代碼跌造,詳細(xì)代碼請(qǐng)參考Github:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.moveTo(mStartPointX, mStartPointY);
// 輔助點(diǎn)
canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
canvas.drawText("控制點(diǎn)1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText);
canvas.drawText("控制點(diǎn)2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText);
canvas.drawText("起始點(diǎn)", mStartPointX, mStartPointY, mPaintAuxiliaryText);
canvas.drawText("終止點(diǎn)", mEndPointX, mEndPointY, mPaintAuxiliaryText);
// 輔助線
canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
// 三階貝塞爾曲線
mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY);
canvas.drawPath(mPath, mPaintBezier);
}
模擬網(wǎng)頁
如下所示的網(wǎng)頁,模擬了三階貝塞爾曲線的繪制,可以通過拖動(dòng)曲線來獲取兩個(gè)控制點(diǎn)的坐標(biāo)壳贪,而起始點(diǎn)分別是(0,0)和(1,1)陵珍。
通過這個(gè)網(wǎng)頁,也可以比較方便的獲取三階貝塞爾曲線的控制點(diǎn)坐標(biāo)违施。
貝塞爾曲線應(yīng)用
圓滑繪圖
當(dāng)在屏幕上繪制路徑時(shí)互纯,例如手寫板,最基本的方法是通過Path.lineTo將各個(gè)觸點(diǎn)連接起來磕蒲,而這種方式在很多時(shí)候會(huì)發(fā)現(xiàn)留潦,兩個(gè)點(diǎn)的連接是非常生硬的,因?yàn)樗吘故峭ㄟ^直線來連接的辣往,如果通過二階貝塞爾曲線來將各個(gè)觸點(diǎn)連接兔院,就會(huì)圓滑的多,不會(huì)出現(xiàn)太多的生硬連接排吴。
先來看下代碼秆乳,非常簡(jiǎn)單的繪制路徑代碼:
package com.xys.animationart.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
/**
* 圓滑路徑
* <p/>
* Created by xuyisheng on 16/7/19.
*/
public class DrawPadBezier extends View {
private float mX;
private float mY;
private float offset = ViewConfiguration.get(getContext()).getScaledTouchSlop();
private Paint mPaint;
private Path mPath;
public DrawPadBezier(Context context) {
super(context);
}
public DrawPadBezier(Context context, AttributeSet attrs) {
super(context, attrs);
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.RED);
}
public DrawPadBezier(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.reset();
float x = event.getX();
float y = event.getY();
mX = x;
mY = y;
mPath.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
float x1 = event.getX();
float y1 = event.getY();
float preX = mX;
float preY = mY;
float dx = Math.abs(x1 - preX);
float dy = Math.abs(y1 - preY);
if (dx >= offset || dy >= offset) {
// 貝塞爾曲線的控制點(diǎn)為起點(diǎn)和終點(diǎn)的中點(diǎn)
float cX = (x1 + preX) / 2;
float cY = (y1 + preY) / 2;
// mPath.quadTo(preX, preY, cX, cY);
mPath.lineTo(x1, y1);
mX = x1;
mY = y1;
}
}
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mPath, mPaint);
}
}
先來看下通過mPath.lineTo來實(shí)現(xiàn)的繪圖,效果如下所示:
圖片中的拐點(diǎn)有明顯的鋸齒效果钻哩,即通過直線的連接屹堰,再來看下通過貝塞爾曲線來連接的效果,通常情況下街氢,貝塞爾曲線的控制點(diǎn)取兩個(gè)連續(xù)點(diǎn)的中點(diǎn):
mPath.quadTo(preX, preY, cX, cY);
通過二階貝塞爾曲線的連接效果如圖所示:
可以明顯的發(fā)現(xiàn)扯键,曲線變得更加圓滑了。
曲線變形
通過控制貝塞爾曲線的控制點(diǎn)珊肃,就可以實(shí)現(xiàn)對(duì)一條路徑的修改荣刑。所以,利用貝塞爾曲線伦乔,可以實(shí)現(xiàn)很多的路徑動(dòng)畫厉亏,例如:
package com.xys.animationart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.BounceInterpolator;
/**
* 曲線變形
* <p/>
* Created by xuyisheng on 16/7/11.
*/
public class PathMorphBezier extends View implements View.OnClickListener{
private Paint mPaintBezier;
private Paint mPaintAuxiliary;
private Paint mPaintAuxiliaryText;
private float mAuxiliaryOneX;
private float mAuxiliaryOneY;
private float mAuxiliaryTwoX;
private float mAuxiliaryTwoY;
private float mStartPointX;
private float mStartPointY;
private float mEndPointX;
private float mEndPointY;
private Path mPath = new Path();
private ValueAnimator mAnimator;
public PathMorphBezier(Context context) {
super(context);
}
public PathMorphBezier(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBezier.setStyle(Paint.Style.STROKE);
mPaintBezier.setStrokeWidth(8);
mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintAuxiliary.setStyle(Paint.Style.STROKE);
mPaintAuxiliary.setStrokeWidth(2);
mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintAuxiliaryText.setStyle(Paint.Style.STROKE);
mPaintAuxiliaryText.setTextSize(20);
setOnClickListener(this);
}
public PathMorphBezier(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mStartPointX = w / 4;
mStartPointY = h / 2 - 200;
mEndPointX = w / 4 * 3;
mEndPointY = h / 2 - 200;
mAuxiliaryOneX = mStartPointX;
mAuxiliaryOneY = mStartPointY;
mAuxiliaryTwoX = mEndPointX;
mAuxiliaryTwoY = mEndPointY;
mAnimator = ValueAnimator.ofFloat(mStartPointY, (float) h);
mAnimator.setInterpolator(new BounceInterpolator());
mAnimator.setDuration(1000);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAuxiliaryOneY = (float) valueAnimator.getAnimatedValue();
mAuxiliaryTwoY = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.moveTo(mStartPointX, mStartPointY);
// 輔助點(diǎn)
canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
canvas.drawText("輔助點(diǎn)1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText);
canvas.drawText("輔助點(diǎn)2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText);
canvas.drawText("起始點(diǎn)", mStartPointX, mStartPointY, mPaintAuxiliaryText);
canvas.drawText("終止點(diǎn)", mEndPointX, mEndPointY, mPaintAuxiliaryText);
// 輔助線
canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
// 三階貝塞爾曲線
mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY);
canvas.drawPath(mPath, mPaintBezier);
}
@Override
public void onClick(View view) {
mAnimator.start();
}
}
這里就是簡(jiǎn)單的改變二階貝塞爾曲線的控制點(diǎn)來實(shí)現(xiàn)曲線的變形。
網(wǎng)上一些比較復(fù)雜的變形動(dòng)畫效果烈和,也是基于這種實(shí)現(xiàn)方式爱只,其原理都是通過改變控制點(diǎn)的位置,從而達(dá)到對(duì)圖形的變換招刹,例如圓形到心形的變化恬试、圓形到五角星的變換,等等疯暑。
波浪效果
波浪的繪制是貝塞爾曲線一個(gè)非常簡(jiǎn)單的應(yīng)用训柴,而讓波浪進(jìn)行波動(dòng),其實(shí)并不需要對(duì)控制點(diǎn)進(jìn)行改變妇拯,而是可以通過位移來實(shí)現(xiàn)幻馁,這里我們是借助貝塞爾曲線來實(shí)現(xiàn)波浪的繪制效果,效果如圖所示:
package com.xys.animationart.views;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;
/**
* 波浪圖形
* <p/>
* Created by xuyisheng on 16/7/11.
*/
public class WaveBezier extends View implements View.OnClickListener {
private Paint mPaint;
private Path mPath;
private int mWaveLength = 1000;
private int mOffset;
private int mScreenHeight;
private int mScreenWidth;
private int mWaveCount;
private int mCenterY;
public WaveBezier(Context context) {
super(context);
}
public WaveBezier(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public WaveBezier(Context context, AttributeSet attrs) {
super(context, attrs);
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.LTGRAY);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
setOnClickListener(this);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mScreenHeight = h;
mScreenWidth = w;
mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
mCenterY = mScreenHeight / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.moveTo(-mWaveLength + mOffset, mCenterY);
for (int i = 0; i < mWaveCount; i++) {
// + (i * mWaveLength)
// + mOffset
mPath.quadTo((-mWaveLength * 3 / 4) + (i * mWaveLength) + mOffset, mCenterY + 60, (-mWaveLength / 2) + (i * mWaveLength) + mOffset, mCenterY);
mPath.quadTo((-mWaveLength / 4) + (i * mWaveLength) + mOffset, mCenterY - 60, i * mWaveLength + mOffset, mCenterY);
}
mPath.lineTo(mScreenWidth, mScreenHeight);
mPath.lineTo(0, mScreenHeight);
mPath.close();
canvas.drawPath(mPath, mPaint);
}
@Override
public void onClick(View view) {
ValueAnimator animator = ValueAnimator.ofInt(0, mWaveLength);
animator.setDuration(1000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffset = (int) animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
}
波浪動(dòng)畫實(shí)際上并不復(fù)雜,但三角函數(shù)確實(shí)對(duì)一些開發(fā)者比較困難宣赔,開發(fā)者可以通過下面的這個(gè)網(wǎng)站來模擬三角函數(shù)圖像的繪制:
https://www.desmos.com/calculator
路徑動(dòng)畫
貝塞爾曲線的另一個(gè)非常常用的功能预麸,就是作為動(dòng)畫的運(yùn)動(dòng)軌跡,讓動(dòng)畫目標(biāo)能夠沿曲線平滑的實(shí)現(xiàn)移動(dòng)動(dòng)畫儒将,也就是讓物體沿著貝塞爾曲線運(yùn)動(dòng),而不是機(jī)械的直線对蒲,本例實(shí)現(xiàn)效果如下所示:
package com.xys.animationart.views;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.xys.animationart.evaluator.BezierEvaluator;
/**
* 貝塞爾路徑動(dòng)畫
* <p/>
* Created by xuyisheng on 16/7/12.
*/
public class PathBezier extends View implements View.OnClickListener {
private Paint mPathPaint;
private Paint mCirclePaint;
private int mStartPointX;
private int mStartPointY;
private int mEndPointX;
private int mEndPointY;
private int mMovePointX;
private int mMovePointY;
private int mControlPointX;
private int mControlPointY;
private Path mPath;
public PathBezier(Context context) {
super(context);
}
public PathBezier(Context context, AttributeSet attrs) {
super(context, attrs);
mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPathPaint.setStyle(Paint.Style.STROKE);
mPathPaint.setStrokeWidth(5);
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mStartPointX = 100;
mStartPointY = 100;
mEndPointX = 600;
mEndPointY = 600;
mMovePointX = mStartPointX;
mMovePointY = mStartPointY;
mControlPointX = 500;
mControlPointY = 0;
mPath = new Path();
setOnClickListener(this);
}
public PathBezier(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
canvas.drawCircle(mStartPointX, mStartPointY, 30, mCirclePaint);
canvas.drawCircle(mEndPointX, mEndPointY, 30, mCirclePaint);
mPath.moveTo(mStartPointX, mStartPointY);
mPath.quadTo(mControlPointX, mControlPointY, mEndPointX, mEndPointY);
canvas.drawPath(mPath, mPathPaint);
canvas.drawCircle(mMovePointX, mMovePointY, 30, mCirclePaint);
}
@Override
public void onClick(View view) {
BezierEvaluator bezierEvaluator = new BezierEvaluator(new PointF(mControlPointX, mControlPointY));
ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator,
new PointF(mStartPointX, mStartPointY),
new PointF(mEndPointX, mEndPointY));
anim.setDuration(600);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
PointF point = (PointF) valueAnimator.getAnimatedValue();
mMovePointX = (int) point.x;
mMovePointY = (int) point.y;
invalidate();
}
});
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.start();
}
}
其中钩蚊,用于改變運(yùn)動(dòng)點(diǎn)坐標(biāo)的關(guān)鍵evaluator如下所示:
package com.xys.animationart.evaluator;
import android.animation.TypeEvaluator;
import android.graphics.PointF;
import com.xys.animationart.util.BezierUtil;
public class BezierEvaluator implements TypeEvaluator<PointF> {
private PointF mControlPoint;
public BezierEvaluator(PointF controlPoint) {
this.mControlPoint = controlPoint;
}
@Override
public PointF evaluate(float t, PointF startValue, PointF endValue) {
return BezierUtil.CalculateBezierPointForQuadratic(t, startValue, mControlPoint, endValue);
}
}
這里的TypeEvaluator計(jì)算用到了計(jì)算貝塞爾曲線上點(diǎn)的計(jì)算算法,這個(gè)會(huì)在后面繼續(xù)講解蹈矮。
貝塞爾曲線進(jìn)階
求貝塞爾曲線上任意一點(diǎn)的坐標(biāo)
求貝塞爾曲線上任意一點(diǎn)的坐標(biāo)砰逻,這一過程,就是利用了De Casteljau算法泛鸟。
http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/de-casteljau.html
利用這一算法蝠咆,有開發(fā)者開發(fā)了一個(gè)演示多階貝塞爾曲線的效果的App,其原理就是通過繪制貝塞爾曲線上的點(diǎn)來進(jìn)行繪制的北滥,地址如下所示:
https://github.com/venshine/BezierMaker
下面這篇文章就詳細(xì)的講解了該算法的應(yīng)用刚操,我的代碼也從這里提取而來:
http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/
計(jì)算
有了公式,只需要代碼實(shí)現(xiàn)就OK了再芋,我們先寫兩個(gè)公式:
package com.xys.animationart.util;
import android.graphics.PointF;
/**
* 計(jì)算貝塞爾曲線上的點(diǎn)坐標(biāo)
* <p/>
* Created by xuyisheng on 16/7/13.
*/
public class BezierUtil {
/**
* B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
*
* @param t 曲線長(zhǎng)度比例
* @param p0 起始點(diǎn)
* @param p1 控制點(diǎn)
* @param p2 終止點(diǎn)
* @return t對(duì)應(yīng)的點(diǎn)
*/
public static PointF CalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {
PointF point = new PointF();
float temp = 1 - t;
point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
return point;
}
/**
* B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
*
* @param t 曲線長(zhǎng)度比例
* @param p0 起始點(diǎn)
* @param p1 控制點(diǎn)1
* @param p2 控制點(diǎn)2
* @param p3 終止點(diǎn)
* @return t對(duì)應(yīng)的點(diǎn)
*/
public static PointF CalculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) {
PointF point = new PointF();
float temp = 1 - t;
point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;
point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;
return point;
}
}
我們來將路徑繪制到View中菊霜,看是否正確:
package com.xys.animationart.views;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import com.xys.animationart.util.BezierUtil;
/**
* 通過計(jì)算模擬二階、三階貝塞爾曲線
* <p/>
* Created by xuyisheng on 16/7/13.
*/
public class CalculateBezierPointView extends View implements View.OnClickListener {
private Paint mPaint;
private ValueAnimator mAnimatorQuadratic;
private ValueAnimator mAnimatorCubic;
private PointF mPointQuadratic;
private PointF mPointCubic;
public CalculateBezierPointView(Context context) {
super(context);
}
public CalculateBezierPointView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CalculateBezierPointView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mAnimatorQuadratic = ValueAnimator.ofFloat(0, 1);
mAnimatorQuadratic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
PointF point = BezierUtil.CalculateBezierPointForQuadratic(valueAnimator.getAnimatedFraction(),
new PointF(100, 100), new PointF(500, 100), new PointF(500, 500));
mPointQuadratic.x = point.x;
mPointQuadratic.y = point.y;
invalidate();
}
});
mAnimatorCubic = ValueAnimator.ofFloat(0, 1);
mAnimatorCubic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
PointF point = BezierUtil.CalculateBezierPointForCubic(valueAnimator.getAnimatedFraction(),
new PointF(100, 600), new PointF(100, 1100), new PointF(500, 1000), new PointF(500, 600));
mPointCubic.x = point.x;
mPointCubic.y = point.y;
invalidate();
}
});
mPointQuadratic = new PointF();
mPointQuadratic.x = 100;
mPointQuadratic.y = 100;
mPointCubic = new PointF();
mPointCubic.x = 100;
mPointCubic.y = 600;
setOnClickListener(this);
}
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mPointQuadratic.x, mPointQuadratic.y, 10, mPaint);
canvas.drawCircle(mPointCubic.x, mPointCubic.y, 10, mPaint);
}
@Override
public void onClick(View view) {
AnimatorSet set = new AnimatorSet();
set.playTogether(mAnimatorQuadratic, mAnimatorCubic);
set.setDuration(2000);
set.start();
}
}
這次我們并沒有通過API提供的貝塞爾曲線繪制方法來繪制二階济赎、三階貝塞爾曲線鉴逞,而是通過時(shí)間t和起始點(diǎn)來計(jì)算一條貝塞爾曲線上的所有點(diǎn),可以發(fā)現(xiàn)司训,通過算法計(jì)算出來的點(diǎn)构捡,與通過API所繪制出來的點(diǎn),是完全吻合的壳猜。
貝塞爾曲線擬合計(jì)算
貝塞爾曲線有一個(gè)非常常用的動(dòng)畫效果——MetaBall算法勾徽。相信很多開發(fā)者都見過類似的動(dòng)畫,例如QQ的小紅點(diǎn)消除蓖谢,UC瀏覽器的下拉刷新loading等等捂蕴。要做好這個(gè)動(dòng)畫,實(shí)際上最重要的就是通過貝塞爾曲線來擬合兩個(gè)圖形闪幽。
效果如圖所示:
矩形擬合
我們來看一下擬合的原理啥辨,實(shí)際上就是通過貝塞爾曲線來連接兩個(gè)圓上的四個(gè)點(diǎn),當(dāng)我們調(diào)整下畫筆的填充方式盯腌,并繪制一些輔助線溉知,我們來看具體是如何進(jìn)行擬合的,如圖所示:
可以發(fā)現(xiàn),控制點(diǎn)為兩圓圓心連線的中點(diǎn)级乍,連接線為圖中的這樣一個(gè)矩形舌劳,當(dāng)圓比較小時(shí),這種通過矩形來擬合的方式幾乎是沒有問題的玫荣,但我們把圓放大甚淡,再來看下這種擬合,如圖所示:
當(dāng)圓的半徑擴(kuò)大之后捅厂,就可以非常明顯的發(fā)現(xiàn)擬合的連接點(diǎn)與圓有一定相交的區(qū)域贯卦,這樣的擬合效果就不好了,我們將畫筆模式調(diào)整回來焙贷,如圖所示:
所以撵割,簡(jiǎn)單的矩形擬合,在圓半徑小的時(shí)候辙芍,是可以的啡彬,但當(dāng)圓半徑變大之后,就需要更加嚴(yán)格的擬合了故硅。
這里我們先來講解下庶灿,如何計(jì)算矩形擬合的幾個(gè)關(guān)鍵點(diǎn)。
從前面那張線圖可以看出契吉,標(biāo)紅的兩個(gè)角是相等的跳仿,而這個(gè)角可以通過兩個(gè)圓心的坐標(biāo)來算出,有了這樣一個(gè)角度捐晶,通過R x cos和 R x sin來計(jì)算矩形的一個(gè)頂點(diǎn)的坐標(biāo)菲语,類似的,其它坐標(biāo)可求惑灵,關(guān)鍵代碼如下所示:
private void metaBallVersion1(Canvas canvas) {
float x = mCircleTwoX;
float y = mCircleTwoY;
float startX = mCircleOneX;
float startY = mCircleOneY;
float dx = x - startX;
float dy = y - startY;
double a = Math.atan(dx / dy);
float offsetX = (float) (mCircleOneRadius * Math.cos(a));
float offsetY = (float) (mCircleOneRadius * Math.sin(a));
float x1 = startX + offsetX;
float y1 = startY - offsetY;
float x2 = x + offsetX;
float y2 = y - offsetY;
float x3 = x - offsetX;
float y3 = y + offsetY;
float x4 = startX - offsetX;
float y4 = startY + offsetY;
float controlX = (startX + x) / 2;
float controlY = (startY + y) / 2;
mPath.reset();
mPath.moveTo(x1, y1);
mPath.quadTo(controlX, controlY, x2, y2);
mPath.lineTo(x3, y3);
mPath.quadTo(controlX, controlY, x4, y4);
mPath.lineTo(x1, y1);
// 輔助線
canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint);
canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint);
canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint);
canvas.drawLine(x1, y1, x2, y2, mPaint);
canvas.drawLine(x3, y3, x4, y4, mPaint);
canvas.drawCircle(controlX, controlY, 5, mPaint);
canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint);
canvas.drawLine(x1, y1, x1, mCircleOneY, mPaint);
canvas.drawPath(mPath, mPaint);
}
切線擬合
如前面所說山上,矩形擬合在半徑較小的情況下,是可以實(shí)現(xiàn)完美擬合的英支,而當(dāng)半徑變大后佩憾,就會(huì)出現(xiàn)貝塞爾曲線與圓相交的情況,導(dǎo)致擬合失敗干花。
那么如何來實(shí)現(xiàn)完美的擬合呢妄帘?實(shí)際上,也就是說貝塞爾曲線與圓的連接點(diǎn)到貝塞爾曲線的控制點(diǎn)的連線池凄,一定是圓的切線抡驼,這樣的話,無論圓的半徑如何變化肿仑,貝塞爾曲線一定是與圓擬合的致盟,具體效果如圖所示:
這時(shí)候我們把畫筆模式調(diào)整回來看下填充效果碎税,如圖所示:
這樣擬合是非常完美的。那么要如何來計(jì)算這些擬合的關(guān)鍵點(diǎn)呢馏锡?在前面的線圖中雷蹂,我標(biāo)記出了兩個(gè)角,這兩個(gè)角分別可以求出杯道,相減匪煌,就可以獲取切點(diǎn)與圓心的夾角了,這樣党巾,通過R x cos和R x sin就可以求出切點(diǎn)的坐標(biāo)了虐杯。
其中,小的角可以通過兩個(gè)圓心的坐標(biāo)來求出昧港,而大的角,可以通過直角三角形(圓心支子、切點(diǎn)创肥、控制點(diǎn))來求出,即控制點(diǎn)到圓心的距離/半徑值朋。
關(guān)鍵代碼如下所示:
private void metaBallVersion2(Canvas canvas) {
float x = mCircleTwoX;
float y = mCircleTwoY;
float startX = mCircleOneX;
float startY = mCircleOneY;
float controlX = (startX + x) / 2;
float controlY = (startY + y) / 2;
float distance = (float) Math.sqrt((controlX - startX) * (controlX - startX) + (controlY - startY) * (controlY - startY));
double a = Math.acos(mRadiusNormal / distance);
double b = Math.acos((controlX - startX) / distance);
float offsetX1 = (float) (mRadiusNormal * Math.cos(a - b));
float offsetY1 = (float) (mRadiusNormal * Math.sin(a - b));
float tanX1 = startX + offsetX1;
float tanY1 = startY - offsetY1;
double c = Math.acos((controlY - startY) / distance);
float offsetX2 = (float) (mRadiusNormal * Math.sin(a - c));
float offsetY2 = (float) (mRadiusNormal * Math.cos(a - c));
float tanX2 = startX - offsetX2;
float tanY2 = startY + offsetY2;
double d = Math.acos((y - controlY) / distance);
float offsetX3 = (float) (mRadiusNormal * Math.sin(a - d));
float offsetY3 = (float) (mRadiusNormal * Math.cos(a - d));
float tanX3 = x + offsetX3;
float tanY3 = y - offsetY3;
double e = Math.acos((x - controlX) / distance);
float offsetX4 = (float) (mRadiusNormal * Math.cos(a - e));
float offsetY4 = (float) (mRadiusNormal * Math.sin(a - e));
float tanX4 = x - offsetX4;
float tanY4 = y + offsetY4;
mPath.reset();
mPath.moveTo(tanX1, tanY1);
mPath.quadTo(controlX, controlY, tanX3, tanY3);
mPath.lineTo(tanX4, tanY4);
mPath.quadTo(controlX, controlY, tanX2, tanY2);
canvas.drawPath(mPath, mPaint);
// 輔助線
canvas.drawCircle(tanX1, tanY1, 5, mPaint);
canvas.drawCircle(tanX2, tanY2, 5, mPaint);
canvas.drawCircle(tanX3, tanY3, 5, mPaint);
canvas.drawCircle(tanX4, tanY4, 5, mPaint);
canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint);
canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint);
canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint);
canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint);
canvas.drawCircle(controlX, controlY, 5, mPaint);
canvas.drawLine(startX, startY, tanX1, tanY1, mPaint);
canvas.drawLine(tanX1, tanY1, controlX, controlY, mPaint);
}
圓的擬合
貝塞爾曲線做動(dòng)畫叹侄,很多時(shí)候都需要使用到圓的特效,而通過二階昨登、三階貝塞爾曲線來擬合圓趾代,也不是一個(gè)非常簡(jiǎn)單的事情,所以丰辣,我直接把結(jié)論拿出來了撒强,具體的算法地址如下所示:
http://spencermortensen.com/articles/bezier-circle/
http://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
有了貝塞爾曲線的控制點(diǎn),再對(duì)其實(shí)現(xiàn)動(dòng)畫笙什,就非常簡(jiǎn)單了飘哨,與之前的動(dòng)畫沒有太大的區(qū)別。
源代碼
本次的講解代碼已經(jīng)全部上傳到Github :
https://github.com/xuyisheng/BezierArt
歡迎大家提issue琐凭。