diff --git a/AppKit/CoreAnimation/CAAnimationGroup.j b/AppKit/CoreAnimation/CAAnimationGroup.j new file mode 100644 index 000000000..e84c5f57d --- /dev/null +++ b/AppKit/CoreAnimation/CAAnimationGroup.j @@ -0,0 +1,66 @@ +/* + * CAAnimationGroup.j + * AppKit + * + * Implements grouping for Core Animation. + */ + +@import +@import "CAAnimation.j" + +@implementation CAAnimationGroup : CAAnimation +{ + CPArray _animations; +} + ++ (id)group +{ + return [[self alloc] init]; +} + +- (id)init +{ + if (self = [super init]) + { + _animations = []; + } + return self; +} + +- (void)setAnimations:(CPArray)anArray +{ + if (_animations === anArray) + return; + + _animations = anArray; +} + +- (CPArray)animations +{ + return _animations; +} + +/* + Iterates through children and executes them recursively. + This effectively runs all grouped animations concurrently. +*/ +- (void)runActionForKey:(CPString)aKey object:(id)anObject arguments:(CPDictionary)arguments +{ + var count = [_animations count], + i = 0; + + for (; i < count; i++) + { + var animation = [_animations objectAtIndex:i]; + + // Recursively call runActionForKey on the child. + // If the child is a CABasicAnimation, it will call [anObject addAnimation:...] + // If the child is another Group, it will recurse here. + if ([animation respondsToSelector:@selector(runActionForKey:object:arguments:)]) + { + [animation runActionForKey:aKey object:anObject arguments:arguments]; + } + } +} + +@end diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j index 20d2dae66..14bd97ca8 100644 --- a/AppKit/CoreAnimation/CALayer.j +++ b/AppKit/CoreAnimation/CALayer.j @@ -118,6 +118,8 @@ var CALayerRegisteredRunLoopUpdates = nil; CGAffineTransform _transformToLayer; CGAffineTransform _transformFromLayer; + + CPMutableDictionary _activeAnimations; } @global document @@ -160,6 +162,8 @@ var CALayerRegisteredRunLoopUpdates = nil; _sublayers = []; + _activeAnimations = [CPMutableDictionary dictionary]; + #if PLATFORM(DOM) _DOMElement = document.createElement("div"); @@ -977,6 +981,168 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) return _delegate; } +/* + Adds an animation to the layer. + Supports CABasicAnimation for Numbers (opacity) and Points (position/anchorPoint). +*/ +- (void)addAnimation:(CAAnimation)anim forKey:(CPString)key +{ + if (!anim) return; + + // Remove existing animation for this key + [self removeAnimationForKey:key]; + + // Determine KeyPath + var keyPath = key; + if ([anim respondsToSelector:@selector(keyPath)] && [anim keyPath]) + keyPath = [anim keyPath]; + + // Determine Start Value + var startValue = nil; + if ([anim respondsToSelector:@selector(fromValue)]) + startValue = [anim fromValue]; + + // Fallback to current layer value + if (startValue == nil) + startValue = [self valueForKey:keyPath]; + + // Determine End Value + var endValue = nil; + if ([anim respondsToSelector:@selector(toValue)]) + endValue = [anim toValue]; + + if (endValue == nil) + return; + + // Determine Duration + var duration = 0.25; + if ([anim respondsToSelector:@selector(duration)]) + duration = [anim duration]; + + // Create a JS context object to hold state (lightweight) + var context = { + "animation": anim, + "keyPath": keyPath, + "startValue": startValue, + "endValue": endValue, + "startTime": [CPDate date], + "duration": duration, + "timer": nil + }; + + // Schedule Timer (approx 60fps) + var timer = [CPTimer scheduledTimerWithTimeInterval:1.0/60.0 + target:self + selector:@selector(_animationTick:) + userInfo:context + repeats:YES]; + + context.timer = timer; + + [_activeAnimations setObject:context forKey:key]; +} + +- (void)removeAnimationForKey:(CPString)key +{ + var context = [_activeAnimations objectForKey:key]; + if (context) + { + var timer = context.timer; + if (timer) + [timer invalidate]; + + [_activeAnimations removeObjectForKey:key]; + } +} + +- (void)removeAllAnimations +{ + var keys = [_activeAnimations allKeys], + count = [keys count]; + + while (count--) + { + [self removeAnimationForKey:[keys objectAtIndex:count]]; + } +} + +- (void)_animationTick:(CPTimer)timer +{ + var context = [timer userInfo], + anim = context.animation, + startTime = context.startTime, + duration = context.duration, + now = [CPDate date]; + + // Calculate Progress + var elapsed = [now timeIntervalSinceDate:startTime], + progress = elapsed / duration; + + if (progress > 1.0) progress = 1.0; + + // Interpolate + var start = context.startValue, + end = context.endValue, + current = nil; + + // Interpolation Logic + if (typeof start === "number") + { + current = start + (end - start) * progress; + } + // Check for CGPoint (Simple JS Objects in Cappuccino) + else if (start && start.x !== undefined && start.y !== undefined) + { + var x = start.x + (end.x - start.x) * progress, + y = start.y + (end.y - start.y) * progress; + current = CGPointMake(x, y); + } + // Check for CGSize + else if (start && start.width !== undefined && start.height !== undefined) + { + var w = start.width + (end.width - start.width) * progress, + h = start.height + (end.height - start.height) * progress; + current = CGSizeMake(w, h); + } + // Check for CGRect + else if (start && start.origin !== undefined && start.size !== undefined) + { + var x = start.origin.x + (end.origin.x - start.origin.x) * progress, + y = start.origin.y + (end.origin.y - start.origin.y) * progress, + w = start.size.width + (end.size.width - start.size.width) * progress, + h = start.size.height + (end.size.height - start.size.height) * progress; + current = CGRectMake(x, y, w, h); + } + + // Apply Value + if (current !== nil) + [self setValue:current forKey:context.keyPath]; + + // Completion + if (progress >= 1.0) + { + [timer invalidate]; + + // Check removedOnCompletion + var shouldRemove = YES; + if ([anim respondsToSelector:@selector(isRemovedOnCompletion)]) + shouldRemove = [anim isRemovedOnCompletion]; + + if (shouldRemove) + { + // Remove from dictionary by finding the key for this context + var allKeys = [_activeAnimations allKeysForObject:context]; + if ([allKeys count] > 0) + [_activeAnimations removeObjectForKey:[allKeys objectAtIndex:0]]; + } + + // Notify Delegate + var delegate = [anim delegate]; + if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)]) + [delegate animationDidStop:anim finished:YES]; + } +} + /* @ignore */ - (void)_setOwningView:(CPView)anOwningView {