Effets de transition avec Lazarus et BGRABitmap

5. Rotations, homothéties et filtres

Gilles Vasseur

Dans le précédent tutoriel de la série, avec les splines et les bandes, nous avons fini d'étudier les techniques mettant en œuvre des masques afin de produire des transitions plus attrayantes et plus variées que par simple superposition. Il est temps de découvrir d'autres classes et méthodes que nous propose la bibliothèque BGRABitmap pour dessiner à l'aide de rotations, d'homothéties ou de filtres.

Commentez

- Titre : Effets de transition avec Lazarus et BGRABitmap

- Auteur : Gilles Vasseur

- Parution : 14 juin 2018

- Mise à jour : 19 juin 2018

- Public visé :

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

Sommaire

- I. L'emploi des rotations

- II. L'emploi des homothéties

- II-A. Les homothéties simples

- II-B. Les homothéties combinées à des rotations

- III. Une unité dédiée à la transformation des images

- IV. Les filtres et les méthodes spéciales

- IV-A. Une application de test

- IV-B. Les effets spéciaux

- IV-C. Les transitions avec filtre

- V. Conclusion

I - L'emploi des rotations

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

En géométrie, une rotation est une transformation, tout comme la translation que nous avons déjà exploitée. Plus spécifiquement, nous parlerons de la rotation plane qui fait tourner d'un angle donné les figures autour d'un point, selon une orientation elle aussi fixée. Comme lors d'une translation, une figure n'est ni déformée ni agrandie par rotation. En revanche, la rotation pose des problèmes spécifiques de rendu d'une image après traitement à cause d'inévitables erreurs d'arrondi : heureusement, ces problèmes de crénelage avec certains angles sont traités par la bibliothèque BGRABitmap grâce aux méthodes de dessin incluant le mot anglais antialias (anticrénelage). Ce sont toujours à elles que nous ferons appel, comme nous l'avons d'ailleurs fait jusqu'à présent.

Il existe plusieurs façons d'obtenir des rotations avec BGRABitmap : une méthode spécifique de la classe TBGRABitmap, mais aussi des outils fournis par l'unité BGRATransform consacrée aux transformations. Dans un premier temps, nous n'utiliserons que la méthode PutImageAngle liée à la classe TBGRABitmap, renvoyant l'étude des autres possibilités aux chapitres suivants.

La méthode PutImageAngle, comme de nombreuses méthodes de la classe TBGRABitmap, prend plusieurs formes :

procedure PutImageAngle(x,y: single; Source: TBGRACustomBitmap; angle: single; AOutputBounds: TRect; imageCenterX: single = 0; imageCenterY: single = 0; AOpacity: Byte=255; ARestoreOffsetAfterRotation: boolean = false; ACorrectBlur: Boolean = false); overload;
procedure PutImageAngle(x,y: single; Source: TBGRACustomBitmap; angle: single; imageCenterX: single = 0; imageCenterY: single = 0; AOpacity: Byte=255; ARestoreOffsetAfterRotation: boolean = false; ACorrectBlur: Boolean = false); overload;
procedure PutImageAngle(x,y: single; Source: TBGRACustomBitmap; angle: single; AOutputBounds: TRect; AResampleFilter: TResampleFilter; imageCenterX: single = 0; imageCenterY: single = 0; AOpacity: Byte=255; ARestoreOffsetAfterRotation: boolean = false); overload;
procedure PutImageAngle(x,y: single; Source: TBGRACustomBitmap; angle: single; AResampleFilter: TResampleFilter; imageCenterX: single = 0; imageCenterY: single = 0; AOpacity: Byte=255; ARestoreOffsetAfterRotation: boolean = false); overload;

Nous voyons que cette méthode est accompagnée de plusieurs paramètres dont certains adoptent une valeur par défaut. C'est sous la forme suivante qu'elle nous intéressera le plus :

procedure PutImageAngle(x,y: single; Source: TBGRACustomBitmap; angle: single; imageCenterX: single = 0; imageCenterY: single = 0; AOpacity: Byte=255; ARestoreOffsetAfterRotation: boolean = false; ACorrectBlur: Boolean = false); overload;

