mirror of
https://github.com/danog/Telegram.git
synced 2024-12-03 09:57:46 +01:00
313 lines
11 KiB
Objective-C
313 lines
11 KiB
Objective-C
/*
|
|
Copyright (c) 2013, Jonathan Willing. All rights reserved.
|
|
Licensed under the MIT license <http://opensource.org/licenses/MIT>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
IN THE SOFTWARE.
|
|
*/
|
|
|
|
#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
|