IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Les interpolations avec Lazarus (2/3)

Des classes pour les interpolations

Après avoir appris à rendre plus plus vivantes vos interfaces grâce aux interpolations, avec le tutoriel qui suit, vous doterez Lazarus de classes spécifiques aux fonctions d'easing. Votre EDI préféré saura alors imiter certaines animations présentes dans d'autres langages de programmation ou frameworks.

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Les programmes de test sont présents dans le répertoire exemples accompagnant le tutoriel.

Les travaux réalisés dans le tutoriel précédent vont fournir la matière à la hiérarchie de classes que nous allons mettre en place.

Nous allons tout d'abord définir une classe de base pour nos interpolations. Cette classe est essentielle : comme elle sera l'ancêtre de nos classes de travail, partout où cette classe de base sera attendue, nous pourrons lui substituer une classe enfant, d'où une grande souplesse.

Si vous avez besoin d'un rafraîchissement de mémoire à propos de la notion de polymorphisme, vous pouvez consulter le tutoriel se rapportant aux principes fondamentaux de la Programmation Orientée Objet.

De cette classe initiale, nous dériverons des classes de plus en plus spécialisées : une première classe introduira les fonctionnalités propres aux interpolations de type easing que deux autres classes filles (ou plutôt petites-filles) spécialiseront, l'une pour les nombres flottants, l’autre pour les entiers. Dans le prochain tutoriel, une solution alternative à base de courbes de Bézier cubiques viendra compléter cette famille avec une classe dédiée.

Dans un souci de clarté, nous construirons deux unités :

  • interpolationstypes pour regrouper les constantes, les types et les fonctions outils qui accompagneront les classes ;
  • interpolations qui contiendra les classes proprement dites (déclarations et implémentations.

II. Les classes d'interpolation

[Exemple Interpolations 03]

II-A. L'unité interpolationstypes

De manière classique, l'unité interpolationstypes définit le type essentiel utilisé pour nos interpolations et des outils gravitant autour. En particulier, nous souhaiterons peut-être transformer des chaînes de caractères en éléments de l'énumération TGVInterpolationType.

Voici le code source de cette unité :

 
Sélectionnez
unit interpolationstypes;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils;

type
  TGVInterpolationType = (intLinear,
    intEaseInQuad, intEaseOutQuad, intEaseInOutQuad, intEaseOutInQuad,
    intEaseInCubic, intEaseOutCubic, intEaseInOutCubic, intEaseOutInCubic,
    intEaseInQuart, intEaseOutQuart, intEaseInOutQuart, intEaseOutInQuart,
    intEaseInQuint, intEaseOutQuint, intEaseInOutQuint, intEaseOutInQuint,
    intEaseInSine, intEaseOutSine, intEaseInOutSine, intEaseOutInSine,
    intEaseInExpo, intEaseOutExpo, intEaseInOutExpo, intEaseOutInExpo,
    intEaseInCirc, intEaseOutCirc, intEaseInOutCirc, intEaseOutInCirc,
    intEaseInElastic, intEaseOutElastic, intEaseInOutElastic, intEaseOutInElastic,
    intEaseInBack, intEaseOutBack, intEaseInOutBack, intEaseOutInBack,
    intEaseInBounce, intEaseOutBounce, intEaseInOutBounce, intEaseOutInBounce
    );

const
  C_InterpolationToStr: array[TGVInterpolationType] of string =
    ('Linear',
    'EaseInQuad', 'EaseOutQuad', 'EaseInOutQuad', 'EaseOutInQuad',
    'EaseInCubic', 'EaseOutCubic', 'EaseInOutCubic', 'EaseOutInCubic',
    'EaseInQuart', 'EaseOutQuart', 'EaseInOutQuart', 'EaseOutInQuart',
    'EaseInQuint', 'EaseOutQuint', 'EaseInOutQuint', 'EaseOutInQuint',
    'EaseInSine', 'EaseOutSine', 'EaseInOutSine', 'EaseOutInSine',
    'EaseInExpo', 'EaseOutExpo', 'EaseInOutExpo', 'EaseOutInExpo',
    'EaseInCirc', 'EaseOutCirc', 'EaseInOutCirc', 'EaseOutInCirc',
    'EaseInElastic', 'EaseOutElastic', 'EaseInOutElastic', 'EaseOutInElastic',
    'EaseInBack', 'EaseOutBack', 'EaseInOutBack', 'EaseOutInBack',
    'EaseInBounce', 'EaseOutBounce', 'EaseInOutBounce', 'EaseOutInBounce');

  C_DefaultDuration = 100;
  C_DefaultInterpolation = intLinear;

  // chaîne en interpolation
 function StrToInterpolation(St: string): TGVInterpolationType;

implementation

function StrToInterpolation(const St: string): TGVInterpolationType;
// *** chaîne en interpolation ***
var
  LInt: TGVInterpolationType;
begin
  Result := intLinear;
  for LInt := low(TGVInterpolationType) to high(TGVInterpolationType) do
    if CompareText(St, C_InterpolationToStr[LInt]) = 0 then
    begin
      Result := LInt;
      Exit;
    end;
end;

end.

À chaque élément de l'énumération correspond une chaîne du tableau de constantes chaînes C_InterpolationGVInterpolationStr.

La traduction d'une chaîne en élément de l'énumération est réalisée par la fonction StrToInterpolation qui parcourt la liste de cette énumération jusqu'à identifier l'élément visé. La fonction de comparaison est CompareText qui ne tient pas compte de la casse. Par défaut, l'interpolation retournée par la fonction est intLinear, ce qui correspond au déroulement habituel d'une animation linéaire.

Cette unité sera à compléter avec la définition de nouvelles classes lors du prochain tutoriel sur les courbes de Bézier cubiques .

II-B. La classe de base TGVBaseInterpolation

La classe baptisée TGVBaseInterpolation contient tout ce que nous sommes en droit d'attendre d'une interpolation. Cependant, elle n'est pas suffisamment spécialisée pour être employée telle quelle dans une application.

Voici le code source de sa déclaration :

 
Sélectionnez
type
  TGVBaseInterpolation = class
  strict private
    fBeginValue: Single;
    fChangeValue: Single;
    fUpdating: Boolean;
    fDuration: Single;
    fStep: Single;
    fOnChange: TNotifyEvent;
    fReverse: Boolean;
  protected
    function GetResult: Single;
    procedure SetBeginValue(AValue: Single);
    procedure SetChangeValue(AValue: Single);
    procedure SetDuration(AValue: Single);
    procedure SetReverse(AValue: Boolean);
    procedure SetStep(AValue: Single);
    function DoResult: Single; virtual; abstract;
    procedure BeginUpdate; virtual;
    procedure EndUpdate; virtual;
    procedure Change; virtual;
    property Result: Single read GetResult;
    property Duration: Single read fDuration write SetDuration default C_DefaultDuration;
    property BeginValue: Single read fBeginValue write SetBeginValue;
    property ChangeValue: Single read fChangeValue write SetChangeValue;
    property Step: Single read fStep write SetStep;
  public
    constructor Create;
    property Updating: Boolean read fUpdating;
    property Reverse: Boolean read fReverse write SetReverse;
    property OnChange: TNotifyEvent read fOnChange write fOnChange;
  end;

Nous retrouvons les propriétés déjà rencontrées maintes fois avec les fonctions d'easing : le début de l'interpolation avec BeginValue, sa fin avec ChangeValue, le pas avec Step et la durée avec Duration.

Vous noterez que ces propriétés ont une portée protected, car nous ne savons pas au niveau de cette classe les propriétés que les classes filles rendront visibles et donc utilisables.

Le résultat de l'interpolation pour un pas donné sera fourni par la propriété Result qui, pour le moment, fera appel par son getter GetResult à la fonction abstraite DoResult.

La classe contenant une fonction abstraite, elle ne pourra pas être utilisée directement, au moins sans précautions !

Cette classe de base offre aussi l'occasion de proposer un système de contrôle de la mise à jour avec BeginUpdate, EndUpdate et la propriété Updating en lecture seule : nous éviterons ainsi de changer de manière brutale certaines données en plein milieu de calculs. De plus, nous avons ajouté la possibilité d'implémenter un gestionnaire d'événement OnChange afin de notifier les changements de l'état de la classe (et donc de ses filles).

L’implémentation de cette classe n'a rien de complexe :

 
Sélectionnez
 TGVBaseInterpolation }

