Effets de transition avec Lazarus et BGRABitmap 3

3. Travailler avec des masques

Gilles Vasseur

Grâce aux précédents tutoriels, vous savez installer la bibliothèque BGRABitmap, bâtir un logiciel de test avec la prise en charge de la vitesse d'affichage et de l'opacité, ainsi qu'implémenter quelques transitions simples. Dans ce tutoriel, nous étudierons des techniques plus complexes mettant en œuvre des masques afin de produire des transitions encore plus attrayantes.

Commentez

- Titre : Effets de transition avec Lazarus et BGRABitmap 3

- Auteur : Gilles Vasseur

- Parution : 26 mai 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. La notion de masque

- II. Les masques rectangulaires

- II-A. Les expansions de rectangles

- II-B. Les contractions de rectangles

- II-C. Jeux avec les côtés de l'image

- II-D. Variantes avec les rectangles

- III. Des masques avec d'autres formes géométriques

- III-A. Les ellipses et les cercles

- III-B. Les polygones et les formes composées

- IV. Conclusion

I - La notion de masque

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

Avec les méthodes employées jusqu'à présent, nous ne pouvons que superposer deux images. Les effets sont en fait produits par un déplacement d'une ou des deux images. Cependant, il est des cas où cette technique est insuffisante : imaginons par exemple une image qui recouvrirait l'autre progressivement, mais en prenant la forme d'une croix qui grandirait jusqu'à couvrir toute la surface.

Le schéma ci-après montre pour trois étapes l'évolution de l'affichage en fonction du temps :

Pour rappel, ces schémas affichent en rouge l'image d'origine et en bleu l'image de destination.

Nous voyons que la procédure qui consisterait à découper l'image de destination en portions à afficher sur l'image d'origine selon la progression de la transition serait complexe à écrire et chronophage. Heureusement, pour nous tirer d'embarras, il existe les masques !

Un masque est une image en tons de gris qui filtre l'affichage de l'image à superposer : plus un pixel est sombre, moins le pixel correspondant de l'image à superposer sera visible. Avec un pixel noir du masque (couleur BGRABlack prédéfinie dans BGRABitmap), nous ferons donc disparaître le pixel de l'image de destination ; avec un pixel blanc (couleur BGRAWhite), nous le garderons visible.

Le fonctionnement de cette technique pour la transition désirée pourrait être représenté ainsi :

Nous dessinerons dans un premier temps l'image d'origine. Dans un deuxième temps, nous dessinerons notre masque qui sera ensuite appliqué à l'image de destination. Le résultat obtenu sera superposé à l'image d'origine pour obtenir l'image finale.

Le lecteur attentif pourra être surpris par le nombre d'images différentes qu'il faut manipuler pour obtenir le résultat escompté. En effet, il en faut deux (classes TPicture ou TBitmap) pour les sources de la transition, une avec une propriété TCanvas pour son résultat, mais aussi deux (classe TBGRABitmap) pour les manipulations propres à la bibliothèque BGRABitmap, ainsi qu'une dernière (toujours TBGRABitmap) pour le masque. Par la suite, pour des raisons que nous expliciterons en temps utile, nous en aurons même besoin d'autres, mettant en œuvre éventuellement des classes supplémentaires !

En fait, les fenêtres et les contrôles de la LCL ne connaissent rien de BGRABitmap : ils ne travaillent qu'avec des TPicture, TBitmap et TCanvas. Il faut voir un objet de type TBGRABitmap comme une zone de travail : quand le travail est terminé, cette zone est recopiée sur un TCanvas (en général avec la méthode Draw).

[Exemple BGRABitmap 13]

La traduction en code demande de revoir notre application de test pour le gestionnaire OnClick. En particulier, il nous faudra une nouvelle variable locale (baptisée LBGRAMask) pour abriter le dessin du masque.

