Skip to content

1.1 曲线和弹性动画的定义与开发

MartinRGB edited this page Dec 13, 2018 · 8 revisions

设计者非常在意动画的曲线实现问题,因为这是使用感觉和动画体验的核心

在主流的动画设计中(After Effect、Origami、Principle、FramerJS),由于动画引擎不同的缘故,导致同样的设计效果不同。

然而,其中的贝塞尔插值函数在开发过程中具有相当的借鉴意义。

一、曲线动画的贝塞尔插值法

ScreenShoot

贝塞尔的原理可以通过这个视频来学习

贝塞尔插值在动画的设计和开发中得以广泛应用,是自定义动画最常用的方式

在设计过程中,设计者可以获取非常有意义的参数 —— 动画时间、属性变化量、以及贝塞尔插值曲线

获取到贝塞尔插值后和上述参数后,开发者可以实现动画的还原。

iOS开发(利用 Core Animation )

CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
animation.timingFunction =[CAMediaTimingFunction  functionWithControlPoints:0.78:0.36:0.14:1.09];

[myView.layer addAnimation:animation forKey:@"myKey"];

Android开发:

PathInterpolator

前端开发者:

div {
  -webkit-transition: all 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
  transition:         all 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94); 
}

参考资料

1.Cubic-Bezier.com

2.Bezier Curve based easing functions – from concept to implementation

3.Bezier Curves and Picasso

二、函数动画

有些情况下,设计者和开发者可能需要使用函数来描述动画,一般应用以下几个场景:

  1. 设计者使用 AE 制作弹性动画时,使用表达式
if (nearestKeyIndex > 0 && currentTime < 1) {
  calculatedVelocity = velocityAtTime(key(nearestKeyIndex).time - thisComp.frameDuration / 10);
  amplitude = 1.1;
  frequency = 2.0;
  decay = 4.0;
  value + calculatedVelocity * amplitude * Math.sin(frequency * currentTime * 2 * Math.PI) / Math.exp(decay * currentTime);
} else {
  value;
}

