New: CAAnimationGroup and implement timer-based animations in CALayer

This commit is contained in:
daboe01
2025-11-20 19:58:24 +01:00
parent e02107102c
commit 25d186bf5c
2 changed files with 232 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
/*
* CAAnimationGroup.j
* AppKit
*
* Implements grouping for Core Animation.
*/
@import <Foundation/CPArray.j>
@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
+166
View File
@@ -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
{