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