Effets de transition avec Lazarus et BGRABitmap 3

3. Travailler avec des masques

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 Donner une note  l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

Image non disponible

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 :

Image non disponible

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 :

 
Sélectionnez
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.FillRect(ClientRect, 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.FillRectAntialias(0, 0, imgResult.ClientWidth, imgResult.ClientHeight, 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 :

Image non disponible

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 :

 
Sélectionnez
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.FillRect(ClientRect, 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.FillRectAntialias(0, 0, imgResult.ClientWidth, imgResult.ClientHeight, 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 :

Image non disponible

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

 
Sélectionnez
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.FillRect(ClientRect, BGRABlack);
          LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));

          // traitement 2 ici (destination)...

          // construction du masque
          LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth, 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;
          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  :

 
Sélectionnez
// traitement 1 ici (source)
LX := -imgResult.ClientWidth * fStep div 100;
LY := -imgResult.ClientHeight * fStep div 100;
LBGRAFrom.FillRect(ClientRect, BGRABlack);
LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency,
  Opacity(False));
// traitement 2 ici (destination)...
LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth,
  imgResult.ClientHeight, 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 :

Image non disponible

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

 
Sélectionnez
// construction du masque
LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth, imgResult.ClientHeight, 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 :

Image non disponible

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 :

 
Sélectionnez
LBGRAMask.FillRectAntialias(imgResult.ClientWidth, 0, imgResult.ClientWidth * (100 - fStep) div 100, imgResult.ClientHeight * fStep div 100, BGRAWhite);
Image non disponible

Le code associé à RightTopExpand sera :

 
Sélectionnez
LBGRAMask.FillRectAntialias(0, imgResult.ClientHeight,
  imgResult.ClientWidth * fStep div 100,
  imgResult.ClientHeight * (100 - fStep) div 100, BGRAWhite);
Image non disponible

Enfin, le code associé à LeftTopExpand sera :

 
Sélectionnez
LBGRAMask.FillRectAntialias(imgResult.ClientWidth,
  imgResult.ClientHeight,imgResult.ClientWidth * (100 - fStep) div 100,
  imgResult.ClientHeight * (100 - fStep) div 100, BGRAWhite);
Image non disponible

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 :

 
Sélectionnez
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.FillRect(ClientRect, BGRABlack);
          LBGRAFrom.PutImage(LX, LY, fBGRATo, dmDrawWithTransparency, Opacity(False));
          // traitement 2 ici (destination)...
          LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth, imgResult.ClientHeight, 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 :

Image non disponible

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 :

 
Sélectionnez
LBGRAMask.FillRectAntialias(imgResult.ClientWidth,
  0,
  imgResult.ClientWidth * fStep div 100,
  imgResult.ClientHeight * (100 - fStep) div 100, BGRAWhite);
Image non disponible

La transition LeftBottomShrink aura quant à elle cette forme :

 
Sélectionnez
LBGRAMask.FillRectAntialias(0,
  imgResult.ClientHeight,
  imgResult.ClientWidth * (100 - fStep) div 100,
  imgResult.ClientHeight * fStep div 100, BGRAWhite);
Image non disponible

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

 
Sélectionnez
LBGRAMask.FillRectAntialias(imgResult.ClientWidth,
  imgResult.ClientHeight,
  imgResult.ClientWidth * fStep div 100,
  imgResult.ClientHeight * fStep div 100, BGRAWhite);
Image non disponible

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 :

Image non disponible

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

 
Sélectionnez
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.FillRect(ClientRect, BGRABlack);
          LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));
          // traitement 2 ici (destination)...
          LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth, imgResult.ClientHeight, 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;
Image non disponible

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 :

Image non disponible

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 :

 
Sélectionnez
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.

Image non disponible

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 :

 
Sélectionnez
LBGRAMask.FillRectAntialias(0, (100 - fStep) * imgResult.ClientHeight div 200, imgResult.ClientWidth, (100 + fStep) * imgResult.ClientHeight div 200, BGRAWhite);
Image non disponible

Pour VerticalShrink, il faudra écrire :

 
Sélectionnez
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);
Image non disponible

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.

 
Sélectionnez
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.

Image non disponible

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 :

 
Sélectionnez
// traitement 2 ici (destination)...
LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth,
  imgResult.ClientHeight, 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);
Image non disponible

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 :

 
Sélectionnez
{** 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 :

 
Sélectionnez
LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth,
  imgResult.ClientHeight, 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.

Image non disponible

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.

 
Sélectionnez
LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth,
  imgResult.ClientHeight, 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.

Image non disponible

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 :

 
Sélectionnez
{ 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 :

Image non disponible

Pour la construction du masque, nous aurons :

Image non disponible

Le code correspondant serait :

 
Sélectionnez
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.FillRect(ClientRect, BGRABlack);
          LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));
          // traitement 2 ici (destination)...
          // triangle bord gauche
          LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth, imgResult.ClientHeight, 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 :

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

peut aussi s'écrire :

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

La transition en action donnera par exemple :

Image non disponible

[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) ;

    Image non disponible
  • 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 ;

    Image non disponible
  • 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) ;

    Image non disponible
  • 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).
Image non disponible

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 :

 
Sélectionnez
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.FillRect(ClientRect, BGRABlack);
          LBGRAFrom.PutImage(LX, LY, fBGRAFrom, dmDrawWithTransparency, Opacity(False));
          // traitement 2 ici (destination)...
          // premier triangle
          LBGRAMask.FillRectAntialias(0, 0, imgResult.ClientWidth, imgResult.ClientHeight, 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 :

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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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.