#import "JNWSpringAnimation.h" #import "NSValue+JNWAdditions.h" #import "TGHacks.h" static const CGFloat JNWSpringAnimationDefaultMass = 5.f; static const CGFloat JNWSpringAnimationDefaultDamping = 30.f; static const CGFloat JNWSpringAnimationDefaultStiffness = 300.f; static const CGFloat JNWSpringAnimationKeyframeStep = 0.001f; static const CGFloat JNWSpringAnimationMinimumThreshold = 0.0001f; @interface JNWSpringAnimation() @property (nonatomic, copy) NSArray *interpolatedValues; @property (nonatomic, assign) BOOL needsRecalculation; @end @implementation JNWSpringAnimation #pragma mark Initialization + (instancetype)animationWithKeyPath:(NSString *)path { return [super animationWithKeyPath:path]; } - (id)init { self = [super init]; _mass = JNWSpringAnimationDefaultMass; _damping = JNWSpringAnimationDefaultDamping; _stiffness = JNWSpringAnimationDefaultStiffness; _durationFactor = 1.0f; _needsRecalculation = NO; return self; } // Since animations are copied before they are added onto the layer, we // take this opportunity to override the copy method and actually // calculate the key frames, and move those over to the new animation. - (id)copyWithZone:(NSZone *)zone { JNWSpringAnimation *copy = [super copyWithZone:zone]; copy.interpolatedValues = self.interpolatedValues; copy.duration = self.interpolatedValues.count * JNWSpringAnimationKeyframeStep * TGAnimationSpeedFactor() * _durationFactor; copy.fromValue = self.fromValue; copy.stiffness = self.stiffness; copy.toValue = self.toValue; copy.damping = self.damping; copy.mass = self.mass; copy.needsRecalculation = NO; return copy; } #pragma mark API - (void)setToValue:(id)toValue { _toValue = toValue; self.needsRecalculation = YES; } - (void)setFromValue:(id)fromValue { _fromValue = fromValue; self.needsRecalculation = YES; } - (void)setStiffness:(CGFloat)stiffness { _stiffness = stiffness; self.needsRecalculation = YES; } - (void)setMass:(CGFloat)mass { _mass = mass; self.needsRecalculation = YES; } - (NSArray *)values { return self.interpolatedValues; } - (void)setDamping:(CGFloat)damping { if (damping <= 0) { NSLog(@"[%@] LOGIC ERROR. `damping` should be > 0.0f to avoid an infinite spring calculation", NSStringFromClass([self class])); damping = 1.0f; } _damping = damping; self.needsRecalculation = YES; } - (CFTimeInterval)duration { if (self.fromValue != nil && self.toValue != nil) { return self.interpolatedValues.count * JNWSpringAnimationKeyframeStep * TGAnimationSpeedFactor() * _durationFactor; } return 0.f; } - (NSArray *)interpolatedValues { if (self.needsRecalculation || _interpolatedValues == nil) { [self calculateInterpolatedValues]; } return _interpolatedValues; } #pragma mark Interpolation - (void)calculateInterpolatedValues { NSAssert(self.fromValue != nil && self.toValue != nil, @"fromValue and or toValue must not be nil."); JNWValueType fromType = [self.fromValue jnw_type]; JNWValueType toType = [self.toValue jnw_type]; NSAssert(fromType == toType, @"fromValue and toValue must be of the same type."); NSAssert(fromType != JNWValueTypeUnknown, @"Type of value could not be determined. Please ensure the value types are supported."); NSArray *values = nil; if (fromType == JNWValueTypeNumber) { values = [self valuesFromNumbers:@[self.fromValue] toNumbers:@[self.toValue] map:^id(CGFloat *values, NSUInteger __unused count) { return @(values[0]); }]; } else if (fromType == JNWValueTypePoint) { CGPoint f = [self.fromValue jnw_pointValue]; CGPoint t = [self.toValue jnw_pointValue]; values = [self valuesFromNumbers:@[@(f.x), @(f.y)] toNumbers:@[@(t.x), @(t.y)] map:^id(CGFloat *values, __unused NSUInteger count) { return [NSValue jnw_valueWithPoint:CGPointMake(values[0], values[1])]; }]; } else if (fromType == JNWValueTypeSize) { CGSize f = [self.fromValue jnw_sizeValue]; CGSize t = [self.toValue jnw_sizeValue]; values = [self valuesFromNumbers:@[@(f.width), @(f.height)] toNumbers:@[@(t.width), @(t.height)] map:^id(CGFloat *values, __unused NSUInteger count) { return [NSValue jnw_valueWithSize:CGSizeMake(values[0], values[1])]; }]; } else if (fromType == JNWValueTypeRect) { // note that CA will not animate the `frame` property CGRect f = [self.fromValue jnw_rectValue]; CGRect t = [self.toValue jnw_rectValue]; values = [self valuesFromNumbers:@[@(f.origin.x), @(f.origin.y), @(f.size.width), @(f.size.height)] toNumbers:@[@(t.origin.x), @(t.origin.y), @(t.size.width), @(t.size.height)] map:^id(CGFloat *values, __unused NSUInteger count) { return [NSValue jnw_valueWithRect:CGRectMake(values[0], values[1], values[2], values[3])]; }]; } else if (fromType == JNWValueTypeAffineTransform) { CGAffineTransform f = [self.fromValue jnw_affineTransformValue]; CGAffineTransform t = [self.toValue jnw_affineTransformValue]; values = [self valuesFromNumbers:@[@(f.a), @(f.b), @(f.c), @(f.d), @(f.tx), @(f.ty)] toNumbers:@[@(t.a), @(t.b), @(t.c), @(t.d), @(t.tx), @(t.ty)] map:^id(CGFloat *values, __unused NSUInteger count) { CGAffineTransform transform; transform.a = values[0]; transform.b = values[1]; transform.c = values[2]; transform.d = values[3]; transform.tx = values[4]; transform.ty = values[5]; return [NSValue jnw_valueWithAffineTransform:transform]; }]; } else if (fromType == JNWValueTypeTransform3D) { CATransform3D f = [self.fromValue CATransform3DValue]; CATransform3D t = [self.toValue CATransform3DValue]; values = [self valuesFromNumbers:@[@(f.m11), @(f.m12), @(f.m13), @(f.m14), @(f.m21), @(f.m22), @(f.m23), @(f.m24), @(f.m31), @(f.m32), @(f.m33), @(f.m34), @(f.m41), @(f.m42), @(f.m43), @(f.m44) ] toNumbers:@[@(t.m11), @(t.m12), @(t.m13), @(t.m14), @(t.m21), @(t.m22), @(t.m23), @(t.m24), @(t.m31), @(t.m32), @(t.m33), @(t.m34), @(t.m41), @(t.m42), @(t.m43), @(t.m44) ] map:^id(CGFloat *values, __unused NSUInteger count) { CATransform3D transform = CATransform3DIdentity; transform.m11 = values[0]; transform.m12 = values[1]; transform.m13 = values[2]; transform.m14 = values[3]; transform.m21 = values[4]; transform.m22 = values[5]; transform.m23 = values[6]; transform.m24 = values[7]; transform.m31 = values[8]; transform.m32 = values[9]; transform.m33 = values[10]; transform.m34 = values[11]; transform.m41 = values[12]; transform.m42 = values[13]; transform.m43 = values[14]; transform.m44 = values[15]; return [NSValue valueWithCATransform3D:transform]; }]; } self.interpolatedValues = values; self.needsRecalculation = NO; } - (NSArray *)valuesFromNumbers:(NSArray *)fromNumbers toNumbers:(NSArray *)toNumbers map:(id (^)(CGFloat *values, NSUInteger count))map { NSAssert(fromNumbers.count == toNumbers.count, @"count of from and to numbers must be equal"); NSUInteger count = fromNumbers.count; // This will never happen, but this is peformed in order to shush the analyzer. if (count < 1) { return [NSArray array]; } CGFloat *distances = calloc(count, sizeof(CGFloat)); CGFloat *thresholds = calloc(count, sizeof(CGFloat)); for (NSUInteger i = 0; i < count; i++) { distances[i] = [toNumbers[i] floatValue] - [fromNumbers[i] floatValue]; thresholds[i] = JNWSpringAnimationThreshold(ABS(distances[i])); } CFTimeInterval step = JNWSpringAnimationKeyframeStep; CFTimeInterval elapsed = 0; CGFloat *stepValues = calloc(count, sizeof(CGFloat)); CGFloat *stepProposedValues = calloc(count, sizeof(CGFloat)); NSMutableArray *valuesMapped = [NSMutableArray array]; while (YES) { BOOL thresholdReached = YES; for (NSUInteger i = 0; i < count; i++) { stepProposedValues[i] = JNWAbsolutePosition(distances[i], (CGFloat)elapsed, 0, self.damping, self.mass, self.stiffness, [fromNumbers[i] floatValue]); if (thresholdReached) thresholdReached = JNWThresholdReached(stepValues[i], stepProposedValues[i], [toNumbers[i] floatValue], thresholds[i]); } if (thresholdReached) break; for (NSUInteger i = 0; i < count; i++) { stepValues[i] = stepProposedValues[i]; } [valuesMapped addObject:map(stepValues, count)]; elapsed += step; } free(distances); free(thresholds); free(stepValues); free(stepProposedValues); return valuesMapped; } BOOL JNWThresholdReached(CGFloat previousValue, CGFloat proposedValue, CGFloat finalValue, CGFloat threshold) { CGFloat previousDifference = ABS(proposedValue - previousValue); CGFloat finalDifference = ABS(previousValue - finalValue); if (previousDifference <= threshold && finalDifference <= threshold) { return YES; } return NO; } BOOL JNWCalculationsAreComplete(CGFloat value1, CGFloat proposedValue1, CGFloat to1, CGFloat value2, CGFloat proposedValue2, CGFloat to2, CGFloat value3, CGFloat proposedValue3, CGFloat to3) { return ((fabs(proposedValue1 - value1) < JNWSpringAnimationKeyframeStep) && (fabs(value1 - to1) < JNWSpringAnimationKeyframeStep) && (fabs(proposedValue2 - value2) < JNWSpringAnimationKeyframeStep) && (fabs(value2 - to2) < JNWSpringAnimationKeyframeStep) && (fabs(proposedValue3 - value3) < JNWSpringAnimationKeyframeStep) && (fabs(value3 - to3) < JNWSpringAnimationKeyframeStep)); } #pragma mark Damped Harmonic Oscillation CGFloat JNWAngularFrequency(CGFloat k, CGFloat m, CGFloat b) { CGFloat w0 = (CGFloat)sqrt(k / m); CGFloat frequency = (CGFloat)sqrt(pow(w0, 2) - (pow(b, 2) / (4*pow(m, 2)))); if (isnan(frequency)) frequency = 0; return frequency; } CGFloat JNWRelativePosition(CGFloat A, CGFloat t, CGFloat phi, CGFloat b, CGFloat m, CGFloat k) { if (A == 0) return A; CGFloat ex = (-b / (2 * m) * t); CGFloat freq = JNWAngularFrequency(k, m, b); return (CGFloat)(A * exp(ex) * cos(freq * t + phi)); } CGFloat JNWAbsolutePosition(CGFloat A, CGFloat t, CGFloat phi, CGFloat b, CGFloat m, CGFloat k, CGFloat from) { return from + A - JNWRelativePosition(A, t, phi, b, m, k); } // This feels a bit hacky. I'm sure there's a better way to accomplish this. CGFloat JNWSpringAnimationThreshold(CGFloat magnitude) { return JNWSpringAnimationMinimumThreshold * magnitude; } #pragma mark Description - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p> mass: %f, damping: %f, stiffness: %f, keyPath: %@, toValue: %@, fromValue %@", self.class, self, self.mass, self.damping, self.stiffness, self.keyPath, self.toValue, self.fromValue]; } @end