function TGVBaseInterpolation.GetResult: Single;
// *** résultat ***
begin
  Result := DoResult;
end;

procedure TGVBaseInterpolation.SetBeginValue(AValue: Single);
// *** valeur de début ***
begin
  if (fBeginValue = AValue) or Updating then // pas si calcul en cours !
    Exit;
  fBeginValue := AValue;
  Change;
end;

procedure TGVBaseInterpolation.SetChangeValue(AValue: Single);
// *** valeur de fin ***
begin
  if (fChangeValue = AValue) or Updating then  // pas si calcul en cours !
    Exit;
  fChangeValue := AValue;
  Change;
end;

procedure TGVBaseInterpolation.SetDuration(AValue: Single);
// *** durée ***
begin
  if (fDuration = AValue) or Updating then   // pas si calcul en cours !
    Exit;
  fDuration := AValue;
  Change;
end;

procedure TGVBaseInterpolation.SetReverse(AValue: Boolean);
// *** retour ? ***
begin
  if (fReverse = AValue) or Updating then  // pas si calcul en cours !
    Exit;
  fReverse := AValue;
  Change;
end;

procedure TGVBaseInterpolation.SetStep(AValue: Single);
// *** étape en cours ***
begin
  if (fStep = AValue) or Updating then   // pas si calcul en cours !
    Exit;
  fStep := AValue;
  Change;
end;

procedure TGVBaseInterpolation.BeginUpdate;
// *** calcul en cours ***
begin
  fUpdating := True;
end;

procedure TGVBaseInterpolation.EndUpdate;
// *** calcul terminé ***
begin
  fUpdating := False;
end;

procedure TGVBaseInterpolation.Change;
// *** changement dans la classe ***
begin
  if Assigned(fOnChange) then
    fOnChange(Self);
end;

constructor TGVBaseInterpolation.Create;
// *** création ***
begin
  inherited Create;
  Duration := C_DefaultDuration;
end;

II-C. La classe TGVCustomEasingInterpolation

De cette classe ancêtre, nous pouvons dériver une classe un peu plus spécialisée qui contiendra les routines nécessaires aux interpolations vues jusqu'à présent. Elle sera aussi l'occasion d'implémenter DoResult qui, nous le rappelons, était abstraite.

