Skip to content

Instantly share code, notes, and snippets.

@raphaelschaad
Last active June 10, 2024 11:47
Show Gist options
  • Save raphaelschaad/6739676 to your computer and use it in GitHub Desktop.
Save raphaelschaad/6739676 to your computer and use it in GitHub Desktop.
All the cool animation curves from `CAMediaTimingFunction` but not limited to use with CoreAnimation. See what you can do with cubic Bezier curves here: http://netcetera.org/camtf-playground.html To get started just "Download Gist", throw the .h and .m files into your Xcode project and you're good to go!
//
// RSTimingFunction.h
//
// Created by Raphael Schaad on 2013-09-28.
// This is free and unencumbered software released into the public domain.
//
#import <UIKit/UIKit.h>
// Common predefined timing functions
extern NSString * const kRSTimingFunctionLinear; // controlPoint1=(0, 0), controlPoint2=(1, 1)
extern NSString * const kRSTimingFunctionEaseIn; // controlPoint1=(0.42, 0), controlPoint2=(1, 1)
extern NSString * const kRSTimingFunctionEaseOut; // controlPoint1=(0, 0), controlPoint2=(0.58, 1)
extern NSString * const kRSTimingFunctionEaseInEaseOut; // controlPoint1=(0.42, 0), controlPoint2=(0.58, 1)
extern NSString * const kRSTimingFunctionDefault; // controlPoint1=(0.25, 0.1), controlPoint2=(0.25, 1)
//
// Like `CAMediaTimingFunction` but for versatile (animation) use: allows you to get the value for any given 'x' with `-valueForX:`.
// Any timing function maps an input value normalized to the interval [0..1] on the curve to an output value also in the interval [0..1].
// The implementation math is based on WebCore (WebKit), which is presumably what CoreAnimation is using under the hood too.
//
@interface RSTimingFunction : NSObject <NSCoding>
// Convenience methods to create a common timing function listed above
- (instancetype)initWithName:(NSString *)name;
+ (instancetype)timingFunctionWithName:(NSString *)name;
// Creates a timing function modelled on a cubic Bezier curve.
// The end points of the curve are at (0,0) and (1,1) and the two points defined by the class instance are its control points. Thus the points defining the Bezier curve are: '[(0,0), controlPoint1, controlPoint2, (1,1)]'
// Example: `RSTimingFunction *heavyEaseInTimingFunction = [RSTimingFunction timingFunctionWithControlPoint1:CGPointMake(0.8, 0.0) controlPoint2:CGPointMake(1.0, 1.0)];`
// [y]^ .---controlPoint2=(1,1)
// | |
// | .
// | ,
// | .
// |___.--'
// +-------.--->
// | [x]
// controlPoint1=(0.8,0)
// To visualize what curves given points will produce, use this great tool: http://netcetera.org/camtf-playground.html
- (instancetype)initWithControlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
+ (instancetype)timingFunctionWithControlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
// This is the meat and potatoes: returns `y` for a given `x` value.
- (CGFloat)valueForX:(CGFloat)x;
// If control points are changed after creation, returned values will reflect the changed curve immediately.
// It's more performant to use multiple timing functions with set control points instead of reusing one and changing its control points over and over.
@property (nonatomic, assign) CGPoint controlPoint1;
@property (nonatomic, assign) CGPoint controlPoint2;
// Optionally hint the duration to improve the precision of the values, e.g. when used for an animation.
// Shorter duration is more performant. Default is 1 second.
@property (nonatomic, assign) NSTimeInterval duration;
@end
//
// RSTimingFunction.m
//
// Created by Raphael Schaad https://github.com/raphaelschaad on 2013-09-28.
// This is free and unencumbered software released into the public domain.
// The cubic Bezier math code is licensed under its original copyright notice included below.
// You can use this code (e.g. in your iOS project) without worries as long as you don't remove that notice.
//
#import "RSTimingFunction.h"
#include <tgmath.h> // type generic math, yo: http://en.wikipedia.org/wiki/Tgmath.h#tgmath.h
// Same values as `CAMediaTimingFunction` defines, so they can be used interchangeably.
NSString * const kRSTimingFunctionLinear = @"linear";
NSString * const kRSTimingFunctionEaseIn = @"easeIn";
NSString * const kRSTimingFunctionEaseOut = @"easeOut";
NSString * const kRSTimingFunctionEaseInEaseOut = @"easeInEaseOut";
NSString * const kRSTimingFunctionDefault = @"default";
// Replicate exact same curves as `CAMediaTimingFunction` defines.
static const CGPoint kLinearP1 = {0.0, 0.0};
static const CGPoint kLinearP2 = {1.0, 1.0};
static const CGPoint kEaseInP1 = {0.42, 0.0};
static const CGPoint kEaseInP2 = {1.0, 1.0};
static const CGPoint kEaseOutP1 = {0.0, 0.0};
static const CGPoint kEaseOutP2 = {0.58, 1.0};
static const CGPoint kEaseInEaseOutP1 = {0.42, 0.0};
static const CGPoint kEaseInEaseOutP2 = {0.58, 1.0};
static const CGPoint kDefaultP1 = {0.25, 0.1};
static const CGPoint kDefaultP2 = {0.25, 1.0};
// NSCoding
static NSString * const kControlPoint1Key = @"controlPoint1";
static NSString * const kControlPoint2Key = @"controlPoint2";
static NSString * const kDurationKey = @"duration";
// Internal constants
static const NSTimeInterval kDurationDefault = 1.0;
// For once use private ivars instead of properties for code readability (also omit leading underscore) and to avoid performance hits.
@interface RSTimingFunction ()
{
// Polynomial coefficients
CGFloat ax;
CGFloat bx;
CGFloat cx;
CGFloat ay;
CGFloat by;
CGFloat cy;
}
@end
@implementation RSTimingFunction
#pragma mark - Accessors
@synthesize controlPoint1 = p1;
- (void)setControlPoint1:(CGPoint)controlPoint1
{
if (!CGPointEqualToPoint(p1, [[self class] normalizedPoint:controlPoint1])) {
p1 = controlPoint1;
[self calculatePolynomialCoefficients];
}
}
@synthesize controlPoint2 = p2;
- (void)setControlPoint2:(CGPoint)controlPoint2
{
if (!CGPointEqualToPoint(p2, [[self class] normalizedPoint:controlPoint2])) {
p2 = controlPoint2;
[self calculatePolynomialCoefficients];
}
}
@synthesize duration = dur;
- (void)setDuration:(NSTimeInterval)duration
{
// Only allow non-negative durations.
duration = MAX(0.0, duration);
if (dur != duration) {
dur = duration;
}
}
#pragma mark - Life Cycle
// Privat designated initializer
- (instancetype)initWithControlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2 duration:(NSTimeInterval)duration
{
self = [super init];
if (self) {
// Don't initialize control points through setter to avoid triggering `-calculatePolynomicalCoefficients` unnecessarily twice.
p1 = [[self class] normalizedPoint:controlPoint1];
p2 = [[self class] normalizedPoint:controlPoint2];
// Manually initialize polynomial coefficients with newly set control points.
[self calculatePolynomialCoefficients];
// Use setter to leverage its value sanitanization.
self.duration = duration;
}
return self;
}
- (instancetype)initWithName:(NSString *)name
{
CGPoint controlPoint1 = [[self class] controlPoint1ForTimingFunctionWithName:name];
CGPoint controlPoint2 = [[self class] controlPoint2ForTimingFunctionWithName:name];
return [self initWithControlPoint1:controlPoint1 controlPoint2:controlPoint2];
}
+ (instancetype)timingFunctionWithName:(NSString *)name
{
return [[self alloc] initWithName:name];
}
- (instancetype)initWithControlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2
{
return [self initWithControlPoint1:controlPoint1 controlPoint2:controlPoint2 duration:kDurationDefault];
}
+ (instancetype)timingFunctionWithControlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2
{
return [[self alloc] initWithControlPoint1:controlPoint1 controlPoint2:controlPoint2];
}
#pragma mark - NSObject Method Overrides
#pragma mark Describing Objects
- (NSString *)description
{
NSString *description = [super description];
description = [description stringByAppendingFormat:@" controlPoint1 = %@;", NSStringFromCGPoint(self.controlPoint1)];
description = [description stringByAppendingFormat:@" controlPoint2 = %@;", NSStringFromCGPoint(self.controlPoint2)];
description = [description stringByAppendingFormat:@" duration = %f;", self.duration];
return description;
}
- (NSString *)debugDescription
{
NSString *debugDescription = [self description];
debugDescription = [debugDescription stringByAppendingFormat:@" ax = %f;", ax];
debugDescription = [debugDescription stringByAppendingFormat:@" bx = %f;", bx];
debugDescription = [debugDescription stringByAppendingFormat:@" cx = %f;", cx];
debugDescription = [debugDescription stringByAppendingFormat:@" ay = %f;", ay];
debugDescription = [debugDescription stringByAppendingFormat:@" by = %f;", by];
debugDescription = [debugDescription stringByAppendingFormat:@" cy = %f;", cy];
return debugDescription;
}
#pragma mark - NSCoding Protocol
- (id)initWithCoder:(NSCoder *)decoder
{
CGPoint controlPoint1 = [decoder decodeCGPointForKey:kControlPoint1Key];
CGPoint controlPoint2 = [decoder decodeCGPointForKey:kControlPoint2Key];
NSTimeInterval duration = [decoder decodeDoubleForKey:kDurationKey];
return [self initWithControlPoint1:controlPoint1 controlPoint2:controlPoint2 duration:duration];
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeCGPoint:self.controlPoint1 forKey:kControlPoint1Key];
[encoder encodeCGPoint:self.controlPoint2 forKey:kControlPoint2Key];
[encoder encodeDouble:self.duration forKey:kDurationKey];
}
#pragma mark - Public Methods
- (CGFloat)valueForX:(CGFloat)x
{
CGFloat epsilon = [self epsilon];
CGFloat xSolved = [self solveCurveX:x epsilon:epsilon];
CGFloat y = [self sampleCurveY:xSolved];
return y;
}
#pragma mark - Private Methods
#pragma mark Cubic Bezier Math
// Cubic Bezier math code is based on WebCore (WebKit)
// http://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h
// http://opensource.apple.com/source/WebCore/WebCore-955.66/page/animation/AnimationBase.cpp
/*
* Copyright (C) 2007, 2008, 2009 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
- (CGFloat)epsilon
{
// Higher precision in the timing function for longer duration to avoid ugly discontinuities
return 1.0 / (200.0 * dur);
}
- (void)calculatePolynomialCoefficients
{
// Implicit first and last control points are (0,0) and (1,1).
cx = 3.0 * p1.x;
bx = 3.0 * (p2.x - p1.x) - cx;
ax = 1.0 - cx - bx;
cy = 3.0 * p1.y;
by = 3.0 * (p2.y - p1.y) - cy;
ay = 1.0 - cy - by;
}
- (CGFloat)sampleCurveX:(CGFloat)t
{
// 'ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
return ((ax * t + bx) * t + cx) * t;
}
- (CGFloat)sampleCurveY:(CGFloat)t
{
return ((ay * t + by) * t + cy) * t;
}
- (CGFloat)sampleCurveDerivativeX:(CGFloat)t
{
return (3.0 * ax * t + 2.0 * bx) * t + cx;
}
// Given an x value, find a parametric value it came from.
- (CGFloat)solveCurveX:(CGFloat)x epsilon:(CGFloat)epsilon
{
CGFloat t0;
CGFloat t1;
CGFloat t2;
CGFloat x2;
CGFloat d2;
NSUInteger i;
// First try a few iterations of Newton's method -- normally very fast.
for (t2 = x, i = 0; i < 8; i++) {
x2 = [self sampleCurveX:t2] - x;
if (fabs(x2) < epsilon) {
return t2;
}
d2 = [self sampleCurveDerivativeX:t2];
if (fabs(d2) < 1e-6) {
break;
}
t2 = t2 - x2 / d2;
}
// Fall back to the bisection method for reliability.
t0 = 0.0;
t1 = 1.0;
t2 = x;
if (t2 < t0) {
return t0;
}
if (t2 > t1) {
return t1;
}
while (t0 < t1) {
x2 = [self sampleCurveX:t2];
if (fabs(x2 - x) < epsilon) {
return t2;
}
if (x > x2) {
t0 = t2;
} else {
t1 = t2;
}
t2 = (t1 - t0) * 0.5 + t0;
}
// Failure.
return t2;
}
#pragma mark Helpers
+ (CGPoint)normalizedPoint:(CGPoint)point
{
CGPoint normalizedPoint = CGPointZero;
// Clamp to interval [0..1]
normalizedPoint.x = MAX(0.0, MIN(1.0, point.x));
normalizedPoint.y = MAX(0.0, MIN(1.0, point.y));
return normalizedPoint;
}
+ (CGPoint)controlPoint1ForTimingFunctionWithName:(NSString *)name
{
CGPoint controlPoint1 = CGPointZero;
if ([name isEqual:kRSTimingFunctionLinear]) {
controlPoint1 = kLinearP1;
} else if ([name isEqual:kRSTimingFunctionEaseIn]) {
controlPoint1 = kEaseInP1;
} else if ([name isEqual:kRSTimingFunctionEaseOut]) {
controlPoint1 = kEaseOutP1;
} else if ([name isEqual:kRSTimingFunctionEaseInEaseOut]) {
controlPoint1 = kEaseInEaseOutP1;
} else if ([name isEqual:kRSTimingFunctionDefault]) {
controlPoint1 = kDefaultP1;
} else {
// Not a predefined timing function
}
return controlPoint1;
}
+ (CGPoint)controlPoint2ForTimingFunctionWithName:(NSString *)name
{
CGPoint controlPoint2 = CGPointZero;
if ([name isEqual:kRSTimingFunctionLinear]) {
controlPoint2 = kLinearP2;
} else if ([name isEqual:kRSTimingFunctionEaseIn]) {
controlPoint2 = kEaseInP2;
} else if ([name isEqual:kRSTimingFunctionEaseOut]) {
controlPoint2 = kEaseOutP2;
} else if ([name isEqual:kRSTimingFunctionEaseInEaseOut]) {
controlPoint2 = kEaseInEaseOutP2;
} else if ([name isEqual:kRSTimingFunctionDefault]) {
controlPoint2 = kDefaultP2;
} else {
// Not a predefined timing function
}
return controlPoint2;
}
@end
@jawj
Copy link

jawj commented Jun 11, 2015

Thanks for this. As an alternative, one can define a simple category on CAMediaTiming function that returns a block to do the calculations. Mozilla's nsSMILKeySpline provides a nice implementation.

See: https://gist.github.com/jawj/4577793b12bf85e77677

@takuoka
Copy link

takuoka commented Oct 6, 2015

@raphaelschaad
Copy link
Author

FYI, in upcoming iOS 10, UIViewPropertyAnimator's init(duration:controlPoint1:controlPoint2:animations:) works very similarly to RSTimingFunction.

Copy link

ghost commented Apr 1, 2017

Sadly, this is only the case for running animations. Setting fractionComplete on UIViewAnimating disregards any timing parameters you set on the animator, which is really annoying. It always linearly paces fraction-based animator updates...

@tcldr
Copy link

tcldr commented Nov 4, 2018

Very handy info. Have created a Swift 4.2 version for anyone who might need it.
https://gist.github.com/tcldr/204c4bc87c5239a53239a46728214715

Usage:

let params = UICubicTimingParameters(animationCurve: .easeIn)
let tf = TimingFunction(timingParameters: params)
tf.progress(at: 0.00) // returns 0.0
tf.progress(at: 0.25) // returns ~0.093
tf.progress(at: 0.50) // returns ~0.315
tf.progress(at: 0.75) // returns ~0.621
tf.progress(at: 1.00) // returns 1.0

@jscalo
Copy link

jscalo commented Jan 30, 2021

Quick note, CGPoint, CGFloat, etc are part of UIKit so RSTimingFunction.h should have

#import <UIKit/UIKit.h>

instead of

#import <Foundation/Foundation.h>

(UIKit imports Foundation so there's no need for both.) Or if you're on Mac, use <AppKit/AppKit.h>.

@jscalo
Copy link

jscalo commented Jan 30, 2021

Another oddity:

In the .h there's

- (float)valueForX:(CGFloat)x;

But in the .m there's:

- (CGFloat)valueForX:(CGFloat)x

This is going to result in weird math conversion errors for the caller. The .h should have CGFloat too.

PS thanks for sharing this. It's really useful!

@raphaelschaad
Copy link
Author

Quick note, CGPoint, CGFloat, etc are part of UIKit …

@jscalo Adjusted, thanks (they're part of of CoreGraphics, which is imported by QuartzCore, which is imported by UIKit).

The .h should have CGFloat too.

Agreed & fixed just now -- this is to ensure compatibility for 64bit devices, thanks! @zakol also mentioned this ↑ -- I'm only 7 years late. 😅

@IainS1986
Copy link

For anyone interested who finds this (awesome gist, thanks @raphaelschaad, exactly what I needed!), here's a basic port to .NET for c#/Xamarin developers.

I've actually ported the Swift 4.2 port by @tcldr, again thanks for the port as it made my life 100x easier!

https://gist.github.com/IainS1986/39de43aadf7f32d2118cb8d1e4aaa716

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment