Effets de transition avec Lazarus et BGRABitmap

7. Le composant TGVTransition complet

Gilles Vasseur

Après l'étude de nombreuses transitions, nous avons commencé à implémenter le composant TGVTransition : les interpolations, l'opacité et la vitesse devraient être maîtrisées ; la structure générale du composant a été fixée. Ce tutoriel propose à présent une implémentation complète des fonctionnalités attendues pour des transitions d'image à image avec la bibliothèque BGRABitmap installée dans Lazarus.

Commentez

- Titre : Effets de transition avec Lazarus et BGRABitmap

- Auteur : Gilles Vasseur

- Parution : 1 juillet 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. Avant-propos

- II. Création du paquet et squelette du composant

- III. Ajout des propriétés essentielles

- III-A. Les nouvelles propriétés actives

- III-B. Le traitement des bandes

- III-C. Les interpolations

- III-D. Le lissage des transitions

- III-E. L'opacité

- III-F. Le constructeur de la classe

- IV. Ajout des images et du timer

- IV-A. Les images

- IV-B. Quelques améliorations de la base du composant

- IV-C. Le TTimer

- V. La méthode Paint et les procédures de calculs

- V-A. La méthode Paint

- V-B. Les méthodes de travail sur les images

- VI. Les événements et les états

- VI-A. Les gestionnaires d'événements

- VI-B. Le traitement des états

- VI-C. Les états en action

- VII. Le composant complet

- VII-A. L'implémentation des transitions

- VII-B. Les méthodes de traitement des images

- VII-C. Finition de la classe

- VIII. Conclusion

I - Avant-propos

Si vous voulez suivre le tutoriel en construisant pas à pas le composant, il faut créer le paquet et un composant selon la méthode expliquée dans ce tutoriel. Les informations nécessaires pour renseigner les caractéristiques du paquet et du composant sont fournies au cours de ce chapitre. Notez cependant que le paquet est à la fois un paquet de conception et d'exécution.

Les paquets d'exécution contiennent toutes les unités dont l'application a besoin pour fonctionner.
Les paquets de conception contiennent les déclarations pour l'enregistrement des composants et des éditeurs de propriétés qui seront intégrés à l'EDI.
Les paquets de conception et exécution comprennent les deux précédents.

Si vous voulez concentrer votre attention sur les problèmes liés au sujet des transitions d'image à image, vous pouvez partir du squelette fourni dans le premier répertoire d'exemple. Notez que le paquet BGRABitmapPack fait partie des paquets requis et qu'il faut par conséquent l'ajouter au paquet en cours de construction.

Afin de ne pas alourdir inutilement le texte qui suit, le code de toutes les méthodes n'est pas fourni. Seules les plus intéressantes sont détaillées. Il vous est donc demandé de vous reporter aux fichiers proposés à chaque étape pour voir la totalité des changements opérés.

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

II - Création du paquet et squelette du composant

[Exemple BGRABitmap 45]

Le squelette du composant s'appuie sur le fichier transitionstypes.pas déjà décrit dans le tutoriel précédent et qui contient les déclarations indispensables à son bon fonctionnement. Il fait de même appel au fichier defines.inc qui, comme annoncé, contient les directives de compilation. Ces deux fichiers sont disponibles dans le répertoire des exemples.

La première décision à prendre en matière de nouveau composant est de choisir l'ancêtre le plus adapté. Dans notre cas, il faut partir d'un contrôle graphique puisque l'affichage des images est au cœur des fonctionnalités offertes. La classe TGraphicControl est la candidate retenue : servant de base aux composants tels que TImage ou TPaintBox, elle a les fonctionnalités requises, en particulier un canevas pour le dessin.

Nous pouvions aussi envisager de fonder notre composant sur un composant comme TPanel (ou son ancêtre immédiat TCustomPanel) qui présente l’avantage d'offrir d'origine une surface opaque et la possibilité d'intégrer d'autres composants, mais qui peut paraître trop général pour se spécialiser dans le dessin.

La deuxième décision concerne l'icône associée au composant pour son affichage dans la palette. Ici, nous l'avons intégrée dans un fichier .lrs nommé transitions.lrs et appelé dans la partie initialization de l'unité d'accueil du composant. Cette icône est contenue dans le fichier initial nommé TGVTransition.png.

La dernière décision consiste à choisir les propriétés héritées des ancêtres et que nous souhaitons rendre publiques. Il ne faudrait en effet pas que l'utilisateur ait accès à des propriétés inexploitables, voire nocives !

Les ancêtres possèdent souvent des propriétés cachées par la directive protected que vous pouvez rendre accessibles à la demande en les énumérant, précédées de property, dans une partie public ou published.

Comme un constructeur et un destructeur seront nécessaires, ne serait-ce que pour instancier et détruire les images de travail, nous prévoirons aussi les squelettes des méthodes spéciales Create et Destroy.

Ensuite, nous compléterons notre classe avec quelques fonctions utiles : une fonction statique de classe Copyright pour le copyright, une fonction Count pour renvoyer le nombre de transitions disponibles et une propriété à lecture seule Version pour récupérer la version en cours du composant. Dans la section protected, nous avons aussi ajouté une procédure virtuelle appelée Change afin de centraliser les modifications apportées à l'état du composant. Le corps de cette procédure est pour l'instant vide, mais il sera évidemment renseigné au moment de l'implémentation des notifications.

Enfin, dans la partie interface de l'unité, nous déclarerons une procédure Register pour l'enregistrement de notre composant. Son implémentation sera présente dans la partie implementation.

Voici le code proposé :

unit transitions;

{$I defines.inc}

interface

uses
Classes, SysUtils, Controls, ExtCtrls, Graphics, Types,
BGRABitmapTypes, BGRABitmap,
transitionstypes; // liste déjà complète, même si inutile à ce stade

type

{ TGVTransition }

TGVTransition = class(TGraphicControl)
strict private
function GetVersion: string;
protected
procedure Change; virtual;
public
// copyright du composant
class function Copyright: string; static;
// création du composant
constructor Create(AOwner: TComponent); override;
// destruction du composant
destructor Destroy; override;
// nombre de transitions
function Count: Integer;
published
// propriétés héritées
property Color;
property ParentColor;
property Align;
property Anchors;
property AutoSize;
property BorderSpacing;
property Constraints;
property DragCursor;
property DragMode;
property Enabled;
property OnChangeBounds;
property OnClick;
property OnDblClick;
property OnDragDrop;
property OnDragOver;
property OnEndDrag;
property OnMouseDown;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseMove;
property OnMouseUp;
property OnMouseWheel;
property OnMouseWheelDown;
property OnMouseWheelUp;
property OnPaint;
property OnResize;
property OnStartDrag;
property ParentShowHint;
property PopupMenu;
property ShowHint;
property Visible;

// version du composant
property Version: string read GetVersion;

end;

procedure Register;

implementation

uses
LResources;

procedure Register;
begin
RegisterComponents('gvsoft', [TGVTransition]);
end;

{ TTransition }

function TGVTransition.GetVersion: string;
// *** version du composant ***
begin
Result := C_GVVersionSt;
end;

constructor TGVTransition.Create(AOwner: TComponent);
// *** construction du composant ***
begin
inherited Create(AOwner);
end;

destructor TGVTransition.Destroy;
// *** destruction du composant ***
begin
inherited Destroy;
end;

function TGVTransition.Count: Integer;
// *** nombre de transitions ***
begin
Result := Ord(High(TGVTransitionType)) + 1;
end;

procedure TGVTransition.Change;
// *** notification de changement d'état ***
begin

end;

class function TGVTransition.Copyright: string;
// *** copyright ***
begin
Result := C_GVVersionSt +  ' - © Gilles Vasseur';
end;

initialization
{$I transitions.lrs}
end.

Si ce code est compilable, il ne fournit aucune fonctionnalité vraiment intéressante. À nous de le compléter !

Vous avez des doutes sur l'utilisation de private, protected, public, published et leurs dérivés ? Je vous conseille la lecture de mon tutoriel sur la programmation orientée objet, en particulier la partie concernant la notion de portée.

III - Ajout des propriétés essentielles

Nous allons à présent déclarer et implémenter les propriétés essentielles, qui ont pour la plupart déjà été décrites précédemment.

[Exemple BGRABitmap 46]

III-A - Les nouvelles propriétés actives

La partie interface de l'unité du composant indique précisément les déclarations à faire :

interface

uses
Classes, SysUtils, Controls, ExtCtrls, Graphics, Types,
BGRABitmapTypes, BGRABitmap,
transitionstypes;

type

TGVTransitionStateEvent = procedure(Sender: TObject; State: TGVTransitionState;
Trans: TGVTransitionType; Step: Integer) of object;

{ TGVTransition }

TGVTransition = class(TGraphicControl)
strict private
fSmooth: TGVSmooth;
fSpecialEffect: TGVSpecialEffect;
fAutoStrips: boolean;
fHorizontalStrips, fVerticalStrips: Integer;
fInterpolation: TGVInterpolation;
fOpacityState: TGVOpacityState;
fSpeed: Integer;
fTransition: TGVTransitionType;
fStep: Integer;
fInc: Integer;
fLastLoop: Integer;
function GetVersion: string;
procedure SetAutoStrips(AValue: boolean);
procedure SetHorizontalStrips(AValue: Integer);
procedure SetInterpolation(AValue: TGVInterpolation);
procedure SetOpacityState(AValue: TGVOpacityState);
procedure SetSmooth(AValue: TGVSmooth);
procedure SetSpecialEffect(AValue: TGVSpecialEffect);
procedure SetSpeed(AValue: Integer);
procedure SetTransition(AValue: TGVTransitionType);
procedure SetVerticalStrips(AValue: Integer);
function DestinationOpacity: Integer; inline;
function SourceOpacity: Integer; inline;
protected
function ComputeInterpolation(AStart, AEnd: Single): Single; virtual;
function ComputeInterpolationInt(AStart, AEnd: Single): Integer;
procedure Change; virtual;

// liste des transitions

public
// copyright du composant
class function Copyright: string; static;
// créatiton du composant
constructor Create(AOwner: TComponent); override;
// destruction du composant
destructor Destroy; override;
// nombre de transitions
function Count: Integer;
// opacité en cours (de 0 à 255)
function GetOpacity(Up: Boolean = True): Integer;
// calcul du nombre de bandes pour ASize
function NumberOfStrips(ASize: Integer): Integer;
// étape en cours
property Step: Integer read fStep;

published
// propriétés héritées
// […] voir le listing précédent pour la liste de ces propriétés

// version du composant
property Version: string read GetVersion;
// interpolation d'image à image
property Interpolation: TGVInterpolation read fInterpolation
write SetInterpolation default intLinear;
// vitesse de transition
property Speed: Integer read fSpeed write SetSpeed default C_DefaultSpeed;
// type d'opacité utilisée
property Opacity: TGVOpacityState
read fOpacityState write SetOpacityState default opaBoth;
// type de transition
property Transition: TGVTransitionType
read fTransition write SetTransition default traNone;
// mode automatique de calcul du nombre de bandes
property AutoStrips: Boolean read fAutoStrips write SetAutoStrips;
// nombre de bandes horizontales (modifiable si AutoStrips est à False)
property HorizontalStrips: Integer read fHorizontalStrips
write SetHorizontalStrips;
// nombre de bandes verticales (modifiable si AutoStrips est à False)
property VerticalStrips: Integer read fVerticalStrips write SetVerticalStrips;
// effets spéciaux
property SpecialEffect: TGVSpecialEffect read fSpecialEffect write SetSpecialEffect default speNone;
// lissage des transitions
property Smooth: TGVSmooth read fSmooth write SetSmooth default smoMedium;
end;

Nous reconnaissons ce qui va nous permettre de traiter l'opacité, le nombre de bandes de certaines transitions, les interpolations, la vitesse, les effets spéciaux, le lissage des transitions et, bien sûr, le type de transition en cours.

D'une manière générale, il s'agit surtout de setters et de getters, les premiers ayant la plupart du temps une structure proche de celle-ci :

procedure TGVTransition.SetInterpolation(AValue: TGVInterpolation);
// *** détermination de l'interpolation ***
begin
if fInterpolation = AValue then
exit;
fInterpolation := AValue;
Change;
end;

Après avoir évité un changement sans intérêt, nous mettons à jour le champ visé et nous notifions le changement de l'état du composant par le biais de la méthode Change (toujours vide pour le moment).

III-B - Le traitement des bandes

Les variantes concernent quelques propriétés plus complexes. Ainsi, le traitement des bandes nécessite plusieurs méthodes listées ci-après :

procedure TGVTransition.SetAutoStrips(AValue: boolean);
// *** calcul automatique du nombre de bandes ***
begin
if fAutoStrips = AValue then
exit;
fAutoStrips := AValue;
if fAutoStrips then
begin
fHorizontalStrips := NumberOfStrips(ClientHeight);
fVerticalStrips := NumberOfStrips(ClientWidth);
end;
end;

procedure TGVTransition.SetHorizontalStrips(AValue: Integer);
// *** nombre de bandes horizontales ***
begin
if (fHorizontalStrips = AValue) or AutoStrips then
exit;
fHorizontalStrips := Max(AValue, C_MinHorizontalStrips);
Change;
end;

procedure TGVTransition.SetVerticalStrips(AValue: Integer);
// *** nombre de bandes verticales ***
begin
if (fVerticalStrips = AValue) or AutoStrips then
exit;
fVerticalStrips := Max(AValue, C_MinVerticalStrips);
Change;
end;

function TGVTransition.NumberOfStrips(ASize: Integer): Integer;
// *** calcul automatique du nombre de bandes ***
begin
if ASize < C_MinComputeSize then
Result := 1
else
if ASize < (C_MaxComputeSize div 5) then
Result := 5
else
if ASize < (C_MaxComputeSize div 2) then
Result := 10
else
if ASize < C_MaxComputeSize then
Result := ASize div C_DefaultStripWidth
else
Result := 30;
end;

Que ce soit de manière automatique ou par prise en compte de la demande de l'utilisateur, des calculs sont en effet nécessaires pour adapter le nombre de bandes à la taille des images. La méthode essentielle est la fonction NumberOfStrips qui fait les calculs pour un résultat automatisé le plus pertinent possible.

III-C - Les interpolations

Sans surprise, les interpolations reprennent quant à elles les schémas construits dans le tutoriel précédent :

procedure TGVTransition.SetInterpolation(AValue: TGVInterpolation);
// *** détermination de l'interpolation ***
begin
if fInterpolation = AValue then
exit;
fInterpolation := AValue;
Change;
end;

function TGVTransition.ComputeInterpolation(AStart, AEnd: Single): Single;
// *** calcul d'une interpolation ***
var
LDiff: Single;
LSign, LStep: Integer;

function Exponant(AExp: byte): Single; inline;
begin
Result := LDiff * Power(LStep, AExp) / Power(100, AExp);
end;

function DownExponant(AExp: byte): Single; inline;
begin
Result := LDiff - LDiff * Power(100 - LStep, AExp) /
Power(100, AExp);
end;

begin
LDiff := Abs(AEnd - AStart);
if AStart > AEnd then
LSign := -1
else
LSign := 1;
LStep := fStep * 100 div fLastLoop;
case Interpolation of
intLinear: Result := Exponant(1);
intQuadratic: Result := Exponant(2);
intCubic: Result := Exponant(3);
intQuartic: Result := Exponant(4);
intQuintic: Result := Exponant(5);
intSinus: Result := Exponant(1) * sin(pi * LStep / 200);
intSlowDownQuadratic: Result := DownExponant(2);
intSlowDownCubic: Result := DownExponant(3);
intSlowDownQuartic: Result := DownExponant(4);
intSlowDownQuintic: Result := DownExponant(5);
intSpring: Result := Exponant(1) * Power(cos(pi * LStep / 100), 2);
intExpo: Result := Exponant(1) * (Power(2, LStep / 100) - 1);
intSqrt: Result := Exponant(1) * (Sqrt(LStep) / 10);
intBounceCos: Result := Exponant(1) + (cos(LStep * pi / 100) + 1) * Exponant(1);
intSlowDownCos: Result := Exponant(1) + (cos(LStep * pi / 100) + 1) * LStep;
intStepsCos: Result := Exponant(1) + Power((cos(LStep * pi / 100) + 1), 2) * LStep;
intCosine: Result := Exponant(1) * (1-cos(LStep/100*Pi))*0.5;
intHalfCosine: Result := Exponant(1) * ((1-cos(LStep/100*Pi))*0.25 + LStep/100*0.5);
end;
Result := AStart + LSign * Result;
end;