Voici le code proposé pour l'effet baptisé CrossExpand :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRAFrom, LBGRATo, LBGRAMask: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
// création de l'image d'origine
LBGRAFrom := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
// création de l'image de destination
LBGRATo := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
// création du masque
LBGRAMask := TBGRABitmap.Create(imgResult.ClientWidth, ClientHeight, BGRABlack);
try
fStep := 0;
// la boucle des dessins commence...
repeat
// étape en cours
Inc(fStep);
// traitement 1 ici (source)
LX := 0;
LY := 0;
LBGRAFrom.Fill(BGRABlack);
// image d'origine
LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));
// traitement 2 ici (destination)...
// le dessin de la croix commence au centre de l'image
LX := (imgResult.ClientWidth div 2) * fStep div 100;
LY := (imgResult.ClientHeight div 2) * fStep div 100;
// construction du masque
// entièrement transparent au début
LBGRAMask.Fill(BGRABlack);
LBGRAMask.FillRectAntialias(-LX + imgResult.ClientWidth div 2, 0,
LX + imgResult.ClientWidth div 2, imgResult.ClientHeight,
BGRAWhite);
LBGRAMask.FillRectAntialias(0, -LY + imgResult.ClientHeight div 2,
imgResult.ClientWidth, LY + imgResult.ClientHeight div 2,
BGRAWhite);
// image de destination...
LBGRATo.PutImage(0, 0, fBGRATo, dmSet);
// ... à laquelle on applique le masque
LBGRATo.ApplyMask(LBGRAMask);
// destination sur origine
LBGRAFrom.PutImage(0, 0, LBGRATo, dmDrawWithTransparency, Opacity);
// le résultat est affiché
LBGRAFrom.Draw(imgResult.Canvas, 0, 0, False);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRAMask.Free; // libération du masque
end;
finally
LBGRATo.Free; // libération de l'image de destination
end;
finally
LBGRAFrom.Free; // libération de l'image d'origine
btnGo.Enabled := True;
end;
end;

Bien que ce code suive l'algorithme annoncé et qu'il soit largement commenté, nous noterons que le programmeur doit prendre garde de toujours libérer les ressources allouées pour les images et faire très attention à la gestion de la transparence afin qu'elle soit correctement prise en charge si elle est activée.

Le traitement de l'image d'origine est superflu en l'état actuel du projet, mais il permettra si besoin de s'adapter à des situations plus complexes.

Comme nous travaillons dans une boucle, le contenu du masque doit être réinitialisé à chaque étape. Cette remarque explique la présence, en tout début de travail sur le masque, d'un remplissage avec un rectangle entièrement noir, donc produisant une image invisible. Il s'agit d'une convention puisque nous aurions aussi pu partir d'une image blanche à noircir pour les parties à masquer, même si cela nous aurait conduit à dessiner quatre rectangles au lieu de deux.

À l'exécution, nous obtiendrons un écran comme ci-après :

La réciproque de la transition CrossExpand pourra être baptisée CrossShrink. Nous aurons seulement par exemple à inverser les images et à modifier légèrement les calculs précédents pour l'obtenir.

Le code deviendra alors :

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

L'effet produira des écrans comme suit :

[Exemple BGRABitmap 14]

Du premier essai, nous pouvons tirer un squelette de méthode réutilisable. Notre programme de test devient alors, en ne modifiant que la partie étudiée ci-dessus :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRAFrom, LBGRATo, LBGRAMask: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRAFrom := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRATo := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRAMask := TBGRABitmap.Create(imgResult.ClientWidth, ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
// traitement 1 ici (source)
LX := 0;
LY := 0;
LBGRAFrom.Fill(BGRABlack);
LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));

// traitement 2 ici (destination)...

// construction du masque
LBGRAMask.Fill(BGRABlack);

LBGRATo.PutImage(0, 0, fBGRATo, dmSet);
LBGRATo.ApplyMask(LBGRAMask);
LBGRAFrom.PutImage(0, 0, LBGRATo, dmDrawWithTransparency, Opacity);
LBGRAFrom.Draw(imgResult.Canvas, 0, 0, False);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRAMask.Free;
end;
finally
LBGRATo.Free;
end;
finally
LBGRAFrom.Free;
btnGo.Enabled := True;
end;
end;

À partir de cette trame, nous pouvons construire de nombreuses autres transitions dès lors qu'elles produisent des images résultant de portions de l'image de destination.

Nous savons aussi que certains cas impliquent l'inversion des images avec des calculs un peu modifiés. Une autre solution de ce problème est possible désormais avec la technique des masques : nous pouvons par exemple faire disparaître progressivement l'image d'origine et ne dessiner avec un masque que la portion valide (celle à voir) de l'image de destination. L'inversion ne portera alors au pire que sur le noir et le blanc du masque.

Par exemple, voici une réécriture avec les masques de la transition LeaveTopLeft  :