Les classes intermédiaires ne sont pas des subtilités superflues. En effet, elles permettent de construire un arbre des classes en réduisant le code à écrire pour leurs filles. Par exemple, la classe TGVCustomEasingInterpolation aura tout mis en place pour une classe qui traiterait des entiers et une autre qui s'intéresserait aux valeurs flottantes.

Voici la déclaration de cette classe TGVCustomEasingInterpolation intermédiaire :

 
Sélectionnez
{ TGVCustomEasingInterpolation }

  TGVCustomEasingInterpolation = class(TGVBaseInterpolation)
  strict private
    fInterpolation: TGVInterpolationType;
  protected
    function InPower(APower: Integer): Single;
    function OutPower(APower: Integer): Single;
    function InOutPower(APower: Integer): Single;
    function OutInPower(APower: Integer): Single;
    function EaseInSine: Single;
    function EaseOutSine: Single;
    function EaseInOutSine: Single;
    function EaseOutInSine: Single;
    function EaseInExpo: Single;
    function EaseOutExpo: Single;
    function EaseInOutExpo: Single;
    function EaseOutInExpo: Single;
    function EaseInCirc: Single;
    function EaseOutCirc: Single;
    function EaseInOutCirc: Single;
    function EaseOutInCirc: Single;
    function EaseInElastic(AStart, AEnd, AStep: Single): Single;
    function EaseOutElastic(AStart, AEnd, AStep: Single): Single;
    function EaseInOutElastic: Single;
    function EaseOutInElastic: Single;
    function EaseInBack: Single;
    function EaseOutBack: Single;
    function EaseInOutBack: Single;
    function EaseOutInBack: Single;
    function EaseInOutBounce: Single;
    function EaseOutInBounce: Single;
    function EaseInBounce(AStart, AEnd, AStep: Single): Single;
    function EaseOutBounce(AStart, AEnd, AStep: Single): Single;

    function DoResult: Single; override;
    procedure SetInterpolation(AValue: TGVInterpolationType);
    property Interpolation: TGVInterpolationType read fInterpolation
      write SetInterpolation default C_DefaultInterpolation;
  public
    constructor Create;
    function InterpolationResult(AStep: Single): Single;
  end;

Nous profitons de cette classe intermédiaire pour introduire sa nouveauté essentielle, à savoir la propriété Interpolation qui restera de type protected, car nous ne savons pas, encore une fois, exactement la forme que cette propriété prendra dans les classes filles ni même si elle devra être visible en l'état.

Voici le code qui accompagne la classe et qui reprend pour l'essentiel les routines étudiées lors de l'implémentation des fonctions d'easing :

 
Sélectionnez
{ TGVCustomEasingInterpolation }

procedure TGVCustomEasingInterpolation.SetInterpolation(
  AValue: TGVInterpolationType);
// *** choix de l'interpolation ***
begin
  if (fInterpolation = AValue) or Updating  then
    Exit;
  fInterpolation := AValue;
  Change;
end;

constructor TGVCustomEasingInterpolation.Create;
// *** création ***
begin
  inherited Create;
  fInterpolation := intLinear;
end;

function TGVCustomEasingInterpolation.InPower(APower: Integer): Single;
// *** calcul d'une interpolation de type IN pour les puissances ***
begin
  Result := ChangeValue * Power(Step / Duration, APower) + BeginValue;
end;

function TGVCustomEasingInterpolation.OutPower(APower: Integer): Single;
// *** calcul d'une interpolation de type OUT pour les puissances ***
var
  LSign: Integer;
begin
  LSign := ifthen(Odd(APower), 1, -1);
  Result := LSign * ChangeValue * (Power(Step / Duration - 1, APower) + LSign)
    + BeginValue;
end;

function TGVCustomEasingInterpolation.InOutPower(APower: Integer): Single;
// *** calcul d'une interpolation de type INOUT pour les puissances ***
var
  LSign: Integer;
  LStep: Single;
begin
  LSign := ifthen(Odd(APower), 1, -1);
  LStep := Step / Duration * 2;
  if LStep < 1 then
    Result := ChangeValue / 2 * Power(LStep, APower) + BeginValue
  else
    Result :=  LSign * ChangeValue / 2 * (Power(LStep - 2, APower) + LSign * 2)
      + BeginValue;
end;

function TGVCustomEasingInterpolation.OutInPower(APower: Integer): Single;
// *** calcul d'une interpolation OUTIN pour les puissances ***
var
  LSign: Integer;
begin
  LSign := ifthen(Odd(APower), 1, -1);
  if Step < Duration / 2 then
    Result := LSign * ChangeValue / 2 * (Power(2 * Step / Duration - 1, APower)
      + LSign) + BeginValue
  else
    Result := ChangeValue / 2 * Power((Step * 2 - Duration) / Duration, APower)
      + BeginValue + ChangeValue / 2;
end;

function TGVCustomEasingInterpolation.EaseInSine: Single;
// *** INSINE ***
begin
  Result := - ChangeValue * cos(Step / Duration * Pi / 2) + ChangeValue + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseOutSine: Single;
// *** OUTSINE ***
begin
  Result := ChangeValue * sin(Step / Duration * Pi / 2) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseInOutSine: Single;
// *** INOUTSINE ***
begin
  Result := - ChangeValue / 2 * (cos(Step / Duration * Pi) - 1) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseOutInSine: Single;
// *** OUTINSINE ***
begin
  if Step < Duration / 2 then
    Result := ChangeValue / 2 * sin(2 * Step / Duration * Pi / 2) + BeginValue
  else
    Result := - ChangeValue / 2 * cos((2 * Step - Duration) / Duration * Pi / 2)
      + ChangeValue + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseInExpo: Single;
// *** INEXPO ***
begin
  Result := ifthen(Step = 0, BeginValue,
    ChangeValue * Power(2, 10 * (Step / Duration - 1)) + BeginValue);
end;

function TGVCustomEasingInterpolation.EaseOutExpo: Single;
// *** OUTEXPO ***
begin
  Result := ifthen(Step = Duration, ChangeValue + BeginValue,
    ChangeValue * (- Power(2, -10 * Step / Duration) + 1) + BeginValue);
end;

function TGVCustomEasingInterpolation.EaseInOutExpo: Single;
// *** INOUTEXPO ***
begin
  if Step = 0 then
    Result := BeginValue
  else
  if Step = Duration then
    Result := ChangeValue + BeginValue
  else
  begin
    Step := Step / Duration * 2;
    if Step < 1 then
      Result := ChangeValue / 2 * Power(2, 10 * (Step - 1)) + BeginValue
    else
      Result := ChangeValue / 2 * (- Power(2, - 10 * (Step - 1)) + 2) + BeginValue;
  end;
end;

function TGVCustomEasingInterpolation.EaseOutInExpo: Single;
// *** OUTINEXPO ***
begin
  if Step = 0 then
    Result := BeginValue
  else
  if Step = Duration then
    Result := BeginValue + ChangeValue
  else
  if Step < Duration / 2 then
    Result := ChangeValue / 2 * (- Power(2, -5 * Step / Duration) + 1) + BeginValue
  else
    Result := ChangeValue / 2 * (- Power(2, -10 * (2 * Step - Duration) / Duration) + 1)
      + BeginValue  + ChangeValue / 2;
end;

function TGVCustomEasingInterpolation.EaseInCirc: Single;
// *** INCIRC ***
begin
  Result := - ChangeValue * (Sqrt(1 - Power(Step / Duration, 2)) - 1) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseOutCirc: Single;
// *** OUTCIRC ***
begin
  Result := ChangeValue * Sqrt(1 - Power(Step / Duration - 1, 2)) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseInOutCirc: Single;
// *** INOUTCIRC ***
var
  LStep: Single;
begin
  LStep := Step / Duration * 2;
  if LStep < 1 then
    Result := - ChangeValue / 2 * (Sqrt(1 - Power(LStep, 2)) - 1) + BeginValue
  else
    Result := ChangeValue / 2 * (Sqrt(1 - Power(LStep - 2, 2)) + 1) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseOutInCirc: Single;
// *** OUTINCIRC ***
begin
  if Step < Duration / 2 then
    Result := - ChangeValue / 2 * (Sqrt(1 - Power(2 * Step / Duration, 2)) - 1)
      + BeginValue
  else
    Result := - ChangeValue / 2 * (Sqrt(1 - Power((2 * Step - Duration) / Duration, 2))
      - 1) + BeginValue + ChangeValue / 2;
end;

function TGVCustomEasingInterpolation.EaseInElastic(AStart, AEnd, AStep: Single):
  Single;
// *** INELASTIC ***
var
  LStep: Single;
begin
  if AStep = 0 then
    Result := AStart
  else
  if AStep = Duration then
    Result := AStart + AEnd
  else
  begin
    LStep := AStep / Duration - 1;
    Result := - (AEnd * Power(2, 10 * LStep) * sin((LStep * Duration
      - (Duration * 0.3 / 4)) * 2 * Pi / (Duration * 0.3))) + AStart;
  end;
end;

function TGVCustomEasingInterpolation.EaseOutElastic(AStart, AEnd, AStep: Single
  ): Single;
// *** OUTELASTIC ***
begin
  if AStep = 0 then
    Result := AStart
  else
  if AStep = Duration then
    Result := AEnd + AStart
  else
  begin
    Result := AEnd * Power(2, - 10 * AStep / Duration) *
      sin((AStep - (Duration * 0.3 / 4)) * 2 * Pi / (Duration * 0.3)) +
      AEnd + AStart;
  end;
end;

function TGVCustomEasingInterpolation.EaseInOutElastic: Single;
// *** INOUTELASTIC ***
var
  LStep: Single;
begin
  if Step = 0 then
    Result := BeginValue
  else
  if Step = Duration then
    Result := BeginValue + ChangeValue
  else
  begin
    LStep := Step / Duration * 2;
    // 0.45 = 0.3*1.5  0.1125 = 0.45 / 4
    if LStep < 1 then
    begin
      LStep := LStep - 1;
      Result := - (ChangeValue * Power(2, 10 * LStep) * sin((LStep * Duration -
        Duration * 0.1125) * 2 * Pi / (Duration * 0.45))) / 2 + BeginValue;
    end
    else
    begin
      LStep := LStep - 1;
      Result :=  ChangeValue * Power(2, - 10 * LStep) * sin((LStep * Duration -
        Duration * 0.1125) * 2 * Pi / (Duration * 0.45)) / 2 + ChangeValue + BeginValue;
    end;
  end;
end;

function TGVCustomEasingInterpolation.EaseOutInElastic: Single;
// *** OUTINELASTIC ***
begin
  if Step < Duration / 2 then
    Result := EaseOutElastic(BeginValue, ChangeValue / 2, Step * 2)
  else
    Result := EaseInElastic(BeginValue + ChangeValue / 2, ChangeValue / 2,
      Step * 2 - Duration);
end;

function TGVCustomEasingInterpolation.EaseInBack: Single;
// *** INBACK ***
begin
  // 1.70158 = 10% 2.592389 = 20% 3.394051 = 30% 4.15574465 = 40%
  Result := ChangeValue * Power(Step / Duration, 2) * (2.70158 *
    Step / Duration - 1.70158) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseOutBack: Single;
// *** OUTBACK ***
begin
  Result := ChangeValue * (Power(Step / Duration - 1, 2) *
    (2.70158 * (Step / Duration - 1) + 1.70158) + 1) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseInOutBack: Single;
// *** INOUTBACK ***
var
  LStep: Single;
begin
  // 2.5949095 = 1.70158 * 1.525
  LStep := Step / Duration * 2;
  if LStep < 1 then
    Result := ChangeValue / 2 * Power(LStep, 2) * (3.594905 * LStep - 2.594905)
      + BeginValue
  else
    Result := ChangeValue / 2 * (Power(LStep - 2, 2) * (3.594905 * (LStep - 2)
      + 2.594905) + 2) + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseOutInBack: Single;
// *** OUTINBACK ***
begin
  if Step < Duration / 2 then
    Result := ChangeValue / 2 * (Power(2 * Step / Duration - 1, 2) *
     (2.70158 * (2 * Step / Duration - 1) + 1.70158) + 1) + BeginValue
  else
    Result := ChangeValue / 2 * Power((2 * Step - Duration) / Duration, 2) * (2.70158 *
    (2 * Step - Duration) / Duration - 1.70158) + BeginValue + ChangeValue / 2;
end;

function TGVCustomEasingInterpolation.EaseInOutBounce: Single;
// *** INOUTBOUNCE ***
begin
  if Step < Duration / 2 then
    Result := EaseInBounce(0, ChangeValue, Step * 2) / 2 + BeginValue
  else
    Result := EaseOutBounce(0, ChangeValue, Step * 2 - Duration) / 2 +
      ChangeValue / 2  + BeginValue;
end;

function TGVCustomEasingInterpolation.EaseOutInBounce: Single;
// *** OUTINBOUNCE ***
begin
  if Step < Duration / 2 then
    Result := EaseOutBounce(BeginValue, ChangeValue / 2, Step * 2)
  else
    Result := EaseInBounce(BeginValue + ChangeValue / 2, ChangeValue / 2,
      Step * 2 - Duration);
end;

function TGVCustomEasingInterpolation.EaseInBounce(AStart, AEnd, AStep: Single
  ): Single;
// *** INBOUNCE ***
begin
  Result := AEnd - EaseOutBounce(0, AEnd, Duration - AStep) + AStart;
end;

function TGVCustomEasingInterpolation.EaseOutBounce(AStart, AEnd, AStep: Single): Single;
// *** OUTBOUNCE ***
var
  LStep: Single;
begin
  LStep := AStep / Duration;
  if LStep < 1 / 2.75 then
    Result := AEnd * 7.5625 * Power(LStep, 2) + AStart
  else
  if LStep < 2 / 2.75 then
  begin
    LStep := LStep - 1.5 / 2.75;
    Result := AEnd * (7.5625 * Power(LStep, 2) + 0.75) + AStart;
  end
  else
  if LStep < 2.5 / 2.75 then
  begin
    LStep := LStep - 2.25 / 2.75;
    Result := AEnd * (7.5625 * Power(LStep, 2) + 0.9375) + AStart;
  end
  else
  begin
    LStep := LStep - 2.625 / 2.75;
    Result := AEnd * (7.5625 * Power(LStep, 2) + 0.984375) + AStart;
  end;
end;

function TGVCustomEasingInterpolation.InterpolationResult(AStep: Single
  ): Single;
// *** résultat d'une étape ***
begin
  Step := AStep;
  Result := DoResult;
end;

function TGVCustomEasingInterpolation.DoResult: Single;
// *** effectue l'interpolation ***
begin
  Result := 0;
  BeginUpdate;
  try
    case Interpolation of
      // *** linéaire ***
      intLinear: Result := ChangeValue * Step / Duration + BeginValue;
      // *** quadratique ***
      intEaseInQuad: Result := InPower(2);
      intEaseOutQuad: Result := OutPower(2);
      intEaseInOutQuad: Result := InOutPower(2);
      intEaseOutInQuad: Result := OutInPower(2);
      // *** cubique ***
      intEaseInCubic: Result := InPower(3);
      intEaseOutCubic: Result := OutPower(3);
      intEaseInOutCubic: Result := InOutPower(3);
      intEaseOutInCubic: Result := OutInPower(3);
      // *** quartique ***
      intEaseInQuart: Result := InPower(4) ;
      intEaseOutQuart: Result := OutPower(4);
      intEaseInOutQuart:  Result := InOutPower(4);
      intEaseOutInQuart: Result := OutInPower(4);
      // *** quintique ***
      intEaseInQuint: Result := InPower(5);
      intEaseOutQuint: Result := OutPower(5);
      intEaseInOutQuint:  Result := InOutPower(5);
      intEaseOutInQuint: Result := OutInPower(5);
      // *** sinus ***
      intEaseInSine: Result := EaseInSine;
      intEaseOutSine: Result := EaseOutSine;
      intEaseInOutSine: Result := EaseInOutSine;
      intEaseOutInSine: Result := EaseOutInSine;
      // *** exponentielle ***
      intEaseInExpo: Result := EaseInExpo;
      intEaseOutExpo: Result := EaseOutExpo;
      intEaseInOutExpo: Result := EaseInOutExpo;
      intEaseOutInExpo: Result := EaseOutInExpo;
      // *** cercle ***
      intEaseInCirc: Result := EaseInCirc;
      intEaseOutCirc: Result := EaseOutCirc;
      intEaseInOutCirc: Result := EaseInOutCirc;
      intEaseOutInCirc: Result := EaseOutInCirc;
      // *** élastique ***
      intEaseInElastic: Result := EaseInElastic(BeginValue, ChangeValue, Step);
      intEaseOutElastic: Result := EaseOutElastic(BeginValue, ChangeValue, Step);
      intEaseInOutElastic: Result := EaseInOutElastic;
      intEaseOutInElastic: Result := EaseOutInElastic;
      // *** retour ***
      intEaseInBack: Result := EaseInBack;
      intEaseOutBack: Result := EaseOutBack;
      intEaseInOutBack: Result := EaseInOutBack;
      intEaseOutInBack: Result := EaseOutInBack;
      // *** rebond ***
      intEaseInBounce: Result := EaseInBounce(BeginValue, ChangeValue, Step);
      intEaseOutBounce: Result := EaseOutBounce(BeginValue, ChangeValue, Step);
      intEaseInOutBounce: Result := EaseInOutBounce;
      intEaseOutInBounce: Result := EaseOutInBounce;
    end;
  if Reverse then
    Result := ChangeValue - Result;
  finally
    EndUpdate;
  end;
end;

Nous avons essayé de simplifier les routines en limitant les paramètres nécessaires à leur invocation. Seules les fonctions d'easing les plus complexes ou les plus générales requièrent des paramètres à cause des valeurs multiples qu'elles manipulent.

La méthode DoResult est enfin implémentée : comme nous pouvions nous y attendre, elle fonctionne à la manière d'un poste d'aiguillage afin de renvoyer vers la bonne routine d'exécution d'une routine d'easing. Elle intègre par ailleurs la prise en charge de la propriété Reverse qui prend en charge l'inversion des points de début et de fin d'interpolation.

La fonction InterpolationResult qui renvoie à cette méthode servira dès qu'il s'agira de déterminer le résultat de l'interpolation pour une étape donnée : le pas est fixé avant d'appeler DoResult.

II-D. La classe TGVFloatInterpolation

La classe TGVFloatInterpolation est le genre de classe que nous aimons rencontrer puisqu'elle se contente de rendre publique une série de propriétés de son ancêtre TGVCustomInterpolation :

 
Sélectionnez
{ TGVFloatInterpolation }

  TGVFloatInterpolation = class(TGVCustomEasingInterpolation)
  public
    property Interpolation;
    property BeginValue;
    property ChangeValue;
    property Duration;
    property Result;
    property Step;
  end;

Cette classe ne figure par conséquent pas dans la partie implementation de l'unité. C'est bien son ancêtre qui lui fournit par héritage pratiquement toutes les fonctionnalités dont elle a besoin !

II-E. La classe TGVIntInterpolation

La dernière classe à nous intéresser est similaire à celle qui précède à ceci près qu'elle traite les données comme des entiers. Cette transformation passe par une série de redéfinitions qu'indique l'interface reproduite ci-après :

 
Sélectionnez
{ TGVIntInterpolation }

  TGVIntInterpolation = class(TGVFloatInterpolation)
  strict private
    function GetBeginValue: Integer;
    function GetChangeValue: Integer;
    function GetDuration: Integer;
    function GetRes: Integer;
    function GetStep: Integer;
    procedure SetBeginValue(AValue: Integer);
    procedure SetChangeValue(AValue: Integer);
    procedure SetDuration(AValue: Integer);
    procedure SetStep(AValue: Integer);
  public
    function InterpolationResult(AStep: Integer): Integer;
    property Result: Integer read GetRes;
    property Duration: Integer read GetDuration write SetDuration default C_DefaultDuration;
    property Step: Integer read GetStep write SetStep;
    property BeginValue: Integer read GetBeginValue write SetBeginValue;
    property ChangeValue: Integer read GetChangeValue write SetChangeValue;
  end;

L'implémentation de ses différentes méthodes propres n'est pas complexe si l'on fait intervenir l'héritage. Voici une proposition de code :

 
Sélectionnez
{ TGVIntInterpolation }

function TGVIntInterpolation.GetBeginValue: Integer;
// *** valeur de départ ***
begin
  Result := Round(inherited BeginValue);
end;

function TGVIntInterpolation.GetChangeValue: Integer;
// ** changement attendu ***
begin
  Result := Round(inherited ChangeValue);
end;

function TGVIntInterpolation.GetDuration: Integer;
// *** durée ***
begin
  Result := Round(inherited Duration);
end;

function TGVIntInterpolation.GetRes: Integer;
// *** résultat de l'étape de l'interpolation ***
begin
  Result := Round(DoResult);
end;

function TGVIntInterpolation.GetStep: Integer;
// *** étape en cours ***
begin
  Result := Round(inherited Step);
end;