function TGVTransition.ComputeInterpolationInt(AStart, AEnd: Single): Integer;
// *** calcul d'une interpolation (entiers) ***
begin
Result := Round(ComputeInterpolation(AStart, AEnd));
end;

La méthode ComputeInterpolation est la plus conséquente puisqu'elle est en charge du calcul des valeurs nécessaires.

III-D - Le lissage des transitions

Le lissage des transitions mérite que nous nous y arrêtions un peu. Voici la méthode setter qui lui est associée :

procedure TGVTransition.SetSmooth(AValue: TGVSmooth);
// *** lissage des transitions ***
begin
if fSmooth = AValue then
exit;
fSmooth := AValue;
case fSmooth of
smoHigh: begin
fInc := C_DefaultInc div 2;
fLastLoop := C_DefaultLastLoop;
end;
smoMedium: begin
fInc := C_DefaultInc;
fLastLoop := C_DefaultLastLoop;
end;
smoNone, smoLow: begin
fInc := C_DefaultInc;
fLastLoop := C_DefaultLastLoop div 2;
end;
smoFast: begin
fInc := C_DefaultInc * 2;
fLastLoop := C_DefaultLastLoop div 2;
end;
end;
Change;
end;

Comme nous l'avions décidé, il s'agit d'adapter le nombre de pas de la boucle de dessin d'une transition et la valeur de ce pas pour établir un équilibre entre la vitesse d'exécution et la fluidité du rendu de la transition.

III-E - L'opacité

L'opacité dépendant elle aussi du nombre de pas de la boucle d'affichage, mais aussi de ce à quoi elle est appliquée (source, destination ou les deux), elle est calculée selon le code suivant :

procedure TGVTransition.SetOpacityState(AValue: TGVOpacityState);
// *** détermination de l'opacité ***
begin
if fOpacityState = AValue then
exit;
fOpacityState := AValue;
Change;
end;

function TGVTransition.DestinationOpacity: Integer;
// *** opacité pour l'image de destination ***
begin
Result := ifthen(fOpacityState in [opaDestination, opaBoth], GetOpacity, C_MaxOpacity);
end;

function TGVTransition.SourceOpacity: Integer;
// *** opacité de l'image d'origine ***
begin
Result := ifthen(fOpacityState in [opaSource, opaBoth], GetOpacity(False),
C_MaxOpacity);
end;

function TGVTransition.GetOpacity(Up: Boolean): Integer;
// *** calcul de l'opacité en fonction du pourcentage effectué ***
begin
Result := ifthen(Up, fStep * C_MaxOpacity div fLastLoop, C_MaxOpacity -
fStep * C_MaxOpacity div fLastLoop);
end;

Encore une fois, une méthode concentre l'essentiel du mécanisme : GetOpacity. Son fonctionnement est fondé sur un simple calcul de pourcentage.

III-F - Le constructeur de la classe

Enfin, cette étape de construction du composant doit adapter le constructeur Create afin de mettre à jour les valeurs par défaut :

constructor TGVTransition.Create(AOwner: TComponent);
// *** construction du composant ***
begin
inherited Create(AOwner);
fInterpolation := intLinear;
fSpeed := C_DefaultSpeed;
fTransition := traNone;
fOpacityState := opaBoth;
fSpecialEffect := speNone;
fHorizontalStrips := NumberOfStrips(ClientHeight);
fVerticalStrips := NumberOfStrips(ClientWidth);
fSmooth := smoMedium;
fInc := C_DefaultInc;
fLastLoop := C_DefaultLastLoop;
end;

Il est en effet indispensable que ces valeurs soient en accord avec celles intégrées au fichier .lfm. Ce constructeur sera complété au fur et à mesure de nos besoins.

IV - Ajout des images et du timer

[Exemple BGRABitmap 47]

IV-A - Les images

Le composant ne serait rien s'il ne pouvait manipuler des images ! Nous allons donc, au cours de cette étape, implémenter les objets nécessaires à notre travail.

En premier lieu, il nous faut déclarer des champs pour les images elles-mêmes :

- fPictureFrom et fPictureTo de type TPicture pour dialoguer avec la LCL ;

- fBGRAFrom et fBGRATo de type TBGRABitmap pour conserver des originaux de tailles identiques des images de travail ;

- fLBGRAFrom et fLBGRATo de type TBGRABitmap pour travailler localement suivant la transition en cours, permettant ainsi de perdre momentanément un peu de qualité au cours d'un redimensionnement par exemple ;

- fLBGRAMask de type TBGRABitmap pour abriter un masque.

Ces sept champs figurent dans la partie strict private de la classe :

fLBGRAFrom, fLBGRATo, fLBGRAMask: TBGRABitmap;
fPictureFrom, fPictureTo: TPicture;
fBGRAFrom, fBGRATo: TBGRABitmap;

Ce nombre peut sembler élevé, mais la complexité de certaines transitions rend nécessaire cette profusion d'outils graphiques.

L'accès à ces champs n'est jamais direct. Les images d'origine et de destination sont en fait des propriétés définies ainsi dans la partie published de la classe TGVTransition :

// image d'origine
property PictureFrom: TPicture read GetPictureFrom write SetPictureFrom;
// image de destination
property PictureTo: TPicture read GetPictureTo write SetPictureTo;

Les setters et getters sont alors les suivants :

function TGVTransition.GetPictureFrom: TPicture;
// *** restitution de l'image d'origine ***
begin
Result := fPictureFrom;
end;

function TGVTransition.GetPictureTo: TPicture;
// *** restitution de l'image de destination ***
begin
Result := fPictureTo;
end;

procedure TGVTransition.SetPictureFrom(AValue: TPicture);
// *** acquisition de l'image d'origine ***
begin
fPictureFrom.Assign(AValue);
FreeAndNil(fBGRAFrom);
fBGRAFrom := TBGRABitmap.Create(AValue.Bitmap);
BGRAReplace(fBGRAFrom, fBGRAFrom.Resample(ClientWidth, ClientHeight));
Change;
end;

procedure TGVTransition.SetPictureTo(AValue: TPicture);
// *** acquisition de l'image de destination ***
begin
fPictureTo.Assign(AValue);
FreeAndNil(fBGRATo);
fBGRATo := TBGRABitmap.Create(AValue.Bitmap);
BGRAReplace(fBGRATo, fBGRATo.Resample(ClientWidth, ClientHeight));
Change;
end;

Si les objets de type TPicture sont évidents à créer, ceux qui abritent les originaux normalisés en taille sont un peu plus complexes : ils sont libérés si nécessaire avant d'être recréés et redimensionnés.

Ces objets doivent être, selon les principes du Pascal Objet, créés et libérés proprement. Il faut par conséquent compléter ainsi le constructeur et le destructeur de notre classe :

constructor TGVTransition.Create(AOwner: TComponent);
// *** construction du composant ***
begin
inherited Create(AOwner);
fInterpolation := intLinear;
fSpeed := C_DefaultSpeed;
fTransition := traNone;
fOpacityState := opaBoth;
fSpecialEffect := speNone;
fPictureFrom := TPicture.Create;
fPictureTo := TPicture.Create;
fBGRAFrom := TBGRABitmap.Create(ClientWidth, ClientHeight, BGRABlack);
fBGRATo := fBGRAFrom.Duplicate as TBGRABitmap;
fLBGRAFrom := fBGRAFrom.Duplicate as TBGRABitmap;
fLBGRATo := fBGRAFrom.Duplicate as TBGRABitmap;
fLBGRAMask := fBGRAFrom.Duplicate as TBGRABitmap;
fHorizontalStrips := NumberOfStrips(ClientHeight);
fVerticalStrips := NumberOfStrips(ClientWidth);
fSmooth := smoMedium;
fInc := C_DefaultInc;
fLastLoop := C_DefaultLastLoop;
end;