// traitement 1 ici (source)
LX := -imgResult.ClientWidth * fStep div 100;
LY := -imgResult.ClientHeight * fStep div 100;
LBGRAFrom.Fill(BGRABlack);
LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency,
Opacity(False));
// traitement 2 ici (destination)...
LBGRAMask.Fill(BGRAWhite);
LBGRAMask.FillRectAntialias(LX, LY, LX + imgResult.ClientWidth,
LY + imgResult.ClientHeight, BGRABlack);
LBGRATo.PutImage(0, 0, fBGRATo, dmSet);
LBGRATo.ApplyMask(LBGRAMask);
LBGRAFrom.PutImage(0, 0, LBGRATo, dmDrawWithTransparency, Opacity);
LBGRAFrom.Draw(imgResult.Canvas, 0, 0, False);
Application.ProcessMessages;

Toutes les transitions simples étudiées dans le deuxième tutoriel pourraient être réécrites avec des masques. Nous y perdrions en simplicité du code et très légèrement en vitesse d'affichage, mais nous n'aurions besoin que d'une méthode pour générer toutes les transitions. Nous utiliserons cette possibilité lorsque nous réaliserons une application reprenant l'ensemble des transitions implémentées.

II - Les masques rectangulaires

Si nous avons envisagé le cas d'une croix, nous pouvons bien entendu partir d'une ellipse, d'un rectangle ou d'une forme encore plus complexe.

Une première série de transitions aura trait à l'expansion ou à la contraction progressive d'un rectangle qui remplacera l'image d'origine par celle de destination ou découvrira cette dernière.

II-A - Les expansions de rectangles

[Exemple BGRABitmap 15]

La transition RightBottomExpand agrandira un rectangle en direction du point inférieur droit de l'image finale, recouvrant peu à peu l'image d'origine par celle de destination.

Le schéma associé à cette transition est donc le suivant :

Le code à insérer dans notre application modèle ne comporte qu'une ligne :

// construction du masque
LBGRAMask.Fill(BGRABlack);
// nouvelle ligne insérée
LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth * fStep div 100, imgResult.ClientHeight * fStep div 100, BGRAWhite);

En effet, le schéma montre clairement que le masque est réduit à un rectangle toujours situé dans le coin supérieur gauche de l'image.

Un aperçu de son action est fourni ci-après :

Les transitions de la même famille ressemblent beaucoup à RightBottomExpand, aussi nous contenterons-nous de fournir le code associé à chacune et un instantané de la transition en action.

Bien sûr, vous aurez eu le courage et la sagesse de trouver le code par vous-même, au besoin avec l'aide d'un schéma approprié !

Le code associé à LeftBottomExpand sera :

LBGRAMask.FillRectAntialias(imgResult.ClientWidth, 0, imgResult.ClientWidth * (100 - fStep) div 100, imgResult.ClientHeight * fStep div 100, BGRAWhite);

Le code associé à RightTopExpand sera :

LBGRAMask.FillRectAntialias(0, imgResult.ClientHeight,
imgResult.ClientWidth * fStep div 100,
imgResult.ClientHeight * (100 - fStep) div 100, BGRAWhite);

Enfin, le code associé à LeftTopExpand sera :

LBGRAMask.FillRectAntialias(imgResult.ClientWidth,
imgResult.ClientHeight,imgResult.ClientWidth * (100 - fStep) div 100,
imgResult.ClientHeight * (100 - fStep) div 100, BGRAWhite);

II-B - Les contractions de rectangles

Nous passerons rapidement sur la présentation des contractions puisqu'elles forment un ensemble de transitions inverses de celles qui viennent d'être étudiées. Cette fois-ci, c'est l'image d'origine qui disparaîtra peu à peu dans un rectangle de plus en plus petit.

[Exemple BGRABitmap 16]

Une seule de ces transitions sera un peu plus détaillée : LeftTopShrink. Même si, comme nous l'avons vu plus haut, il ne s'agit qu'une des possibilités, le code du gestionnaire OnClick intervertira les images d'origine et de destination puisque c'est la première qui devra être affectée par la contraction. De même, les calculs inverseront la progression de l'étape fournie par fStep en utilisant une soustraction qui fixera à 100 le point de départ.