procedure TGVIntInterpolation.SetBeginValue(AValue: Integer);
// *** valeur de démarrage ***
begin
  if Round(inherited BeginValue) = AValue then
    Exit;
  inherited SetBeginValue(AValue);
end;

procedure TGVIntInterpolation.SetChangeValue(AValue: Integer);
// *** changement attendu ***
begin
  if Round(inherited ChangeValue) = AValue then
    Exit;
  inherited SetChangeValue(AValue);
end;

procedure TGVIntInterpolation.SetDuration(AValue: Integer);
// *** durée de l'interpolation ***
begin
  if Round(inherited Duration) = AValue then
    Exit;
  inherited SetDuration(AValue);
end;

procedure TGVIntInterpolation.SetStep(AValue: Integer);
// *** pas d'une interpolation ***
begin
  if Round(inherited Step) = AValue then
    Exit;
  inherited SetStep(AValue);
end;

function TGVIntInterpolation.InterpolationResult(AStep: Integer): Integer;
// *** résultat d'une étape ***
begin
  Step := AStep;
  Result := Round(DoResult);
end;

D'aucuns penseront que cette classe est superflue. Une version très simplifiée aurait en effet consisté à utiliser un arrondi à un entier (avec la fonction Round) appliqué à l'unique résultat de l'interpolation. Nous avons préféré garder un contrôle complet des entrées, interdisant ainsi toute utilisation de valeurs flottantes pour une classe limitée à des entiers.

III. Deux applications de test

[Exemples Interpolations 04 et 05]

L'objectif de ces applications de test est de montrer les classes définies en action. Nous voulons choisir une courbe d'easing pour l'appliquer à un contrôle (ici, un bouton). Nous aimerions aussi voir toutes les fonctions à l’œuvre en un test continu.

Entre les deux applications, seules les parties métier seront différentes : la première utilisera des boucles alors que la seconde fera appel à un TTimer.

