// // NSAnimation.m // mySTEP // // Created by Dr. H. Nikolaus Schaller on Sat Mar 06 2006. // Copyright (c) 2006 DSITRI. // // This file is part of the mySTEP Library and is provided // under the terms of the GNU Library General Public License. // #import #import #import #import NSString *NSAnimationProgressMarkNotification=@"NSAnimationProgressMarkNotification"; NSString *NSAnimationProgressMark=@"NSAnimationProgressMark"; NSString *NSViewAnimationTargetKey=@"NSViewAnimationTargetKey"; NSString *NSViewAnimationStartFrameKey=@"NSViewAnimationStartFrameKey"; NSString *NSViewAnimationEndFrameKey=@"NSViewAnimationEndFrameKey"; NSString *NSViewAnimationEffectKey=@"NSViewAnimationEffectKey"; NSString *NSViewAnimationFadeInEffect=@"NSViewAnimationFadeInEffect"; NSString *NSViewAnimationFadeOutEffect=@"NSViewAnimationFadeOutEffect"; @implementation NSAnimation - (void) addProgressMark:(NSAnimationProgress) progress; { if(progress > 1.0) progress=1.0; else if(progress < 0.0) progress=0.0; [_progressMarks addObject:[NSNumber numberWithFloat:progress]]; } - (NSAnimationBlockingMode) animationBlockingMode; { return _animationBlockingMode; } - (NSAnimationCurve) animationCurve; { return _animationCurve; } - (void) clearStartAnimation; { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSAnimationProgressMarkNotification object:_startAnimation]; [_startAnimation removeProgressMark:_startAnimationProgress]; [_startAnimation release]; _startAnimation=nil; } - (void) clearStopAnimation; { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSAnimationProgressMarkNotification object:_stopAnimation]; [_stopAnimation removeProgressMark:_stopAnimationProgress]; [_stopAnimation release]; _stopAnimation=nil; } - (NSAnimationProgress) currentProgress; { return _currentProgress; } - (float) currentValue; { // transform [0..1] -> [0..1] if([_delegate respondsToSelector:@selector(animation:valueForProgress:)]) return [_delegate animation:self valueForProgress:_currentProgress]; #if 0 NSLog(@"currentValue from progress %f with curve %d", _currentProgress, _animationCurve); #endif switch(_animationCurve) { case NSAnimationEaseInOut: if(_currentProgress < 0.5) return 2.0*_currentProgress*_currentProgress; // slowly speed up from start else return 1.0-2.0*((1.0-_currentProgress)*(1.0-_currentProgress)); // slow down at end case NSAnimationEaseIn: return 1.0-((1.0-_currentProgress)*(1.0-_currentProgress)); // slow down at end case NSAnimationEaseOut: return _currentProgress*_currentProgress; // slowly speed up from start case NSAnimationLinear: return _currentProgress; } #if 1 NSLog(@"undefined curve %d", _animationCurve); #endif return 0.0; } - (id) delegate; { return _delegate; } - (NSTimeInterval) duration; { return _duration; } - (float) frameRate; { return _frameRate; } - (id) initWithDuration:(NSTimeInterval) duration animationCurve:(NSAnimationCurve) curve; { if((self=[super init])) { _duration=duration; if(!curve) _animationCurve=NSAnimationEaseInOut; else _animationCurve=curve; _animationBlockingMode=NSAnimationBlocking; // default is blocking } return self; } - (id) copyWithZone:(NSZone *) zone; { return NIMP; } - (void) dealloc; { [_timer release]; [_startDate release]; [_progressMarks release]; [super dealloc]; } - (BOOL) isAnimating; { return _timer != nil; } - (NSArray *) progressMarks; { return _progressMarks; } - (void) removeProgressMark:(NSAnimationProgress) progress; { NSEnumerator *e=[_progressMarks objectEnumerator]; NSNumber *n; while((n=[e nextObject])) if([n floatValue] == progress) [_progressMarks removeObjectIdenticalTo:n]; } - (NSArray *) runLoopModesForAnimating; { return nil; // default means any mode } - (void) setAnimationBlockingMode:(NSAnimationBlockingMode) mode; { _animationBlockingMode=mode; } - (void) setAnimationCurve:(NSAnimationCurve) curve; { _animationCurve=curve; } - (void) setCurrentProgress:(NSAnimationProgress) progress; { _currentProgress=progress; } - (void) setDelegate:(id) delegate; { _delegate=delegate; } - (void) setDuration:(NSTimeInterval) duration; { _duration=duration; } - (void) setFrameRate:(float) fps; { _frameRate=fps; } - (void) setProgressMarks:(NSArray *) progress; { [_progressMarks autorelease]; _progressMarks=[progress mutableCopy]; } - (void) _animate:(NSTimer *) timer; { // called every 1/fps seconds from NSTimer float progress=[[NSDate date] timeIntervalSinceDate:_startDate]/_duration; NSEnumerator *e; NSNumber *mark; #if 0 NSLog(@"_animate progress=%f", progress); #endif e=[_progressMarks objectEnumerator]; if(progress > 1.0) progress=1.0; // limit while((mark=[e nextObject])) { // send any intermediate marks float m=[mark floatValue]; if(m > _currentProgress && m <= progress) { // send NSAnimationProgressMarkNotification where the delegate should be registered [_delegate animation:self didReachProgressMark:progress]; // WARNING: this is not necessarily ascending order! } } [self setCurrentProgress:progress]; if(_currentProgress >= 1.0) // test iVar if setCurrentProgress was redefined in subclass... { // done [_timer invalidate]; // this will auto-remove the timer from runloops [_timer release]; _timer=nil; [_startDate release]; _startDate=nil; [_delegate animationDidEnd:self]; #if 1 NSLog(@"animationDidEnd"); #endif [self release]; } } - (void) startAnimation; { #if 1 NSLog(@"startAnimation"); #endif if(!_timer && (!_delegate || [_delegate animationShouldStart:self])) { NSRunLoop *loop=[NSRunLoop currentRunLoop]; [self retain]; // protect us from being deallocated while the timer is running (or does a notification registration retain its target?) _startDate=[NSDate new]; _timer=[[NSTimer timerWithTimeInterval:_frameRate>0?(1.0/_frameRate):(1/50.0) target:self selector:@selector(_animate:) userInfo:nil repeats:YES] retain]; [self setCurrentProgress:0.0]; // we can't have marks at 0.0 #if 0 NSLog(@"animation started with timer %@", _timer); #endif switch(_animationBlockingMode) { case NSAnimationBlocking: { // run in custom mode blocking user interaction [loop addTimer:_timer forMode:@"NSAnimation"]; while(_timer) // stopAnimation should break this loop [loop runMode:@"NSAnimation" beforeDate:[NSDate distantFuture]]; break; } case NSAnimationNonblockingThreaded: NSLog(@"can't schedule NSAnimationNonblockingThreaded: %@", self); case NSAnimationNonblocking: { // schedule in all specified modes NSArray *modes=[self runLoopModesForAnimating]; // schedule only in specified modes if(modes) { // schedule in all specified modes NSEnumerator *e=[modes objectEnumerator]; NSString *mode; while((mode=[e nextObject])) [loop addTimer:_timer forMode:mode]; } else { // schedule in default modes [loop addTimer:_timer forMode:NSDefaultRunLoopMode]; [loop addTimer:_timer forMode:NSModalPanelRunLoopMode]; [loop addTimer:_timer forMode:NSEventTrackingRunLoopMode]; } break; } } #if 1 NSLog(@"timer scheduled: %@", _timer); #endif } } - (void) _startAnimation:(NSNotification *) n { // conditional start of animation if([n object] == _startAnimation && [[[n userInfo] objectForKey:NSAnimationProgressMark] floatValue] == _startAnimationProgress) [self startAnimation]; // yeah! } - (void) startWhenAnimation:(NSAnimation *) animation reachesProgress:(NSAnimationProgress) start; { // make us observe the other animation [self clearStartAnimation]; [animation addProgressMark:start]; _startAnimation=[animation retain]; // remember _startAnimationProgress=start; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_startAnimation:) name:NSAnimationProgressMarkNotification object:animation]; } - (void) stopAnimation; { #if 1 NSLog(@"stopAnimation"); #endif if(_timer) { // was started [self autorelease]; [_timer invalidate]; [_timer release]; _timer=nil; // if blocking, this also breaks runloop [_startDate release]; _startDate=nil; } [_delegate animationDidStop:self]; } - (void) _stopAnimation:(NSNotification *) n { // conditional start of animation if([n object] == _stopAnimation && [[[n userInfo] objectForKey:NSAnimationProgressMark] floatValue] == _stopAnimationProgress) [self startAnimation]; // yeah! } - (void) stopWhenAnimation:(NSAnimation *) animation reachesProgress:(NSAnimationProgress) stop; { // make us observe the other animation [self clearStartAnimation]; _stopAnimation=[animation retain]; // remember _stopAnimationProgress=stop; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_stopAnimation:) name:NSAnimationProgressMarkNotification object:animation]; } - (void) encodeWithCoder:(NSCoder *) coder; { NIMP; } - (id) initWithCoder:(NSCoder *) coder; { return NIMP; } @end @implementation NSViewAnimation struct _NSViewAnimation { // private structure NSRect start; NSRect delta; id target; int effect; BOOL windowTarget; }; - (void) setCurrentProgress:(NSAnimationProgress) progress; { // overwrite this method so that we don't modify the progress marks and delegate mechanisms int i; float percentage; struct _NSViewAnimation *record=_private; #if 0 NSLog(@"NSViewAnimation setCurrentProgress %f", progress); #endif [super setCurrentProgress:progress]; percentage=[self currentValue]; // get percentage (using curve) #if 0 NSLog(@" progress=%f percentage=%f for %d targets", _currentProgress, percentage, _count); #endif for(i=0; i<_count; i++) { // process all animations NSRect pos; #if 0 NSLog(@"NSViewAnimation setCurrentProgress:%f with value: %f", progress, percentage); #endif #if 1 NSLog(@"effect=%d", record->effect); #endif if(!record->target) continue; // ignore nil target pos.origin.x = record->start.origin.x + percentage*record->delta.origin.x; pos.origin.y = record->start.origin.y + percentage*record->delta.origin.y; pos.size.width = record->start.size.width + percentage*record->delta.size.width; pos.size.height = record->start.size.height + percentage*record->delta.size.height; #if 1 NSLog(@"new frame %@", NSStringFromRect(pos)); #endif if(record->windowTarget) [record->target setFrame:pos display:YES]; // NSWindow else { // NSView [record->target setFrame:pos]; [record->target setNeedsDisplay:YES]; } if(record->effect) { if(progress == 0.0 && record->effect > 0) { // start of fade in #if 1 NSLog(@"start of fade in"); #endif if(record->windowTarget) [record->target orderFront:self]; else [record->target setHidden:NO]; // unhide } else if(progress == 1.0 && record->effect < 0) { // end of fade out #if 1 NSLog(@"end of fade out"); #endif if(record->windowTarget) [record->target orderOut:self]; else [record->target setHidden:YES]; // hide view } // set alpha of window or view based on percentage (fade in) or 1-percentage (fade out) } record++; } #if 1 NSLog(@"NSViewAnimation setCurrentProgress done"); #endif } - (void) startAnimation { // create internal tables NSEnumerator *e=[_viewAnimations objectEnumerator]; NSDictionary *dict; struct _NSViewAnimation *record=_private=objc_realloc(_private, (_count=[_viewAnimations count])*sizeof(struct _NSViewAnimation)); // adjust size if necessary/allocate while((dict=[e nextObject])) { // translate into internal data id val; record->target=[dict objectForKey:NSViewAnimationTargetKey]; if(record->target) { // // kill from any other animation active for this target since they may interfere!!! // record->windowTarget=[record->target isKindOfClass:[NSWindow class]]; val=[dict objectForKey:NSViewAnimationStartFrameKey]; if(val) record->start=[val rectValue]; else record->start=[record->target frame]; val=[dict objectForKey:NSViewAnimationEndFrameKey]; if(val) record->delta=[val rectValue]; else record->delta=[record->target frame]; record->delta.origin.x-=record->start.origin.x; record->delta.origin.y-=record->start.origin.y; record->delta.size.width-=record->start.size.width; record->delta.size.height-=record->start.size.height; val=[dict objectForKey:NSViewAnimationEffectKey]; if([val isEqualToString:NSViewAnimationFadeInEffect]) record->effect=1; else if([val isEqualToString:NSViewAnimationFadeOutEffect]) record->effect=-1; else record->effect=0; } record++; } #if 0 // This is from NSWindow // should be used to calculate the optimal duration - (NSTimeInterval) animationResizeTime:(NSRect) rect { static float t=0.0; float chg; // pixels of change if(t == 0.0) { // replace by NSWindowResizeTime from NSUserDefaults if defined t=0.2; // default t/=150.0; // time per 150 pixels movement } chg = fabs(rect.origin.x-frame.origin.x); chg += fabs(rect.origin.y-frame.origin.y); chg += fabs(rect.size.width-frame.size.width); chg += fabs(rect.size.height-frame.size.height); return chg*t; } #endif [super startAnimation]; } - (id) initWithViewAnimations:(NSArray *) animations; { if((self=[super initWithDuration:0.5 animationCurve:NSAnimationEaseInOut])) { _viewAnimations=[animations retain]; [self setAnimationBlockingMode:NSAnimationNonblocking]; } return self; } - (void) dealloc; { if(_private) objc_free(_private); [_viewAnimations release]; [super dealloc]; } - (void) setWithViewAnimations:(NSArray *) animations; { ASSIGN(_viewAnimations, animations); } - (NSArray *) viewAnimations; { return _viewAnimations; } @end @implementation NSObject (NSAnimation) - (void) animation:(NSAnimation *) ani didReachProgressMark:(NSAnimationProgress) progressMark; { return; } - (void) animationDidEnd:(NSAnimation *) ani; { return; } - (void) animationDidStop:(NSAnimation *) ani; { return; } - (BOOL) animationShouldStart:(NSAnimation *) ani; { return NO; } // delegate must override @end