Home > Delphi, JavaScript, Object Pascal, OP4JS, Smart Mobile Studio > Tweening with Smart Mobile Studio, Part 3

Tweening with Smart Mobile Studio, Part 3

December 30, 2015 Leave a comment Go to comments

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

Get the code on GitHub

Get the code on GitHub

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.

Advertisements
  1. December 30, 2015 at 6:42 pm

    Some time ago, I’ve created SMART HORROR GAME, using CSS3 animations

    Live preview:
    https://rawgit.com/smartpascal/smartms/master/smartsaw/www/preview.html

    This animation uses a lot of CSS3 effects such as:
    @keyframes example-ani{
    0%{transform:translate3d(102px, 22px, 0px) rotateX(0deg) rotateY(0deg) rotateZ(-33.5deg) scale3d(0.7426, 0.7426, 1);}
    18%{transform:translate3d(104px, 89px, 0px) rotateX(0deg) rotateY(0deg) rotateZ(-65.6deg) scale3d(0.7426, 0.7426, 1);}
    100%{transform:translate3d(160px, 89px, 0px) rotateX(0deg) rotateY(0deg) rotateZ(-65.6deg) scale3d(0.27259999999999995, 0.27259999999999995, 1);}}

    I’m just curious, if we can create such animation using the tweening library.

    • Jon Lennart Aasenden
      December 30, 2015 at 7:07 pm

      Yes that should be possible, but it has to be setup differently. Each effect here represents one or more tweens. This is where property mapping comes into play, or at least code which initializes a tween, then translates that value into .. well, rotating something, skewing something and so on.
      Tweening in itself is just numbers, its up to the effects to interpret those numbers into something that makes sense.

  2. Shane Holmes
    December 31, 2015 at 7:04 pm

    WOW, very nice addition to SMS! Thanks for all the hard work!

  3. January 7, 2016 at 8:02 pm

    I’m learning Smart Pascal programming by developing a simple smart bomb game using tweening motion.

    Main screen preview (chrome-only):
    https://rawgit.com/smartpascal/smartms/master/smartBomb/www/index.html

    but I’m really stuck with the collision detection, when one of the mines hit the rocket.
    Any idea?

  1. No trackbacks yet.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: