Effets de transition avec Lazarus et BGRABitmap

1. Installation et application de démonstration

Ce tutoriel est destiné aux programmeurs désireux d'exploiter au mieux le graphisme avec Lazarus. Il s'appuie sur la bibliothèque BGRABitmap, un outil open source et multiplateforme de grande qualité proposé par Johann Elsass.

Si la classe TBitmap qui accompagne Lazarus est dotée d'un canevas pour le dessin, BGRABitmap possède des caractéristiques supplémentaires devenues indispensables de nos jours : la prise en charge de la transparence et de l'anticrénelage (antialiasing), ainsi qu'une vitesse d'exécution bien plus satisfaisante que les outils livrés par défaut. De surcroît, BGRABitmap est régulièrement mise à jour depuis des années et a prouvé sa stabilité et son efficacité à travers des applications comme LazPaint.

Plutôt que de reprendre des tutoriels déjà en ligne, il m'a paru préférable d'en rédiger un nouveau à partir d'un projet unique, mais a priori intéressant : un composant image capable de gérer des transitions entre les images qu'il est amené à afficher.

13 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

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

Dans ce tutoriel, je proposerai une application rudimentaire qui servira de modèle pour le test des transitions. Cette application sera l'occasion de mettre en œuvre les techniques de base de la bibliothèque. Ensuite, dans des tutoriels futurs, je présenterai dans le détail une série de transitions : il s'agira de bien comprendre ce qui est attendu et comment le rendu répondra à cette attente. Plus tard encore, je fournirai une application plus conséquente pour tester un composant prenant en charge les techniques étudiées. Enfin, un composant entièrement opérationnel sera élaboré de telle façon qu'il puisse être intégré à la palette de Lazarus.

Les connaissances en mathématiques pour la plupart des transitions seront limitées aux opérations élémentaires. Si nécessaires, des éclaircissements seront apportés au coup par coup.

II. Installation et application modèle

Afin de pouvoir procéder à des tests, une application plutôt simple nous servira de modèle. À partir d'une image source, il s'agira de passer à une image de destination grâce à une transition déterminée. La vitesse de transition ainsi que l'opacité des images en jeu seront paramétrables.

II-A. L'interface utilisateur

[Exemple BGRABitmap 01]

Commençons par créer une application à partir du menu « Fichier », option « Nouveau... ». Dans la fenêtre qui s'ouvre, choisissons « Fichier », « Application ».

Image non disponible

En choisissant « Fichier », « Application », nous avons créé la forme la plus fréquente des applications gérées par Lazarus, à savoir une application graphique utilisant la bibliothèque LCL multiplateforme comme interface graphique.

Un bon réflexe consiste à sauvegarder tout de suite cet embryon de programme. Créons un répertoire de travail nommé bgratransitions dans lequel figurera un sous-répertoire nommé 01. Sauvegardons-y notre projet avec Ctrl-Maj-S ou l'icône correspondante sous le nom projet01.lpi. De même, à l'invite, enregistrons la fiche principale sous le nom de main.pas.

L'interface utilisateur ne met en jeu que quelques composants des plus communs. En premier lieu, il nous faut trois TImage : une image pour la source, une pour la destination et une pour dessiner la transition en cours. Nous leur donnerons des tailles différentes afin de rendre l'application plus souple quant au format des images à afficher. Un TButton nous servira à démarrer la transition. Un TCheckBox indiquera si la transparence doit être prise en compte. Enfin, un couple TLabel et TTrackBar nous permettra de régler la vitesse de la transition.

Voici à quoi pourra ressembler cette interface :

Image non disponible

Pour une meilleure exploitation de l'application, nous devons modifier certaines propriétés des composants utilisés.

En ce qui concerne la fiche principale, nous pouvons :

  • la rebaptiser en donnant à sa propriété Name la valeur MainForm ;
  • la centrer dans l'écran avec sa propriété Position mise à poScreenCenter ;
  • afficher un message adapté en en-tête avec la propriété Caption mise à 'Test des transitions - G. Vasseur 2018'.

L'image source (en haut à gauche) aura sa propriété Name fixée à imgFrom alors que l'image de destination (en haut à droite) sera renommée imgTo. Toutes deux auront leur propriété Stretch à True afin de toujours dessiner entièrement l'image, quelles que soient ses dimensions d'origine.

L'image pour le dessin de la transition sera nommée imgResult. Comme le calcul de la transition tiendra compte des dimensions de la surface de dessin, il est inutile de modifier la propriété Stretch de ce composant.

Le bouton pour l'exécution de la transition sera renommé btnGo tandis que sa propriété Caption deviendra « Démarrez ! ». Nous pouvons d'ores et déjà double-cliquer sur ce bouton pour créer le squelette du gestionnaire OnClick qui lui est attaché. Nous renseignerons ultérieurement ce gestionnaire chargé de lancer la transition.

Le composant de type TCheckBox sera nommé cbOpacity et sa propriété Caption deviendra « Transparence ». Nous nous occuperons plus tard de l'action liée à la modification de sa coche.