Finalement, voici la méthode OnClick modifiée :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRAFrom, LBGRATo, LBGRAMask: TBGRABitmap;
LY, LX: Integer;
begin
btnGo.Enabled := False;
LBGRAFrom := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRATo := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRAMask := TBGRABitmap.Create(imgResult.ClientWidth, ClientHeight, BGRABlack);
try
fStep := 0;
repeat
Inc(fStep);
// traitement 1 ici (source)
LX := 0;
LY := 0;
LBGRAFrom.Fill(BGRABlack);
LBGRAFrom.PutImage(LX, LY, fBGRATo, dmDrawWithTransparency, Opacity(False));
// traitement 2 ici (destination)...
LBGRAMask.Fill(BGRABlack);
LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth * (100 - fStep) div 100,
imgResult.ClientHeight * (100 - fStep) div 100, BGRAWhite);
LBGRATo.PutImage(0, 0, fBGRAFrom, dmSet);
LBGRATo.ApplyMask(LBGRAMask);
LBGRAFrom.PutImage(0, 0, LBGRATo, dmDrawWithTransparency, Opacity);
LBGRAFrom.Draw(imgResult.Canvas, 0, 0, False);
imgResult.Repaint;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRAMask.Free;
end;
finally
LBGRATo.Free;
end;
finally
LBGRAFrom.Free;
btnGo.Enabled := True;
end;
end;

L'application de test en action affichera par exemple :

Pour parvenir à leurs fins, les transitions de la même famille ne modifieront que la ligne de calcul du masque.

Ainsi, la transition RightTopShrink sera implémentée de cette façon :

LBGRAMask.FillRectAntialias(imgResult.ClientWidth,
0,
imgResult.ClientWidth * fStep div 100,
imgResult.ClientHeight * (100 - fStep) div 100, BGRAWhite);

La transition LeftBottomShrink aura quant à elle cette forme :

LBGRAMask.FillRectAntialias(0,
imgResult.ClientHeight,
imgResult.ClientWidth * (100 - fStep) div 100,
imgResult.ClientHeight * fStep div 100, BGRAWhite);

Enfin, la transition RightBottomShrink fermera la marche avec ce code :

LBGRAMask.FillRectAntialias(imgResult.ClientWidth,
imgResult.ClientHeight,
imgResult.ClientWidth * fStep div 100,
imgResult.ClientHeight * fStep div 100, BGRAWhite);

Le principe des variantes est toujours le même : à partir d'une formule, nous déclinons les possibilités en jouant sur les coordonnées ou les tailles.

II-C - Jeux avec les côtés de l'image

Parmi les transitions à définir grâce aux masques, celles qui exploitent les côtés des images sont parmi les plus faciles à implémenter. Nous définirons ainsi quatre nouvelles transitions : HorizontalExpand, HorizontalShrink, VerticalExpand et VerticalShrink. Avec ces transitions, nous reprenons bien évidemment notre modèle habituel de test.

[Exemple BGRABitmap 17]

Voici le schéma correspondant à HorizontalExpand :

Son implémentation en découle immédiatement :

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

La transition HorizontalShrink obéira quant à elle à un schéma très proche de la précédente. Toutefois, il faudra deux rectangles pour construire le masque :

Comme souvent, il serait possible d'implémenter autrement cette transition en faisant remarquer qu'elle peut être considérée comme la disparition progressive en allant vers le centre de l'image d'origine. Il faudrait alors, comme nous l'avons fait en plusieurs occasions, inverser les images de travail. Nous n'aurions alors qu'un rectangle de masque à dessiner.

Son implémentation ne différera par conséquent que par les deux lignes pour calculer les coordonnées des rectangles nécessaires au masque :

LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth * fStep div 200, imgResult.ClientHeight, BGRAWhite);
LBGRAMask.FillRectAntialias(-1 + imgResult.ClientWidth * (200 - fStep) div 200, 0, imgResult.ClientWidth, imgResult.ClientHeight, BGRAWhite);

Vous aurez remarqué la constante -1 placée en début d'expression du second rectangle constituant le masque : elle est là pour éviter l'arrondi du calcul sur les pixels qui produirait une ligne verticale non couvrante.

Les deux dernières transitions opèrent de la même façon, mais en jouant sur la verticale. Nous nous contenterons de fournir le code qui les concerne.

Pour VerticalExpand, nous aurons :

LBGRAMask.FillRectAntialias(0, (100 - fStep) * imgResult.ClientHeight div 200, imgResult.ClientWidth, (100 + fStep) * imgResult.ClientHeight div 200, BGRAWhite);

