Les interpolations avec Lazarus (2/3)

Des classes pour les interpolations

Gilles Vasseur

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.

- Titre : Les interpolations avec Lazarus (2/3)

- Auteur : Gilles Vasseur

- Parution : 29 juin 2018

- Public visé :

- Licence : 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 et 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.

Sommaire

- I. Introduction

- II. Les classes d'interpolation

- II-A. L'unité interpolationstypes

- II-B. La classe de base TGVBaseInterpolation

- II-C. La classe TGVCustomEasingInterpolation

- II-D. La classe TGVFloatInterpolation

- II-E. La classe TGVIntInterpolation

- III. Deux applications de test

- III-A. L'interface utilisateur

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

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

- IV. Conclusion

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é :

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 :

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 :

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 :

{ 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 :

{ 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 :

{ 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 :

{ 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 :

{ 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 :

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 :

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 :

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 :

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 :

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 :

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 :

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