Le composant de type TTrackBar sera renommé tbarSpeed. Sa propriété Min sera fixée à 1. Sa propriété Max sera portée à 10. Quant à sa propriété Position, nous lui donnerons la valeur 3. Ses différentes valeurs seront expliquées en temps utile. Le gestionnaire de changement de valeur sera lui aussi étudié à cette occasion.

Enfin, le TLabel s'appellera lblSpeed et sa propriété Caption deviendra « Vitesse ». Comme cette étiquette se rapporte au composant de type TTrackBar, il est conseillé de le lier à lui grâce à la propriété FocusControl. Le choix du composant cible se fait grâce à la liste déroulante qui apparaît lorsque nos cliquons sur la valeur de la propriété.

FocusControl n'aura pas d'utilité dans notre application, mais il vaut mieux prendre l'habitude de renseigner cette propriété indispensable dans certains cas où l'utilisation du clavier est obligatoire.

Pour ceux qui sont habitués à la lecture des fichiers de description des fiches (suffixe .lfm), voici la fiche que j'ai utilisée :

 
Sélectionnez
object MainForm: TMainForm
  Left = 392
  Height = 450
  Top = 210
  Width = 677
  Caption = 'Test des transitions - G. Vasseur 2018'
  ClientHeight = 450
  ClientWidth = 677
  OnCreate = FormCreate
  OnDestroy = FormDestroy
  Position = poScreenCenter
  LCLVersion = '1.8.0.6'
  object imgFrom: TImage
    Left = 24
    Height = 154
    Top = 32
    Width = 232
    Stretch = True
  end
  object imgTo: TImage
    Left = 304
    Height = 90
    Top = 32
    Width = 104
    Stretch = True
  end
  object imgResult: TImage
    Left = 304
    Height = 224
    Top = 208
    Width = 298
  end
  object btnGo: TButton
    Left = 24
    Height = 25
    Top = 407
    Width = 75
    Caption = 'Démarrer !'
    OnClick = btnGoClick
    TabOrder = 0
  end
  object cbOpacity: TCheckBox
    Left = 24
    Height = 19
    Top = 365
    Width = 91
    Caption = 'Transparence'
    TabOrder = 1
  end
  object tbarSpeed: TTrackBar
    Left = 24
    Height = 25
    Top = 320
    Width = 100
    Max = 10
    Min = 1
    Position = 3
    TabOrder = 2
  end
  object lblSpeed: TLabel
    Left = 24
    Height = 15
    Top = 292
    Width = 36
    Caption = 'Vitesse'
    FocusControl = tbarSpeed
    ParentColor = False
  end
end

À présent, l'inspecteur d'objets devrait avoir cette apparence :

Image non disponible

Appuyez sur F11 pour faire apparaître l'inspecteur d'objets s'il est invisible.

Pour parfaire notre interface, il suffit de charger les images fournies avec les fichiers du tutoriel. Nous devons double-cliquer sur la propriété Picture des deux composants TImage et charger l'image voulue. Cette dernière sera tronquée dans la fenêtre de chargement, mais, après validation du choix par un clic sur le bouton « OK », elle devrait occuper tout l'espace d'affichage du composant... si sa propriété Stretch est bien à True !

Voici ce à quoi devrait ressembler notre interface définitive :

Image non disponible

Les deux photos de travail sont des photos personnelles. La première est celle d'une mésange bleue et d'un chardonneret qui ont cohabité quelques secondes sur le rebord d'un pot de fleur. La seconde est celle d'un rouge-gorge dans un arbre. Les deux ont été prises au téléobjectif (200 mm) avec un Nikon D5500 depuis la fenêtre de ma cuisine. Il est ainsi prouvé qu'on peut s'intéresser aux oiseaux, aimer cuisiner, tout en trouvant aussi du plaisir à programmer !

II-B. L'installation de BGRABitmap

Une fois notre interface utilisateur prête, nous pouvons nous intéresser au code chargé d'afficher une transition entre nos deux photos d'oiseaux. Comme nous allons utiliser la bibliothèque BGRABitmap, nous devons tout d'abord nous la procurer et l'installer.

Pour se procurer la bibliothèque, c'est ici que ça se passe. Après avoir téléchargé le fichier zip, nous pouvons, par exemple, le décompacter dans le dossier components de Lazarus. La seule partie utile pour nous est le sous-répertoire bgrabitmap que nous placerons directement dans le répertoire components ou à tout autre endroit accessible en lecture et écriture.

L'installation du paquet est très simple. Depuis le gestionnaire de fichiers du système d'exploitation, ouvrons le paquet bgrabitmappack.lpk. L'EDI devrait se charger pour afficher la fenêtre suivante :

Image non disponible

Si le chargement d'un paquet (.lpk), d'un projet (.lpi ou .lpr) ou d'un fichier de code source (.pas ou .pp) ne s'effectue pas automatiquement, vous pouvez toujours opérer depuis l'EDI avec l'option « Ouvrir » du menu principal « Fichier ».

Comme le paquet ne contient aucun composant, il n'installera rien sur la palette. Nous avons seulement à demander sa compilation en cliquant sur l'icône marquée « Compiler ». En quelques secondes, le tour est joué !

Désormais, si nous désirons utiliser cette bibliothèque, nous procéderons comme indiqué ci-après.

Nous commençons par demander à voir l'inspecteur de projet depuis le menu Projet :