Pour VerticalShrink, il faudra écrire :

LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth,
imgResult.ClientHeight * fStep div 200, BGRAWhite);
LBGRAMask.FillRectAntialias(0, -1 + imgResult.ClientHeight * (200 - fStep) div 200, imgResult.ClientWidth, imgResult.ClientHeight, BGRAWhite);

II-D - Variantes avec les rectangles

[Exemple BGRABitmap 18]

Une autre idée serait de faire apparaître un rectangle au centre de l'image d'origine et de le faire croître pour découvrir l'image de destination. C'est l'objet de la transition RectOut. Comme nous avons une certaine expérience de ces transitions mettant en œuvre un masque, nous nous contenterons de fournir directement le code nécessaire.

LX := (imgResult.ClientWidth div 2) * fStep div 100;
LY := (imgResult.ClientHeight div 2) * fStep div 100;
LBGRAMask.FillRectAntialias(- LX + imgResult.ClientWidth div 2, - LY +
imgResult.ClientHeight div 2, LX + imgResult.ClientWidth div 2, LY +
imgResult.ClientHeight div 2, BGRAWhite);

Les variables locales LX et LY sont utilisées afin d'éviter de recalculer les coordonnées pour chaque portion de la formule.

De même, l'inverse RectIn de cette opération est plutôt facile à obtenir. Afin de varier les solutions apportées aux problèmes posés, au lieu d'intervertir les images selon la technique déjà employée à plusieurs reprises, nous pouvons très bien inverser les couleurs du masque pour obtenir le même résultat : le blanc devient noir et réciproquement.

Le code obtenu sera alors :

// traitement 2 ici (destination)...
LBGRAMask.Fill(BGRAWhite);
LX := (imgResult.ClientWidth div 2) * fStep div 100;
LY := (imgResult.ClientHeight div 2) * fStep div 100;
LBGRAMask.FillRectAntialias(LX, LY, imgResult.ClientWidth - LX,
imgResult.ClientHeight - LY, BGRABlack);

III - Des masques avec d'autres formes géométriques

Jusqu'à présent, nous n'avons utilisé que les rectangles comme outils de construction du masque, mais d'autres formes géométriques aussi complexes que voulu peuvent créer des effets intéressants. C'est ce que nous allons voir ci-après.

III-A - Les ellipses et les cercles

Pourquoi se limiter à des rectangles ? La bibliothèque BGRABitmap propose d'autres formes prédéfinies dont les ellipses, par ailleurs transformables en cercles en choisissant des coordonnées adaptées. La prudence s'impose cependant, car il faut les choisir de telle façon que l'image de destination puisse de toute façon entièrement recouvrir l'image d'origine.

Avec BGRABitmap, une ellipse pleine est dessinée grâce à un point à partir duquel sont calculés le rayon horizontal et celui vertical. Nous avons deux procédures utiles à notre disposition. Leur dernier paramètre précise si c'est une couleur ou une texture qui est utilisée :

{** Fills an ellipse }
procedure FillEllipseAntialias(x, y, rx, ry: single; c: TBGRAPixel); override;
{** Fills an ellipse with a ''texture'' }
procedure FillEllipseAntialias(x, y, rx, ry: single; texture: IBGRAScanner); override;

[Exemple BGRABitmap 19]

La première transition du type ellipsoïdal sera EllipseOut. En voici le code caractéristique :

LBGRAMask.Fill(BGRABlack);
LBGRAMask.FillEllipseAntialias(imgResult.ClientWidth div 2,
imgResult.ClientHeight div 2, fStep * imgResult.ClientWidth div 140,
fStep * imgResult.ClientHeight div 140, BGRAWhite);

Nous sommes revenus à l'application modèle sans intervertir le blanc et le noir, d'où l'initialisation dans la première ligne de traitement du masque avec une surface entièrement noire.

De même, nous pouvons imaginer la transition EllipseIn qui formera une ellipse de plus en plus petite pour la faire disparaître au centre de l'image de résultat.

LBGRAMask.Fill(BGRAWhite);
LBGRAMask.FillEllipseAntialias(imgResult.ClientWidth div 2,
imgResult.ClientHeight div 2, (100 - fStep) * imgResult.ClientWidth
div 140, (100 - fStep) * imgResult.ClientHeight div 140, BGRABlack);