destructor TGVTransition.Destroy;
// *** destruction du composant ***
begin
fBGRAFrom.Free;
fBGRATo.Free;
fLBGRAFrom.Free;
fLBGRATo.Free;
fLBGRAMask.Free;
fPictureTo.Free;
fPictureFrom.Free;
inherited Destroy;
end;

IV-B - Quelques améliorations de la base du composant

Nous allons profiter de cet apport pour améliorer notre classe en lui adjoignant quelques fonctionnalités supplémentaires.

Tout d'abord, nous allons définir une taille par défaut du composant grâce à la surcharge de la fonction de classe GetControlClassDefaultSize déclarée dans la partie protected :

class function GetControlClassDefaultSize: TSize; override;

Son implémentation est plutôt simple :

class function TGVTransition.GetControlClassDefaultSize: TSize;
// *** taille par défaut ***
begin
Result := inherited GetControlClassDefaultSize;
Result.cx := C_DefaultWidth;
Result.cy := C_DefaultHeight;
end;

Pour que cette définition soit prise en compte, il faut l'invoquer dans le constructeur :

constructor TGVTransition.Create(AOwner: TComponent);
// *** construction du composant ***
begin
inherited Create(AOwner);
with GetControlClassDefaultSize do
SetInitialBounds(0, 0, CX, CY);

Les composants visuels ont souvent besoin de redéfinir leur taille par défaut. Comme GetControlClassDefaultSize est une fonction de classe, elle peut être invoquée sans instancier la classe qui l'intègre (voir Les méthodes avec Free Pascal pour plus de précisions).

Nous pouvons aussi prévoir une méthode appelée DoReset pour remettre à zéro les différentes images à partir des composants TPicture enregistrés :

procedure TGVTransition.DoReset;
// *** remise à zéro ***
begin
if Assigned(fPictureFrom) and Assigned(fPictureTo) then
begin
if (fPictureFrom.Height > 0) and (fPictureFrom.Width > 0) and
(fPictureTo.Height > 0) and (fPictureTo.Width > 0) then
begin
FreeAndNil(fBGRAFrom);
fBGRAFrom := TBGRABitmap.Create(fPictureFrom.Bitmap);
BGRAReplace(fBGRAFrom, fBGRAFrom.Resample(ClientWidth, ClientHeight));
fLBGRAFrom.Assign(fBGRAFrom);
FreeAndNil(fBGRATo);
fBGRATo := TBGRABitmap.Create(fPictureTo.Bitmap);
BGRAReplace(fBGRATo, fBGRATo.Resample(ClientWidth, ClientHeight));
fLBGRATo.Assign(fBGRATo);
FreeAndNil(fLBGRAMask);
fLBGRAMask := TBGRABitmap.Create(ClientWidth, ClientHeight, BGRABlack);
fStep := 0;
Change;
end;
end;
end;

Cette méthode est déclarée virtuelle dans la partie protégée de la classe en vue de son adaptation à des descendants prévus pour d'autres situations.

Il est important de vérifier l'existence et la validité en termes de taille les images proposées : la fonction Assigned détecte l'assignation et les tests sur les dimensions vérifient que l'image n'ait pas de dimensions nulles, ce qui provoquerait des erreurs à l'exécution.

Toujours dans la perspective d'améliorer notre base de composant, nous allons lui adjoindre une nouvelle propriété baptisée Quality : elle choisira le filtre de redimensionnement le plus adapté à l'utilisateur. Nous en reproduisons ci-après le setter :

procedure TGVTransition.SetQuality(AValue: TGVQuality);
// *** qualité des transformations (homothéties) ***
begin
if fQuality = AValue then
exit;
fQuality := AValue;
case fQuality of
quaLow: fLBGRAFrom.ResampleFilter := rfBox;
quaNormal: fLBGRAFrom.ResampleFilter := rfHalfCosine;
quaBest: fLBGRAFrom.ResampleFilter := rfBestQuality;
end;
end;

Dans l'état actuel du composant final, cette propriété n'est guère utile, mais son importance pourra être développée en approfondissant la gestion des filtres.

IV-C - Le TTimer

Pour clore ce chapitre, nous allons introduire un élément essentiel du mécanisme du composant. Contrairement aux exercices qui ont émaillé les tutoriels précédents, nous allons en effet oublier la boucle de réalisation d'une transition pour adopter une solution plus adaptée à la programmation événementielle. Cette solution passe par l'utilisation d'un objet de type TTimer à déclarer avec les autres champs privés :

fTimer: TTimer;

Pour l'accompagner, dans la partie protégée, nous déclarerons une procédure apte à gérer l'événement déclenché par ce composant lorsqu'il a atteint la durée qui lui a été fixée :

procedure OnTimerEvent(Sender: TObject);

Cet événement sera utilisé pour effectuer les tâches relatives à une étape de l'affichage de la transition :

procedure TGVTransition.OnTimerEvent(Sender: TObject);
// *** événement lié au timer ***
begin
// DoLoop; (en attente!)
end;

Encore une fois, le constructeur Create doit être complété :

constructor TGVTransition.Create(AOwner: TComponent);
// *** construction du composant ***
begin
inherited Create(AOwner);
// […]
fTimer := TTimer.Create(Self);
fTimer.Enabled := False;
fTimer.Interval := C_DefaultTimer - (fSpeed div 2);
fTimer.OnTimer := @OnTimerEvent;

En revanche, comme l'objet est créé avec le paramètre Self, c'est le composant TGVTransition qui prendra en charge sa destruction : nous n'avons donc pas à nous en préoccuper.

Contrairement à Delphi, Free Pascal impose, dans son mode par défaut du Pascal Objet, l'utilisation du signe @ devant (entre autres) une méthode à affecter à un gestionnaire d'événement.

Un dernier détail et nous en aurons fini avec cette étape : la vitesse d'exécution dépend de la valeur donnée à la propriété Interval du TTimer. Il nous faut par conséquent modifier ainsi le setter de la propriété de gestion de la vitesse Speed :

procedure TGVTransition.SetSpeed(AValue: Integer);
// *** détermination de la vitesse ***
begin
if fSpeed = AValue then
exit;
fSpeed := AValue;
fTimer.Interval := C_DefaultTimer - (fSpeed div 2);
Change;
end;

V - La méthode Paint et les procédures de calculs

[Exemple BGRABitmap 48]

Nos propriétés sont installées, le compteur pour les étapes de la réalisation d'une transition est en place, les images sont prêtes à être traitées : nous allons par conséquent nous occuper des grandes structures du traitement recherché.

V-A - La méthode Paint

Comme elle va être appelée à chaque étape, la méthode Paint est primordiale, mais elle est paradoxalement peu fournie, surtout pour la partie réservée à l'exécution :

procedure TGVTransition.Paint;
// *** dessin ***
var
LX, LY: Integer;
begin
if csDesigning in ComponentState then
begin
with Canvas do
begin
Font.Color := clBlue;
Font.Size := 16;
LX := (Self.Width - TextWidth(Self.Name)) div 2;
if LX < 0 then
LX := 0;
LY := (Self.Height - TextHeight(Self.Name)) div 2;
if LY < 0 then
LY := 0;
TextOut(LX, LY, Self.Name);
end;
exit;
end;
if Assigned(OnPaint) then
begin
Canvas.Font := Font;
Canvas.Brush.Color := Color;
inherited Paint;
end;
if Assigned(fLBGRAFrom) then
fLBGRAFrom.Draw(Canvas, 0, 0);
end;

Seules les deux dernières lignes se rapportent à l'affichage proprement dit : si fLBGRAFrom est affectée, c'est elle qui contient l'image qui doit être reproduite sur le canevas du composant, donc visible.

Lorsque nous serons en mode conception (marqué par la présence de csDesigning dans ComponentState), nous nous contenterons d'écrire le nom du composant en son centre. En dehors de ces deux tâches, la méthode vérifie si un gestionnaire d'événement lui est rattaché et lance son exécution si besoin est.

V-B - Les méthodes de travail sur les images

Avant de dessiner, il faut avoir établi quelle image doit être affichée. Suivant les transitions, nous avons vu que le traitement différait. Il pouvait exiger des étapes que nous allons formaliser ainsi :