Image non disponible

La fenêtre qui s'affiche indique les fichiers et paquets utilisés par le projet en cours. Pour utiliser le paquet BGRABitmap, nous cliquons avec le bouton droit de la souris sur « Paquets requis » :

Image non disponible

Aussitôt s'affiche un menu surgissant rudimentaire avec pour seule option la possibilité d'ajouter un paquet :

Image non disponible

Dans la liste proposée, nous choisissons BGRABitmapPack avant de valider ce choix avec le bouton marqué « Ok » :

Image non disponible

Le paquet est dorénavant intégré au projet. Il sera par conséquent possible d'y faire référence, Lazarus retrouvant les unités nécessaires en cas de besoin. L'intégration du paquet est explicitement marquée dans l'arborescence du projet :

Image non disponible

Nous pouvons à présent travailler avec notre nouveau projet en toute sérénité !

II-C. L'application de base

Grâce aux étapes précédentes, nous avons une interface utilisateur en place et une bibliothèque prête à l'emploi. Reste à concevoir la partie métier de l'application modèle.

Dans un premier temps, afin de nous simplifier la tâche, nous laisserons de côté les notions de vitesse et de transparence pour nous concentrer sur la construction des objets nécessaires. Nous en profiterons pour réfléchir aux éléments indispensables à toute transition dans l'affichage de deux images, c'est-à-dire au contenu du gestionnaire OnClick de l'unique bouton de l'application.

II-C-1. La construction des objets

Pour travailler, la bibliothèque BGRABitmap fait appel à au moins deux unités (BGRABitmapTypes et BGRABitmap) que nous allons immédiatement ajouter à la clause uses de l'interface de l'unité principale baptisée ici main.pas :

 
Sélectionnez
unit main;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
  StdCtrls, ComCtrls, BGRABitmapTypes, BGRABitmap;

L'unité BGRABitmapTypes, comme son nom l'indique, définit les types utilisés par la bibliothèque tandis que l'unité BGRABitmap s'occupe d'adapter la plupart des demandes du programmeur aux différentes plates-formes gérées. Ainsi, derrière un même appel peut se cacher une façon différente de traiter un problème graphique sans que l'utilisateur n'ait à s'en soucier.

La classe essentielle à utiliser est TBGRABitmap. C'est elle qui prend en charge l'anticrénelage et la transparence à travers une organisation sur 32 bits de chaque pixel. BGRA est d'ailleurs l'acronyme de Blue, Green, Red et Alpha, chacun de ces termes désignant un canal permettant de déterminer une des caractéristiques de chaque pixel.

Un pixel est en effet obtenu à partir de la composition d'un octet pour la quantité de bleu, d'un autre pour le vert, d'un troisième pour le rouge, et d'un dernier pour l'opacité (canal alpha). Plus un octet contient une valeur forte plus le canal correspondant est prononcé. En particulier, une valeur de 0 pour le canal alpha indique une complète transparence alors qu'une valeur de 255 du même canal marque une opacité totale.

Pour accéder à ces pixels et dessiner, il faut tout d'abord créer des objets de type TBGRABitmap. Dans la partie privée de la classe représentant la fiche principale (TMainForm), nous déclarons par conséquent deux objets de travail :

 
Sélectionnez
 private
    fBGRAFrom, fBGRATo: TBGRABitmap;

Chaque objet abritera une image, ce qu'indiquera leur création. Les constructeurs possibles pour de tels objets sont multiples, mais nous ne sous servirons pour notre première application que de celui qui prend en paramètre un TBitmap tel que fourni par défaut par la LCL. Ce type de constructeur récupère le bitmap d'un autre objet afin de pouvoir le manipuler à sa guise avant de le restituer grâce à une méthode nommée Draw. Ici, il s'agit de récupérer le bitmap de chaque composant TImage.

Le meilleur emplacement pour créer de tels objets est bien souvent le gestionnaire FormCreate de la fiche principale. Nous en obtenons le squelette en double-cliquant dans l'inspecteur d'objets sur la partie valeur de la propriété OnCreate de l'objet MainForm. Ce squelette est alors à renseigner ainsi :

 
Sélectionnez
procedure TMainForm.FormCreate(Sender: TObject);
// *** construction des objets de travail ***
begin
  fBGRAFrom := TBGRABitmap.Create(imgFrom.Picture.Bitmap);
  fBGRATo := TBGRABitmap.Create(imgTo.Picture.Bitmap);
end;

Comme nous ne savons pas a priori quelle taille fait une image à traiter, après l'avoir créée, nous devons la redimensionner pour que nos deux images soient compatibles et puissent fusionner dans la troisième, celle du résultat de la transition à une étape donnée.

Par conséquent, nous complétons notre gestionnaire avec un appel à la méthode Resample de l'objet créé. Cette méthode sait redimensionner de manière élégante, c'est-à-dire avec peu de perte grâce à un algorithme adapté, tout bitmap qui lui est confié.