La technique consistant à inverser le blanc et le noir du masque a été réutilisée ici.

III-B - Les polygones et les formes composées

Grâce aux procédures fournies avec BGRABitmap, nous sommes en mesure de dessiner toutes sortes de figures, de la plus simple (par exemple, un triangle) à la plus complexe. Pour ce faire, outre les rectangles et les ellipses déjà vus, nous disposons d'un outil très utile : FillPolyAntialias. Cette méthode construit un polynôme dont les points sont donnés en paramètre sous forme d'un tableau ouvert de TPointF et remplit ensuite la surface ainsi délimitée par la couleur fournie en second paramètre.

TPointF est un enregistrement défini dans la RTL de Free Pascal. Il considère les points comme un enregistrement de deux réels (type single) sur lesquels l'utilisateur peut intervenir grâce à un ensemble conséquent de routines et d'opérateurs de classe :

{ TPointF }
TPointF =
{$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}
packed
{$endif FPC_REQUIRES_PROPER_ALIGNMENT}
record
x,y : Single;
public
function Add(const apt: TPoint): TPointF;
function Add(const apt: TPointF): TPointF;
function Distance(const apt : TPointF) : Single;
function DotProduct(const apt : TPointF) : Single;
function IsZero : Boolean;
function Subtract(const apt : TPointF): TPointF;
function Subtract(const apt : TPoint): TPointF;
procedure SetLocation(const apt :TPointF);
procedure SetLocation(const apt :TPoint);
procedure SetLocation(ax,ay : Longint);
procedure Offset(const apt :TPointF);
procedure Offset(const apt :TPoint);
procedure Offset(dx,dy : Longint);

function  Scale (afactor:Single)  : TPointF;
function  Ceiling : TPoint;
function  Truncate: TPoint;
function  Floor   : TPoint;
function  Round   : TPoint;
function  Length  : Single;
class operator = (const apt1, apt2 : TPointF) : Boolean;
class operator <> (const apt1, apt2 : TPointF): Boolean;
class operator + (const apt1, apt2 : TPointF): TPointF;
class operator - (const apt1, apt2 : TPointF): TPointF;
class operator - (const apt1 : TPointF): TPointF;
class operator * (const apt1, apt2: TPointF): Single; // scalar product
class operator * (const apt1: TPointF; afactor: single): TPointF;
class operator * (afactor: single; const apt1: TPointF): TPointF;
end;

La seule véritable difficulté est de choisir correctement les points et de prévoir avec rigueur leur déplacement au cours du déroulement de la transition !

[Exemple BGRABitmap 20]

Un premier exemple illustrera ce que nous venons de découvrir. Nous pouvons imaginer une transition qui superposerait comme d'habitude l'image de destination à celle d'origine en utilisant quatre triangles isocèles dont la base serait un des côtés du rectangle que forme l'image et dont le sommet opposé à cette base suivrait le milieu du côté où il se situe pour rejoindre le centre de la même image. La transition verrait donc les triangles s'étendre jusqu'à se rejoindre au centre de l'image. Le schéma de fonctionnement serait donc le suivant :

Pour la construction du masque, nous aurons :

Le code correspondant serait :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRAFrom, LBGRATo, LBGRAMask: TBGRABitmap;
LY, LX: Integer;
LPts: array of TPointF;
begin
btnGo.Enabled := False;
LBGRAFrom := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRATo := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRAMask := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
SetLength(LPts, 3);
repeat
Inc(fStep);
// traitement 1 ici (source)
LX := 0;
LY := 0;
LBGRAFrom.Fill(BGRABlack);
LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));
// traitement 2 ici (destination)...
// triangle bord gauche
LBGRAMask.Fill(BGRABlack);
LPts[0].x := 0;
LPts[0].y := 0;
LPts[1].x := imgResult.ClientWidth / 2 * fStep / 100;
LPts[1].y := imgResult.ClientHeight / 2;
LPts[2].x := 0;
LPts[2].y := imgResult.ClientHeight;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
// triangle opposé (bord droit)
LPts[0].x := imgResult.ClientWidth;
LPts[0].y := 0;
LPts[1].x := imgResult.ClientWidth - imgResult.ClientWidth / 2 * fStep / 100;
LPts[1].y := imgResult.ClientHeight / 2;
LPts[2].x := imgResult.ClientWidth;
LPts[2].y := imgResult.ClientHeight;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
// triangle bord bas
LPts[0].x := 0;
LPts[0].y := imgResult.ClientHeight;
LPts[1].x := imgResult.ClientWidth / 2;
LPts[1].y := imgResult.ClientHeight - imgResult.ClientHeight / 2 * fStep / 100;
LPts[2].x := imgResult.ClientWidth;;
LPts[2].y := imgResult.ClientHeight;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
//triangle opposé (bord haut)
LPts[0].x := 0;
LPts[0].y := 0;
LPts[1].x := imgResult.ClientWidth / 2;
LPts[1].y := imgResult.ClientHeight / 2 * fStep / 100;
LPts[2].x := imgResult.ClientWidth;
LPts[2].y := 0;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
LBGRATo.PutImage(0, 0, fBGRATo, dmSet);
LBGRATo.ApplyMask(LBGRAMask);
LBGRAFrom.PutImage(0, 0, LBGRATo, dmDrawWithTransparency, Opacity);
LBGRAFrom.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRAMask.Free;
end;
finally
LBGRATo.Free;
end;
finally
LBGRAFrom.Free;
btnGo.Enabled := True;
end;
end;