效果如下:

  1. 同样的,在 AE 中也可以利用函数描述近似贝塞尔曲线的缓动动画(Ease&Wizz JS 文件下载
//去
var p = 0.8;		// period for elastic
var a = 50;			// amplitude for elastic
var s = 1.70158;	// overshoot amount for "back"
function outQuart(t, b, c, d, a, p) {
	return -c * ((t=t/d-1)*t*t*t - 1) + b;
}
//回
var p = 0.8;		// period for elastic
var a = 50;			// amplitude for elastic
var s = 1.70158;	// overshoot amount for "back"
function inOutQuad(t, b, c, d, a, p) {
	if ((t/=d/2) < 1) return c/2*t*t + b;
	return -c/2 * ((--t)*(t-2) - 1) + b;
}

效果如下:

那么这种缓动函数效果,iOS 中有 AHEasing 这个第三方库

3.1 案例1:利用函数可以将手势交互变化量动画属性良好结合,实现真实、细腻的交互效果(案例来自 Kitten Yang 的 《A Guide to iOS animation》 Github地址

这个跟随 Pan 手势变化的交互效果,代码如下

-(void)panGestureRecognized:(UIPanGestureRecognizer *)pan{
 
    static CGPoint initialPoint;
    CGFloat factorOfAngle = 0.0f;
    CGFloat factorOfScale = 0.0f;
    CGPoint transition = [pan translationInView:self.superview];
 
    if (pan.state == UIGestureRecognizerStateBegan) {
 
        initialPoint = self.center;
 
    }else if(pan.state == UIGestureRecognizerStateChanged){
 
 
        self.center = CGPointMake(initialPoint.x,initialPoint.y + transition.y);
 
        CGFloat Y =MIN(SCROLLDISTANCE,MAX(0,ABS(transition.y)));
 
        //一个开口向下,顶点(SCROLLDISTANCE/2,1),过(0,0),(SCROLLDISTANCE,0)的二次函数
        factorOfAngle = MAX(0,-4/(SCROLLDISTANCE*SCROLLDISTANCE)*Y*(Y-SCROLLDISTANCE));
        //一个开口向下,顶点(SCROLLDISTANCE,1),过(0,0),(2*SCROLLDISTANCE,0)的二次函数
        factorOfScale = MAX(0,-1/(SCROLLDISTANCE*SCROLLDISTANCE)*Y*(Y-2*SCROLLDISTANCE));
 
        CATransform3D t = CATransform3DIdentity;
        t.m34  = 1.0/-1000;
        t = CATransform3DRotate(t,factorOfAngle*(M_PI/5), transition.y>0?-1:1, 0, 0);
        t = CATransform3DScale(t, 1-factorOfScale*0.2, 1-factorOfScale*0.2, 0);
 
        self.layer.transform = t;
    }
}

3.2 案例2:获取 Pan 手势的手指位置后,传递给贝塞尔图形,实现拉窗帘一般的效果。

窗帘代码如下:

func createPulledPath(width:CGFloat,point:CGPoint) -> UIBezierPath{
        let height = view.bounds.height
        let offset = width + point.x
        
        let path = UIBezierPath()
        path.moveToPoint(CGPoint(x: -mtExtenedEdgesOffset, y: -mtExtenedEdgesOffset))
        path.addLineToPoint(CGPoint(x: width, y: -mtExtenedEdgesOffset))
        
        // add curve
        path.addCurveToPoint(CGPoint(x: offset, y: point.y),
            controlPoint1: CGPoint(x: width, y: point.y * mtControlPointRatio),
            controlPoint2: CGPoint(x: offset, y: point.y - mtControlPointPulledDistance))
        
        path.addCurveToPoint(CGPoint(x: width, y: height + mtExtenedEdgesOffset),
            controlPoint1: CGPoint(x: offset, y: point.y + mtControlPointPulledDistance),
            controlPoint2: CGPoint(x: width, y: point.y + (height - point.y) * (1 - mtControlPointRatio)))
        
        path.addLineToPoint(CGPoint(x: -mtExtenedEdgesOffset, y: height + mtExtenedEdgesOffset))
        
        path.closePath()
        return path

        
}

然后利用贝塞尔反解函数,传入手指当前点和贝塞尔图形信息,获取实时状态下,曲线上各个点的位置,然后传给方块,获取方块x、y代码如下:

func usefourmulaGetX(width:CGFloat,point:CGPoint,x0:CGFloat,x1:CGFloat,x2:CGFloat,x3:CGFloat,t:CGFloat) -> CGFloat {
        let xValue:CGFloat = x0*(1-t)*(1-t)*(1-t) + 3*x1*t*(1-t)*(1-t) + 3*x2*t*t*(1-t) + x3*t*t*t
        return xValue
    }
    
func usefourmulaGetY(width:CGFloat,point:CGPoint,y0:CGFloat,y1:CGFloat,y2:CGFloat,y3:CGFloat,t:CGFloat) -> CGFloat {
        let yValue:CGFloat = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
        return yValue
    }

由上述案例可见,在制作交互式动画时,函数式动画可以发挥极大的作用,然而对于设计者和开发者来说,成本略大。

而一般情况下,普通的贝塞尔曲线动画就能满足设计者和开发者的需求。

4.安卓开发时,使用 RapidInterpolator ,也可以调节原声插值器动画 以及 easing.net那套Easing函数动画

参考资料

1.缓动函数速查表

2.Easing Equations

3.More Animation Curves than You Can Shake a Stick at

4.AHEasing 开源库 (可以尝试简单修改以便与 Ease & Wizz 的JS函数对接)

三、弹性动画

一般来说有以下几种弹性动画的制作方法,设计与开发对接的形式也有所不同:

1.使用 UIkit Dynamic 中的SnapBehavior

2.使用 UIView 动画的 Spring 形式

3.使用上文提到的函数来描述一根弹性动画曲线

//UI动力学
self.Cardview.center = CGPointMake(160, 219.5)
self.snapBehavior = UISnapBehavior(item: self.Cardview, snapToPoint: CGPointMake(160, 219.5))

//UIView - Spring
UIView.animateWithDuration(0.5, delay: 0,usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: .CurveEaseOut, animations: {
                self.introbtn.frame.origin.y = 692
                }, completion: { finished in
            })

这几种方法不一一赘述,当设计者在 AE 中使用弹性函数时,开发者需要解读 AE 表达式,转写到程序中 当开发者使用 UI 动力学和 UIView Spring动画时,设计者需要在开发者的代码中进行参数调试,以便达到最佳效果。

4.使用 Facebook POP 与 Origami

利用 Origami 这一设计工具结合 POP 动画库,可以高度还原设计效果,设计者给出 BouncinessSpeed 参数即可

Quartz Composer

POP动画的写法类似CAAnimation:

    POPSpringAnimation *switchFrameAnim = [POPSpringAnimation animation];
    switchFrameAnim.property = [POPAnimatableProperty propertyWithName:kPOPViewFrame];
    switchFrameAnim.springBounciness = 20;
    switchFrameAnim.springSpeed = 10;
    switchFrameAnim.toValue = [NSValue valueWithCGRect:CGRectMake(137.5, 450,  100, 100)];
    [_myBtn pop_addAnimation:switchFrameAnim forKey:@"switchBtnFrame"];

效果对比:

效果对比

POP动画可以做 CompletionBlock:

    anim.completionBlock = ^(POPAnimation *anim, BOOL finished) {
    if(finished){NSLog(@"Animation finished!");}};
    }

POP动画可以获取动画的 Progress:

//获取某一动画属性的Progress
    POPSpringAnimation *progressAnim = [POPSpringAnimation animation];
    progressAnim.springBounciness = smallBounciness;
    progressAnim.springSpeed = smallSpeed;
    progressAnim.property = [POPAnimatableProperty propertyWithName:@"popAnimationProgress" initializer:^(POPMutableAnimatableProperty *prop) {
        prop.readBlock = ^(ViewController *obj, CGFloat values[]) {
            values[0] = obj.popAnimationProgress;
        };
        prop.writeBlock = ^(ViewController *obj, const CGFloat values[]) {
            obj.popAnimationProgress = values[0];
        };
        prop.threshold = 0.001;
    }];
    [self pop_addAnimation:progressAnim forKey:@"popAnimation"];

可以给任何属性进行自定义动画:

//阴影动画
    POPSpringAnimation *shadowAnim = [POPSpringAnimation animation];
    //Custom Animation Property
    _animateableshadowOpacityProperty = [POPAnimatableProperty propertyWithName:@"shadowOpacity" initializer:^(POPMutableAnimatableProperty *prop) {
        // read value
        prop.readBlock = ^(CALayer * shadowOpacity , CGFloat values[]) {
            values[0] = shadowOpacity.shadowOpacity;
        };
        // write value
        prop.writeBlock = ^(CALayer * shadowOpacity, const CGFloat values[]) {
            shadowOpacity.shadowOpacity = values[0];
        };
        // dynamics threshold
        prop.threshold = 0.01;
    }];
    shadowAnim.property = _animateableshadowOpacityProperty;
    shadowAnim.springBounciness = smallBounciness;
    shadowAnim.springSpeed = smallSpeed;
    [_shadowView.layer pop_addAnimation:shadowAnim forKey:@"shadowOpacity"];

5.转换 spring-rk4 到 Facebook pop bouncy animation 效果对比 Framer JS 的 spring-rk4动画,可以使用ReboundJS中的转化公式,转为POP动画,方便了 iOS 开发 和 Web 开发

convert = (tension, friction) ->
	result = 
		tension :  Utils.round (tension - 194.0) / 3.62 + 30.0
		friction : Utils.round (friction - 25.0) / 3.0 + 8.0
	return result
 

print convert 600, 60
# » {tension:142, friction:20}

参考资料

1.POP使用指南

2.POP进阶指南

四、定义常用曲线动画

贝塞尔插值 缓动类型 效果 图表
bezier(0.33,0,0.25,1) EaseOut1
bezier(0.33,0,0.33,1) EaseOut2
bezier(0.33,0,0.46,1) EaseOut3
bezier(0.55,0,0.1,1) EaseOut4(SwiftOut)
bezier(0.78,0.36,0.14,1.09) EaseOut5(Back)
bezier(0.66,0,0.33,1) EaseInOut1
bezier(0.33,0,0.66,1) EaseInOut2

以上曲线效果可以通过贝塞尔插值法进行复用,保证了设计效果和开发效果的一致,其他类型曲线效果待定