Le redimensionnement se fait ici en fonction de la largeur et de la hauteur de l'image d'accueil, à savoir imgResult :

 
Sélectionnez
procedure TMainForm.FormCreate(Sender: TObject);
// *** construction des objets de travail ***
begin
  fBGRAFrom := TBGRABitmap.Create(imgFrom.Picture.Bitmap);
  fBGRAFrom := fBGRAFrom.Resample(imgResult.ClientWidth, imgResult.ClientHeight) as TBGRABitmap;
  fBGRATo := TBGRABitmap.Create(imgTo.Picture.Bitmap);
  fBGRATo := fBGRATo.Resample(imgResult.ClientWidth, imgResult.ClientHeight) as TBGRABitmap;
end;

Vous remarquerez le transtypage du résultat de la méthode Resample par as accompagné du type TBGRABitmap. En effet, la méthode renvoie un objet de type TBGRACustomBitmap qui doit devenir le type TBGRABitmap attendu par les variables affectées.

La méthode Resample accepte d'autres modes que celui par défaut pour redimensionner l'image prise en charge. Ces modes établissent tous un compromis entre la vitesse d'exécution et la finesse du traitement, le résultat dépendant du poids de chacun de ces deux facteurs. Le mode par défaut rmFineResample conviendra sans problème à notre exemple.

II-C-2. La destruction des objets

La destruction des objets se fera naturellement dans le gestionnaire FormDestroy de la fiche principale. Il est lui aussi obtenu en double-cliquant dans l'inspecteur d'objets sur la partie valeur de la propriété OnDestroy de l'objet MainForm. Sa rédaction est des plus simples :

 
Sélectionnez
procedure TMainForm.FormDestroy(Sender: TObject);
// *** destruction des objets de travail ***
begin
  fBGRAFrom.Free;
  fBGRATo.Free;
end;

Rappelez-vous qu'un objet créé doit toujours être détruit sous peine de fuites de mémoire gênantes par accumulation. Les objets d'interface et la plupart des composants sont détruits automatiquement par des procédés spéciaux intégrés que ne possèdent par les objets ordinaires.

II-C-3. L'action du bouton

Nous disposons de deux objets graphiques pour travailler. Le gestionnaire de l'événement OnClick du bouton btnGo va à présent abriter la logique permettant l'affichage des transitions.

Ici aussi nous déclarerons localement une variable LBGRATemp de type TBGRABitmap. C'est elle qui servira à manipuler nos deux premiers objets. Après l'avoir créée, elle sera peinte en noir et remplie avec l'image source. Un traitement à définir sera appliqué, en particulier sur les coordonnées des images de travail (pour le moment, cette partie sera vide). L'image de destination sera alors peinte par transparence sur la première. Enfin, le résultat sera transmis au composant imgResult de type TImage qu'il faudra repeindre pour qu'il rende compte immédiatement des changements effectués.

Le tout est inséré dans une boucle gérée par la variable fStep de type byte à déclarer dans la partie privée de la fiche principale : il faudra boucler 100 fois pour décrire le cycle complet d'une transition.

 
Sélectionnez
private
  fBGRAFrom, fBGRATo: TBGRABitmap;
  fStep: Byte;

Au niveau programmation, voici ce que donne ce gestionnaire de l'événement OnClick du bouton :

 
Sélectionnez
procedure TMainForm.btnGoClick(Sender: TObject);
// *** dessin ***
var
  LBGRATemp: TBGRABitmap;
  LY, LX: Integer;
begin
  // on désactive le bouton le temps de la boucle
  btnGo.Enabled := False;
  // on crée le bitmap provisoire de travail
  LBGRATemp := TBGRABitmap.Create(imgResult.ClientWidth, imgResult.ClientHeight, BGRABlack);
  // on protège la boucle pour être sûr de libérer les ressources
  try
    // on fixe les coordonnées par défaut
    LX := 0;
    LY := 0;
    // on initialise le compteur de boucle
    fStep := 0;
    // entrée dans la boucle
    repeat
      // premier pas
      Inc(fStep);
      // le bitmap de travail est peint en noir
      LBGRATemp.FillRect(ClientRect, BGRABlack);
      // on y peint l'image source
      LBGRATemp.PutImage(0, 0, fBGRAFrom, dmSet);

      // traitement ici... à venir !!!
      // on peint la seconde image par transparence
      LBGRATemp.PutImage(LX, LY, fBGRATo, dmDrawWithTransparency);
      // le résultat est peint sur le canevas de l'image de résultat
      LBGRATemp.Draw(imgResult.Canvas, 0, 0);
      // on force cette image à se redessiner
      imgResult.Repaint;
      // on ralentit la boucle
      sleep(10);
    // sortie de la boucle au centième passage effectué
    until fStep = 100;
  // fin de la protection
  finally
    // libération des ressources
    LBGRATemp.Free;
    // le bouton est de nouveau opérationnel
    btnGo.Enabled := True;
  end;
end;

Il est bon de protéger les ressources en les incluant dans un bloc try... finally. Vous vous assurez ainsi de retrouver une situation stable, avec des ressources libérées proprement et des composants dans l'état souhaité malgré les aléas des manipulations de l'utilisateur.

Les commentaires ligne par ligne devraient aider à la compréhension du fonctionnement de cette méthode. L'essentiel tient dans une boucle effectuée cent fois, chaque passage correspondant à une étape de la transition.

