Les transitions entre images sous Lazarus avec BGRABitmap (XI) - La notion de masque
par
, 03/04/2018 à 13h35 (840 Affichages)
Grâce aux précédents billets de la série, 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 billet, nous étudierons des techniques plus complexes mettant en œuvre des masques afin de produire des transitions encore plus attrayantes.
La notion de masque
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.
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 :
Code pascal : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 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); imgResult.Repaint; 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 dans l'état actuel, 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 :
Code pascal : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 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); 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'effet produira des écrans comme suit :
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 :
Code pascal : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 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); imgResult.Repaint; 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 :
Code pascal : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 // 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); imgResult.Repaint;
Toutes les transitions simples étudiées jusqu'ici 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.