Archive
Tweening with Smart Mobile Studio, Part 3
This is the third installment on my tween implementation for Smart Mobile Studio. Check out the first article here, and the second article here.
If you have followed this blog and read the two previous installments, you should have a good idea what tweening is, why it’s awesome and “must have” for any serious HTML5 developer. But let’s do a quick re-cap of where we are:
In the first article i did a verbatim conversion of SimpleTween.js, which is a popular and well written javascript library. It’s small, fast, compact and lacks the bling more advanced libraries bring. This gave us the edge since it’s easier to build on a small and clean core.
Once started, CSS animations cannot be stopped or aborted.
They have to run their course
In the second article I setup a GitHub account for Smart Mobile Studio code. I also re-wrote the entire tween system. Instead of having one class representing a single tween (which is overkill considering the data), I established a multi-tween system. A system where you can define as many tween elements as you like, each with different properties and animationtypes, and update them all within a single class.
I also introduced automatic updates, so you dont manually have to call getValue() to update and synchronize the tween to it’s timeline. In fact i have added two methods. First there is a normal TTimer update that executes on interval. Secondly there is a requestAnimationFrame() callback loop, which synchronizes the update with how the browser redraws the display. On a decent PC running Chrome or Safari (both WebKit engines) you can get 120 Fps. It all depends on what the tween elements are supposed to update and what you draw.
Right, so let’s move on with this article.
Software Tweening vs. Hardware Tweening
What is a tween? Well, a tween is just a matter of transforming a value into another value. Forcing the transformation to occur within a given timeframe, applying a mathematical formula to make the transformation smooth and non static.
If you are wondering why we need tweens, think about it this way: Let’s say you want to move a picture of a spaceship across the screen. Or perhaps you want a side menu to slide into view in a serious bussiness application. How will you achieve this?
WC3 RFC documentation gives little clue, but I suspect the
animations are translated into CUDA bytecodes and fed to the GPU
You are probably thinking: oh i can just use a timer, then increment the left position of the spaceship or menu, then keep on going until it reaches the destination co-ordinates.
You are right, you could do it that way. But does it look good? What about the time it takes for your fancy sidemenu or spaceship to scroll into view like that? Will it look good on an older PC? Can you trust it to complete in 1 second or 500 milliseconds? And wont it be utterly booring to just have something move in a single, monotone pace from A to B?
Tweens bring several aspects to the table and helps us improve presentation
- Establishes a timeline
- Can be manually updated
- Mathematically calculates next step
- Skips frames to respect the timeline
- Mathematical easing ensures smooth accelleration
- Formula-based increments are visually pleasing to the eye
One of the most important aspects are no doubt the first two. Being able to define a timeline that the entire animation should execute within is so valuable. It really means everything when writing complex, responsive user interfaces. Because with a timeline you can trust, you can chain transformations together. If you have a tween running that is set to execute for 2 seconds, then you know and can trust that it will be finished in exactly 2 seconds. This means you can now (although you dont write it like this) do silly stuff like this, and you can trust that it works:
Starttween1; W3_Callback( StartTween2, 1000); W3_Callback( StartTween3, 2000); W3_Callback( StartTween4, 3000);
The above would execute all at once, but the W3_CallBack() methods ensure that each call is made step by step. This will create the impression that one long animation is taking place. Why? Because tween2 will start after 1 second (when tween1 is finished), tween3 after 2 seconds and tween4 after 3 seconds. Each of these has a timeline of 1 second, and when you can trust that timeline you have more freedom to build complex scenes without worrying about speed.
But the most valuable point for us is that we can manually update the tweens. One of the hardest things to program under HTML5 are without a shadow of a doubt CSS3 transformations. Tweening is built into all browsers these days, it’s really a fantastic system and makes full use of hardware accelleration to produce flicker free, ultra smooth graphical operations.
The problem is that these GPU based CSS transformations are notoriously difficult to get right. Smart Mobile Studio has a system unlike any other, giving you full control of CSS in crystal clear classes that makes writing complex CSS animations a walk in the park. But CSS transformations suffer a monumental downside in that, once started, CSS animations cannot be stopped or aborted. They have to run their course and can only be released when it’s all done. Why? Well the WC3 RFC documentation gives little clue, but I suspect the animations are translated into CUDA bytecodes and fed to the GPU.
Using software tweens to produce effects
Since the previous article I put in a couple of hours late last night and created the foundation classes for “tween based effects”. You may be wondering how changing a single (or multiple) floating point values following a formula is going to produce something useful, but wait and see.
The effect foundation classes serves several functions:
- Abstract you from the underlying mechanisms
- Each effect class is spesific
- Each effect class expose spesific properties
- Simplify setup and teardown of tweens and/or other dependencies
- Allow direct CSS style and element attribute mapping
The first 4 points are self explanatory i think. I mean, you dont want an effect class with a ton of properties that should be left alone; you want an effect class that exposes just the properties relevant to that effect, and nothing more. You also want to make the effect classes “core agnostic”, meaning that you as a user should not need to know if the class uses CSS or Tweening to achieve its goal.
The really interesting part is the style and attribute mapping. Do you remember how I added a Id property to the tween elements? So each tween can be addressed by name and not just by index or an object reference? Well there is a reason for that.
By default the Id property is just a string, a string that must be unique and just distinguishes one tween from another. This is helpful when you have say, 20 transformations executing at the same time – and you dont want (or cant) keep track of their indexes or object references.
Like this:
var LItem: TTweenElement; // Get the tween-element by name LItem := FTween["first"]; // Finished yet? if not LItem.Expired then begin //Do something with the tween data end;
But what if you instead of using the Id for names, use it to identify a CSS style property instead?
See where I’m going with this? Effects naturally applies values to CSS styles. It can be position, color, size, scale, opacity or whatever — but ultimately the values a tween generates end up being applied to a CSS style. In the model i posted yesterday that had to be done manually. You had to grab the value in the OnUpdated event and manually set the “left” property to make something move.
Well, with property and attribute mapping, this can all be done automatically for you.
Property binding
To bind a tween to a CSS property or HTML element’s attributes is not new. In fact, thats what most high-end libraries offer. Libraries like JQuery, Framework 7, Tween.js and pretty much all of them allow you to do this.
In the new model binding is handled by a class called TPropertyBinding. This binding takes over the OnUpdated event of TTweenElement, grabs the value and applies it whatever style or attribute you are binding to. Neat? Yeah i think so. TWidgetEffect, which is the effect class you want to use for property binding, exposes a nice Bind() method.
TPropertyBindingtype = (pbStyle,pbAttribute); TPropertyBinding = class(TObject) protected procedure HandleElementUpdated(item:TTweenElement); public property Effect:TCustomControlEffect; Property Tween:TTweenElement; property BindingType:TPropertyBindingtype; Constructor Create(aEffect:TCustomControlEffect; aTween:TTweenElement; aBindingtype:TPropertyBindingtype); end; TWidgetEffect = class(TCustomControlEffect) private FBindings: Array of TPropertyBinding; public procedure DoSetupTween;override; function Bind(PropertyName:String;&Type:TPropertyBindingtype):TPropertyBinding; end;
So let’s demonstrate with a clean cut example. Let’s say you have a side-menu, a panel, on the left side of the screen. You dont want this to be visible all the time so it remains hidden until the user clicks a glyph. When the user clicks the glyph, the menu slides in from the left. So what would it take to create an effect class that does this? Not much!
type TMoveEffect = Class(TWidgetEffect) public procedure DoSetupTween;override; end; procedure TMoveEffect.DoSetupTween; begin // Bind to the "obj.style.left" property var Binding := Bind('left',pbStyle); // Get the tween object from the binding var Tween := Binding.Tween; // Setup the tween object properly Tween.AnimationType := ttQuartInOut; Tween.Behavior:=tbSingle; Tween.StartValue :=-200; // start off the left edge of the screen Tween.Duration := 400; // last for 400 ms Tween.Distance := 200; // move 200 pixels to the right end;
In a real effect your would expose the tween options as properties, here i just hard-coded them. It’s just an example. To use the effect and apply it to our side-menu, you would call it like this:
var effect := TMoveEffect.Create(MenuPanel); effect.execute( procedure () begin effect.free; end);
See how cool this is? We simply create the effect, pass the target control as a parameter in the constructor, execute it – passing the OnFinish callback as a parameter, and thats it. When the transformation is done it calls the OnFinished event, and there we release the instance.
Binding types
If you know your way around Smart Mobile Studio, you are probably thinking “But wait a minute, CSS styles are not all floating point values, and even if they were -things like opacity is formated differently and goes from 0.0 to 1.0”. Some CSS styles are even string based and requires a completely different representation. Like say, when you want to apply a tween on a color. A floating point number of 700.56 is not going to do much for you.
The next step is naturally to introduce different types of binding classes. Right now we have a single generic binding class that just takes the tween value and applies it to a named style. That will work on properties like Left, Top, Width and Height; but we need bindings that translates the values into correct data.
So, new-years eve not withstanding, I will add more binding types which target spesific types of CSS properties in the weeks to come. I dont know when i have enough time to finish this, but hopefully I get to squeeze in an hour or two here and there.
Updates
You will find the updated units on GitHub, so head over to: https://github.com/quartexNOR/smartpascal to grab the latest. For those that cannot use Github right now, here is the latest source:
unit system.animation.effects; interface uses SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms, SmartCL.Fonts, SmartCL.Borders, SmartCL.Application, System.Types, System.Colors, system.animation.tween; type TCustomEffect = class(Tobject) protected procedure DoExecute;virtual;abstract; procedure DoCancel;virtual;abstract; procedure DoPause;virtual;abstract; procedure DoResume;virtual;abstract; function DoGetActive:Boolean;virtual;abstract; public Property OnEffectStarted:TNotifyEvent; Property OnEffectDone:TNotifyEvent; Property OnEffectPaused:TNotifyEvent; property OnEffectCanceled:TNotifyEvent; Property Active:Boolean read DoGetActive; procedure Execute(OnReady:TNotifyEvent);virtual; procedure Cancel;virtual; procedure Pause;virtual; procedure Resume;virtual; end; TCustomTweenEffect = class(TCustomEffect) private FTween: TW3Tween; protected procedure DoExecute;override; procedure DoCancel;override; procedure DoPause;override; procedure DoResume;override; function DoGetActive:Boolean;override; protected procedure DoSetupTween;virtual; procedure DoTearDownTween;virtual; protected procedure HandleTweenDone(sender:TObject);virtual; procedure HandleTweenStart(sender:TObject);virtual; procedure HandleTweenUpdated(Sender:TObject);virtual; property Core:TW3Tween read Ftween; public Property Busy:Boolean read ( FTween.active ); procedure Execute(OnReady:TNotifyEvent);override; procedure Cancel;override; procedure Pause;override; procedure Resume;override; Constructor Create;virtual; Destructor Destroy;Override; end; TCustomControlEffect = class(TCustomTweenEffect) private FControl: TW3MovableControl; public Property Control:TW3MovableControl read FControl; Constructor Create(aControl: TW3MovableControl);overload;virtual; end; TMoveXEffect = class(TCustomControlEffect) protected procedure DoSetupTween;override; procedure DoTearDownTween;override; public Property Duration:float; Property FromX: Integer; Property Distance:Integer; end; TMoveYEffect = class(TCustomControlEffect) protected procedure DoSetupTween;override; procedure DoTearDownTween;override; public Property Duration:float; Property FromY: Integer; Property Distance:Integer; end; TPropertyBindingtype = (pbStyle,pbAttribute); TPropertyBinding = class(TObject) protected procedure HandleElementUpdated(item:TTweenElement); public property Effect:TCustomControlEffect; Property Tween:TTweenElement; property BindingType:TPropertyBindingtype; Constructor Create(aEffect:TCustomControlEffect; aTween:TTweenElement; aBindingtype:TPropertyBindingtype); end; TWidgetEffect = class(TCustomControlEffect) private FBindings: Array of TPropertyBinding; public procedure DoSetupTween;override; function Bind(PropertyName:String;&Type:TPropertyBindingtype):TPropertyBinding; end; implementation procedure TWidgetEffect.DoSetupTween; begin var Binding := Bind('left',TPropertyBindingtype.pbAttribute); end; function TWidgetEffect.Bind(PropertyName:String;&Type:TPropertyBindingtype):TPropertyBinding; var Ltween: TTweenElement; begin Ltween := Core.Add(PropertyName); result := TpropertyBinding.Create(self, Ltween, &Type); end; //############################################################################# // TPropertyBinding //############################################################################# Constructor TPropertyBinding.Create(aEffect:TCustomControlEffect; aTween:TTweenElement; aBindingtype:TPropertyBindingtype); begin inherited Create; Effect := aEffect; Tween := aTween; Bindingtype := aBindingType; Tween.OnUpdated := HandleElementUpdated; end; procedure TPropertyBinding.HandleElementUpdated(item:TTweenElement); begin case Bindingtype of pbStyle: w3_setStyle(Effect.Control.Handle,item.id,item.value); pbAttribute: w3_SetAttrib(Effect.Control.Handle,item.id,item.value); end; end; //############################################################################# // TMoveXEffect //############################################################################# procedure TMoveYEffect.DoSetupTween; var LObj: TTweenElement; begin Core.OnUpdated := NIL; LObj := Core.Add("ypos"); LObj.StartValue := FromY; LObj.Distance := Distance ; LObj.Duration := Duration; LObj.AnimationType := ttQuadInOut; LObj.Behavior := tbSingle; LObj.OnUpdated := procedure (item:TTweenElement) begin Control.Top := round ( Item.Value ); end; end; procedure TMoveYEffect.DoTearDownTween; begin Core.Delete("ypos"); end; //############################################################################# // TMoveXEffect //############################################################################# procedure TMoveXEffect.DoSetupTween; var LObj: TTweenElement; begin Core.OnUpdated := NIL; LObj := Core.Add("xpos"); LObj.StartValue := FromX; LObj.Distance := Distance ; LObj.Duration := Duration; LObj.AnimationType := ttQuadInOut; LObj.Behavior := tbSingle; LObj.OnUpdated := procedure (item:TTweenElement) begin Control.Left := round ( Item.Value ); end; end; procedure TMoveXEffect.DoTearDownTween; begin Core.Delete("xpos"); end; //############################################################################# // TCustomControlEffect //############################################################################# Constructor TCustomControlEffect.Create(aControl: TW3MovableControl); begin inherited Create; FControl := AControl; end; //############################################################################# // TCustomTweenEffect //############################################################################# Constructor TCustomTweenEffect.Create; begin inherited Create; FTween:=TW3Tween.Create; FTween.OnFinished := HandleTweenDone; FTween.OnStarted := HandleTweenStart; FTween.OnUpdated := HandleTweenUpdated; Ftween.SyncRefresh := true; end; Destructor TCustomTweenEffect.Destroy; begin Ftween.free; inherited; end; procedure TCustomTweenEffect.DoTearDownTween; begin end; procedure TCustomTweenEffect.DoSetupTween; begin end; procedure TCustomTweenEffect.HandleTweenStart(sender:TObject); begin if assigned(OnEffectStarted) then OnEffectStarted(self); end; procedure TCustomTweenEffect.HandleTweenDone(sender:TObject); begin // Tear down objects created if required DoTearDownTween; if assigned(OnEffectDone) then OnEffectDone(self); end; procedure TCustomTweenEffect.HandleTweenUpdated(Sender:TObject); begin end; procedure TCustomTweenEffect.Execute(OnReady:TNotifyEvent); begin // Keep onReady callback? if assigned(OnReady) then OnEffectDone := OnReady; DoExecute; end; procedure TCustomTweenEffect.DoExecute; begin DoSetupTween; FTween.Execute; end; procedure TCustomTweenEffect.DoCancel; begin FTween.Cancel; end; procedure TCustomTweenEffect.DoPause; begin FTween.Pause; end; procedure TCustomTweenEffect.DoResume; begin FTween.Resume; end; function TCustomTweenEffect.DoGetActive:Boolean; begin result := FTween.Active; end; procedure TCustomTweenEffect.Cancel; begin FTween.Cancel; end; procedure TCustomTweenEffect.Pause; begin FTween.Pause; end; procedure TCustomTweenEffect.Resume; begin FTween.Resume; end; //############################################################################# // TCustomEffect //############################################################################# procedure TCustomEffect.Execute(OnReady:TNotifyEvent); begin if DoGetActive then Cancel; DoExecute; end; procedure TCustomEffect.Pause; begin DoPause; end; procedure TCustomEffect.Cancel; begin DoCancel; end; procedure TCustomEffect.Resume; begin DoResume; end; end.
And the tweening library:
unit system.animation.tween; interface uses System.Types, SmartCL.Time, SmartCL.System; type TW3TweenAnimationType = ( ttlinear, ttQuadIn, ttQuadOut, ttQuadInOut, ttCubeIn, ttCubeOut, ttCubeInOut, ttQuartIn, ttQuartOut, ttQuartInOut ); TTweenBehavior = ( tbSingle, // Execute once and then stops tbRepeat, // Repeats the tween sequence tbOscillate // Executes between A and B in oscillating manner ); TTweenData = class; TTweenElement = class; TW3TweenEase = class; TW3Tween = class; TW3TweenEase = class public class function Linear(t,b,c,d:float):float; class function QuadIn(t, b, c, d:float):float; class function QuadOut(t, b, c, d:float):float; class function QuadInOut(t, b, c, d:float):float; class function CubeIn(t, b, c, d:float):float; class function CubeOut(t, b, c, d:float):float; class function CubeInOut(t, b, c, d:float):float; class function QuartIn(t, b, c, d:float):float; class function QuartOut(t, b, c, d:float):float; class function QuartInOut(t, b, c, d:float):float; end; TW3TweenItemUpdatedEvent = procedure (Item:TTweenElement); TW3TweenUpdatedEvent = procedure (Sender:TObject); TW3TweenStartedEvent = procedure (sender:TObject); TW3TweenFinishedEvent = procedure (sender:TObject); TW3TweenFinishedPartialEvent = procedure (sender:TObject); TTweenState = (tsIdle,tsRunning,tsPaused,tsDone); TTweenData = class public Property Id: String; Property StartTime: TDateTime; Property StartValue: Float; Property Distance: Float; Property Duration: Float; Property AnimationType: TW3TweenAnimationType; Property Behavior: TTweenBehavior; function Expired: Boolean;virtual; Procedure Reset;virtual; end; TTweenElement = class(TTweenData) public Property State:TTweenState; Property Value:Float; Property OnFinished:TNotifyEvent; Property OnUpdated:TW3TweenItemUpdatedEvent; procedure Update(const aValue:Float);virtual; Procedure Reset;override; Constructor Create;virtual; end; TW3Tween = class private FTimer: TW3Timer; FValues: Array of TTweenElement; FActive: Boolean; FPartial: Boolean; FInterval: Integer; protected Procedure HandleSyncUpdate;virtual; procedure HandleUpdateTimer(Sender:TObject);virtual; function Update(const Item:TTweenElement):Float;virtual; public function ObjectOf(const Id:String):TTweenElement; function IndexOf(Id:String):Integer; Property Active:Boolean read ( FActive ); Property Item[const index:Integer]:TTweenElement read (FValues[index]); property Tween[const Id:String]:TTweenElement read ObjectOf;default; property Count:Integer read ( FValues.Length ); Property Interval:Integer read FInterval write ( TInteger.EnsureRange(Value,1,10000) ); Property SyncRefresh:Boolean; Property IgnoreOscillate:Boolean; function Add(Id:String):TTweenElement;overload; Function Add(Id:String;const aStartValue,aDistance,aDuration:float; const aAnimationType:TW3TweenAnimationType; const aBehavior:TTweenBehavior):TTweenElement;overload; Procedure Delete(index:Integer);overload; procedure Delete(Id:String);overload; procedure Delete(const TweenIds:Array of String);overload; procedure Clear;overload; procedure Execute(Finished:TProcedureRef);overload; procedure Execute;overload; procedure Execute(const TweenObjects:Array of TTweenElement);overload; Procedure Pause(const Index:Integer);overload; procedure Pause(const Tween:TTweenElement);overload; procedure Pause(const Objs:Array of TTweenElement);overload; procedure Pause(const Ids:Array of String);overload; procedure Pause;overload; Procedure Resume(const index:Integer);overload; procedure Resume(const Tween:TTweenElement);overload; procedure Resume(const Objs:Array of TTweenElement);overload; procedure Resume(const Ids:Array of String);overload; procedure Resume;overload; procedure Cancel;overload; Constructor Create;virtual; Destructor Destroy;Override; published Property OnPartialFinished:TW3TweenFinishedPartialEvent; Property OnUpdated:TW3TweenUpdatedEvent; Property OnFinished:TW3TweenFinishedEvent; Property OnStarted:TW3TweenStartedEvent; end; function GetTimeCode:float; function Round100(const Value:float):float; implementation Function GetTimeCode:float; begin asm @result = Date.now(); end; end; function Round100(const Value:float):float; begin result := Round( Value * 100) / 100; end; //############################################################################ // TW3Tween //############################################################################ Constructor TW3Tween.Create; begin inherited Create; FTimer := TW3Timer.Create; Interval := 10; IgnoreOscillate := true; end; Destructor TW3Tween.Destroy; begin Cancel; Clear; FTimer.free; inherited; end; procedure TW3Tween.Clear; begin While FValues.length>0 do begin FValues[FValues.length-1].free; Fvalues.Delete(FValues.length-1,1); end; end; function TW3Tween.Update(const Item:TTweenElement):Float; var LTotal: float; function PerformX(t, b, c, d:float):float; begin result := 0.0; case Item.AnimationType of ttlinear: result := TW3TweenEase.Linear(t,b,c,d); ttQuadIn: result := TW3TweenEase.QuadIn(t,b,c,d); ttQuadOut: result := TW3TweenEase.QuadOut(t,b,c,d); ttQuadInOut: result := TW3TweenEase.QuadInOut(t,b,c,d); ttCubeIn: result := TW3TweenEase.CubeIn(t,b,c,d); ttCubeOut: result := TW3TweenEase.CubeOut(t,b,c,d); ttCubeInOut: result := TW3TweenEase.CubeInOut(t,b,c,d); ttQuartIn: result := TW3TweenEase.QuartIn(t,b,c,d); ttQuartOut: result := TW3TweenEase.QuartOut(t,b,c,d); ttQuartInOut: result := TW3TweenEase.QuartInOut(t,b,c,d); end; end; begin if not Item.Expired then begin LTotal := PerformX(GetTimeCode-Item.StartTime, Item.StartValue, Item.Distance, Item.Duration); end else if Item.behavior = tbSingle then begin LTotal := Item.StartValue + Item.Distance; end else if Item.behavior = tbRepeat then begin Item.StartTime := GetTimeCode; LTotal := PerformX(GetTimeCode-Item.StartTime, Item.StartValue, Item.Distance, Item.Duration); end else if Item.behavior = tbOscillate then begin Item.StartValue := Item.StartValue + Item.Distance; Item.Distance := -Item.Distance; Item.StartTime := GetTimeCode; LTotal := PerformX(GetTimeCode-Item.StartTime, Item.StartValue, Item.Distance, Item.Duration); Item.State := tsDone; end; result := Round100(LTotal); end; procedure TW3Tween.HandleUpdateTimer(Sender:TObject); var LItem: TTweenElement; LCount: Integer; LDone: Integer; begin (* Tween objects cleared while active? *) if FValues.Length<1 then begin Cancel; exit; end; LCount := 0; LDone := 0; for LItem in FValues do begin (* The start time-code is set in the first update *) if LItem.State=tsIdle then begin LItem.StartTime := GetTimeCode; LItem.State := tsRunning; end; // Animation paused? Continue with next if LItem.State = tsPaused then continue; // Expired? Keep track of it if LItem.Expired then begin if IgnoreOscillate then begin //if (LItem.Behavior <> tbOscillate) then if (LItem.Behavior = tbSingle) then inc(LCount) else inc(LDone); end else inc(LCount); end; // Update the element with the new value LItem.Update(Update(LItem)); // finished on this run? if LItem.Expired then begin if not Litem.State = tsDone then begin LItem.State := tsDone; if assigned(LItem.OnFinished) then LItem.OnFinished(LItem); end; end; end; if assigned(OnUpdated) then OnUpdated(Self); if LCount = (FValues.Length - LDone) then begin if IgnoreOscillate then begin if not FPartial then begin FPartial := True; // make sure this happens only once! if assigned(OnPartialFinished) then OnPartialFinished(self); end; end; end; (* If all tweens have expired, stop the animation *) If LCount = FValues.Length then begin //Writeln('Stopping at ' + LCount.toString + ' tweens done'); Cancel; end; end; Procedure TW3Tween.HandleSyncUpdate; begin HandleUpdateTimer(NIL); if Active then W3_RequestAnimationFrame(HandleSyncUpdate); end; procedure TW3Tween.Execute(Const TweenObjects:Array of TTweenElement); var LItem: TTweenElement; LId: String; begin if not Active then begin if TweenObjects.length<0 then begin for LItem in TweenObjects do begin LId := LItem.Id.Trim; if LId.length>0 then begin if self.IndexOf(LId)<0 then FValues.add(LItem) else Raise EW3Exception.CreateFmt ('Execute failed, a tween-object with id [%s] already exists in collection error',[LId]); end else Raise EW3Exception.Create ('Execute failed, could not inject tween object, element missing qualified id error'); end; end; Execute; end else raise exception.Create('Tween already executing error'); end; procedure TW3Tween.Execute(Finished:TProcedureRef); begin self.OnFinished := procedure (sender:Tobject) begin if assigned(Finished) then Finished; end; Execute; end; procedure TW3Tween.Execute; begin if not Active then begin FPartial := false; case SyncRefresh of true: begin // Initiate loop in 100ms W3_Callback( procedure () begin W3_RequestAnimationFrame( HandleSyncUpdate ); end, 100); end; false: begin FTimer.OnTime := HandleUpdateTimer; FTimer.Delay := Interval; FTimer.Enabled := true; end; end; FActive := True; if assigned(OnStarted) then OnStarted(self); end else raise exception.Create('Tween already executing error'); end; procedure TW3Tween.Delete(const TweenIds:Array of String); var Lid: String; LObj: TTweenElement; LIndex: Integer; begin if tweenIds.length>0 then begin // Only remove tweens defined in Parameter list for LId in Tweenids do begin LObj := ObjectOf(Lid); if LObj<>NIL then begin LIndex := FValues.IndexOf(LObj); FValues.delete(LIndex,1); LObj.free; end; end; end; end; Procedure TW3Tween.Resume(const index:Integer); var LObj: TTweenElement; begin if Active then begin if (Index>=0) and (Index<FValues.Count) then begin LObj := FValues[index]; if LObj.State=tsPaused then begin FValues[index].StartTime := GetTimeCode; FValues[Index].State := tsRunning; end; end; end; end; procedure TW3Tween.Resume(const Tween:TTweenElement); begin if Active then Begin if Tween<>NIL then Begin if FValues.IndexOf(Tween)>=0 then begin if Tween.state=tsPaused then begin Tween.StartTime := GetTimeCode; Tween.State := tsPaused; end; end; end; end; end; procedure TW3Tween.Resume(const Objs:Array of TTweenElement); var LObj: TTweenElement; begin if Active then begin if Objs.length>0 then Begin for LObj in Objs do begin if LObj<>NIl then Resume(LObj); end; end; end; end; procedure TW3Tween.Resume(const Ids:Array of String); var LId: String; begin if Active then begin for LId in Ids do begin Resume(ObjectOf(Lid)); end; end; end; procedure TW3Tween.Resume; var LObj: TTweenElement; begin if Active then begin if FValues.length>0 then Begin for LObj in FValues do begin if LObj<>NIl then Resume(LObj); end; end; end; end; Procedure TW3Tween.Pause(const Index:Integer); var LObj: TTweenElement; begin if Active then begin if (Index>=0) and (Index<FValues.Count) then begin LObj := FValues[index]; if LObj.State=tsRunning then FValues[Index].State := tsPaused; end; end; end; procedure TW3Tween.Pause(const Tween:TTweenElement); begin if Active then Begin if Tween<>NIL then Begin if FValues.IndexOf(Tween)>=0 then begin if Tween.state=tsRunning then Tween.State := tsPaused; end; end; end; end; procedure TW3Tween.Pause(const Objs:Array of TTweenElement); var LObj: TTweenElement; begin if Active then begin if Objs.length>0 then Begin for LObj in Objs do begin if LObj<>NIl then Pause(LObj); end; end; end; end; procedure TW3Tween.Pause(const Ids:Array of String); var LId: String; begin if Active then begin for LId in Ids do begin Pause(ObjectOf(Lid)); end; end; end; procedure TW3Tween.Pause; var LObj: TTweenElement; begin if Active then begin if FValues.length>0 then Begin for LObj in FValues do begin Pause(LObj); end; end; end; end; procedure TW3Tween.Cancel; begin if Active then begin try FTimer.enabled := false; FTimer.OnTime := NIL; finally FActive := false; if assigned(OnFinished) then OnFinished(self); end; end; end; Procedure TW3Tween.Delete(index:Integer); var LObj: TTweenElement; begin LObj:=FValues[index]; FValues.Delete(Index,1); LObj.free; end; procedure TW3Tween.Delete(Id:String); begin Delete(FValues.indexOf(ObjectOf(Id))); end; function TW3Tween.Add(Id:String):TTweenElement; begin Id := id.trim.lowercase; if id.length>0 then begin if IndexOf(Id)<0 then begin result := TTweenElement.Create; result.Id := Id; FValues.add(result); end else raise EW3Exception.CreateFmt ('A tween-object with id [%s] already exists in collection error',[Id]); end else raise EW3Exception.Create('Invalid tween-object id [empty] error'); end; Function TW3Tween.Add(Id:String;const aStartValue,aDistance,aDuration:float; const aAnimationType:TW3TweenAnimationType; const aBehavior:TTweenBehavior):TTweenElement; begin Id := id.trim.lowercase; if id.length>0 then begin if IndexOf(Id)<0 then begin result := TTweenElement.Create; result.StartValue := aStartValue; result.Distance := aDistance; result.Duration := aDuration; result.AnimationType := aAnimationtype; result.Behavior := aBehavior; result.StartTime := GetTimeCode; result.Id := Id; FValues.add(result); end else raise EW3Exception.CreateFmt ('A tween-object with id [%s] already exists in collection error',[Id]); end else raise EW3Exception.Create('Invalid tween-object id [empty] error'); end; function TW3Tween.IndexOf(Id:String):Integer; var x: Integer; begin result := -1; id := Id.trim.lowercase; if id.length>0 then begin for x:=0 to FValues.Count-1 do begin if Id.EqualsText(FValues[x].Id) then begin result := x; break; end; end; end; end; function TW3Tween.ObjectOf(const Id:String):TTweenElement; var LIndex: Integer; begin LIndex := IndexOf(Id); if LIndex>=0 then result := FValues[LIndex] else result := NIL; end; //############################################################################ // TTweenData //############################################################################ function TTweenData.Expired: Boolean; begin result := StartTime + Duration < GetTimeCode; end; Procedure TTweenData.Reset; begin StartTime := 0; StartValue := 0; Distance := 0; Duration := 0; Animationtype := ttlinear; Behavior := tbSingle; end; //############################################################################ // TTweenElement //############################################################################ Constructor TTweenElement.Create; begin inherited Create; State := tsIdle; end; Procedure TTweenElement.Reset; begin inherited Reset; State := tsIdle; end; procedure TTweenElement.Update(const aValue:Float); begin Value := aValue; if assigned(OnUpdated) then OnUpdated(self); end; //############################################################################# // TW3TweenEase //############################################################################# {$HINTS OFF} class function TW3TweenEase.QuartIn(t, b, c, d:float):float; begin asm @t /= @d; return @c * @t * @t * @t * @t + @b; end; end; class function TW3TweenEase.QuartOut(t, b, c, d:float):float; begin asm @t /= @d; @t--; return -@c * (@t * @t * @t * @t - 1) + @b; end; end; class function TW3TweenEase.QuartInOut(t, b, c, d:float):float; begin asm @t /= @d/2; if (@t < 1) { return @c / 2 * @t * @t * @t * @t + @b; } @t -= 2; return -@c / 2 * (@t * @t * @t * @t - 2) + @b; end; end; class function TW3TweenEase.CubeInOut(t, b, c, d:float):float; begin asm @t /= @d/2; if (@t < 1) { return @c / 2 * @t * @t * @t + @b; } @t -= 2; return @c/2 * (@t * @t * @t + 2) + @b; end; end; class function TW3TweenEase.CubeOut(t, b, c, d:float):float; begin asm @t /= @d; @t--; return @c * (@t * @t * @t + 1) + @b; end; end; class function TW3TweenEase.CubeIn(t, b, c, d:float):float; begin asm @t /= @d; return @c * @t * @t * @t + @b; end; end; class function TW3TweenEase.QuadInOut(t, b, c, d:float):float; begin asm @t /= @d/2; if (@t < 1) { return @c / 2 * @t * @t * @t + @b; } @t -= 2; @result = @c/2*(@t * @t * @t + 2) + @b; end; end; class function TW3TweenEase.QuadOut(t, b, c, d:float):float; begin asm @t /= @d; return -@c * @t * (@t - 2) + @b; end; end; class function TW3TweenEase.QuadIn(t, b, c, d:float):float; begin asm @t /= @d; return @c * @t * @t + @b; end; end; class function TW3TweenEase.Linear(t,b,c,d:float):float; begin asm @result = @c * @t / @d + @b; end; end; {$HINTS ON} end.
You must be logged in to post a comment.