L'emploi d'une boucle de type repeat...until au lieu d'une boucle for s'explique par le fait que le pas des itérations est susceptible de changer en fonction de la vitesse désirée. En fait, nous verrons un peu plus loin que BGRABitmap n'est pas vraiment concernée par ce problème.

Nous noterons quand même un constructeur nouveau qui prend comme paramètres deux entiers (l'un pour la largeur, l'autre pour la hauteur de l'image) ainsi qu'une valeur de couleur (ici, une constante prédéfinie) de type TBGRAPixel défini dans l'unité BGRABitmapTypes.

Des méthodes très utiles appartenant à la classe TBGRABitmap sont par ailleurs utilisées dans notre propre méthode. FillRect remplit un rectangle avec une couleur donnée, ici le noir. PutImage dessine à une position donnée une image fournie en paramètre avec un mode déterminé (par transparence, par aplat...). Enfin, Draw dessine sur un canevas fourni en paramètre l'image contenue dans l'objet de type TBGRABitmap en cause.

Le type TBGRAPixel intéressera certains lecteurs curieux. Il s'agit en fait d'un enregistrement étendu qui encapsule une certain nombre de méthodes capables d'intervenir sur les données internes de type byte (les canaux déjà évoqués).

En voici la déclaration :

 
Sélectionnez
TBGRAPixel = packed record
  private
    function GetClassIntensity: word;
    function GetClassLightness: word;
    procedure SetClassIntensity(AValue: word);
    procedure SetClassLightness(AValue: word);
  public
    {$IFDEF BGRABITMAP_RGBAPIXEL}
    red, green, blue, alpha: byte;
    {$ELSE}
    blue, green, red, alpha: byte;
    {$ENDIF}
    procedure FromRGB(ARed,AGreen,ABlue: Byte; AAlpha: Byte = 255);
    procedure FromColor(AColor: TColor; AAlpha: Byte = 255);
    procedure FromString(AStr: string);
    procedure FromFPColor(AColor: TFPColor);
    procedure ToRGB(out ARed,AGreen,ABlue,AAlpha: Byte); overload;
    procedure ToRGB(out ARed,AGreen,ABlue: Byte); overload;
    function ToColor: TColor;
    function ToString: string;
    function ToGrayscale(AGammaCorrection: boolean = true): TBGRAPixel;
    function ToFPColor: TFPColor;
    class Operator := (Source: TBGRAPixel): TColor;
    class Operator := (Source: TColor): TBGRAPixel;
    property Intensity: word read GetClassIntensity write SetClassIntensity;
    property Lightness: word read GetClassLightness write SetClassLightness;
  end;

Dans le listing de notre méthode, nous remarquons l'emploi de Repaint pour redessiner l'image affichant l'étape en cours de la transition. Contrairement à d'autres méthodes comme Invalidate ou Update qui demandent aussi de redessiner l'image (totalement pour la première, partiellement pour la seconde), Repaint force le rafraîchissement du dessin sans passer par une file d'attente. L'avantage essentiel de cette méthode est l'obtention sans délai du dessin ; son inconvénient principal est un risque de scintillement.

Une ultime remarque s'impose enfin : le traitement du graphisme est suffisamment rapide grâce à cette bibliothèque pour qu'il soit (parfois) nécessaire de le ralentir. Voilà pourquoi nous avons introduit une attente dans la boucle. Sans elle, certains effets auraient été invisibles !

Une expérience intéressante consisterait à retirer imgResult avant le Repaint. Dès lors, c'est la fiche entière qui devrait être repeinte, ce qui ralentirait de manière conséquente la vitesse d'affichage.

Pour l'instant, faut d'une transition active, l'exécution semble erronément figée : il faut attendre que le bouton « Démarrer » redevienne cliquable pour fermer l'application.

II-C-4. La finition de l'unité

Notre unité est prête, si ce n'est de menues améliorations à prévoir. Pour le moment, nous nous contenterons de modifier par programmation la propriété Caption de la fenêtre principale pour y afficher le nom de la transition en cours. Pour ce faire, nous créons une chaîne de ressource qui facilitera une éventuelle traduction et une référence à cette chaîne dans le gestionnaire de création de la fiche.

Voici notre unité terminée :

 
Sélectionnez
unit main;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
  StdCtrls, ComCtrls, BGRABitmapTypes, BGRABitmap;

type

  { TMainForm }

  TMainForm = class(TForm)
    btnGo: TButton;
    cbOpacity: TCheckBox;
    imgFrom: TImage;
    imgTo: TImage;
    imgResult: TImage;
    lblSpeed: TLabel;
    tbarSpeed: TTrackBar;
    procedure btnGoClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    fBGRAFrom, fBGRATo: TBGRABitmap;
    fStep: Byte;
  public
  end;

resourcestring
  rsTestName = 'Test des transitions - G. Vasseur 2018 - XXXXXXXXX';

var
  MainForm: TMainForm;

implementation

{$R *.lfm}

{ TMainForm }

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
    LX := 0;
    LY := 0;
    fStep := 0;
    repeat
      Inc(fStep);
      LBGRATemp.FillRect(ClientRect, BGRABlack);
      LBGRATemp.PutImage(0, 0, fBGRAFrom, dmSet);
      // traitement ici...
      LBGRATemp.PutImage(LX, LY, fBGRATo, dmDrawWithTransparency);
      LBGRATemp.Draw(imgResult.Canvas, 0, 0);
      imgResult.Repaint;
      sleep(10);
    until fStep = 100;
  finally
    LBGRATemp.Free;
    btnGo.Enabled := True;
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
// *** construction des objets de travail ***
begin
  Caption := rsTestName;
  fBGRAFrom := TBGRABitmap.Create(imgFrom.Picture.Bitmap);
  fBGRAFrom := fBGRAFrom.Resample(imgResult.ClientWidth, imgResult.ClientHeight) as TBGRABitmap;
  fBGRATo := TBGRABitmap.Create(imgTo.Picture.Bitmap);
  fBGRATo := fBGRATo.Resample(imgResult.ClientWidth, imgResult.ClientHeight) as TBGRABitmap;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
// *** destruction des objets de travail ***
begin
  fBGRAFrom.Free;
  fBGRATo.Free;
end;

end.

L'ensemble peut être compilé... mais rien d'extraordinaire ne se passera puisque nous n'avons prévu aucune transition particulière : nous obtenons simplement ce que nous avons demandé, à savoir l'affichage de l'image de destination.

Nous faudra-t-il encore patienter pour obtenir de véritables transitions ? Non, car nous allons procéder à un essai avec une ligne qui remplacera ou suivra le commentaire inclus dans la méthode :

 
Sélectionnez
// traitement ici...
LY := - imgResult.ClientHeight + imgResult.ClientHeight * fStep div
      100; // OVERDOWN

Par ce simple calcul, nous demandons que l'ordonnée de l'image de destination soit incrémentée selon le pourcentage de transition réalisée. Sachant que l'ordonnée d'origine est située au-dessus du point 0 (exactement à une distance égale à la hauteur de l'image) et que les ordonnées vont croissant en informatique, cette incrémentation de -imgResult.ClientHeight à 0 va faire apparaître progressivement l'image de destination.

Nous obtenons ainsi notre première transition ! Notre capacité à la paramétrer est encore très faible, mais nous allons bientôt la doter de fonctionnalités relatives à la vitesse d'exécution puis au dessin par transparence.

La première mouture de l'application est incluse dans le répertoire 01 des fichiers d'exemples.

Image non disponible

II-D. Introduction de la vitesse

[Exemple BGRABitmap 02]

En matière de vitesse, deux cas sont à examiner suivant la rapidité de traitement et d'affichage des outils graphiques utilisés : utilisation de la LCL standard ou mise en œuvre de la bibliothèque BGRABitmap.

Avec la LCL, il y a fort à parier que l'accélération sera nécessaire : on pourra par exemple supprimer l'instruction d'attente (sleep) et diminuer le nombre d'étapes d'affichage en incrémentant le pas fStep d'un nombre supérieur à 1. Toutefois, il faudra prendre garde à la limite fixée à 100. En effet, seuls les diviseurs de 100 conviendront comme pas : 1, 2, 4, 5, 10, 20, 25, 50. D'autres valeurs sont possibles, mais au prix d'une vérification de la limite et de l'achèvement de la transition. Il va sans dire que les pas supérieurs à 20 ne seront pas pertinents pour l'affichage, car plus le pas sera élevé plus l'affichage sera saccadé.

Avec BGRABitmap, le problème est au contraire une trop grande rapidité d'exécution ! Pour s'en convaincre, il suffit de reprendre l'exemple précédent avec notre première transition et de retirer l'instruction sleep. Heureusement, la maîtrise de la vitesse en est grandement simplifiée puisque nous n'aurons qu'à jouer sur le paramètre fourni à sleep pour ralentir ou accélérer l'affichage !

Dans notre fiche principale, nous allons introduire une nouvelle propriété nommée Speed qui renverra vers un champ privé fSpeed :

 
Sélectionnez
private
    fBGRAFrom, fBGRATo: TBGRABitmap;
    fSpeed: Byte; // vitesse
    procedure SetSpeed(AValue: Byte);
  public
    property Speed: Byte read fSpeed write SetSpeed default C_DefaultSpeed;
  end;

La constante C_DefaultSpeed vaudra 80 : elle fixe la valeur par défaut de la vitesse. Elle sera déclarée au début de l'unité, avant la fiche :

 
Sélectionnez
const
  C_DefaultSpeed = 80; // vitesse par défaut

type

  { TMainForm }

  TMainForm = class(TForm)

En général, il est préférable d'éviter les constantes numériques codées en dur dans le code. En effet, le changement d'une valeur grâce à une constante nommée sera immédiatement propagé dans tout le code qui sera par ailleurs bien plus lisible.

La méthode SetSpeed pourra ressembler à ceci :

 
Sélectionnez
procedure TMainForm.SetSpeed(AValue: Byte);
// *** détermination de la vitesse ***
begin
  if fSpeed = AValue then
    Exit;
  fSpeed := AValue;
end;

Notons que cette méthode est inutile dans l'état puisqu'elle n'effectue pas de traitements spécifiques sur le champ interne fSpeed. Cependant, l'éventuelle introduction de tests comme le déclenchement d'un gestionnaire (OnSpeedChange, par exemple) se fera par ce biais sans effort particulier.

À présent que la vitesse est accessible en tant que propriété, il reste à l'utiliser à la place de la constante fournie à la procédure sleep :

 
Sélectionnez
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
    LX := 0;
    LY := 0;
    fStep := 0;
    repeat
      Inc(fStep);
      LBGRATemp.FillRect(ClientRect, BGRABlack);
      LBGRATemp.PutImage(0, 0, fBGRAFrom, dmSet);
      // traitement ici...
      LBGRATemp.PutImage(LX, LY, fBGRATo, dmDrawWithTransparency);
      LBGRATemp.Draw(imgResult.Canvas, 0, 0);
      imgResult.Repaint;
      sleep(100 - fSpeed); // changement ici
    until fStep = 100;
  finally
    LBGRATemp.Free;
    btnGo.Enabled := True;
  end;
end;

Plus la vitesse augmente moins le paramètre fourni à sleep doit être élevé, d'où la soustraction.

Il ne faut pas oublier de mentionner la valeur par défaut de cette vitesse dans le gestionnaire de création de la fiche et d'adapter la propriété Position par défaut du composant tbarSpeed :

 
Sélectionnez
procedure TMainForm.FormCreate(Sender: TObject);
// *** construction des objets de travail ***
begin
  Caption := rsTestName;
  fBGRAFrom := TBGRABitmap.Create(imgFrom.Picture.Bitmap);
  fBGRAFrom := fBGRAFrom.Resample(imgResult.ClientWidth, imgResult.ClientHeight) as TBGRABitmap;
  fBGRATo := TBGRABitmap.Create(imgTo.Picture.Bitmap);
  fBGRATo := fBGRATo.Resample(imgResult.ClientWidth, imgResult.ClientHeight) as TBGRABitmap;
  fSpeed := C_DefaultSpeed; // vitesse
  tbarSpeed.Position:= Speed;
end;

Dans notre exemple, il nous faut aussi gérer le changement de vitesse via le composant tbarSpeed, ce qui est très facile si nous considérons le gestionnaire OnChange qui lui est associé. Nous double-cliquons dans l'inspecteur d'objets sur l'événement correspondant du composant et nous fournissons le code suivant pour le traitement :

 
Sélectionnez
procedure TMainForm.tbarSpeedChange(Sender: TObject);
// *** changement de vitesse ***
begin
  Speed := tbarSpeed.Position;
end;

Par cette unique ligne de code, nous signifions que la vitesse prendra la même valeur que la position du curseur du composant. Il ne reste alors qu'à réintroduire notre transition de test dans le gestionnaire OnClick de notre unique bouton :

 
Sélectionnez
// traitement ici...
LY := - imgResult.ClientHeight + imgResult.ClientHeight * fStep div 100; // OVERDOWN

Nous pouvons désormais régler la vitesse de transition en jouant avec le curseur du composant tbarSpeed. La mouture améliorée de l'application est incluse dans le répertoire 02 des fichiers d'exemples.

II-E. Introduction de l'opacité

[Exemple BGRABitmap 03]

Un élément que nous aimerions déterminer est sans doute l'opacité des images, car cette fonctionnalité nous permettrait de les dessiner avec une transparence plus ou moins forte. La bibliothèque BGRABitmap propose de nombreuses routines dont un des paramètres est justement l'opacité.

Le degré de transparence est fourni dans la composition d'une couleur par un entier de 0 à 255 appelé canal alpha. Comme indiqué plus haut, la valeur 255 rend un pixel totalement opaque tandis que la valeur 0 le rend totalement transparent. Les valeurs intermédiaires permettent de nuancer cette transparence.

Toujours dans un souci d'une meilleure lisibilité de notre code, nous allons définir une constante fixant la valeur maximale de l'opacité :

 
Sélectionnez
uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
  StdCtrls, ComCtrls, BGRABitmapTypes, BGRABitmap;

const
  C_DefaultSpeed = 80;
  C_MaxOpacity = 255; // opacité maximale

Plutôt que de calculer à la main l'opacité pour un pourcentage donné de transition réalisée, nous allons créer une fonction appelée Opacity qui la calculera automatiquement selon une fonction linéaire tout en offrant à l'utilisateur la possibilité d'activer ou de désactiver ce traitement particulier.

Il nous faut par conséquent un champ de type booléen pour l'activation de l'opacité, une propriété manipulant ce champ et une fonction renvoyant la valeur de l'opacité suivant l'état du système à un temps déterminé. Ces éléments sont déclarés dans la classe abritant la fiche principale :

 
Sélectionnez
{ TMainForm }

  TMainForm = class(TForm)
    btnGo: TButton;
    cbOpacity: TCheckBox;
    imgFrom: TImage;
    imgTo: TImage;
    imgResult: TImage;
    lblSpeed: TLabel;
    tbarSpeed: TTrackBar;
    procedure btnGoClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure tbarSpeedChange(Sender: TObject);
  private
    fBGRAFrom, fBGRATo: TBGRABitmap;
    fStep: Byte;
    fSpeed: Byte;
    fWithOpacity: Boolean; // opacité
    procedure SetWithOpacity(AValue: Boolean);
    procedure SetSpeed(AValue: Byte);
  public
    function Opacity(Up: Boolean = True): Byte; // opacité à utiliser
    property Speed: Byte read fSpeed write SetSpeed default C_DefaultSpeed;
    property WithOpacity: Boolean read fWithOpacity write SetWithOpacity;
  end;

Le traitement de la propriété WithOpacity est similaire à celui correspondant à la propriété Speed :

 
Sélectionnez
procedure TMainForm.SetWithOpacity(AValue: Boolean);
// *** détermination de l'opacité ***
begin
  if fWithOpacity = AValue then
    Exit;
  fWithOpacity := AValue;
end;

La propriété WithOpacity telle qu'elle est définie ici appelle la même remarque que sa consœur : en l'état, une simple écriture de la valeur du champ interne aurait été possible, mais l'écriture d'une méthode particulière prépare des extensions comme un gestionnaire d'événement dédié.

D'ores et déjà, nous pouvons réagir au changement de la case à cocher chargée de gérer la prise en compte ou non de l'opacité. En double-cliquant sur cette case à cocher, Lazarus crée un gestionnaire que nous renseignerons ainsi :

 
Sélectionnez
procedure TMainForm.cbOpacityChange(Sender: TObject);
// *** gestion de la transparence ***
begin
  WithOpacity := cbOpacity.Checked;
end;

L'opacité sera dorénavant active si la case est cochée. Par défaut, nous savons que le champ sera initialisé à False lors de la création de la fiche : il n'est donc pas nécessaire de lui donner explicitement cette valeur dans la méthode OnCreate.

Reste bien sûr la fonction Opacity introduite par nos soins. Son paramètre fixé par défaut à True indiquera si l'opacité doit augmenter avec le pourcentage réalisé de transition ou si elle doit au contraire diminuer. Cette possibilité nous permettra par exemple de faire apparaître une image tout en faisant disparaître l'autre.

Il nous faut donc distinguer plusieurs cas, suivant si la prise en compte de l'opacité est activée ou non, et suivant la valeur du paramètre Up. Voici le code proposé :

 
Sélectionnez
function TMainForm.Opacity(Up: Boolean): Byte;
// *** calcul de l'opacité en fonction du pourcentage effectué ***
begin
  if fWithOpacity then
  begin
    if Up then
      Result := fStep * C_MaxOpacity div 100
    else
      Result := C_MaxOpacity - fStep * C_MaxOpacity div 100;
  end
  else
    Result := C_MaxOpacity;
end;

La méthode ne pose pas de problèmes insurmontables. Il s'agit de ramener un pourcentage à une valeur comprise entre 0 et 255, c'est-à-dire dans l'intervalle autorisé des valeurs de l'opacité. Si l'opacité est inactive, l'image sera totalement opaque. Si le paramètre de la fonction est à False, une soustraction permet de faire décroître l'opacité.

Pour que tout cela fonctionne, il faut encore modifier les paramètres des méthodes de BGRABitmap qui gèrent la transparence. Le gestionnaire du clic sur le bouton devient alors :

 
Sélectionnez
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
    LX := 0;
    LY := 0;
    fStep := 0;
    repeat
      Inc(fStep);
      LBGRATemp.FillRect(ClientRect, BGRABlack);
      LBGRATemp.PutImage(0, 0, fBGRAFrom, dmSet, Opacity(False)); // opacité !
      // traitement ici...
      LY := - imgResult.ClientHeight + imgResult.ClientHeight * fStep div
      100; // OVERDOWN
      LBGRATemp.PutImage(LX, LY, fBGRATo, dmDrawWithTransparency, Opacity); // opacité !
      LBGRATemp.Draw(imgResult.Canvas, 0, 0);
      imgResult.Repaint;
      sleep(100 - fSpeed);
    until fStep = 100;
  finally
    LBGRATemp.Free;
    btnGo.Enabled := True;
  end;
end;

Nous avons choisi de faire disparaître peu à peu l'image d'origine tandis que l'image qui la recouvre voit sa visibilité s'accroître dans les mêmes proportions. Il suffit de lancer notre application qui servira de modèle avec l'unique transition que nous connaissons et de cocher la case marqué « Transparence » pour obtenir des images comme celle-ci :

Image non disponible

Nous voyons que le rouge-gorge commence à se dessiner alors que la mésange et le chardonneret s'évanouissent déjà.

La fonction Opacity définit par défaut la valeur de son paramètre Up p à True. Dès lors, il n'est pas nécessaire d'indiquer de paramètre si c'est la valeur recherchée. À la place de Opacity, nous aurions cependant tout aussi bien pu écrire Opacity(True) ou Opacity().

[Exemple BGRABitmap 04]

L'exemple ci-dessus contient l'application qui reprend tous les éléments étudiés plus haut.

III. Conclusion

La première partie de notre travail s'achève ici : nous avons installé la bibliothèque BGRABitmap et réalisé l'application qui nous servira de modèle pour les tests. Grâce à cette dernière, nous aurons deux images pour travailler et de quoi gérer l'opacité et la vitesse d'affichage de la résultante du traitement par nos transitions. Le tutoriel suivant définira et implémentera les transitions elles-mêmes.

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.