// // LOTAnimatableColorValue.m // LottieAnimator // // Created by brandon_withrow on 6/23/16. // Copyright © 2016 Brandon Withrow. All rights reserved. // #import "LOTAnimatableColorValue.h" @interface LOTAnimatableColorValue () @property (nonatomic, readonly) NSArray *colorKeyframes; @property (nonatomic, readonly) NSArray *keyTimes; @property (nonatomic, readonly) NSArray *timingFunctions; @property (nonatomic, readonly) NSTimeInterval delay; @property (nonatomic, readonly) NSTimeInterval duration; @property (nonatomic, readonly) NSNumber *startFrame; @property (nonatomic, readonly) NSNumber *durationFrames; @property (nonatomic, readonly) NSNumber *frameRate; @end @implementation LOTAnimatableColorValue - (instancetype)initWithColorValues:(NSDictionary *)colorValues frameRate:(NSNumber *)frameRate { self = [super init]; if (self) { _frameRate = frameRate; NSArray *value = colorValues[@"k"]; if ([value isKindOfClass:[NSArray class]] && [[(NSArray *)value firstObject] isKindOfClass:[NSDictionary class]] && [(NSArray *)value firstObject][@"t"]) { //Keframes [self _buildAnimationForKeyframes:value]; } else { //Single Value, no animation _initialColor = [[self _colorValueFromArray:value] copy]; } } return self; } - (void)_buildAnimationForKeyframes:(NSArray *)keyframes { NSMutableArray *keyTimes = [NSMutableArray array]; NSMutableArray *timingFunctions = [NSMutableArray array]; NSMutableArray *colorValues = [NSMutableArray array]; _startFrame = keyframes.firstObject[@"t"]; NSNumber *endFrame = keyframes.lastObject[@"t"]; NSAssert((_startFrame && endFrame && _startFrame.integerValue < endFrame.integerValue), @"Lottie: Keyframe animation has incorrect time values or invalid number of keyframes"); // Calculate time duration _durationFrames = @(endFrame.floatValue - _startFrame.floatValue); _duration = _durationFrames.floatValue / _frameRate.floatValue; _delay = _startFrame.floatValue / _frameRate.floatValue; BOOL addStartValue = YES; BOOL addTimePadding = NO; UIColor *outColor = nil; for (NSDictionary *keyframe in keyframes) { // Get keyframe time value NSNumber *frame = keyframe[@"t"]; // Calculate percentage value for keyframe. //CA Animations accept time values of 0-1 as a percentage of animation completed. NSNumber *timePercentage = @((frame.floatValue - _startFrame.floatValue) / _durationFrames.floatValue); if (outColor) { //add out value [colorValues addObject:(id)[[outColor copy] CGColor]]; [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; outColor = nil; } UIColor *startColor = [self _colorValueFromArray:keyframe[@"s"]]; if (addStartValue) { // Add start value if (startColor) { if (keyframe == keyframes.firstObject) { _initialColor = startColor; } [colorValues addObject:(id)[[startColor copy] CGColor]]; if (timingFunctions.count) { [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; } } addStartValue = NO; } if (addTimePadding) { // add time padding NSNumber *holdPercentage = @(timePercentage.floatValue - 0.00001); [keyTimes addObject:[holdPercentage copy]]; addTimePadding = NO; } // add end value if present for keyframe UIColor *endColor = [self _colorValueFromArray:keyframe[@"e"]]; if (endColor) { [colorValues addObject:(id)[[endColor copy] CGColor]]; /* * Timing Function for time interpolations between keyframes * Should be n-1 where n is the number of keyframes */ CAMediaTimingFunction *timingFunction; NSDictionary *timingControlPoint1 = keyframe[@"o"]; NSDictionary *timingControlPoint2 = keyframe[@"i"]; if (timingControlPoint1 && timingControlPoint2) { // Easing function CGPoint cp1 = [self _pointFromValueDict:timingControlPoint1]; CGPoint cp2 = [self _pointFromValueDict:timingControlPoint2]; timingFunction = [CAMediaTimingFunction functionWithControlPoints:cp1.x :cp1.y :cp2.x :cp2.y]; } else { // No easing function specified, fallback to linear timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; } [timingFunctions addObject:timingFunction]; } // add time [keyTimes addObject:timePercentage]; // Check if keyframe is a hold keyframe if ([keyframe[@"h"] boolValue]) { // set out value as start and flag next frame accordinly outColor = startColor; addStartValue = YES; addTimePadding = YES; } } _colorKeyframes = colorValues; _keyTimes = keyTimes; _timingFunctions = timingFunctions; } - (UIColor *)_colorValueFromArray:(NSArray *)colorArray { if (colorArray.count == 4) { BOOL shouldUse255 = NO; for (NSNumber *number in colorArray) { if (number.floatValue > 1) { shouldUse255 = YES; } } return [UIColor colorWithRed:colorArray[0].floatValue / (shouldUse255 ? 255.f : 1.f) green:colorArray[1].floatValue / (shouldUse255 ? 255.f : 1.f) blue:colorArray[2].floatValue / (shouldUse255 ? 255.f : 1.f) alpha:colorArray[3].floatValue / (shouldUse255 ? 255.f : 1.f)]; } return nil; } - (CGPoint)_pointFromValueDict:(NSDictionary *)values { NSNumber *xValue = @0, *yValue = @0; if ([values[@"x"] isKindOfClass:[NSNumber class]]) { xValue = values[@"x"]; } else if ([values[@"x"] isKindOfClass:[NSArray class]]) { xValue = values[@"x"][0]; } if ([values[@"y"] isKindOfClass:[NSNumber class]]) { yValue = values[@"y"]; } else if ([values[@"y"] isKindOfClass:[NSArray class]]) { yValue = values[@"y"][0]; } return CGPointMake([xValue floatValue], [yValue floatValue]); } - (BOOL)hasAnimation { return (self.colorKeyframes.count > 0); } - (nullable CAKeyframeAnimation *)animationForKeyPath:(nonnull NSString *)keypath { if (self.hasAnimation == NO) { return nil; } CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:keypath]; keyframeAnimation.keyTimes = self.keyTimes; keyframeAnimation.values = self.colorKeyframes; keyframeAnimation.timingFunctions = self.timingFunctions; keyframeAnimation.duration = self.duration; keyframeAnimation.beginTime = self.delay; keyframeAnimation.fillMode = kCAFillModeForwards; return keyframeAnimation; } - (NSString *)description { return self.initialColor.description; } @end