- prétraitement indépendant des images (par exemple, pour les splines) ;

- traitement de l'image d'origine (par exemple, pour les effets spéciaux ou son déplacement) ;

- traitement avec masque de l'image de destination (pour de très nombreuses transitions) ;

- traitement sans masque de l'image à afficher (par exemple pour les rotations et les redimensionnements).

Ces différents traitements donnent naissance à autant de méthodes définies comme suit :

procedure InitTransition; virtual;
procedure ComputeSource; virtual;
procedure ComputeDestination; virtual;
procedure ComputeFinalDraw; virtual;

Leur caractère virtuel dans la partie protégée de la classe assure qu'elles pourront être surchargées avec souplesse par d'éventuelles classes descendantes. Pour le moment, leur contenu restera vide : il contiendra des appels aux méthodes des transitions elles-mêmes, ainsi qu'à des méthodes associées aux images de travail. Le fichier inclus des transitions trouvera naturellement sa place après elles.

Leurs squelettes sont alors ceux-ci :

procedure TGVTransition.InitTransition;
// *** pré-traitement ***
begin

end;

procedure TGVTransition.ComputeSource;
// *** traitement de l'image source ***
begin

end;

procedure TGVTransition.ComputeDestination;
// *** traitement de l'image destination ***
begin

end;

procedure TGVTransition.ComputeFinalDraw;
// *** traitement final de l'image ***
begin

end;

// {$I transitions.inc }

VI - Les événements et les états

[Exemple BGRABitmap 49]

Afin d'assurer le bon fonctionnement de notre composant, nous allons le doter de gestionnaires d'événements et d'un automate permettant de tenir compte des différents états qu'il peut prendre.

VI-A - Les gestionnaires d'événements

Du point de vue des événements, nous en définirons trois :

TGVTransition = class(TGraphicControl)
strict private
// […]
fTransitionChange, fLooping: TNotifyEvent;
fStateChanged: TGVTransitionStateEvent;
// […]
protected
// […]
procedure Change; virtual;
procedure StateChanged; virtual;
procedure Looping; virtual;
// […]

published
// […]
// le composant a changé - gestionnaire de l'événement
property OnChange: TNotifyEvent read fTransitionChange
write fTransitionChange;
// l'état de la transition a changé - gestionnaire de l'événement
property OnStateChange: TGVTransitionStateEvent read fStateChanged
write fStateChanged;
// étape suivante - gestionnaire de l'événement
property OnLoop: TNotifyEvent read fLooping write fLooping;
end;

Il s'agit de réagir au changement du composant (par exemple à la modification de la vitesse choisie), à celui de son état (par exemple de l'état de démarrage à celui d'en cours d'exécution) ou encore au passage à l'étape suivante d'une transition. Chaque gestionnaire est associé à une méthode spécifique chargée de l'exécuter s'il est affecté : Change pour OnChange, StateChanged pour OnStateChange et Looping pour OnLoop.

À partir de ce mécanisme, moyennant des appels appropriés à ces méthodes spécifiques, les changements suivis seront notifiés à l'application appelante qui fera ce que bon lui semble des informations accessibles. Par exemple, comme nous avions déjà intégré un appel à Change dans tous les setters concernés, tout changement des propriétés suivies déclenchera un appel, s'il existe, au gestionnaire OnChange.

Il faut cependant prendre garde au temps consommé par ces méthodes traitées en dehors du composant : elles peuvent ralentir de manière significative ses performances ! Cet avertissement concerne tout particulièrement le gestionnaire OnLoop appelé à chaque étape de la transition.

Le traitement des états fait appel à une procédure de notification spéciale qui sera définie comme suit :

TGVTransitionStateEvent = procedure(Sender: TObject; State: TGVTransitionState;
Trans: TGVTransitionType; Step: Integer) of object;

L'utilisateur aura ainsi accès immédiatement à l'état, mais aussi à la transition et à l'étape en cours.

L'implémentation des gestionnaires est on ne peut plus classique :

procedure TGVTransition.Change;
// *** notification de changement d'état ***
begin
if Assigned(fTransitionChange) then
fTransitionChange(Self);
end;

procedure TGVTransition.StateChanged;
// *** notification de changement d'état ***
begin
if Assigned(fStateChanged) then
fStateChanged(Self, fState, fTransition, fStep);
end;

procedure TGVTransition.Looping;
// *** notification de l'étape suivante ***
begin
if Assigned(fLooping) then
fLooping(Self);
end;

Si le gestionnaire est bien affecté, nous l'exécutons, sinon nous quittons la procédure sans agir. Le paramètre Self est là pour indiquer que c'est une instance de notre composant qui a effectué la notification.

VI-B - Le traitement des états

La propriété State est à première vue une propriété parmi les autres. Pourtant, c'est sur elle que repose le bon déroulement des opérations au sein du composant ! Considérant le champ privé fState, voici sa définition dans la partie publique :

// état en cours de la transition
property State: TGVTransitionState read fState write SetState default trsWaiting;

Pourquoi avoir choisi la partie publique et non la partie publiée comme pour la plupart des autres propriétés ? C'est tout simplement que cette propriété en lecture et écriture n'a aucun sens à la conception puisqu'elle ne marque que des états en cours d'exécution.

Son setter s'occupe en particulier de déclencher ou d'arrêter le TTimer, d'où l'exécution ou l'arrêt de la transition :

procedure TGVTransition.SetState(AValue: TGVTransitionState);
// *** changement d'état ***
begin
if fState = AValue then
exit;
fState := AValue;
if Assigned(fTimer) then
fTimer.Enabled := (fState = trsRunning);
StateChanged;
end;

Nous pouvons enfin définir la méthode DoLoop en suspens dans le gestionnaire d'événement OnTimer :

procedure TGVTransition.DoLoop;
// *** boucle de la transition ***
begin
Inc(fStep, fInc);
ComputeSource;
ComputeDestination;
ComputeFinalDraw;
Looping;
Repaint;
if fStep >= fLastLoop then
begin
State := trsEnding;
State := trsWaiting;
end;
end;

Cette méthode suit l'ordre séquentiel d'une étape, depuis l'incrémentation de son numéro jusqu'à la sortie pour l'étape suivante ou le retour à un état d'attente si la transition est achevée. Il s'agit en effet d'effectuer les opérations dans cet ordre :

- calculer l'image d'origine ;

- calculer l'image de destination avec masque ;

- calculer l'image définitive même si aucun masque n'est nécessaire ;

- notifier le changement d'étape ;

- demander un dessin immédiat de l'image en cours ;

- ajuster l'état du composant au cas où le nombre d'étapes requis serait atteint.

Nous pouvons aussi enlever le commentaire dans ce gestionnaire :

procedure TGVTransition.OnTimerEvent(Sender: TObject);
// *** événement lié au timer ***
begin
DoLoop;
end;

Enfin, grâce à ces états, nous pouvons restreindre l'usage de certaines propriétés. Par exemple, il est contre-indiqué de changer de transition en pleine exécution de l'une d'entre elles. Nous modifierons le code du setter en conséquence :

procedure TGVTransition.SetTransition(AValue: TGVTransitionType);
// *** détermination de la transition active ***
begin
if (fTransition = AValue) or (fState = trsRunning) then
exit;
fTransition := AValue;
Change;
end;

Nous aurions pu déclencher une exception au lieu d'ignorer ce changement de transition alors qu'une autre est en cours d'exécution, mais il nous a semblé préférable de laisser cette responsabilité au développeur. Après tout, c'est à lui de faire en sorte que l’utilisateur ne puisse pas sélectionner ce paramètre via l'interface homme/machine (IHM) fournie.

Reste bien entendu à ne pas oublier d'indiquer dans le constructeur la valeur par défaut de l'état :

constructor TGVTransition.Create(AOwner: TComponent);
// *** construction du composant ***
begin
inherited Create(AOwner);
// […]
fState := trsWaiting;

VI-C - Les états en action

[Exemple BGRABitmap 50]

En tenant compte de l'état en cours, nous sommes à même d'obtenir une bonne maîtrise du composant. Pour vous en convaincre, nous allons définir dans la partie publique de notre classe une série de méthodes liées à des actions :

