1.曲線介紹
貝塞爾曲線(Bézier curve),又稱貝茲曲線或貝濟(jì)埃曲線,是應(yīng)用于二維圖形應(yīng)用程序的數(shù)學(xué)曲線。在計(jì)算機(jī)圖形學(xué)中也是相當(dāng)重要的參數(shù)曲線。
2.公式介紹
線性公式
給定點(diǎn)P0、P1,線性貝茲曲線只是一條兩點(diǎn)之間的直線。這條線由下式給出:
當(dāng)參數(shù)t變化時(shí),其過程如下:
且其等同于線性插值。
二次方公式
二次方貝茲曲線的路徑由給定點(diǎn)P0、P1、P2的函數(shù)B(t)追蹤:
當(dāng)參數(shù)t變化時(shí),其過程如下:
TrueType字型就運(yùn)用了以貝茲樣條組成的二次貝茲曲線。
三次方公式
P0、P1、P2、P3四個(gè)點(diǎn)在平面或在三維空間中定義了三次方貝茲曲線。曲線起始于P0走向P1,并從P2的方向來到P3。一般不會(huì)經(jīng)過P1或P2;這兩個(gè)點(diǎn)只是在那里提供方向資訊。P0和P1之間的間距,決定了曲線在轉(zhuǎn)而趨進(jìn)P3之前,走向P2方向的“長(zhǎng)度有多長(zhǎng)”。
曲線的參數(shù)形式為:
當(dāng)參數(shù)t變化時(shí),其過程如下:
一般參數(shù)公式
階貝茲曲線可如下推斷。給定點(diǎn)P0、P1、…、Pn,其貝茲曲線即:
如上公式可如下遞歸表達(dá): 用表示由點(diǎn)P0、P1、…、Pn所決定的貝茲曲線。
用平常話來說,階的貝茲曲線,即雙階貝茲曲線之間的插值。
3.公式運(yùn)用
根據(jù)公式自己用線段代替曲線實(shí)現(xiàn)系統(tǒng)貝塞爾曲線效果
實(shí)現(xiàn)效果:
1.首先確定控制點(diǎn),以及其它兩點(diǎn)
2.確定切分信息,在此我把[0,1]切分成了100份
3.根據(jù)公式計(jì)算切分的每個(gè)點(diǎn)
4.創(chuàng)建定時(shí)器每一定時(shí)間繪制特定點(diǎn)數(shù)
5.利用系統(tǒng)函數(shù)創(chuàng)建貝塞爾曲線,與自己繪制的進(jìn)行對(duì)比。
//
// FormulaView.m
// Bezier
//
// Created by qinmin on 2017/1/16.
// Copyright ? 2017年 qinmin. All rights reserved.
//
#import "FormulaView.h"
@interface FormulaView ()
{
CGFloat _t;
CGFloat _deltaT;
CGPoint _p1;
CGPoint _p2;
CGPoint _control;
CGPoint *_pointArr;
CADisplayLink *_displayLink;
int _currentIndex;
}
@end
@implementation FormulaView
- (void)dealloc
{
if (_pointArr) {
free(_pointArr);
_pointArr = NULL;
}
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self setBackgroundColor:[UIColor whiteColor]];
[self setupPoint];
[self setupSliceInfo];
[self createBezierPoint];
[self setupTimer];
}
return self;
}
- (void)setupPoint
{
_p1 = CGPointMake(10, 100);
_p2 = CGPointMake(400, 100);
_control = CGPointMake(100, 800);
}
// 切分點(diǎn)信息
- (void)setupSliceInfo
{
_t = 0.0;
_deltaT = 0.01;
_currentIndex = 0;
}
// 創(chuàng)建線段的切分點(diǎn)
- (void)createBezierPoint
{
int count = 1.0/_deltaT;
_pointArr = (CGPoint *)malloc(sizeof(CGPoint) * (count+1));
// t的范圍[0,1]
for (int i = 0; i < count+1; i++) {
float t = i * _deltaT;
// 二次方計(jì)算公式
float cx = (1-t)*(1-t)*_p1.x + 2*t*(1-t)*_control.x + t*t*_p2.x;
float cy = (1-t)*(1-t)*_p1.y + 2*t*(1-t)*_control.y + t*t*_p2.y;
_pointArr[i] = CGPointMake(cx, cy);
}
}
- (void)setupTimer
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTick:)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)timerTick:(CADisplayLink *)displayLink
{
_currentIndex += 1;
int count = 1.0/_deltaT;
if (_currentIndex > count+1) {
_currentIndex = 1;
}
[self setNeedsDisplay];
}
// 畫圖
- (void)drawRect:(CGRect)rect
{
if (_pointArr == NULL) {
return;
}
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ctx, 4);
// 系統(tǒng)貝塞爾曲線
[[UIColor blackColor] setStroke];
CGContextMoveToPoint(ctx, _p1.x, _p1.y);
CGContextAddQuadCurveToPoint(ctx, _control.x, _control.y, _p2.x, _p2.y);
CGContextDrawPath(ctx, kCGPathStroke);
// 線段代替曲線
[[UIColor redColor] setStroke];
CGContextMoveToPoint(ctx, _pointArr[0].x, _pointArr[0].y);
for (int i = 1; i < _currentIndex; i++) {
CGContextAddLineToPoint(ctx, _pointArr[i].x, _pointArr[i].y);
}
CGContextDrawPath(ctx, kCGPathStroke);
}
@end
這里最重要的是根據(jù)公式,計(jì)算每一個(gè)點(diǎn)的信息
實(shí)現(xiàn)類似彈簧效果
實(shí)現(xiàn)效果:
1.首先確定除控制點(diǎn)以外的其它兩點(diǎn),也就是繩子上兩球的球心
2.設(shè)置整個(gè)場(chǎng)景球的重力,以及繩子的彈力
3.計(jì)算貝塞爾曲線的控制點(diǎn),由于控制點(diǎn)并不是最終和球接觸的那個(gè)點(diǎn),這個(gè)點(diǎn)因該是t=0.5時(shí)曲線上的點(diǎn)
4.創(chuàng)建定時(shí)器每一定時(shí)間更新小球位置
5.檢測(cè)小球碰撞到繩子的時(shí)候,給小球施加彈力,并且讓繩子與小球共同運(yùn)動(dòng)。
6.小球向上運(yùn)動(dòng)與繩子分離的時(shí)候,撤掉彈力的作用。
//
// MyView.h
// UIkit
//
// Created by qinmin on 2017/1/14.
// Copyright ? 2017年 qinmin. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface Ball : NSObject
@property (nonatomic, assign) float speed;
@property (nonatomic, assign) float accelerate;
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) CGSize size;
@end
@interface Rope : NSObject
@property (nonatomic, assign) float k;
@property (nonatomic, assign) float x;
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) CGPoint control;
@property (nonatomic, assign) CGPoint start;
@property (nonatomic, assign) CGPoint end;
@end
@interface BallView : UIView
- (void)stop;
- (void)start;
@end
//
// MyView.m
// UIkit
//
// Created by qinmin on 2017/1/14.
// Copyright ? 2017年 qinmin. All rights reserved.
//
#import "BallView.h"
@implementation Ball
@end
@implementation Rope
@end
@interface BallView ()
{
CADisplayLink *_displayLink;
Ball *_ball;
Rope *_rope;
}
@end
@implementation BallView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self setBackgroundColor:[UIColor whiteColor]];
[self setupTimer];
[self setupBall];
[self setupRope];
}
return self;
}
- (void)setupTimer
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTick:)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)setupBall
{
_ball = [[Ball alloc] init];
_ball.accelerate = 300; //加速度
_ball.speed = 0; //速度
_ball.size = CGSizeMake(30, 30); //球大小
_ball.position = CGPointMake(195, 100); //球的初始位置
}
- (void)setupRope
{
_rope = [[Rope alloc] init];
_rope.position = CGPointMake(200, 420); //繩子中間點(diǎn)的位置
_rope.start = CGPointMake(150, 420); //繩子起點(diǎn)
_rope.end = CGPointMake(270, 420); //繩子終點(diǎn)
_rope.control = CGPointMake(210, 420); //繩子的控制點(diǎn)
_rope.x = 0; //繩子偏移量
_rope.k = 20; //勁度系數(shù)
}
- (void)timerTick:(CADisplayLink *)displayLink
{
//NSLog(@"%f", displayLink.duration);
CGRect ballRect = CGRectMake(_ball.position.x, _ball.position.y+1, _ball.size.width, _ball.size.height);
BOOL ropeMove = NO;
//球與繩子碰撞檢測(cè)
if (CGRectContainsPoint(ballRect, _rope.position)) {
ropeMove = YES;
// delta x
// f = kx
_rope.x = _rope.position.y - _rope.end.y;
}else {
//球向上離開繩子
_rope.x = 0;
}
_ball.speed += (_ball.accelerate - _rope.k * _rope.x) * displayLink.duration;
float s = _ball.speed * displayLink.duration;
_ball.position = CGPointMake(_ball.position.x, _ball.position.y + s);
// 球與繩子碰撞檢測(cè)
if (ropeMove) {
float x = _ball.position.x + _ball.size.width/2;
float y = _ball.position.y + _ball.size.height;
// 中間點(diǎn) 公式的t為0.5
float t = 0.5;
// 根據(jù)公式逆推出控制點(diǎn)
float cx = (x - (1-t)*(1-t)*_rope.start.x - t*t*_rope.end.x)/(2*t*(1-t));
float cy = (y - (1-t)*(1-t)*_rope.start.y - t*t*_rope.end.y)/(2*t*(1-t));
_rope.position = CGPointMake(x, y);
_rope.control = CGPointMake(cx, cy);
// fix 小球未與繩子接觸,小球的位置高于繩子的位置
if (y <= _rope.end.y) {
_rope.position = CGPointMake((_rope.end.x+_rope.start.x)/2, _rope.start.y);
_rope.control = _rope.position;
}
}
//fix position
if (_ball.position.y < 100) {
_ball.position = CGPointMake(_ball.position.x, 100);
}
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
// 畫兩個(gè)固定點(diǎn)
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddPath(path, NULL, [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_rope.start.x-_ball.size.width/2, _rope.start.y-_ball.size.height/2, _ball.size.width, _ball.size.height)].CGPath);
CGPathAddPath(path, NULL, [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_rope.end.x-_ball.size.width/2, _rope.end.y-_ball.size.height/2, _ball.size.width, _ball.size.height)].CGPath);
CGRect ballRect = CGRectMake(_ball.position.x, _ball.position.y, _ball.size.width, _ball.size.height);
CGPathAddPath(path, NULL, [UIBezierPath bezierPathWithOvalInRect:ballRect].CGPath);
CGContextAddPath(ctx, path);
CGContextDrawPath(ctx, kCGPathFillStroke);
CGPathRelease(path);
// 畫繩子的貝塞爾曲線
CGContextMoveToPoint(ctx, _rope.start.x, _rope.start.y);
CGContextAddQuadCurveToPoint(ctx, _rope.control.x, _rope.control.y, _rope.end.x, _rope.end.y);
// 畫最高點(diǎn)標(biāo)記線
CGContextMoveToPoint(ctx, _ball.position.x-30, 100);
CGContextAddLineToPoint(ctx, _ball.position.x +50, 100);
CGContextDrawPath(ctx, kCGPathStroke);
}
- (void)stop
{
[_displayLink invalidate];
_displayLink = nil;
}
- (void)start
{
[self setupTimer];
}
@end
這里最重要的是根據(jù)公式,t=0.5 計(jì)算出控制點(diǎn)的位置
實(shí)現(xiàn)類似小船在波浪行駛的效果
效果:
1.首先用貝塞爾曲線作為波浪,由于需要實(shí)現(xiàn)連續(xù)不斷的效果。所以至少需要兩個(gè)曲線,當(dāng)然可以加入更多波形,在此只弄了簡(jiǎn)單的兩個(gè)波形。
2.設(shè)置波形的水平速度,然后連續(xù)運(yùn)。當(dāng)波浪離開右側(cè)屏幕上,讓其重新回到左側(cè)屏幕。
3.計(jì)算t值,主要是根據(jù)當(dāng)前小球在貝塞爾曲線的比例,比例也就是我們需要的t值。
4.根據(jù)三次方公式計(jì)算出波形對(duì)應(yīng)y值,也就是小球的y值。
//
// ShipView.m
// Bezier
//
// Created by qinmin on 2017/1/16.
// Copyright ? 2017年 qinmin. All rights reserved.
//
#import "ShipView.h"
@implementation Ship
@end
@implementation Wave
@end
@interface ShipView ()
{
CADisplayLink *_displayLink;
Wave *_wave1;
Wave *_wave2;
Ship *_ship;
}
@end
@implementation ShipView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self setBackgroundColor:[UIColor whiteColor]];
[self setupTimer];
[self setupWave];
[self setupShip];
}
return self;
}
- (void)setupTimer
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTick:)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)setupWave
{
_wave1 = [[Wave alloc] init];
_wave1.cp1 = CGPointMake(200, 150);
_wave1.cp2 = CGPointMake(300, 280);
_wave1.p1 = CGPointMake(0, 200);
_wave1.p2 = CGPointMake(420, 200);
_wave1.speed = 320;
_wave2 = [[Wave alloc] init];
_wave2.cp1 = CGPointMake(200-420+1, 150);
_wave2.cp2 = CGPointMake(300-420+1, 250);
_wave2.p1 = CGPointMake(0-420+1, 200);
_wave2.p2 = CGPointMake(420-420+1, 200);
_wave2.speed = _wave1.speed;
}
- (void)setupShip
{
_ship = [[Ship alloc] init];
_ship.size = CGSizeMake(30, 30);
_ship.position = CGPointMake(210, 250);
}
- (void)addDeltaX:(CGFloat)x forWave:(Wave *)wave
{
// fix 無線循環(huán)
if (wave.p1.x + x >= 420) {
x = - 420 * 2 + 8;
}
CGPoint cp1 = wave.cp1;
cp1.x += x;
wave.cp1 = cp1;
CGPoint cp2 = wave.cp2;
cp2.x += x;
wave.cp2 = cp2;
CGPoint p1 = wave.p1;
p1.x += x;
wave.p1 = p1;
CGPoint p2 = wave.p2;
p2.x += x;
wave.p2 = p2;
}
- (void)timerTick:(CADisplayLink *)displayLink
{
CGFloat delta = displayLink.duration * _wave1.speed;
[self addDeltaX:delta forWave:_wave1];
[self addDeltaX:delta forWave:_wave2];
Wave *currentWave;
if (_wave1.p1.x <= _ship.position.x && _ship.position.x <= _wave1.p2.x) {
currentWave = _wave1;
}else {
currentWave = _wave2;
}
// 計(jì)算t值
float t = (_ship.position.x - currentWave.p1.x)/(currentWave.p2.x - currentWave.p1.x);
// 由t值,計(jì)算出y值
float y = currentWave.p1.y*pow(1-t, 3) + 3*currentWave.cp1.y*t*pow(1-t, 2) +3*currentWave.cp2.y*t*t*(1-t)+currentWave.p2.y*pow(t, 3);
CGPoint position = _ship.position;
position.y = y-_ship.size.height-2;
_ship.position = position;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddPath(path, NULL, [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_ship.position.x, _ship.position.y, _ship.size.width, _ship.size.height)].CGPath);
CGContextAddPath(ctx, path);
CGContextDrawPath(ctx, kCGPathFillStroke);
CGPathRelease(path);
CGContextMoveToPoint(ctx, _wave1.p1.x, _wave1.p1.y);
CGContextAddCurveToPoint(ctx, _wave1.cp1.x, _wave1.cp1.y, _wave1.cp2.x, _wave1.cp2.y, _wave1.p2.x, _wave1.p2.y);
CGContextDrawPath(ctx, kCGPathStroke);
CGContextMoveToPoint(ctx, _wave2.p1.x, _wave2.p1.y);
CGContextAddCurveToPoint(ctx, _wave2.cp1.x, _wave2.cp1.y, _wave2.cp2.x, _wave2.cp2.y, _wave2.p2.x, _wave2.p2.y);
CGContextDrawPath(ctx, kCGPathStroke);
}
@end
實(shí)現(xiàn)類似橡皮泥效果
效果:
這里我就不過多解釋代碼,代碼還值得優(yōu)化
//
// DotView.m
// Bezier
//
// Created by qinmin on 2017/1/15.
// Copyright ? 2017年 qinmin. All rights reserved.
//
#import "DotView.h"
@implementation Dot
@end
@interface DotView ()
{
Dot *_startDot;
Dot *_endDot;
CGPoint _controlPoint;
CGFloat _lastDistance;
}
@end
@implementation DotView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self setBackgroundColor:[UIColor whiteColor]];
[self setupDots];
[self setupGesture];
[self setupControlPoint];
}
return self;
}
- (void)setupDots
{
_startDot = [[Dot alloc] init];
_startDot.position = CGPointMake(100, 200);
_startDot.size = CGSizeMake(50, 50);
_endDot = [[Dot alloc] init];
_endDot.position = CGPointMake(100, 290);
_endDot.size = CGSizeMake(50, 50);
}
- (void)setupGesture
{
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
[self addGestureRecognizer:panGesture];
}
- (void)setupControlPoint
{
CGFloat cpx = (_startDot.position.x + _endDot.position.x)/2;
CGFloat cpy = 0.5;
_controlPoint = CGPointMake(cpx, cpy);
}
- (void)handleGesture:(UIPanGestureRecognizer *)gesture
{
CGPoint offset = [gesture locationInView:self];
//CGPoint velocity = [gesture translationInView:self];
//NSLog(@"%@", NSStringFromCGPoint(velocity));
CGPoint endPos = _endDot.position;
endPos.x = offset.x;
endPos.y = offset.y;
_endDot.position = endPos;
float distance = sqrtf(pow(_startDot.position.x - _endDot.position.x, 2) + pow(_startDot.position.y - _endDot.position.y, 2));
CGSize size;
if (distance - _lastDistance < 0.00000) {
size = _startDot.size;
size.width = size.width + 0.001 * distance;
size.height = size.width;
NSLog(@"%@", NSStringFromCGSize(size));
if (size.width > 50 ) {
size.width = size.height = 30;
}
}else {
size = _startDot.size;
size.width = size.width - 0.001 * distance;
size.height = size.width;
NSLog(@"%@", NSStringFromCGSize(size));
if (size.width < 15 ) {
size.width = size.height = 15;
}
}
_startDot.size = _endDot.size = size;
_lastDistance = distance;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddPath(path, NULL, [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_startDot.position.x-_startDot.size.width/2, _startDot.position.y-_startDot.size.height/2, _startDot.size.width, _startDot.size.height)].CGPath);
CGPathAddPath(path, NULL, [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_endDot.position.x-_endDot.size.width/2, _endDot.position.y-_endDot.size.height/2, _endDot.size.width, _endDot.size.height)].CGPath);
CGContextAddPath(ctx, path);
CGContextDrawPath(ctx, kCGPathFillStroke);
CGPathRelease(path);
// 求動(dòng)點(diǎn)相對(duì)于x軸的偏移角
// a = (1,0), b = (end.x-start.x, end.y-start.y), cost=a*b/(|a||b|)
float cost = (_endDot.position.x-_startDot.position.x)/sqrtf(pow(_endDot.position.x-_startDot.position.x, 2) + pow(_endDot.position.y-_startDot.position.y, 2));
float t = acosf(cost);
float sint = sin(t);
// 修正動(dòng)點(diǎn)在定點(diǎn)上方時(shí)候的角度問題
int i = 1;
if (_endDot.position.y < _startDot.position.y) {
cost = cos(-t);
sint = sin(-t);
i = -1;
}
float deltax = _startDot.size.width/2 * sint;
float deltay = _startDot.size.height/2 * cost;
float cpx = (_startDot.position.x+_endDot.position.x)/2;
float cpy = (_startDot.position.y+_endDot.position.y)/2 - _controlPoint.y*cost;
float cpy1 = (_startDot.position.y+_endDot.position.y)/2 + _controlPoint.y*cost;
// 畫四邊形
CGContextSetLineWidth(ctx, 0);
CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
CGContextMoveToPoint(ctx, _startDot.position.x+deltax-1*i, _startDot.position.y-deltay);
CGContextAddLineToPoint(ctx, _startDot.position.x-deltax+1*i, _startDot.position.y+deltay);
CGContextAddLineToPoint(ctx, _endDot.position.x+deltax-1*i, _endDot.position.y-deltay);
CGContextMoveToPoint(ctx, _endDot.position.x+deltax-1*i, _endDot.position.y-deltay);
CGContextAddLineToPoint(ctx, _endDot.position.x-deltax+1*i, _endDot.position.y+deltay);
CGContextAddLineToPoint(ctx, _startDot.position.x-deltax+1*i, _startDot.position.y+deltay);
CGContextDrawPath(ctx, kCGPathEOFillStroke);
// 畫貝塞爾曲線
[[UIColor whiteColor] setFill];
CGContextSetLineWidth(ctx, 0);
CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
CGContextMoveToPoint(ctx, _startDot.position.x+deltax+1*i, _startDot.position.y-deltay);
CGContextAddQuadCurveToPoint(ctx, cpx, cpy, _endDot.position.x+deltax+1*i, _endDot.position.y-deltay);
CGContextMoveToPoint(ctx, _startDot.position.x-deltax-1*i, _startDot.position.y+deltay);
CGContextAddQuadCurveToPoint(ctx, cpx, cpy1, _endDot.position.x-deltax-1*i, _endDot.position.y+deltay);
CGContextDrawPath(ctx, kCGPathEOFillStroke);
}
@end
最后
這篇文章講述了貝塞爾曲線的公式定義,以及用它來實(shí)現(xiàn)一些特殊的效果。如果能理解好貝塞爾曲線的原理,對(duì)動(dòng)畫效果開發(fā)是很有幫助的。
更深入的理解方程推到過程請(qǐng)參照:
1.如何得到貝塞爾曲線的曲線長(zhǎng)度和 t 的近似關(guān)系
2.貝塞爾曲線掃盲
目前代碼已經(jīng)放到github上面,傳送門:Bezier