En effet, il nous est demandé de fournir les coordonnées de destination de l'image en rotation, la source de l'image à faire tourner, l'angle (en degrés) de la rotation, les coordonnées du centre de rotation, l'opacité à appliquer au résultat, ainsi que deux éventuelles corrections (replacer l'image dans sa position originelle et corriger les effets de flou). Les paramètres essentiels nous fournissent la possibilité de varier les effets.

[Exemple BGRABitmap 29]

Les masques étant superflus pour l'application d'une rotation complète de l'image sur une autre image, le gestionnaire OnClick de la transition appelée CWCenterRotate pourra être réduit à quelque chose comme :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LX := imgResult.ClientWidth div 2;
LY := imgResult.ClientHeight div 2;
LBGRATemp.PutImageAngle(LX, LY, fBGRATo, fStep / 100 * 360, imgResult.ClientWidth / 2, imgResult.ClientHeight / 2, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Avec LX et LY, nous déplaçons l'image à faire tourner au centre de l'image de résultat. La rotation est fournie grâce à une formule de proportionnalité de fStep : un tour complet vaut 360 degrés et les valeurs positives opèrent par convention une rotation dans le sens des aiguilles d'une montre. Le centre de rotation choisi est le centre de l'image, d'où le calcul de ses coordonnées à partir de ClientWidth et ClientHeight. Enfin, l'opacité est rendue grâce à la fonction habituelle Opacity.

La transition CCWCenterRotate n'exigerait qu'une inversion du sens de rotation, donc l'ajout d'un signe - (moins) devant le paramètre de l'angle de rotation.

Vous aurez remarqué que les coordonnées des points sont des valeurs réelles si bien que le signe de division utilisé pour une meilleure précision est / et non l'instruction div.

[Exemple BGRABitmap 30]

Les variantes sont faciles à imaginer. Par exemple, pourquoi ne pas faire progresser le centre de rotation du point de coordonnées (0, 0) vers le centre de l'image ? La méthode de travail prendrait alors la forme suivante pour la transition CWSpiral :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX:= 0;
LY:= 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LX := imgResult.ClientWidth div 2;
LY := imgResult.ClientHeight div 2;
LBGRATemp.PutImageAngle(LX, LY, fBGRATo, fStep / 100 * 360,
fStep / 100 * imgResult.ClientWidth / 2, // changement
fStep / 100 * imgResult.ClientHeight / 2, // changement
Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

L'idée qui sous-tend cette variante est de combiner la rotation de l'image à une translation de son centre de rotation, d'où l'obtention d'une spirale. Pour cela, nous avons transformé le pas en un pourcentage de la translation et de la rotation.

Pour obtenir une rotation tournant dans le sens inverse des aiguilles d'une montre (transition CCWSpiral), nous n'aurions qu'à rendre négatif l'angle de rotation.

[Exemple BGRABitmap 31]

Nous pouvons au contraire faire varier les coordonnées du coin supérieur gauche de l'image et laisser constant le centre de rotation. Il faut simplement prendre garde au fait que si les valeurs sont mal choisies la transition risque de se réaliser en majeure partie hors de la fenêtre d'affichage ! À la fin de la transition, il est nécessaire que les variables LX et LY soient toutes les deux à 0 pour un recouvrement total de l'image de départ.

Voici le code proposé pour une transition baptisée CWRotateRight :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LX := fStep * imgResult.ClientWidth div 100 - imgResult.ClientWidth div 2;
LY := fStep * imgResult.ClientHeight div 100 - imgResult.ClientHeight div 2;
LBGRATemp.PutImageAngle(LX, LY, fBGRATo, fStep / 100 * 360,
imgResult.ClientWidth / 2,
imgResult.ClientHeight / 2, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Une copie d'écran de la transition en cours a donné :

Il est bien entendu possible de créer une transition qui provoquera une rotation dans le sens opposé des aiguilles d'une montre : encore une fois, le signe de l'angle indiqué dans la méthode PutImageAngle aura juste à être inversé.

Nous voyons que les rotations sont plutôt faciles à mettre en œuvre dans la mesure où l'aire de leur exécution reste visible au maximum durant la transition. C'est combinées aux homothéties que nous allons à présent étudier qu'elles révéleront toutes leurs potentialités.

II - L'emploi des homothéties

En géométrie, une homothétie est une transformation correspondant à un agrandissement ou à une réduction. À partir d'elle, nous pouvons élaborer différentes transitions qui feront toutes appel à une seule méthode de TBGRABitmap, à savoir Resample. Oui, il s'agit tout naturellement de la méthode que nous avons déjà utilisée pour redimensionner les images afin qu'elles aient toutes une taille identique !

II-A - Les homothéties simples

La première transition qui peut venir à l'esprit est celle qui consiste à faire apparaître l'image de destination au centre de celle du résultat en augmentant sa taille au fur et à mesure de la progression de l'effet désiré.

Du point de vue de l'algorithme à employer, cette transition paraît simple : à chaque pas, l'image de destination est redimensionnée à la taille voulue puis dessinée au centre de l'image de départ. Il n'est donc pas nécessaire d'employer un masque puisque cette transition fonctionne par recouvrement. En revanche, nous allons avoir besoin d'un objet de type TBGRABitmap supplémentaire.

[Exemple BGRABitmap 32]

En effet, appliquons notre algorithme en tapant le code suivant :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
BGRAReplace(fBGRATo, fBGRATo.Resample(fStep * imgResult.ClientWidth  div 100,
fStep * imgResult.ClientHeight div 100));
LX := (imgResult.ClientWidth - fBGRATo.Width) div 2;
LY := (imgResult.ClientHeight - fBgraTo.Height) div 2;
LBGRATemp.PutImage(LX, LY, fBGRATo, dmDrawWithTransparency, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Nous avons bien créé l'image de destination en la redimensionnant, défini l'emplacement central de son dessin en calculant les coordonnées de son point supérieur gauche et recouvert l'image de départ. Pourtant, à l'exécution, l'image de destination n'est pas dessinée : un rectangle gris recouvre peu à peu l'image de départ, et c'est tout !

L'explication de ce résultat surprenant est simple : le redimensionnement se fait en perdant ou en créant des informations. Lorsque la taille de l'image diminue, des informations sont définitivement perdues ; lorsque cette même taille augmente, des informations sont ajoutées pour remplir les vides créés. Pour que ce travail se passe au mieux, la bibliothèque BGRABitmap dispose d'algorithmes dédiés, mais il n'en reste pas moins qu'une dégradation de l'image est inévitable, surtout si le rapport d'homothétie est important ou si l'homothétie s'applique plusieurs fois. Pour ce qui est de notre étude, nous sommes dans les deux cas puisque notre image commence par un rectangle virtuel aux dimensions nulles (donc sans informations !) et que l'agrandissement nécessite une centaine de redimensionnements.

Afin de bien visualiser la déperdition inévitable, il nous suffit de modifier une seule ligne à notre code :

fStep := 40;

Nous commençons ainsi le dessin à partir d'une image avec une taille non nulle, ce qui est déjà plus pertinent.

Voici ce que donne l'exécution de l'application si nous la laissons en l'état :

La transition a bien fonctionné si nous ne considérons que son déroulement, mais le résultat final est décevant, car l'image obtenue est très floue. Comment pourrait-il en être autrement avec 60 redimensionnements ?

[Exemple BGRABitmap 33]

Une solution est de ne pas travailler avec l'image originale, mais de recopier à chaque tour cette dernière dans une image de travail provisoire. Évidemment, le temps d'exécution sera légèrement dégradé puisque, en plus de la création et de la libération de l'objet provisoire, il faudra compter autant de copies que de pas d'exécution.

Voici le code proposé :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp, LBGRAResample: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LBGRAResample := fBGRATo.Resample(fStep * imgResult.ClientWidth  div 100,
fStep * imgResult.ClientHeight div 100) as TBGRABitmap;
try
LX := (imgResult.ClientWidth - LBGRAResample.Width) div 2;
LY := (imgResult.ClientHeight - LBGRAResample.Height) div 2;
LBGRATemp.PutImage(LX, LY, LBGRAResample, dmDrawWithTransparency, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
finally
LBGRAResample.Free;
end;
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Il nous a fallu remplacer les occurrences de l'image de destination (fBGRATo) par notre nouvelle image de travail baptisée LBGRAResample. C'est elle que nous redimensionnons à chaque tour, mais comme nous avons pris soin dans le même temps de faire d'elle la copie de l'image originale, nous ne percevons pas de dégradation sensible de son rendu.

Il est aussi possible de faire appel à la méthode StretchPutImage qui présente l'avantage de grouper en son sein le redimensionnement et le dessin. Il n'est donc plus besoin de l'image intermédiaire. Voici le code à utiliser :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LX := (imgResult.ClientWidth - fStep * imgResult.ClientWidth div 100) div 2;
LY := (imgResult.ClientHeight - fStep * imgResult.ClientHeight div 100) div 2;
LBGRATemp.StretchPutImage(Rect(LX, LY, LX + fStep * imgResult.ClientWidth div 100,
LY + fStep * imgResult.ClientHeight div 100), fBGRATo, dmDrawWithTransparency,
Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Le code de calcul est un peu plus complexe à cause du calcul du rectangle de dessin, mais la solution est plus élégante avec l'économie d'une nouvelle instanciation.

Dans le composant final, c'est cette solution qui sera utilisée. Avec le système des interpolations, en plus d'être plus rapide, elle deviendra même bien plus simple à mettre en œuvre. Par la suite, nous ne conserverons la méthode avec Resample qu'à des fins pédagogiques pour bien identifier les étapes du travail ou pour simplifier les formules de calcul.

Voici un instantané pris lors du déroulement de cette transition nommée GrowCenter :

[Exemple BGRABitmap 34]

En considérant cette dernière transition, nous pouvons en construire d'autres dont nous ne présenterons que la portion de code caractéristique avec l'emploi de l'objet intermédiaire LBGRAResample. Sans lui, les calculs seront à peine plus complexes puisqu'il faudra calculer la largeur et la hauteur de l'image agrandie en fonction du pas en cours.

La transition qui agrandira l'image de destination en la faisant apparaître dans le coin supérieur gauche de l'image du résultat pour la faire rejoindre son coin inférieur droit sera nommée GrowBottomRight et aura pour code caractéristique :

LX := 0;
LY := 0;

Oui, c'est suffisant ! De même, voici celui de sa sœur GrowBottomLeft :

LX := imgResult.ClientWidth - LBGRAResample.Width;
LY := 0;

Dans le même esprit, voici celui de GrowTopRight qui part du coin inférieur gauche pour rejoindre le coin supérieur droit :

LX := 0;
LY := imgResult.ClientHeight - LBGRAResample.Height;

Le pendant de la transition précédente sera nommé GrowTopLeft et partira du coin inférieur droit pour rejoindre le coin supérieur gauche :

LX := imgResult.ClientWidth - LBGRAResample.Width;
LY := imgResult.ClientHeight - LBGRAResample.Height;

Sans surprise, ces différentes transitions n'impliquent que la modification du point d'origine de leur dessin.

Voici un écran capté avec la transition GrowTopLeft :

[Exemple BGRABitmap 35]

Plutôt que de faire varier les deux dimensions de l'image agrandie, il est envisageable de se contenter d'une seule. Nous obtiendrons ainsi un recouvrement de l'image de départ avec l'image de destination en montrant cette dernière comme de moins en moins compressée.

Le code proposé est le suivant :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp, LBGRAResample: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LBGRAResample := fBGRATo.Resample(fStep * imgResult.ClientWidth  div 100,
imgResult.ClientHeight) as TBGRABitmap;
try
//GrowOverRight
LX := 0;
LY := 0;
LBGRATemp.PutImage(LX, LY, LBGRAResample, dmDrawWithTransparency, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
finally
LBGRAResample.Free;
end;
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Par rapport aux transitions qui viennent d'être étudiées, comme annoncé, la seule ligne vraiment importante est celle qui concerne le redimensionnement :

LBGRAResample := fBGRATo.Resample(fStep * imgResult.ClientWidth  div 100, imgResult.ClientHeight) as TBGRABitmap;

C'est en effet à ce niveau que la largeur de l'image est modifiée alors que sa hauteur reste constante. La transition dessine l'image de destination sur celle d'origine, de la gauche vers la droite. Elle portera le nom de GrowOverRight :

Des opérations du même genre peuvent être effectuées à partir des différents bords de l'image à agrandir. Nous pouvons ainsi agrandir l'image vers la gauche en partant du bord droit grâce à la transition GrowOverLeft en ne modifiant qu'une ligne de code :

LX := imgResult.ClientWidth - LBGRAResample.Width;

Pour une transition GrowOverDown, qui partira du haut de l'image pour aller vers le bas, nous modifierons le code de la façon suivante :

LBGRAResample := fBGRATo.Resample(imgResult.ClientWidth,
fStep * imgResult.ClientHeight div 100) as TBGRABitmap;
try
LX := 0;
LY := 0;

D'une manière similaire, la transition opposée (GrowOverUp) sera implémentée comme suit :

LBGRAResample := fBGRATo.Resample(imgResult.ClientWidth,
fStep * imgResult.ClientHeight div 100) as TBGRABitmap;
try
LX := 0;
LY := imgResult.ClientHeight - LBGRAResample.Height;

Comme pour la série de transitions vue précédemment, il est aussi possible d'utiliser la méthode StretchPutImage afin d'éviter la création d'un objet intermédiaire. Voici par exemple le code nécessaire à la transition GrowTopLeft :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// GrowTopLeft
LX := imgResult.ClientWidth - fStep * imgResult.ClientWidth div 100;
LY := imgResult.ClientHeight - fStep * imgResult.ClientHeight div 100;
LBGRATemp.StretchPutImage(Rect(LX, LY, imgResult.ClientWidth,
imgResult.ClientHeight), fBGRATo, dmDrawWithTransparency,
Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Voici un résultat intermédiaire de la transition GrowOverUp :

II-B - Les homothéties combinées à des rotations

La combinaison de rotations et d'homothéties, avec d’éventuelles translations, est elle aussi susceptible de produire des effets remarquables. Nous allons en suggérer quelques-uns ci-après.

En premier lieu, nous pouvons faire tourner l'image de destination tout en l'agrandissant au centre de l'image d'origine. Il s'agit toujours d'une transition avec recouvrement et qui ne demande par conséquent pas de masque.

[Exemple BGRABitmap 36]

Le code suivant est une proposition d'implémentation de GrowCenterCWRotate :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp, LBGRAResample: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LBGRAResample := fBGRATo.Resample(fStep * imgResult.ClientWidth div 100,
fStep * imgResult.ClientHeight div 100) as TBGRABitmap;
try
LX := imgResult.ClientWidth div 2;
LY := imgResult.ClientHeight div 2;
LBGRATemp.PutImageAngle(LX, LY, LBGRAResample,
fStep * 360 / 100, LBGRAResample.Width div 2, LBGRAResample.Height div 2, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
finally
LBGRAResample.Free;
end;
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

L'image de destination est tout d'abord redimensionnée avant de se voir appliquer une rotation centrale dans le sens des aiguilles d'une montre. La transition GrowCenterCCWRotate qui tournera dans le sens contraire des aiguilles d'une montre sera obtenue par simple changement de signe de l'angle de rotation :

LBGRATemp.PutImageAngle(LX, LY, LBGRAResample, -fStep * 360 / 100, LBGRAResample.Width div 2, LBGRAResample.Height div 2, Opacity);

Voici une vidéo qui montre la transition GrowCenterCWRotate en action :

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://gilles-vasseur.developpez.com/tutoriels/transitions/bgra5/fichiers/growcentercwrotate.webm

Exemple BGRABitmap 37]

Dans le prolongement de cette approche, nous pouvons affecter différemment les paramètres internes de cette méthode de base. Par exemple, nous pouvons faire varier le point d'application du recouvrement de l'image grâce à une translation et choisir le coin supérieur gauche de l'image pour centre de rotation. La transition obtenue pourra s'appeler GrowCWRotate. En voici le code :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp, LBGRAResample: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LBGRAResample := fBGRATo.Resample(fStep * imgResult.ClientWidth div 100,
fStep * imgResult.ClientHeight div 100) as TBGRABitmap;
try
LX := (imgResult.ClientWidth - fStep * imgResult.ClientWidth div 100) div 2;
LY := imgResult.ClientHeight - fStep * imgResult.ClientHeight div 100;
LBGRATemp.PutImageAngle(LX, LY, LBGRAResample,
fStep * 360 / 100, 0, 0, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
finally
LBGRAResample.Free;
end;
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Sa consœur GrowCCWRotate qui fera tourner l'image dans le sens opposé des aiguilles d'une montre n'aura, comme toujours, qu'une différence de signe à apporter à l'angle de rotation. Voici par conséquent l'unique ligne de code qui la distinguera de la précédente :

LBGRAResample := fBGRATo.Resample(fStep * imgResult.ClientWidth div 100, - fStep * imgResult.ClientHeight div 100) as TBGRABitmap;

La vidéo qui suit rend compte de l'intérêt de cette nouvelle transition :

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://gilles-vasseur.developpez.com/tutoriels/transitions/bgra5/fichiers/growcwrotate.webm

Exemple BGRABitmap 38]

Sans chercher à être exhaustif en matière de variantes, une autre idée serait de n'appliquer l'agrandissement que sur la largeur de l'image et de fixer le centre de rotation au centre de l'image du résultat. Voici le code de la transition que nous baptiserons GrowWidthCWRotate  :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp, LBGRAResample: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LBGRAResample := fBGRATo.Resample(fStep * imgResult.ClientWidth div 100,
imgResult.ClientHeight) as TBGRABitmap;
try
LX := imgResult.ClientWidth div 2;
LY := imgResult.ClientHeight div 2;
LBGRATemp.PutImageAngle(LX, LY, LBGRAResample,
fStep * 360 / 100, imgResult.ClientWidth div 2,
imgResult.ClientHeight div 2, Opacity);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
finally
LBGRAResample.Free;
end;
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Pas mal, non ? Nous choisirons parmi ces variantes celles que nous voudrons : il ne s'agit pas simplement de multiplier les transitions, mais bien de chercher celles qui seront les plus susceptibles d'intéresser l'utilisateur final du composant à venir !

III - Une unité dédiée à la transformation des images

Comme nous l'indiquions en introduction de ce tutoriel, translations, homothéties et rotations sont aussi utilisables via une unité spécialisée dans les applications affines : BGRATransform.

Cette unité va d'autant plus nous être utile qu'elle simplifie souvent l'écriture du code des transformations vues jusqu'à maintenant. Imaginons par exemple que nous souhaitions produire une transition nommée RotateCWGrowTopLeft qui combinerait trois éléments dans cet ordre :

- une rotation dans le sens des aiguilles d'une montre de l'image de destination selon un angle donné ;

- une mise à l'échelle par homothétie de cette même image ;

- une translation toujours de cette même image lui faisant traverser l'image du résultat, de la droite vers la gauche, et du bas vers le haut.

[Exemple BGRABitmap 39]

Avec les outils dont nous disposons, nous devrions nous en tirer, mais la nouvelle unité proposée simplifiera grandement le code à fournir :

implementation

uses
BGRATransform;

{$R *.lfm}

{ TMainForm }

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LBGRATransform: TBGRAAffineBitmapTransform;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(0, 0, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LBGRATransform := TBGRAAffineBitmapTransform.Create(fBGRATo);
try
//LX := 0;
//LY := 0;
LBGRATransform.RotateDeg(fStep * 360 / 100);
LBGRATransform.Scale(fStep / 100);
LBGRATransform.Translate(imgResult.ClientWidth - fStep *
imgResult.ClientWidth / 100, imgResult.ClientHeight - fStep   * imgResult.ClientHeight / 100);
LBGRATemp.Fill(LBGRATransform, dmDrawWithTransparency);
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
finally
LBGRATransform.Free;
end;
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Nous avons tout d'abord introduit une clause uses afin de pouvoir utiliser l'unité désirée. À la place de l'objet de travail de type TBGRABitmap habituel, nous avons déclaré et créé un objet de la classe TBGRAAffineBitmapTransform. C'est à ce dernier que nous avons appliqué les méthodes qui nous intéressent et que nous allons décrire à présent.

La méthode RotateDeg produit une rotation en degrés. Elle prend un paramètre indiquant des degrés dont les valeurs positives indiquent une rotation dans le sens des aiguilles d'une montre.

Il existe aussi un équivalent avec un paramètre en radians dont les valeurs positives, conformément aux conventions habituelles, indiquent une rotation dans le sens inverse des aiguilles d'une montre.

La méthode Scale met à l'échelle l'image selon le paramètre réel fourni. En fait, elle existe sous deux formes surchargées :

procedure Scale(sx,sy: single); overload;
procedure Scale(factor: single); overload;

La première forme permet de distinguer les rapports d'homothéties suivant les deux axes possibles alors que la première forme applique uniformément le même rapport.

La méthode Translate déplace l'image affectée par les deux données fournies (respectivement, la largeur et la hauteur).

Une texture est un objet implémentant l'interface IBGRAScanner (par exemple, un objet de type TBGRABitmap ou TBGRAAffineBitmapTransform), qui peut être vu comme une image sans limites dont la surface est couverte d'un motif qui se répète.

La méthode Fill connaît plusieurs définitions surchargées :

procedure Fill(texture: IBGRAScanner; mode: TDrawMode); override;
procedure Fill(texture: IBGRAScanner); override;
procedure Fill(c: TBGRAPixel; start, Count: integer); override;

Pour rappel, c'est sous une forme simplifiée et issue de l'ancêtre TBGRACustomBitmap que nous l'avons jusqu'à présent utilisée afin d'effacer le contenu d'une surface. C'est sous la première forme du tableau qu'elle nous sera la plus utile dorénavant : elle a besoin d'une texture, donc d'une image transformée, ainsi que d'une indication du type de dessin souhaité (ici, par transparence).

Le code proposé définit RotateGrowCWTopLeft que voici en action :

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://gilles-vasseur.developpez.com/tutoriels/transitions/bgra5/fichiers/rotategrowcwtopleft.webm

L'inversion du signe devant fStep lors de la rotation suffit pour définir la transition RotateGrowCCWTopLeft :

LBGRATransform.RotateDeg(-fStep * 360 / 100);

Comme nous l'avons déjà fait précédemment, nous pourrions jouer avec d'autres paramètres : la largeur ou la hauteur peuvent rester fixes, le redimensionnement peut varier selon une formule plus complexe qu'une simple linéarité, la translation peut suivre une autre direction…

Il serait fastidieux de décrire à chaque fois toutes les possibilités : n'hésitez pas à garder les transitions qui vous paraissent les plus esthétiques, les plus surprenantes ou les plus amusantes ! Quant aux autres, oubliez-les tout simplement !

[Exemple BGRABitmap 40]

L'intérêt de cette unité ne s'arrête pas là. En effet, elle dispose d'autres outils très utiles pour des effets encore plus originaux. Par exemple, TBGRATwirlScanner propose une distorsion de l'image qui, associée à la transformation TBGRAAffineScannerTransform, peut surprendre.

Le code proposé sera le suivant :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp: TBGRABitmap;
LBGRATransform: TBGRAAffineScannerTransform;
LTwirl: TBGRATwirlScanner;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
LX := 0;
LY := 0;
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement ici...
LBGRATransform := TBGRAAffineScannerTransform.Create(fBGRATo);
try
//LX := 0;
//LY := 0;
LBGRATransform.Scale(fStep / 100);
LTwirl := TBGRATwirlScanner.Create(LBGRATransform,
Point(imgResult.ClientWidth div 2, imgResult.ClientHeight div 2), (100 - fStep) *
ifthen(imgResult.ClientWidth > imgResult.ClientHeight, imgResult.ClientWidth, imgResult.ClientHeight));
try
LBGRATemp.Fill(LTwirl, dmDrawWithTransparency);
finally
LTwirl.Free;
end;
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
finally
LBGRATransform.Free;
end;
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

Nous voyons que la première image de type TBGRAAffineScannerTransform est soumise à une rotation puis qu'elle est soumise à une distorsion grâce à l'objet de type TBGRATwirlScanner. Le résultat est alors appliqué à l'image finale. Comme nous travaillons avec un objet implémentant l'interface IBGRAScanner, l'image sera répétée sur la surface pour la recouvrir totalement, en particulier lorsque le rétrécissement de l'image de travail sera fort.

La fonction ifthen qui est employée pour retourner la plus grande des deux valeurs ClientWidth ou ClientHeight est présente dans l'unité math qui doit par conséquent être ajoutée à la clause uses dans la partie implementation.

Une courte vidéo rend compte de la transition MultiTwirl en action :

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://gilles-vasseur.developpez.com/tutoriels/transitions/bgra5/fichiers/multitwirl.webm

Ce genre de transition couvrant d'emblée la totalité de l'espace, l'opacité perd une grande partie de son sens.

IV - Les filtres et les méthodes spéciales

Nous allons dans ce chapitre voir qu'il est possible d'adjoindre des filtres aux opérations étudiées jusqu'à présent ou de les affecter grâce à certaines méthodes non encore vues de la classe TBGRABitmap. Certains de ces nouveaux outils nous intéresseront peu, car ils opèrent en bloc, sans étapes exploitables par notre boucle : ils pourront cependant s'appliquer à l'image d'origine pour des effets spéciaux. D'autres sont adaptables à notre objectif : nous les détaillerons tout particulièrement.

IV-A - Une application de test

[Exemple BGRABitmap 41]

Plutôt que d'expliquer l'action de la série de méthodes en jeu dans ce chapitre, il est sans doute préférable de les voir en action. C'est l'objet de l'application simpliste qui suit : elle ne sera même pas interactive, se contentant d'afficher une série d'images affectées chacune par une méthode différente.

L'interface aligne par conséquent dix-neuf composants TImage. Le premier contiendra l'image de travail, les autres les images obtenues après l'opération. Des composants TLabel afficheront en toutes lettres les effets appliqués.

Voici ce que donne cette interface rudimentaire :

Le fichier LFM correspondant, débarrassé des données liées à l'image de travail, est celui-ci :

object MainForm: TMainForm
Left = 269
Height = 709
Top = 192
Width = 859
Caption = 'Essai de quelques méthodes de la classe TBGRABimap'
ClientHeight = 709
ClientWidth = 859
OnCreate = FormCreate
OnDestroy = FormDestroy
LCLVersion = '1.8.2.0'
object imgMain: TImage
Left = 16
Height = 146
Top = 16
Width = 170
Stretch = True
end
object imgNone: TImage
Left = 16
Height = 90
Top = 200
Width = 90
Stretch = True
end
object lblNone: TLabel
Left = 16
Height = 15
Top = 307
Width = 64
BorderSpacing.Around = 10
Caption = 'Pas d''action'
ParentColor = False
end
object imgVerticalFlip: TImage
Left = 120
Height = 90
Top = 200
Width = 90
Stretch = True
end
object imgHorizontalFlip: TImage
Left = 224
Height = 90
Top = 200
Width = 90
Stretch = True
end
object lblVerticalFlip: TLabel
Left = 120
Height = 15
Top = 307
Width = 57
BorderSpacing.Around = 10
Caption = 'VerticalFlip'
ParentColor = False
end
object lblHorizontalFlip: TLabel
Left = 224
Height = 15
Top = 307
Width = 74
BorderSpacing.Around = 10
Caption = 'HorizontalFlip'
ParentColor = False
end
object imgNegative: TImage
Left = 328
Height = 90
Top = 200
Width = 90
Stretch = True
end
object lblNegative: TLabel
Left = 328
Height = 15
Top = 307
Width = 47
BorderSpacing.Around = 10
Caption = 'Negative'
ParentColor = False
end
object imgLinearNegative: TImage
Left = 432
Height = 90
Top = 200
Width = 90
Stretch = True
end
object lblLinearNegative: TLabel
Left = 432
Height = 15
Top = 307
Width = 79
BorderSpacing.Around = 10
Caption = 'LinearNegative'
ParentColor = False
end
object imgInplaceGrayscale: TImage
Left = 536
Height = 90
Top = 200
Width = 90
Stretch = True
end
object lblInplaceGrayscale: TLabel
Left = 536
Height = 15
Top = 307
Width = 88
BorderSpacing.Around = 10
Caption = 'InplaceGrayscale'
ParentColor = False
end
object imgSwapRedBlue: TImage
Left = 640
Height = 90
Top = 200
Width = 90
Stretch = True
end
object lblSwapRedBlue: TLabel
Left = 640
Height = 15
Top = 307
Width = 71
BorderSpacing.Around = 10
Caption = 'SwapRedBlue'
ParentColor = False
end
object imgInplaceNormalize: TImage
Left = 744
Height = 90
Top = 200
Width = 90
Stretch = True
end
object lblInplaceNormalize: TLabel
Left = 744
Height = 15
Top = 307
Width = 92
BorderSpacing.Around = 10
Caption = 'InplaceNormalize'
ParentColor = False
end
object imgPixelate: TImage
Left = 16
Height = 90
Top = 368
Width = 90
end
object lblFilterPixelate: TLabel
Left = 16
Height = 15
Top = 480
Width = 66
Caption = 'FilterPixelate'
ParentColor = False
end
object imgFilterBlurMotion: TImage
Left = 120
Height = 90
Top = 368
Width = 90
end
object lblFilterBlurMotion: TLabel
Left = 120
Height = 15
Top = 480
Width = 86
Caption = 'FilterBlurMotion'
ParentColor = False
end
object imgFilterSmartZoom3: TImage
Left = 224
Height = 90
Top = 368
Width = 90
end
object lblFilterSmartZoom3: TLabel
Left = 224
Height = 15
Top = 480
Width = 95
Caption = 'FilterSmartZoom3'
ParentColor = False
end
object imgFilterMedian: TImage
Left = 328
Height = 90
Top = 368
Width = 90
end
object lblFilterSmooth: TLabel
Left = 328
Height = 15
Top = 480
Width = 66
Caption = 'FilterMedian'
ParentColor = False
end
object imgFilterContour: TImage
Left = 432
Height = 90
Top = 368
Width = 90
end
object lblFilterContour: TLabel
Left = 432
Height = 15
Top = 480
Width = 70
Caption = 'FilterContour'
ParentColor = False
end
object imgFilterEmboss: TImage
Left = 536
Height = 90
Top = 368
Width = 90
end
object lblFilterEmboss: TLabel
Left = 536
Height = 15
Top = 480
Width = 67
Caption = 'FilterEmboss'
ParentColor = False
end
object imgFilterSphere: TImage
Left = 640
Height = 90
Top = 368
Width = 90
end
object lblFilterSphere: TLabel
Left = 640
Height = 15
Top = 480
Width = 62
Caption = 'FilterSphere'
ParentColor = False
end
object imgFilterRotate: TImage
Left = 744
Height = 90
Top = 368
Width = 90
end
object lblFilterRotate: TLabel
Left = 744
Height = 15
Top = 480
Width = 60
Caption = 'FilterRotate'
ParentColor = False
end
object imgFilterPlane: TImage
Left = 16
Height = 90
Top = 512
Width = 90
end
object lblFilterPlane: TLabel
Left = 16
Height = 15
Top = 623
Width = 55
Caption = 'FilterPlane'
ParentColor = False
end
object imgFilterTwirl: TImage
Left = 120
Height = 90
Top = 512
Width = 90
end
object lblFilterTwirl: TLabel
Left = 120
Height = 15
Top = 623
Width = 64
Caption = 'lblFilterTwirl'
ParentColor = False
end
end

Le code lié à cette application est lui aussi d'une extrême simplicité. Dans le gestionnaire de création de la fiche principale OnCreate, nous créons l'image de travail et nous lui appliquons successivement chaque filtre pour afficher le résultat dans le composant TImage prévu. La seule difficulté consiste à distinguer les méthodes ordinaires qui sont appelées sans précautions particulières et les filtres qui sont des méthodes qui créent un objet comme le faisait déjà Resample : dans ce dernier cas, il faut prévoir la libération de la ressource de l'image puisqu'elle va être recréée par le filtre.

Voici le code proposé :

procedure TMainForm.FormCreate(Sender: TObject);
var
LBmp: TBGRABitmap;
begin
BGRABmp := TBGRABitmap.Create(imgMain.Picture.Bitmap);
BGRAReplace(BGRABmp, BGRABmp.Resample(imgNone.ClientWidth, imgNone.ClientHeight));
BGRABmp.Draw(imgNone.Canvas, 0, 0, True);
BGRABmp.VerticalFlip;
BGRABmp.Draw(imgVerticalFlip.Canvas, 0, 0, True);
BGRABmp.VerticalFlip;
BGRABmp.HorizontalFlip;
BGRABmp.Draw(imgHorizontalFlip.Canvas, 0, 0, True);
BGRABmp.HorizontalFlip;
BGRABmp.Negative;
BGRABmp.Draw(imgNegative.Canvas, 0, 0, True);
BGRABmp.Negative;
BGRABmp.LinearNegative;
BGRABmp.Draw(imgLinearNegative.Canvas, 0, 0, True);
BGRABmp.LinearNegative;
BGRABmp.SwapRedBlue;
BGRABmp.Draw(imgSwapRedBlue.Canvas, 0, 0, True);
BGRABmp.SwapRedBlue;
BGRABmp.InplaceNormalize;
BGRABmp.Draw(imgInplaceNormalize.Canvas, 0, 0, True);
BGRABmp.InplaceNormalize;
LBmp := BGRABmp.FilterPixelate(8, True) as TBGRABitmap;
LBmp.Draw(imgPixelate.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterBlurMotion(30, 0, True) as TBGRABitmap;
LBmp.Draw(imgFilterBlurMotion.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterMedian(moHighSmooth) as TBGRABitmap;
LBmp.Draw(imgFilterMedian.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterSmartZoom3(moMediumSmooth) as TBGRABitmap;
LBmp.Draw(imgFilterSmartZoom3.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterContour as TBGRABitmap;
LBmp.Draw(imgFilterContour.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterEmboss(90, 128) as TBGRABitmap;
LBmp.Draw(imgFilterEmboss.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterSphere as TBGRABitmap;
LBmp.Draw(imgFilterSphere.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterRotate(PointF(imgFilterRotate.ClientWidth / 2,
imgFilterRotate.ClientHeight / 2), 45) as TBGRABitmap;
LBmp.Draw(imgFilterRotate.Canvas, 0, 0);
LBmp.Free;
LBmp := BGRABmp.FilterPlane as TBGRABitmap;
LBmp.Draw(imgFilterPlane.Canvas, 0, 0);
Lbmp.Free;
LBmp := BGRABmp.FilterTwirl(Point(imgFilterTwirl.ClientWidth div 2,
imgFilterTwirl.ClientHeight div 2), imgFilterTwirl.ClientWidth) as     TBGRABitmap;
LBmp.Draw(imgFilterTwirl.Canvas, 0, 0);
LBmp.Free;
BGRABmp.InplaceGrayscale;
BGRABmp.Draw(imgInplaceGrayscale.Canvas, 0, 0, True);
end;

Les filtres ont été regroupés vers la fin du gestionnaire. La seule exception est la méthode GrayScale qui est appliquée tout à la fin : elle est en effet irréversible !

L'exécution de cette application produit une fenêtre comme celle qui suit :

Le choix est large et nous allons en profiter pour y puiser de nouveaux outils !

IV-B - Les effets spéciaux

Comme nous l'avons dit plus haut, parmi les méthodes présentées, certaines ne se prêtent pas facilement à des transitions parce qu'elles s'appliquent en une seule fois. D'autres comme FilterRotate existent déjà sous une forme plus adaptée à notre propos. Une idée à retenir serait de réserver les plus pertinentes d'entre elles à une modification du fond de la transition, c'est-à-dire de l'image d'origine. Nous pouvons par exemple imaginer que l'image d'origine soit immédiatement transformée par la méthode FilterContour afin de dessiner uniquement le contour des objets décelables sur elle. C'est sur cette image transformée que serait dessinée celle de destination.

Nous verrons dans le composant final que cette idée sera développée en proposant une option d'affichage baptisée « effets spéciaux ».

Sans guère nous étendre pour le moment sur cette option, nous noterons qu'elle exploitera les méthodes selon le tableau suivant :

Nom de l'effet

Méthode

Effet obtenu

None

Aucune

Pas d'effet actif

Emboss

FilterEmboss

Gaufrage de l'image (paramétrable, mais avec des différences peu visibles)

Contour

FilterContour

Contour des objets décelables sur l'image

Negative

Negative

Effet de négatif photo

NegativeLinear

NegativeLinear

Effet de négatif photo sans correction de gamma

SwapRedBlue

SwapRedBlue

Inversion du rouge et du bleu

Sphere

FilterSphere

Intégration dans une sphère

Rotate

FilterRotate

Rotation (paramétrable)

GrayScale

InplaceGrayScale

Niveaux de gris

Pixelate

FilterPixelate

Pixellisation de l'image (paramétrable)

Twirl

FilterTwirl

Distorsion par enroulement
(paramétrable)

BlurMotion

FilterBlurMotion

Flou orienté
(paramétrable)

Cette option opérera donc par un prétraitement de l'image d'origine suivi du traitement de l'image de destination elle-même.

IV-C - Les transitions avec filtre

Certaines des méthodes entrevues sont cependant directement exploitables du point de vue qui nous intéresse et peuvent engendrer de nouvelles transitions originales. Nous en verrons plus spécialement deux : FilterPixelate et FilterBlurMotion.

[Exemple BGRABitmap 42]

Nous connaissons déjà la première puisque nous avons suggéré de l'intégrer aux options de prétraitement. Elle peut en fait donner naissance à une transition inédite : il s'agira de faire apparaître en bloc l'image de destination, mais avec une décroissance progressive de la taille des pixels affichés jusqu'au minimum possible.

Voici le code proposé (à noter qu'il n'utilise pas les masques) :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp, LBGRAPixelate: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
LX := 0;
LY := 0;
Inc(fStep);
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement 2 ici (destination)...
LBGRAPixelate := fBGRATo.Duplicate as TBGRABitmap;
try
BGRAReplace(LBGRAPixelate, LBGRAPixelate.FilterPixelate(100 - fStep, True) as TBGRABitmap);
LBGRATemp.PutImage(LX, LY, LBGRAPixelate, dmDrawWithTransparency, Opacity);
finally
LBGRAPixelate.Free;
end;
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

La méthode FilterPixelate est déclarée ainsi au cœur de la classe TBGRABitmap :

function FilterPixelate(pixelSize: integer; useResample: boolean; filter: TResampleFilter = rfLinear): TBGRACustomBitmap; override;

Son paramètre essentiel nommé pixelsize permet de déterminer la taille d'affichage du pixel représenté sous la forme d'un carré dont la longueur du côté est donnée par le nombre de pixels à utiliser. Le paramètre useResample indique si un algorithme de redimensionnement doit être mis en œuvre tandis que le paramètre filter fournit le nom de cet algorithme (par défaut, il vaut rfLinear).

Voici une courte vidéo de cette transition (appelée tout simplement Pixelate) en action :

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://gilles-vasseur.developpez.com/tutoriels/transitions/bgra5/fichiers/Pixelate.webm

Dans le même esprit, nous allons construire une transition qui partira d'un flou marqué à une image parfaitement claire.

[Exemple BGRABitmap 43]

Le modèle de code de la transition que nous nommerons BlurMotion reprendra celui de la transition précédente Pixelate :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRATemp, LBGRABlurMotion: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
repeat
LX := 0;
LY := 0;
Inc(fStep);
LBGRATemp.Fill(BGRABlack);
LBGRATemp.PutImage(LX, LY, fBGRAFrom, dmSet, Opacity(False));
// traitement 2 ici (destination)...
LBGRABlurMotion := fBGRATo.Duplicate as TBGRABitmap;
try
BGRAReplace(LBGRABlurMotion, LBGRABlurMotion.FilterBlurMotion(100 - fStep,
100 - fStep, False));
LBGRATemp.PutImage(LX, LY, LBGRABlurMotion, dmDrawWithTransparency, Opacity);
finally
LBGRABlurMotion.Free;
end;
LBGRATemp.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRATemp.Free;
btnGo.Enabled := True;
end;
end;

La fonction FilterBlurMotion renvoie elle aussi un objet de type TBGRABitmap, d'où la nécessité de passer par un système assez lourd de destruction d'objet à chaque tour si nous souhaitons gagner en netteté.

La vidéo suivante rend compte du résultat de notre travail :

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://gilles-vasseur.developpez.com/tutoriels/transitions/bgra5/fichiers/filterblurmotion.webm

La lourdeur de réalisation de cette transition s'accompagne d'une lenteur d'exécution qui, à moins de trouver une optimisation du code, fera y réfléchir à deux fois avant de l'intégrer sans adaptation au composant final ! Il ne s'agit en rien d'une critique de l'implémentation de l'effet, mais d'une remarque inhérente à l'effet lui-même.

V - Conclusion

Avec ce tutoriel se termine la découverte des principales transitions qui seront utilisées par le composant final. Dès le prochain épisode, nous poserons les fondations du composant TGVTransition en commençant par les effets d'interpolation, une gestion mieux maîtrisée de l'opacité et de la fluidité de l'affichage, ainsi qu'une implémentation de son squelette.

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