Il est préférable d'utiliser le modèle de la seconde application avec un composant TTimer, en particulier sous Linux. Les résultats sont plus fluides qu'avec une boucle. Notez que cette dernière doit toujours inclure un Application.ProcessMessages pour le traitement des événements (dont l'affichage !).

III-A. L'interface utilisateur

L'interface utilisateur des deux applications pourra ressembler à ceci :

Image non disponible

La fiche principale baptisée MainForm comprend :

  • un composant TPanel renommé pnlDraw qui accueille lui-même deux TButton (btnFloat pour les interpolations flottantes et btnInt pour les interpolations entières) ;
  • un composant TComboBox nommé cboxInterpolations qui contiendra la liste des interpolations disponibles ;
  • un composant TButton nommé btnTest qui permettra de tester toutes les interpolations en un unique clic.

Les interactions avec cette interface se feront avec des clics sur les boutons ou sur le panneau. Dans le dernier cas, ce sont les deux boutons (valeur flottante et valeur entière) qui seront activés.

Quatre méthodes seront communes à nos applications :

  • création de la fiche principale ;
  • destruction de la fiche principale ;
  • redimensionnement de la fiche principale ;
  • changement dans la boîte de choix de l'interpolation.

Ainsi, après avoir déclaré fInter (une variable de type TGVIntInterpolation), et fFloat (une variable de type TGVFloatInterpolation), dans la partie privée de la classe de la fiche principale, nous pourrons écrire :

 
Sélectionnez
uses
  interpolationstypes;

{ TMainForm }

procedure TMainForm.FormCreate(Sender: TObject);
// *** création de la fiche ***
var
  LSt: string;
begin
  fInter := TGVIntInterpolation.Create;
  fFloat := TGVFloatInterpolation.Create;
  cboxInterpolations.Items.Clear;
  for LSt in C_InterpolationToStr do
    cboxInterpolations.Items.Add(LSt);
  cboxInterpolations.ItemIndex := Ord(TGVInterpolationType(fInter.Interpolation));
  btnInt.Left := 0;
  btnFloat.Left := 0;
end;

// […]

procedure TMainForm.FormDestroy(Sender: TObject);
// *** destruction de la fiche ***
begin
  fInter.Free;
  fFloat.Free;
end;

procedure TMainForm.FormResize(Sender: TObject);
// *** fiche redimensionnée ***
begin
  btnFloat.Left := 0;
  btnInt.Left := 0;
end;

Le code de redimensionnement est là pour pouvoir réinitialiser correctement les paramètres de début des interpolations.

En dehors de la création des objets nécessaires, nous avons rempli la liste des choix possibles d'interpolations à partir des valeurs contenues dans l'unité interpolationstypes à mentionner dans la clause uses de la partie implementation de l'unité.

Enfin, le changement de choix dans la boîte cboxInterpolations déclenchera le gestionnaire suivant :

 
Sélectionnez
procedure TMainForm.cboxInterpolationsChange(Sender: TObject);
// *** changement d'interpolation ***
begin
  btnInt.Left := 0;
  btnFloat.Left := 0;
  fInter.Interpolation := TGVInterpolationType(cboxInterpolations.ItemIndex);
  fFloat.Interpolation := fInter.Interpolation;
end;

Il s'agit d'une simple mise à jour des boutons et des objets d'interpolation.

Le fichier LFM correspondant à l'interface est celui-ci :

 
Sélectionnez
object MainForm: TMainForm
  Left = 269
  Height = 188
  Top = 192
  Width = 715
  Caption = 'Test des interpolations (easing) - G. Vasseur 2018'
  ClientHeight = 188
  ClientWidth = 715
  OnCreate = FormCreate
  OnDestroy = FormDestroy
  OnResize = FormResize
  LCLVersion = '1.8.2.0'
  object pnlDraw: TPanel
    Left = 0
    Height = 136
    Top = 0
    Width = 715
    Align = alTop
    ClientHeight = 136
    ClientWidth = 715
    TabOrder = 0
    OnClick = pnlDrawClick
    object btnFloat: TButton
      Left = 16
      Height = 57
      Top = 8
      Width = 75
      Caption = '<=Float=>'
      OnClick = btnFloatClick
      TabOrder = 0
    end
    object btnInt: TButton
      Left = 16
      Height = 57
      Top = 72
      Width = 75
      Caption = '<=Int=>'
      OnClick = btnIntClick
      TabOrder = 1
    end
  end
  object cboxInterpolations: TComboBox
    Left = 16
    Height = 23
    Top = 144
    Width = 208
    ItemHeight = 15
    OnChange = cboxInterpolationsChange
    TabOrder = 1
    Text = 'cboxInterpolations'
  end
  object btnTest: TButton
    Left = 624
    Height = 25
    Top = 144
    Width = 75
    Caption = 'Test'
    OnClick = btnTestClick
    TabOrder = 2
  end
end

L'interface étant établie, il ne nous reste qu'à rédiger la partie métier de nos applications élémentaires.

III-B. La partie métier de l'application avec boucles

En ce qui concerne la version avec boucles, chaque gestionnaire de OnClick des contrôles affectés comprendra une boucle pour réaliser les étapes nécessaires à l'interpolation en cours.

Les gestionnaires pour les boutons à valeur flottante ou à valeur entière seront très proches :

 
Sélectionnez
procedure TMainForm.btnFloatClick(Sender: TObject);
// *** interpolation flottante ***
var
  Li: Integer;
begin
  fFloat.Reverse :=  (btnFloat.Left <> 0);
  fFloat.BeginValue := 0;
  fFloat.ChangeValue := pnlDraw.ClientWidth - btnFloat.Width;
  for Li := 1 to Round(fFloat.Duration) do
  begin
    btnFloat.Left := Round(fFloat.InterpolationResult(Li));
    sleep(10);
    pnlDraw.Repaint;
    Application.ProcessMessages;
  end;
end;

procedure TMainForm.btnIntClick(Sender: TObject);
// *** interpolation entière ***
var
  Li: Integer;
begin
  fInter.Reverse :=  (btnInt.Left <> 0);
  fInter.BeginValue := 0;
  fInter.ChangeValue := pnlDraw.ClientWidth - btnInt.Width;
  for Li := 1 to fInter.Duration do
  begin
    btnInt.Left := fInter.InterpolationResult(Li);
    sleep(10);
    pnlDraw.Repaint;
    Application.ProcessMessages;
  end;
end;

Le gestionnaire du clic sur le panneau regroupera les deux précédents en une seule méthode :

 
Sélectionnez
procedure TMainForm.pnlDrawClick(Sender: TObject);
// *** clic sur le panneau de dessin ***
var
  Li: Integer;
begin
  fInter.Reverse :=  (btnInt.Left <> 0);
  fInter.BeginValue := 0;
  fInter.ChangeValue := pnlDraw.ClientWidth - btnInt.Width;
  fFloat.Reverse :=  (btnFloat.Left <> 0);
  fFloat.BeginValue := 0;
  fFloat.ChangeValue := pnlDraw.ClientWidth - btnFloat.Width;
  for Li := 1 to fInter.Duration do
  begin
    btnFloat.Left := Round(fFloat.InterpolationResult(Li));
    btnInt.Left := fInter.InterpolationResult(Li);
    sleep(10);
    pnlDraw.Repaint;
    Application.ProcessMessages;
  end;
end;

Enfin, le gestionnaire du clic sur le bouton de test parcourra la liste des interpolations et appellera pour chacune d'elles la méthode précédente :

 
Sélectionnez
procedure TMainForm.btnTestClick(Sender: TObject);
// *** test ***
var
  LInter: TGVInterpolationType;
begin
  cboxInterpolations.Enabled := False;
  btnTest.Enabled := False;
  pnlDraw.Enabled := False;
  try
    for LInter := Low(TGVInterpolationType) to High(TGVInterpolationType) do
    begin
      fInter.Interpolation := LInter;
      fFloat.Interpolation := LInter;
      cboxInterpolations.ItemIndex := Ord(LInter);
      pnlDrawClick(Sender);
    end;
  finally
    cboxInterpolations.Enabled := True;
    btnTest.Enabled := True;
    pnlDraw.Enabled := True;
  end;
end;

L'ensemble a le mérite de la simplicité, mais, comme nous l'avons déjà indiqué, c'est au détriment de l'efficacité.

III-C. La partie métier de l'application utilisant un timer

Utiliser un composant TTimer permet de mieux profiter de la programmation événementielle ramenée dans l'exemple précédent à un traitement séquentiel au sein d'une boucle. Si ce mélange des genres a permis d'élaborer un exemple facile à programmer, les problèmes d'affichage ne manquent pas de se poser, soit par une fluidité à revoir, soit par un blocage complet (essayez par exemple de supprimer sous Linux l'appel à Application.ProcessMessages).

IV. Conclusion

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2018 Gilles Vasseur. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.