// démarrage de la transition
procedure Start;
// arrêt de la transition
procedure Stop;
// remise à zéro du composant
procedure Reset;
// suspension de la transition en cours (si elle existe)
procedure Suspend;
// reprise de la transition suspendue (si elle existe)
procedure Resume;
// indicateur de transition en cours
function IsRunning: Boolean;

Leur implémentation est très liée aux états possibles du composant :

procedure TGVTransition.Start;
// *** déclenchement de la transition ***
begin
case fState of
trsRunning: exit;
trsSuspended: State := trsRestarting;
else
fStep := 0;
InitTransition;
State := trsStarting;
end;
State := trsRunning;
end;

procedure TGVTransition.Stop;
// *** arrêt d'une transition ***
begin
if State = trsRunning then
begin
State := trsStopped;
fStep := fLastLoop;
State := trsWaiting;
end;
end;

procedure TGVTransition.Reset;
// *** remise à zéro de l'affichage ***
begin
DoReset;
Repaint;
end;

procedure TGVTransition.Suspend;
// *** suspension de la transition ***
begin
if fState = trsRunning then
State := trsSuspended;
end;

procedure TGVTransition.Resume;
// reprise d'une animation ***
begin
if fState = trsSuspended then
Start;
end;

function TGVTransition.IsRunning: Boolean;
// *** transition en cours ? ***
begin
Result := (fState = trsRunning);
end;

L'état le plus complexe est trsSuspended, car il demande d'être vigilant en cas de démarrage ou de redémarrage d'une transition. Il ne faudrait pas dans ce cas réinitialiser le compteur des boucles.

Un autre usage des états peut être illustré par la méthode Resize chargée de gérer le redimensionnement du composant. Pendant ce redimensionnement, il faut mettre la transition en pause pour recalculer, entre autres, les bandes peut-être inadaptées aux nouvelles dimensions.

Voici le code proposé :

procedure TGVTransition.Resize;
// *** le contrôle est redimensionné ***
var
LState: TGVTransitionState;
begin
LState := fState;
State := trsPaused;
try
inherited Resize;
if AutoStrips then
begin
fHorizontalStrips := NumberOfStrips(ClientHeight);
fVerticalStrips := NumberOfStrips(ClientWidth);
end;
if not (csDesigning in ComponentState) and not (fState = trsRunning) then
DoReset;
finally
State := LState;
end;
end;

L'aspect intéressant de cette routine est la conservation de l'état dans la variable locale LState pour mettre le composant en mode pause jusqu'à la fin du traitement où l'état initial est récupéré.

VII - Le composant complet

[Exemple BGRABitmap 51]

Pour obtenir un composant opérationnel complet, il ne manque que deux étapes : l'implémentation de toutes les transitions et leur utilisation dans les méthodes laissées vides jusqu'à présent, qui auront en charge leur exploitation.

VII-A - L'implémentation des transitions

Pour ce qui est de l'implémentation des transitions, elle s'appuie sur le travail préparatoire effectué dans les tutoriels précédents. Les transitions sont même souvent d'un aspect plus simple que leurs équivalents antérieurs puisque la gestion des étapes est prise en charge en dehors des routines les implémentant et que l'utilisation des interpolations rend a priori le code plus lisible.

Voici la routine qui permet de déplacer l'image d'origine vers la droite :

procedure TGVTransition.DoRight;
// *** image vers la droite ***
begin
fX := ComputeInterpolation(0, ClientWidth);
end;

On ne peut imaginer plus simple !

Voici la transition qui étend verticalement l’image de destination grâce à un masque :

procedure TGVTransition.DoVerticalExpand;
// *** VerticalExpand ***
begin
fLBGRAMask.FillRectAntialias(ComputeInterpolation(ClientWidth / 2, 0), 0,
ComputeInterpolation(ClientWidth / 2, ClientWidth), ClientHeight,
BGRAWhite, False);
end;

Les rotations sont souvent regroupées par deux : celle qui tourne dans le sens des aiguilles d'une montre avec celle qui tourne dans le sens contraire. Un paramètre booléen permet alors de discriminer les deux cas.

Voici par exemple la rotation qui se déplace vers la droite :

procedure TGVTransition.DoRotateRight(CCW: boolean);
// *** CCWRotateRight et CWRotateRight ***
var
L2Pi: Integer;
begin
if CCW then
L2Pi := -360
else
L2Pi := 360;
fLBGRAFrom.PutImageAngle(ComputeInterpolation(0, ClientWidth / 2),
ComputeInterpolation(ClientHeight, ClientHeight / 2),
fLBGRATo, ComputeInterpolation(0, L2Pi), ClientWidth / 2,
ClientHeight / 2, DestinationOpacity);
end;

Enfin, certaines méthodes restent plutôt complexes parce qu'elles utilisent plusieurs transformations. Par exemple, voici la transition qui se dirige vers la gauche tout en effectuant une rotation et en agrandissant l'image de destination :

procedure TGVTransition.DoRotateGrowLeft(CCW: Boolean);
// *** RotateGrowCCWLeft et RotateGrowCWLeft ***
var
LBGRATemp: TBGRAAffineBitmapTransform;
begin
LBGRATemp := TBGRAAffineBitmapTransform.Create(fLBGRATo);
try
LBGRATemp.RotateDeg(ifthen(CCW, ComputeInterpolation(0, -360),
ComputeInterpolation(0, 360)));
LBGRATemp.Scale(fStep / fLastLoop);
LBGRATemp.Translate(ComputeInterpolation(ClientWidth, 0),
0);
fLBGRAFrom.Fill(LBGRATemp, dmDrawWithTransparency);
finally
LBGRATemp.Free;
end;
end;

Comme quelques autres, elle a besoin d'une image temporaire supplémentaire pour arriver à ses fins.

Les transitions les plus originales ont même besoin de deux objets supplémentaires ! Voici par exemple la transition qui agrandit l'image de destination tout en créant un effet de tourbillon :

procedure TGVTransition.DoMultiTwirl;
// *** MultiTwirl ***
var
LBGRATemp: TBGRAAffineScannerTransform;
LTwirl: TBGRATwirlScanner;
begin
LBGRATemp := TBGRAAffineScannerTransform.Create(fLBGRATo);
try
LBGRATemp.Scale(fStep / fLastLoop);
LTwirl := TBGRATwirlScanner.Create(LBGRATemp,
Point(ClientWidth div 2, ClientHeight div 2), (fLastLoop - fStep) *
ifthen(ClientWidth > ClientHeight, ClientWidth, ClientHeight));
try
fLBGRAFrom.Fill(LTwirl, dmDrawWithTransparency);
finally
LTwirl.Free;
end;
finally
LBGRATemp.Free;
end;
end;

L'édition du fichier inclus transitions.inc montrera que certaines transitions sont inédites : il faut bien donner un peu de substance supplémentaire à ce tutoriel, non ?

VII-B - Les méthodes de traitement des images

Reste à compléter les méthodes chargées de l'exploitation de toutes ces transitions en attente.

InitTransition est de loin la plus courte. Son rôle se résume à faire les calculs en amont de l'exécution de la transition. Ce sont les splines qui occupent la place :

procedure TGVTransition.InitTransition;
// *** pré-traitement ***
begin
case Transition of
traSplinesLeft, traSplinesRight: DoPrepareSpline;
traSplinesUp, traSplinesDown: DoPrepareSplineUpOrDown;
end;
end;

ComputeSource s'occupe de l'image d'origine. D'une part, elle déplace l'image si nécessaire ; d'autre part, elle traite les effets spéciaux qui peuvent être appliqués :