Vous aurez remarqué que les coordonnées manipulées par le tableau de points sont exprimées par des nombres flottants, d'où le F qui clôt le nom du type et l'emploi de / pour la division au lieu de div. Notez aussi que nous pourrions simplifier les calculs en supprimant des lignes redondantes (laissées pour une meilleure compréhension des calculs effectués) puisque certaines variables contiennent déjà la valeur à leur affecter.

Vous pouvez raccourcir l'écriture du code en fournissant en une seule instruction l'abscisse et l'ordonnée d'un point. Par exemple :

LPts[1].x := imgResult.ClientWidth / 2 * fStep / 100;
LPts[1].y := imgResult.ClientHeight / 2;

peut aussi s'écrire :

Lpts[1]:= PointF(imgResult.ClientWidth / 2 * fStep / 100,  imgResult.ClientHeight / 2);

La transition en action donnera par exemple :

[Exemple BGRABitmap 21]

Évidemment, nous pouvons compliquer à loisir notre transition, même à partir de figures simples comme les triangles, afin d'obtenir des transitions vraiment spectaculaires. Dans la portion de code qui suit, nous définissons une surface composée de nouveau de quatre triangles, mais en suivant les données et règles suivantes :

- un triangle isocèle dont chaque sommet au départ de la transition est à une distance d'un tiers des bords de l'image de travail (sa hauteur vaut donc un tiers de la hauteur de l'image tandis que sa base vaut un tiers de la largeur de la même image) ;

- un autre triangle isocèle de même taille, mais tête-bêche par rapport au premier, placé de telle manière que ses sommets aient la même abscisse que les sommets du premier alors que ses ordonnées sont décalées vers le bas l'image pour que l'intersection des côtés passe par le milieu de la hauteur de l'image ;

- un triangle dont deux sommets ont toujours comme coordonnées le point supérieur gauche et le point inférieur gauche de l'image tandis que le troisième sommet toujours à mi-hauteur se déplacera du bord gauche de l'image vers le premier quart de l'image sur sa droite (sa base est donc sur le côté gauche de l'image tandis que ses deux autres côtés suivent sa médiatrice) ;

- un triangle dont deux sommets ont toujours comme coordonnées le point supérieur droit et le point inférieur droit de l'image tandis que le troisième sommet toujours à mi-hauteur se déplacera du bord droit de l'image vers le dernier quart de l'image sur sa gauche dans la même proportion que le triangle précédent (il est donc le symétrique du triangle précédent par rapport au milieu de la largeur de l'image).

L'évolution de chacun des points définis est telle que l'étoile formée par les deux triangles tête-bêche doit se dilater, les deux autres triangles s'approchant d'elle jusqu'à la rejoindre.

Le code proposé est le suivant :

procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
LBGRAFrom, LBGRATo, LBGRAMask: TBGRABitmap;
LY, LX: Integer;
LPts: array of TPointF;
begin
btnGo.Enabled := False;
LBGRAFrom := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRATo := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
LBGRAMask := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
try
fStep := 0;
SetLength(LPts, 3);
repeat
Inc(fStep);
// traitement 1 ici (source)
LX := 0;
LY := 0;
LBGRAFrom.Fill(BGRABlack);
LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));
// traitement 2 ici (destination)...
// premier triangle
LBGRAMask.Fill(BGRABlack);
LPts[0].x := imgResult.ClientWidth / 3 - imgResult.ClientWidth / 3 * fStep / 100;
LPts[0].y := imgResult.ClientHeight / 3 * 2 + imgResult.ClientHeight / 3 * fStep / 100;
LPts[1].x := imgResult.ClientWidth / 2;
LPts[1].y := imgResult.ClientHeight / 3 - imgResult.ClientHeight / 3 * fStep / 100;
LPts[2].x := imgResult.ClientWidth / 3 * 2 + imgResult.ClientWidth / 3 * fStep / 100;
LPts[2].y := LPts[0].y;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
// triangle opposé
//LPts[0].x := imgResult.ClientWidth / 3 - imgResult.ClientWidth / 3 * fStep / 100;
LPts[0].y := imgResult.ClientHeight / 2 - imgResult.ClientHeight / 2 * fStep / 100;
LPts[1].x := imgResult.ClientWidth / 3 * 2 + imgResult.ClientWidth / 3 * fStep / 100;
LPts[1].y := LPts[0].y;
LPts[2].x := imgResult.ClientWidth / 2;
LPts[2].y := imgResult.ClientHeight / 6 * 5 + imgResult.ClientHeight / 6 * fStep / 100;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
// triangle côté gauche
LPts[0].x := 0;
LPts[0].y := 0;
LPts[1].x := imgResult.ClientWidth / 4 * fStep / 100;
LPts[1].y := imgResult.ClientHeight / 2;
LPts[2].x := LPts[0].x;
LPts[2].y := imgResult.ClientHeight;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
//triangle côté droit
LPts[0].x := imgResult.ClientWidth;
//LPts[0].y := 0;
LPts[1].x := imgResult.ClientWidth - imgResult.ClientWidth / 4 * fStep / 100;
//LPts[1].y := imgResult.ClientHeight / 2;
LPts[2].x := LPts[0].x;
//LPts[2].y := imgResult.ClientHeight;
LBGRAMask.FillPolyAntialias(LPts, BGRAWhite);
LBGRATo.PutImage(0, 0, fBGRATo, dmSet);
LBGRATo.ApplyMask(LBGRAMask);
LBGRAFrom.PutImage(0, 0, LBGRATo, dmDrawWithTransparency, Opacity);
LBGRAFrom.Draw(imgResult.Canvas, 0, 0);
Application.ProcessMessages;
sleep(100 - fSpeed);
until fStep = 100;
finally
LBGRAMask.Free;
end;
finally
LBGRATo.Free;
end;
finally
LBGRAFrom.Free;
btnGo.Enabled := True;
end;
end;

Dans le code qui précède, certaines lignes de code ont été neutralisées par leur mise en commentaire : il s'agit de lignes qui aident à comprendre les calculs effectués, mais qui sont superflues puisque les bonnes valeurs sont déjà attribuées aux variables visées.

L'effet produit, nommé MegaStar, est plutôt spectaculaire et original. La vidéo suivante devrait en rendre compte :

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

Dans les exemples donnés, nous nous sommes limités aux triangles, mais rien ne nous interdit de créer des polynômes plus complexes. De plus, les masques peuvent être composites en intégrant toutes les formes imaginables (par exemple : deux rectangles, une ellipse et un octogone) dans la mesure où l'image de destination aura remplacé à la fin de la transition la totalité de l'image d'origine.

IV - Conclusion

Avec ce tutoriel, vous aurez appris à implémenter en Pascal (Lazarus) des transitions d'image à image en utilisant des masques. Cette technique est bien plus souple et puissante que la simple superposition d'images : elle permet des effets plus complexes tout en simplifiant le code à écrire pour les obtenir.

À l'occasion de ce travail, outre la richesse de FillRectAntialias, vous aurez aussi découvert de nouvelles méthodes de la classe TBGRABitmap comme ApplyMask, FillEllipseAntialias ou FillPolyAntialias (qui s'appuie sur les enregistrements de type TPointF). Votre boîte à outils s'est par conséquent bien étendue.

Par la suite, nous multiplierons les occasions d'utiliser des masques, abandonnant ainsi définitivement les premières solutions adoptées.

Mes remerciements vont à Alcatîz et à Roland Chastain pour leurs relectures techniques et à f-leb pour la correction orthographique.