unit uanimationbasic; (* Comba - Animation main module ----------------------------- @Licence: (c) 2017 José Mejuto // joshyfun at gmail.com @Licence: LGPL when compiled with FPC (Free Pascal), GNU GPL V3 in other cases. @Links: GPL: https://www.gnu.org/licenses/gpl-3.0.en.html LGPL: https://www.gnu.org/licenses/lgpl-3.0.en.html @Description: This file implements an animation queue and animation item base class. *) {$mode objfpc}{$H+} interface uses Classes, SysUtils, Graphics, math; type TAnimationTransitionMode=(eAnimationTransitionLinear,eAnimationTransitionBallisticIn,eAnimationTransitionBallisticOut,eAnimationTransitionBallisticBoth,eAnimationTransitionBallisticEdge); TAnimationFinalizeBehavior=(eAnimationFinalizeBehaviorCurrent,eAnimationFinalizeBehaviorInitial,eAnimationFinalizeBehaviorFinal); TAnimationOnEndAction=(eAnimationOnEndStop,eAnimationOnEndFree); TAnimationState=(eAnimationStateStopped,eAnimationStateStarted,eAnimationStatePaused,eAnimationStateToBeFreed); TAnimationItem=class; TAnimationOnPerform=procedure (const aElapedMilliseconds: int64) of object; TAnimationOnAnimationEnd=procedure (Sender: TAnimationItem) of object; TAnimationQueue=class; TAnimationException=class(Exception); { TAnimationItem } TAnimationItem=class(TObject) private FFinalizeASAPMode: integer; FUserData: Pointer; FOnPerform: TAnimationOnPerform; FOnAnimationEnd: TAnimationOnAnimationEnd; FFreeWithQueue: Boolean; FDuration: int64; FGroupID: pointer; FAnimationOnEndAction: TAnimationOnEndAction; FTransitionMode: TAnimationTransitionMode; FReverse: Boolean; FRepeats: integer; FAutoReverse: Boolean; FFinalizeBehavior: TAnimationFinalizeBehavior; procedure SetReverse(aValue: Boolean); protected FState: TAnimationState; FStartTick: int64; FPauseTick: int64; FQueue: TAnimationQueue; FStartStopSynchAnimationList: TFPList; FRepeated: integer; FFinalizeAnimation: Boolean; (* TransitionMode: (TAnimationTransitionMode) Linear or "curved" transition between animation values eAnimationTransitionLinear: Transition is linear in time. eAnimationTransitionBallisticIn: Fast -> Slow eAnimationTransitionBallisticOut: Slow -> Fast eAnimationTransitionBallisticBoth: Slow -> Fast -> Slow eAnimationTransitionBallisticEdge: Fast -> Slow -> Fast Calculations are done in CalculateLinearPosition. @Note: This transitions are not fine right now as some transitios are quite difficult to distinguish from others. It will be fixed in the future. *) property TransitionMode: TAnimationTransitionMode read FTransitionMode write FTransitionMode; (* Calculate linear position is the default functions to calculate current animation value based in a source and a target value using the elapsed time. Derived classes can use it or define a new ones with different signatures and names *) function CalculateLinearPosition(const aSource,aTarget: integer; const aElapsedMilliseconds,aDuration: int64): integer; overload; function CalculateLinearPosition(const aSource,aTarget: Single; const aElapsedMilliseconds,aDuration: int64): Single; overload; (* Overridable DoInitialize: This method is called when animation starts or restarts. *) procedure DoInitialize; virtual; (* Overridable DoPerform: In this method derived classes must perform the calculations needed for the animation *) procedure DoPerform; virtual; (* Overridable DoFinalizeAnimation: Method called when animation is about to be finished. Useful to set EXACT values for final animation based on TAnimationFinalizeBehavior *) procedure DoFinalizeAnimation; virtual; (* Animate: Do some calcs and call DoPerform *) procedure Animate; (* ProcessAnimationEnd: Adjust some values to end an animation *) procedure ProcessAnimationEnd; (* FinalizeBehavior: (TAnimationFinalizeBehavior) Which value is expected when animation ends ? eAnimationFinalizeBehaviorCurrent: Respects actual values, whichever it be eAnimationFinalizeBehaviorFinal: (default) At end it is the final value eAnimationFinalizeBehaviorInitial: At end the values is initial value. This set is quite important when playing animations in with AutoReverse and when reversing animations using Reverse() *) property FinalizeBehavior: TAnimationFinalizeBehavior read FFinalizeBehavior write FFinalizeBehavior; public constructor Create; destructor Destroy; override; (* UserData: carries a pointer to user data *) property UserData: Pointer read FUserData write FUserData; (* OnPerform: This event will be called instead class perform if assigned *) property OnPerform: TAnimationOnPerform read FOnPerform write FOnPerform; (* OnAnimationEnd: This event will be called when animation ends *) property OnAnimationEnd: TAnimationOnAnimationEnd read FOnAnimationEnd write FOnAnimationEnd; (* FreeWithQueue: The TAnimationItem will be freed when the Queue is destroyed *) property FreeWithQueue: Boolean read FFreeWithQueue write FFreeWithQueue; (* Duration: How many milliseconds will the animation take *) property Duration: int64 read FDuration write FDuration; (* Reversed: Is animation being played in reverse time ? *) property Reversed: Boolean read FReverse write SetReverse; (* AnimationOnEndAction: What happends when animation is finished: eAnimationOnEndStop: (Default) The animation just finish eAnimationOnEndFree: The animation finished and then the object is freed *) property AnimationOnEndAction: TAnimationOnEndAction read FAnimationOnEndAction write FAnimationOnEndAction; (* AutoReverse: When animation reaches the end it will be played in reverse order if there are enought Repeats to be played *) property AutoReverse: Boolean read FAutoReverse write FAutoReverse; (* Repeats: How many times the animation should be repeated. 0 = means infinite *) property Repeats: integer read FRepeats write FRepeats; (* GroupID: Pointer with an identifier to select a group of animations from the queue. This is useful in example when animating controls as all animations related to a control will share the same GroupID which could be the memory address of the control *) property GroupID: pointer read FGroupID write FGroupID; (* State: The animation play state (TAnimationState) eAnimationStateStopped: Animation in stopped state eanimationStateStarted: Animation is running eAnimationStatePaused: Animation is paused eAnimationStateToBeFreed: Animation is stopped and waiting to be freed *) property State: TAnimationState read FState; (* GetElapsedMilliseconds: Returns the animation elapsed milliseconds *) function GetElapsedMilliseconds: int64; (* FinalizeASAP: Finalizes an animation as soon as possible but waiting the needed time to complete the programmed animation time. @Parameters: aReverseIfNeeded: If True and animation is running in forward mode, it will wait to be run in reverse mode to stop it. *) procedure FinalizeASAP(const aReverseIfNeeded: Boolean=true); (* Reverse: Reverses current animation direction. @Parameters: aReverseFinalBehavior: If True Initial and Final behavior will be interchanged. *) function Reverse(const aReverseFinalBehavior: Boolean=true): Boolean; (* Perform: Just perform the animation *) procedure Perform; (* Start: Starts animation *) procedure Start; (* Pause: Pauses animation *) procedure Pause; (* Stop: Stops animation *) procedure Stop; (* AddSyncStartStopAnimation: When this objects reaches the animation end, in forward or reverse mode, animations added with this function will be reversed or restarted at the same time. This is useful to synchronize some animations for large repeats *) procedure AddSyncStartStopAnimation(const aAnimationItem: TAnimationItem); end; { TAnimationQueue } TAnimationQueue=class(TObject) private FTick: int64; function GetAverageFPS: single; function GetPaused: Boolean; procedure SetPaused(AValue: Boolean); procedure SetReverse(aValue: Boolean); protected type TAnimationQueueState=(eAnimationQueueStopped,eAnimationQueueStarted,eAnimationQueuePaused); protected FState: TAnimationQueueState; FFramesCounter: integer; FStartTick: int64; FReverse: Boolean; FPauseElapsed: int64; FAnimationItems: TFPList; function GetAnimationTick: int64; function intfGetTickCount64: int64; inline; public constructor Create; destructor Destroy; override; (* Start: Starts the animation @Parameters: aStartQueuedAnimations: If True it starts all animations already in the queue @Note: There is no Stop procedure because not calling "Animate" is just a stop. *) procedure Start(const aStartQueuedAnimations: Boolean=false); (* Pause: Pauses animation. It can be unpaused calling it again *) procedure Pause; (* Reverse: Reverses all animations in queue *) procedure Reverse; (* Animate: Animate all objects in the queue. It is the main heartbeat *) procedure Animate; virtual; (* Paint: Animate just only the supplied animation @Parameters: aPerformItem: The animation to be animated :-) *) procedure Paint(const aPerformItem: TAnimationItem); (* Add: Adds one animation to the queue @Parameters: aAnimationItem: The animation to be added aRemoveSameID: Before adding it removes all animations in the queue with the same GroupID, if GroupID is different than nil *) procedure Add(const aAnimationItem: TAnimationItem; const aRemoveSameGroupID: Boolean=false); virtual; (* Remove: Removes one animation from the queue @Parameters: aAnimationItem: The animation to be removed. If animation to be removed has been selected to be freed on stop it will be automatically freed. *) procedure Remove(const aAnimationItem: TAnimationItem); virtual; (* GetAbsoluteAnimationClock: Milliseconds since animation start *) function GetAbsoluteAnimationClock: int64; (* RemoveGroupID: Removes all animations with a given GroupID @Parameters: aClass: It can filter and remove only GroupID animations of the given class *) function RemoveGroupID(const aID: pointer; const aClass: TClass=nil): integer; (* FindGroupID: Returns a TFPList containing all animations of a given GroupID @Parameters: aGroupID: The ID of the group to be returned. @Note: Programmer must free the TFPList returned *) function FindGroupID(const aGroupID: pointer): TFPList; (* AverageFPS: Returns average animations per second since animation start *) property AverageFPS: single read GetAverageFPS; (* Reversed: Is the animation being played in reverse time ? *) property Reversed: Boolean read FReverse write SetReverse; (* Paused: Is animation in pause state ? *) property Paused: Boolean read GetPaused write SetPaused; end; implementation {$IFDEF WINDOWS} Uses windows; //For QueryPerformanceCounter only and it is not a must. {$ENDIF} resourcestring rs_AnimationInOtherQueue='The animation is already in other queue.'; const TFINALIZEASAPMODE_NONE=0; TFINALIZEASAPMODE_NOW=1; TFINALIZEASAPMODE_NEXT=2; TFINALIZEASAPMODE_REVERSED=3; type TSplinePoint=record x,y: single; end; function SplinePoint(x,y: single): TSplinePoint; begin Result.x:=x; Result.y:=y; end; function CalculateSpline(const aPoint0, aPoint3, aPoint1, aPoint2: TSplinePoint; t: single ): TSplinePoint; begin Result.x := power(t,3)*(aPoint3.x+3*(aPoint1.x-aPoint2.x)-aPoint0.x)+3*power(t,2)*(aPoint0.x-2*aPoint1.x+aPoint2.x)+3*t*(aPoint1.x-aPoint0.x)+aPoint0.x; Result.y := power(t,3)*(aPoint3.y+3*(aPoint1.y-aPoint2.y)-aPoint0.y)+3*power(t,2)*(aPoint0.y-2*aPoint1.y+aPoint2.y)+3*t*(aPoint1.y-aPoint0.y)+aPoint0.y; end; function CalculateElastic(const aFactor: single; const aDistance: integer; const aIntensity: single=1.5): integer; begin Result:=trunc(Power(2,10*(aFactor-1))*cos(20*PI*aIntensity/3*aFactor)*aDistance) end; { TAnimationItem } procedure TAnimationItem.SetReverse(aValue: Boolean); begin if FReverse=aValue then Exit; if FDuration=0 then begin FReverse:=aValue; exit; end; if Assigned(FQueue) then begin if aValue then begin FStartTick:=FQueue.GetAbsoluteAnimationClock-(FDuration-GetElapsedMilliseconds); end else begin FStartTick:=FQueue.GetAbsoluteAnimationClock-GetElapsedMilliseconds; end; end; FReverse:=aValue; end; function TAnimationItem.CalculateLinearPosition(const aSource, aTarget: integer; const aElapsedMilliseconds, aDuration: int64): integer; var Factor: Single; Distance: integer; begin if aDuration=0 then begin Result:=aSource; exit; end; Factor:=aElapsedMilliseconds / aDuration; if Factor>1.0 then Factor:=1.0; if Factor<0.0 then Factor:=0.0; case FTransitionMode of eAnimationTransitionLinear: begin end; eAnimationTransitionBallisticIn: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(0.9,1),SplinePoint(0.999,1),Factor).x; end; eAnimationTransitionBallisticOut: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(0.001,1),SplinePoint(0.1,1),Factor).x; end; eAnimationTransitionBallisticBoth: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(0,1),SplinePoint(1,1),Factor).x; end; eAnimationTransitionBallisticEdge: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(1,0),SplinePoint(0,1),Factor).x; end; end; Distance:=aTarget-aSource; Distance:=Round(Distance*Factor); Result:=aSource+Distance; end; function TAnimationItem.CalculateLinearPosition(const aSource, aTarget: Single; const aElapsedMilliseconds, aDuration: int64): Single; var Factor: Single; Distance: Single; begin if aDuration=0 then begin Result:=aSource; exit; end; Factor:=aElapsedMilliseconds / aDuration; if Factor>1.0 then Factor:=1.0; if Factor<0.0 then Factor:=0.0; case FTransitionMode of eAnimationTransitionLinear: begin end; eAnimationTransitionBallisticIn: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(0.9,1),SplinePoint(0.9,1),Factor).x; end; eAnimationTransitionBallisticOut: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(0.1,1),SplinePoint(0.1,1),Factor).x; end; eAnimationTransitionBallisticBoth: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(0,1),SplinePoint(1,1),Factor).x; end; eAnimationTransitionBallisticEdge: begin Factor:=CalculateSpline(SplinePoint(0,0),SplinePoint(1,0),SplinePoint(1,0),SplinePoint(0,1),Factor).x; end; end; Distance:=aTarget-aSource; Distance:=Distance*Factor; Result:=aSource+Distance; end; function TAnimationItem.GetElapsedMilliseconds: int64; var LocalReversed: Boolean; begin if FFinalizeAnimation and ((FRepeats-FRepeated)>=0) then begin LocalReversed:=false; if FAutoReverse then begin if (FRepeats-FRepeated) mod 2 = 1 then begin LocalReversed:=false; end else begin LocalReversed:=true; end; end; FRepeated:=FRepeats; if LocalReversed then begin Result:=0; end else begin Result:=Duration; end; end else begin if FReverse then begin Result:=Duration - (FQueue.GetAbsoluteAnimationClock - FStartTick); end else begin Result:=FQueue.GetAbsoluteAnimationClock - FStartTick; end; end; end; procedure TAnimationItem.FinalizeASAP(const aReverseIfNeeded: Boolean); begin if aReverseIfNeeded then begin FFinalizeASAPMode:=TFINALIZEASAPMODE_REVERSED; end else begin FFinalizeASAPMode:=TFINALIZEASAPMODE_NEXT; end; end; procedure TAnimationItem.ProcessAnimationEnd; var lShouldRepeat: Boolean=false; procedure Restart; var j: integer; begin Start; if Assigned(FStartStopSynchAnimationList) then begin for j := 0 to FStartStopSynchAnimationList.Count-1 do begin TAnimationItem(FStartStopSynchAnimationList[j]).Start; end; end; end; procedure ReverseIt; var j: integer; begin Reversed:=not Reversed; if Assigned(FStartStopSynchAnimationList) then begin for j := 0 to FStartStopSynchAnimationList.Count-1 do begin TAnimationItem(FStartStopSynchAnimationList[j]).ProcessAnimationEnd; end; end; end; procedure DoFinalize; begin case FAnimationOnEndAction of eAnimationOnEndStop: FState:=eAnimationStateStopped; eAnimationOnEndFree: FState:=eAnimationStateToBeFreed; end; if Assigned(FOnAnimationEnd) then begin FOnAnimationEnd(Self); end; end; begin inc(FRepeated); case FFinalizeASAPMode of TFINALIZEASAPMODE_NOW, TFINALIZEASAPMODE_NEXT: begin DoFinalize; end; TFINALIZEASAPMODE_REVERSED: begin if not Reversed then begin ReverseIt; end else begin DoFinalize; end; end; TFINALIZEASAPMODE_NONE: begin if FRepeats=0 then begin lShouldRepeat:=true; end else begin if FRepeated=FDuration then begin ProcessAnimationEnd; end; end; end; end; end; procedure TAnimationItem.DoFinalizeAnimation; begin FFinalizeAnimation:=true; end; procedure TAnimationItem.DoInitialize; begin //Do nothing end; procedure TAnimationItem.Start; begin FStartTick:=FQueue.GetAbsoluteAnimationClock; FState:=eAnimationStateStarted; FRepeated:=0; FReverse:=false; FFinalizeAnimation:=false; end; procedure TAnimationItem.Pause; begin if FState=eAnimationStateStarted then begin FState:=eAnimationStatePaused; if Assigned(FQueue) then begin FPauseTick:=GetElapsedMilliseconds; end; end else if FState=eAnimationStatePaused then begin if Assigned(FQueue) then begin FStartTick:=FQueue.GetAbsoluteAnimationClock-FPauseTick; end; FState:=eAnimationStateStarted; end; end; procedure TAnimationItem.Stop; begin if (FState=eAnimationStateStarted) or (FState=eAnimationStatePaused) or (FState=eAnimationStateStopped) then begin FState:=eAnimationStateStopped; DoFinalizeAnimation; end; end; procedure TAnimationItem.AddSyncStartStopAnimation( const aAnimationItem: TAnimationItem); begin if not Assigned(FStartStopSynchAnimationList) then begin FStartStopSynchAnimationList:=TFPList.Create; end; FStartStopSynchAnimationList.Add(aAnimationItem); end; { TAnimationQueue } function TAnimationQueue.GetAbsoluteAnimationClock: int64; begin Result:=GetAnimationTick - FStartTick; end; function TAnimationQueue.RemoveGroupID(const aID: pointer; const aClass: TClass ): integer; var j: Integer; A: TAnimationItem; begin Result:=0; for j := Pred(FAnimationItems.Count) downto 0 do begin A:=TAnimationItem(FAnimationItems[j]); if A.GroupID=aID then begin if (Assigned(aClass) and (A is aClass)) or not Assigned(aClass) then begin Remove(A); inc(Result); end; end; end; end; function TAnimationQueue.FindGroupID(const aGroupID: pointer): TFPList; var j: Integer; A: TAnimationItem; begin Result:=TFPList.Create; for j := Pred(FAnimationItems.Count) downto 0 do begin A:=TAnimationItem(FAnimationItems[j]); if A.GroupID=aGroupID then begin Result.Add(A); end; end; end; procedure TAnimationQueue.SetReverse(aValue: Boolean); var j: integer; begin if FReverse=aValue then Exit; for j := 0 to FAnimationItems.Count-1 do begin TAnimationItem(FAnimationItems[j]).Reversed:=not TAnimationItem(FAnimationItems[j]).Reversed; end; FReverse:=aValue; end; function TAnimationQueue.GetAverageFPS: single; begin if FState=eAnimationQueueStarted then begin if GetAbsoluteAnimationClock>0 then begin Result:=(FFramesCounter * 1000) / GetAbsoluteAnimationClock; end else begin Result:=0; end; end else begin Result:=0; end; end; function TAnimationQueue.GetPaused: Boolean; begin if FState=eAnimationQueuePaused then begin Result:=true; end else begin Result:=false; end; end; constructor TAnimationQueue.Create; begin FAnimationItems:=TFPList.Create; FState:=eAnimationQueueStopped; end; destructor TAnimationQueue.Destroy; var j: integer; begin for j := Pred(FAnimationItems.Count) downto 0 do begin if TAnimationItem(FAnimationItems[j]).FFreeWithQueue then begin TAnimationItem(FAnimationItems[j]).Free; FAnimationItems.Delete(j); end; end; FreeAndNil(FAnimationItems); inherited Destroy; end; procedure TAnimationQueue.SetPaused(AValue: Boolean); begin If AValue then begin if (FState=eAnimationQueueStarted) then begin FPauseElapsed:=GetAbsoluteAnimationClock; FState:=eAnimationQueuePaused; end; end else begin if FState=eAnimationQueuePaused then begin FStartTick:=GetTickCount64-FPauseElapsed; FState:=eAnimationQueueStarted; end; end; end; procedure TAnimationQueue.Start(const aStartQueuedAnimations: Boolean); var j: Integer; begin FTick:=GetTickCount64; if FState=eAnimationQueuePaused then begin SetPaused(false); end else begin FStartTick:=FTick; if aStartQueuedAnimations then begin for j := 0 to FAnimationItems.Count-1 do begin TAnimationItem(FAnimationItems[j]).Start; end; end; FState:=eAnimationQueueStarted; FFramesCounter:=0; end; end; procedure TAnimationQueue.Pause; begin FPauseElapsed:=GetAbsoluteAnimationClock; FState:=eAnimationQueuePaused; end; procedure TAnimationQueue.Reverse; begin Reversed:=not Reversed; end; procedure TAnimationQueue.Animate; var j: integer; A: TAnimationItem; begin if FState<>eAnimationQueueStarted then exit; FTick:=GetTickCount64; for j := FAnimationItems.Count-1 downto 0 do begin A:=TAnimationItem(FAnimationItems[j]); if A.FState=eAnimationStateToBeFreed then begin Remove(A); end; end; for j := 0 to FAnimationItems.Count-1 do begin A:=TAnimationItem(FAnimationItems[j]); if A.FState=eAnimationStateStarted then begin A.Animate; end; end; inc(FFramesCounter); end; procedure TAnimationQueue.Paint(const aPerformItem: TAnimationItem); begin aPerformItem.DoPerform; end; procedure TAnimationQueue.Add(const aAnimationItem: TAnimationItem; const aRemoveSameGroupID: Boolean); begin if Assigned(aAnimationItem.FQueue) then begin Raise TAnimationException.Create(rs_AnimationInOtherQueue); end; if aRemoveSameGroupID and (aAnimationItem.GroupID<>nil) then begin RemoveGroupID(aAnimationItem.GroupID); end; FAnimationItems.Add(aAnimationItem); aAnimationItem.FQueue:=Self; aAnimationItem.DoInitialize; end; procedure TAnimationQueue.Remove(const aAnimationItem: TAnimationItem); var Index: integer; A: TAnimationItem; j: integer; begin Index:=FAnimationItems.IndexOf(aAnimationItem); if Index<0 then begin Raise TAnimationException.Create('Trying to remove non exist animation'); end; FAnimationItems.Delete(Index); aAnimationItem.DoFinalizeAnimation; //Check if object is in the synchronization list of other object for j := 0 to Pred(FAnimationItems.Count) do begin A:=TAnimationItem(FAnimationItems[j]); if Assigned(A.FStartStopSynchAnimationList) then begin Index:=A.FStartStopSynchAnimationList.IndexOf(aAnimationItem); if Index>-1 then begin //Delete it to avoid crash when synch takes place A.FStartStopSynchAnimationList.Delete(Index); end; end; end; aAnimationItem.Free; end; function TAnimationQueue.GetAnimationTick: int64; begin Result:=FTick; end; function TAnimationQueue.intfGetTickCount64: int64; {$IFDEF WINDOWS} const lUseHighPrecision: Boolean=true; {$ENDIF} begin {$IFDEF WINDOWS} if lUseHighPrecision then begin Result:=0; if not QueryPerformanceCounter(Result) then begin Result:=GetTickCount64; lUseHighPrecision:=false; end; end else begin Result:=GetTickCount64; end; {$ELSE} Result:=GetTickCount64; {$ENDIF} end; end.