procedure TGVTransition.ComputeSource;
// *** traitement de l'image source ***
var
LBGRATemp: TBGRABitmap;
begin
fX := 0;
fY := 0;
fLBGRAFrom.FillRect(ClientRect, Color);
case Transition of
traGrowPushUp, traLeaveUp, traPushUp: DoUp;
traGrowPushDown, traLeaveDown, traPushDown: DoDown;
traGrowPushLeft, traLeaveLeft, traPushLeft: DoLeft;
traGrowPushRight, traLeaveRight, traPushRight: DoRight;
traLeaveBottomRight: DoBottomRight;
traLeaveBottomLeft: DoBottomLeft;
traLeaveTopRight: DoTopRight;
traLeaveTopLeft: DoTopLeft;
end;
// effets spéciaux
case fSpecialEffect of
speEmboss: LBGRATemp := fBGRAFrom.FilterEmboss(ComputeInterpolation(0, 180),
ComputeInterpolationInt(64, 256)) as TBGRABitmap;
speContour: LBGRATemp := fBGRAFrom.FilterContour as TBGRABitmap;
speNegative: begin
LBGRATemp := fBGRAFrom.Duplicate as TBGRABitmap;
LBGRATemp.Negative;
end;
speLinearNegative: begin
LBGRATemp := fBGRAFrom.Duplicate as TBGRABitmap;
LBGRATemp.LinearNegative;
end;
speSwapRedBlue: begin
LBGRATemp := fBGRAFrom.Duplicate as TBGRABitmap;
LBGRATemp.SwapRedBlue;
end;
speSphere: LBGRATemp := fBGRAFrom.FilterSphere as TBGRABitmap;
speRotate: LBGRATemp := fBGRAFrom.FilterRotate(PointF(ClientWidth / 2,
ClientHeight/ 2), ComputeInterpolation(0, 360)) as TBGRABitmap;
speGrayscale: begin
LBGRATemp := fBGRAFrom.Duplicate as TBGRABitmap;
LBGRATemp.InplaceGrayscale;
end;
spePixelate: LBGRATemp := fBGRAFrom.FilterPixelate(ComputeInterpolationInt(2, 24),
True) as TBGRABitmap;
speTwirl: LBGRATemp := fBGRAFrom.FilterTwirl(
Point(ClientWidth div 2, ClientHeight div 2),
ifthen(ClientWidth > ClientHeight, ComputeInterpolation(0, ClientWidth),
ComputeInterpolation(0, ClientHeight))) as TBGRABitmap;
speBlurMotion: LBGRATemp := fBGRAFrom.FilterBlurMotion(ComputeInterpolation(0, 15),
ComputeInterpolation(0, 180), False) as TBGRABitmap;
else
LBGRATemp := fBGRAFrom.Duplicate as TBGRABitmap;
end;
try
fLBGRAFrom.PutImage(Round(fX), Round(fY), LBGRATemp, dmDrawWithTransparency,
SourceOpacity);
finally
LBGRATemp.Free;
end;
end;

ComputeDestination s'occupe de l'image de destination. Sa principale caractéristique est l'application d'un masque produit par les routines appelées en amont :

procedure TGVTransition.ComputeDestination;
// *** traitement de l'image destination ***
begin
fX := 0;
fY := 0;
fLBGRAMask.FillRectAntialias(0, 0, ClientWidth, ClientHeight, BGRABlack);
case Transition of
traUncoverDown: DoUncoverUpOrDown(False);
traUncoverUp: DoUncoverUpOrDown(True);
traUncoverLeft: DoUncover(False);
traUncoverRight: DoUncover(True);
traOverDown, traPushDown: DoOverUpOrDown(False);
traOverUp, traPushUp: DoOverUpOrDown(True);
traOverLeft, traPushLeft: DoOver(False);
traOverRight, traPushRight: DoOver(True);
traUncoverBottomRight: DoUncoverBottom(True);
traUncoverBottomLeft: DoUncoverBottom(False);
traUncoverTopRight: DoUncoverTop(True);
traUncoverTopLeft: DoUncoverTop(False);
traUncoverHorizontalBackAndForth: DoUncoverHorizontalBackAndForth;
traUncoverVerticalBackAndForth: DoUncoverVerticalBackAndForth;
traOverSlipBottomRight: DoOverSlipBottom(True);
traOverSlipBottomLeft: DoOverSlipBottom(False);
traOverSlipTopRight: DoOverSlipTop(True);
traOverSlipTopLeft: DoOverSlipTop(False);
traOverBottomRight: DoOverBottom(True);
traOverBottomLeft: DoOverBottom(False);
traOverTopRight: DoOverTop(True);
traOverTopLeft: DoOverTop(False);
traBottomRightExpand: DoBottomExpand(True);
traBottomLeftExpand: DoBottomExpand(False);
traTopRightExpand: DoTopExpand(True);
traTopLeftExpand: DoTopExpand(False);
traLeaveUp: DoLeaveUpOrDown(True);
traLeaveDown: DoLeaveUpOrDown(False);
traLeaveLeft: DoLeave(False);
traLeaveRight: DoLeave(True);
traBottomRightShrink, traLeaveBottomRight: DoLeaveBottom(True);
traBottomLeftShrink, traLeaveBottomLeft: DoLeaveBottom(False);
traTopRightShrink, traLeaveTopRight: DoLeaveTop(True);
traTopLeftShrink, traLeaveTopLeft: DoLeaveTop(False);
traHorizontalExpand: DoHorizontalExpand;
traVerticalExpand: DoVerticalExpand;
traHorizontalShrink: DoHorizontalShrink;
traVerticalShrink: DoVerticalShrink;
traRectExpand: DoRectExpand;
traRectShrink: DoRectShrink;
traEllipseExpand: DoEllipseExpand;
traEllipseShrink: DoEllipseShrink;
traDiskExpand: DoDiskExpand;
traDiskShrink: DoDiskShrink;
traCrossExpand: DoCrossExpand;
traCrossShrink: DoCrossShrink;
traShrinkExpandDown: DoShrinkExpandUpOrDown(False);
traShrinkExpandUp: DoShrinkExpandUpOrDown(True);
traTrianglesExpand: DoTrianglesExpand;
traTrianglesShrink: DoTrianglesShrink;
traMegaStar: DoMegaStar;
traSplinesDown: DoSplinesDown;
traSplinesUp: DoSplinesUp;
traSplinesRight: DoSplinesRight;
traSplinesLeft: DoSplinesLeft;
traStripsDown: DoStripsUpOrDown(False);
traStripsUp: DoStripsUpOrDown(True);
traStripsRight: DoStrips(True);
traStripsLeft: DoStrips(False);
traStripsInterlacedLeftRight: DoStripsHorizontalInterlaced(True);
traStripsInterlacedRightLeft: DoStripsHorizontalInterlaced(False);
traStripsInterlacedUpDown: DoStripsInterlaced(True);
traStripsInterlacedDownUp: DoStripsInterlaced(False);
traRectUpDown: DoRectUpOrDown(False);
traRectDownUp: DoRectUpOrDown(True);
traWipeRight: DoWipe(True);
traWipeLeft: DoWipe(False);
traOverRightSin: DoOverSin(True);
traOverLeftSin: DoOverSin(False);
traBouncingRight: DoBouncing(True);
traBouncingLeft: DoBouncing(False);
traJumpRight: DoJump(True);
traJumpLeft: DoJump(False);
traFadeIn: DoFadeIn;
//traDummy: DoDummy;
else
DoNegateMask;
end;
fLBGRATo.PutImage(Round(fX), Round(fY), fBGRATo, dmSet);
fLBGRATo.ApplyMask(fLBGRAMask);
end;

Le masque est calculé pour toutes les transitions. Au cas où il ne serait pas utile, il est inversé par la méthode DoNegateMask dont le code est rudimentaire :

procedure TGVTransition.DoNegateMask;
// *** masque inversé ***
begin
fLBGRAMask.Negative;
end;

Nous avons ainsi la certitude que toute l'image sera bien conservée. Ensuite, il suffit de recopier l'image originale fBGRATo dans celle de travail fLBGRATo et d'appliquer le masque fLBGRAMask à cette dernière.

Enfin, ComputeFinalDraw prend en charge le traitement final de l'image, en particulier si aucun masque n'a été appliqué :

