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.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 |