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).