procedure TGVTransition.ComputeFinalDraw;
// *** traitement final de l'image ***
begin
case Transition of
traCCWRotateRight: DoRotateRight(True);
traCWRotateRight: DoRotateRight(False);
traCCWCenterRotate: DoCenterRotate(True);
traCWCenterRotate: DoCenterRotate(False);
traCCWRotateLeft: DoRotateLeft(True);
traCWRotateLeft: DoRotateLeft(False);
traCWSpiral: DoSpiral(False);
traCCWSpiral: DoSpiral(True);
traGrowCenter: DoGrowCenter;
traGrowBottomRight: DoGrowBottom(True);
traGrowBottomLeft: DoGrowBottom(False);
traGrowTopRight: DoGrowTop(True);
traGrowTopLeft: DoGrowTop(False);
traGrowCWRotate: DoGrowRotate(False);
traGrowCCWRotate: DoGrowRotate(True);
traGrowCenterCCWRotate: DoGrowCenterRotate(True);
traGrowCenterCWRotate: DoGrowCenterRotate(False);
traGrowPushRight, traGrowOverRight: DoGrowOver(True);
traGrowPushLeft, traGrowOverLeft: DoGrowOver(False);
traGrowPushUp, traGrowOverUp: DoGrowOverUpOrDown(True);
traGrowPushDown, traGrowOverDown: DoGrowOverUpOrDown(False);
traRotateCCWGrowLeft: DoRotateGrowLeft(True);
traRotateCWGrowLeft: DoRotateGrowLeft(False);
traRotateCCWGrowTopLeft: DoRotateGrowTopLeft(True);
traRotateCWGrowTopLeft: DoRotateGrowTopLeft(False);
traRotateCCWTopLeft: DoRotateTopLeft(True);
traRotateCWTopLeft: DoRotateTopLeft(False);
traRotateCCWBottomRight: DoRotateBottomRight(True);
traRotateCWBottomRight: DoRotateBottomRight(False);
traMappingDown: DoMappingDown;
traMappingUp: DoMappingUp;
traMultiTwirl: DoMultiTwirl;
traTwirl: DoTwirl;
traSphere: DoSphere;
traCylinderRight: DoCylinderRight;
traCylinderLeft: DoCylinderLeft;
traMultiExpand: DoMultiExpand;
traMultiExpandTwirl: DoMultiExpandTwirl;
traDrawFromBounds: DODrawFromBounds;
traOffsetTopLeft: DoOffset(False);
traOffsetBottomRight: DoOffset(True);
traPixelate: DoPixelate;
traBlurMotion: DoBlurMotion;
{$IFDEF Debug_Transitions}
traDummy: DoDummy;
{$ENDIF}
else
fLBGRAFrom.PutImage(0, 0, fLBGRATo, dmDrawWithTransparency, DestinationOpacity);
end;

Les transitions sans masque opèrent directement sur l'image de travail fBGRAFrom alors que les autres transitions utilisent la méthode PutImage finale pour actualiser cette même image. L'essentiel est que fBGRAFrom contienne l'image à afficher : c'est, faut-il le rappeler, elle qui est invoquée dans Paint.

Finalement, toutes ces méthodes se résument à une longue suite de cas possibles auxquels sont appliquées quelques méthodes comme PutImage ou ApplyMask. Si nous ne tenons pas compte des méthodes implémentant les transitions dans le fichier inclus, les effets spéciaux sont les seuls à faire appel à des outils plus sophistiqués.

VII-C - Finition de la classe

La déclaration de la classe est affectée par ces modifications. Nous avons en effet besoin de quelques champs privés pour définir les coordonnées sur lesquelles s'appuieront les transitions pour dessiner :

fY, fX: Single;
fPts, fPtsTemp: array of TPointF;

Pour des résultats plus précis, les transitions du composant utilisent des nombres réels de type Single et non des entiers.

Bien sûr, Il faut aussi déclarer la liste des méthodes des transitions :

// liste des transitions
procedure DoNegateMask;
procedure DoPrepareSpline;
procedure DoPrepareSplineUpOrDown;
procedure DoUp;
procedure DoDown;
procedure DoRight;
procedure DoLeft;
procedure DoBottomRight;
procedure DoBottomLeft;
procedure DoTopRight;
procedure DoTopLeft;
procedure DoUncoverUpOrDown(Up: boolean);
procedure DoUncover(ToRight: boolean);
procedure DoOverUpOrDown(Up: boolean);
procedure DoOver(ToRight: boolean);
procedure DoUncoverBottom(ToRight: boolean);
procedure DoUncoverTop(ToRight: boolean);
procedure DoUncoverHorizontalBackAndForth;
procedure DoUncoverVerticalBackAndForth;
procedure DoOverSlipTop(ToRight: boolean);
procedure DoOverSlipBottom(ToRight: boolean);
procedure DoOverBottom(ToRight: boolean);
procedure DoOverTop(ToRight: boolean);
procedure DoBottomExpand(ToRight: boolean);
procedure DoTopExpand(ToRight: boolean);
procedure DoLeaveUpOrDown(Up: boolean);
procedure DoLeave(ToRight: boolean);
procedure DoLeaveBottom(ToRight: boolean);
procedure DoLeaveTop(ToRight: boolean);
procedure DoVerticalExpand;
procedure DoHorizontalExpand;
procedure DoHorizontalShrink;
procedure DoVerticalShrink;
procedure DoRectExpand;
procedure DoRectShrink;
procedure DoEllipseExpand;
procedure DoEllipseShrink;
procedure DoDiskExpand;
procedure DoDiskShrink;
procedure DoCrossExpand;
procedure DoCrossShrink;
procedure DoShrinkExpandUpOrDown(Up: boolean);
procedure DoTrianglesExpand;
procedure DoTrianglesShrink;
procedure DoMegaStar;
procedure DoSplinesDown;
procedure DoSplinesUp;
procedure DoSplinesRight;
procedure DoSplinesLeft;
procedure DoStrips(ToRight: boolean);
procedure DoStripsUpOrDown(Up: boolean);
procedure DoStripsHorizontalInterlaced(LeftFirst: boolean);
procedure DoStripsInterlaced(UpFirst: boolean);
procedure DoRectUpOrDown(Up: Boolean);
procedure DoWipe(ToRight: boolean);
procedure DoOverSin(ToRight: boolean);
function Parabola(AOffset, AMax, APerCent: Integer): Single;
procedure DoBouncing(ToRight: boolean);
procedure DoJump(ToRight: boolean);
procedure DoBlank;
procedure DoFadeIn;
procedure DoRotateRight(CCW: boolean);
procedure DoCenterRotate(CCW: boolean);
procedure DoRotateLeft(CCW: boolean);
procedure DoSpiral(CCW: Boolean);
procedure DoGrowCenter;
procedure DoGrowBottom(ToRight: Boolean);
procedure DoGrowTop(ToRight: Boolean);
procedure DoGrowRotate(CCW: Boolean);
procedure DoGrowCenterRotate(CCW: Boolean);
procedure DoGrowOver(ToRight: Boolean);
procedure DoGrowOverUpOrDown(Up: Boolean);
procedure DoRotateGrowLeft(CCW:Boolean);
procedure DoRotateGrowTopLeft(CCW:Boolean);
procedure DoRotateTopLeft(CCW:Boolean);
procedure DoRotateBottomRight(CCW:Boolean);
procedure DoMappingDown;
procedure DoMappingUp;
procedure DoMultiTwirl;
procedure DoTwirl;
procedure DoSphere;
procedure DoCylinderRight;
procedure DoCylinderLeft;
procedure DoMultiExpand;
procedure DoMultiExpandTwirl;
procedure DoDrawFromBounds;
procedure DoOffset(ToRight: Boolean);
procedure DoPixelate;
procedure DoBlurMotion;
{$IFDEF Debug_Transitions}
procedure DoDummy;
{$ENDIF}

Les transitions sont plus nombreuses que ces méthodes : c'est que certaines d'entre elles prennent en charge plusieurs transitions aux fonctionnements similaires.

Notre composant est enfin opérationnel !

VIII - Conclusion

Avec ce tutoriel, vous êtes parvenu au terme de la construction du composant TGVTransition. Il vous suffit de l'installer et d'imaginer les applications qui pourront en faire usage. Encore mieux, vous pouvez l'améliorer, soit en le rendant plus rapide, soit en créant de nouvelles transitions spectaculaires. À vous de jouer !

Pour ceux qui chercheraient à compléter leurs connaissances, l'ultime tutoriel de la série proposera l'installation du paquet dans l'EDI Lazarus ainsi que des exemples d’utilisation de ce nouveau composant.

Mes remerciements vont à Alcatîz et à BeanzMaster pour leur relecture technique et à f-leb pour la correction orthographique.