Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines
par
, 14/04/2018 à 20h10 (1346 Affichages)
Dans les précédents billets de la série, nous avons étudié des techniques mettant en œuvre des masques afin de produire des transitions plus attrayantes et plus variées. À présent, nous allons encore diversifier les outils à notre disposition en examinant les splines.
L'utilisation des splines
Avec les méthodes employées pour dessiner, nous avions presque uniquement affaire à des polygones, donc à des angles vifs. Tout au plus avons-nous croisé des ellipses facilement transformables en cercles. Par ailleurs, nous aurions aussi pu exploiter la méthode RoundRectAntialias dont les différentes déclinaisons autorisent le dessin de rectangles aux sommets arrondis.
En voici les définitions au sein de la classe TBGRABitmap :
Code pascal : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 ** Fills a rounded rectangle with antialiasing. The corners have an elliptical radius of ''rx'' and ''ry''. ''options'' specifies how to draw the corners. See [[BGRABitmap Geometry types|geometry types]] } procedure FillRoundRectAntialias(x,y,x2,y2,rx,ry: single; c: TBGRAPixel; options: TRoundRectangleOptions = []; pixelCenteredCoordinates: boolean = true); override; {** Fills a rounded rectangle with a texture } procedure FillRoundRectAntialias(x,y,x2,y2,rx,ry: single; texture: IBGRAScanner; options: TRoundRectangleOptions = []; pixelCenteredCoordinates: boolean = true); override;
Comme l'expliquent les commentaires joints aux déclarations, il s'agit avant tout de définir un rectangle aux coins arrondis grâce à deux rayons ellipsoïdaux et à des options qui indiquent la forme de l'arrondi pour chacun des sommets. Nous n'analyserons pas plus ces méthodes qui présentent un intérêt restreint pour le sujet des transitions. Nous voici donc limités à des formes géométriques de base avec lesquelles nous pouvons faire beaucoup, mais souvent avec des calculs ardus dès qu'il faut obtenir des courbes douces.
Les splines vont lever ces limitations en proposant des courbes adoucies très appréciables par exemple lorsque nous chercherons à imiter un liquide. Selon Wikipédia, « en mathématiques appliquées et en analyse numérique, une spline est une fonction définie par morceaux par des polynômes ». L'article précise qu'elles sont très utilisées dans les problèmes d'interpolation et dans ceux liés au lissage de données expérimentales ou de statistiques.
Le plus simple pour comprendre leur intérêt est de produire une transition avec les outils actuellement en notre possession et d'appliquer ensuite ce nouvel outil. Nous allons proposer une animation qui fera couler l'image de destination depuis le sommet de l'image d'origine. Les coulures seront aléatoires afin d'améliorer le mimétisme de la transition.
Voici le schéma avec calque qui lui est associé :
Première étape sans spline
Dans un premier temps, nous nous contenterons d'utiliser les polygones pour une approximation de cet objectif. Nous allons par conséquent créer un tableau de points qui contiendra ceux du polygone d'origine : nous voulons que les points supérieur gauche et supérieur droit ne bougent pas afin de toujours garder le bord supérieur de l'image comme frontière supérieure, mais que les autres points aient leur ordonnée tirée au hasard puis graduellement augmentée jusqu'à toucher le bord inférieur de l'image à la centième étape.
Du point de vue du code, nous aurons donc quelque chose comme :
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
63
64
65
66
67
68
69
70
71
72
73 procedure TMainForm.btnGoClick(Sender: TObject); // *** dessin *** const C_Points = 20; var LBGRAFrom, LBGRATo, LBGRAMask: TBGRABitmap; LY, LX, LI: Integer; LPts, LPtsTemp: 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, ClientHeight, BGRABlack); try fStep := 0; // préparation des tableaux SetLength(LPts, C_Points); SetLength(LPtsTemp, C_Points); // remplissage du tableau temporaire de travail // et de spoints extrêmes qui ne changeront pas LPts[0] := PointF(0, 0); LPtsTemp[1] := PointF(0, 0); LPts[Length(LPts) - 1] := PointF(imgResult.ClientWidth, 0); LPtsTemp[Length(LPts) - 2] := PointF(imgResult.ClientWidth, 0); for LI := 2 to Length(LPts) - 3 do begin LPtsTemp[LI].x := imgResult.ClientWidth / Length(LPts) * LI; // une astuce qui permet d'accentuer les coulures if random < .0.6 then // l'ordonnée est tirée au hasard LPtsTemp[LI].y := random(imgResult.ClientHeight div 3) else // ou vaut 0 LPtsTemp[Li].y := 0; end; 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); // les ordonnées augmentent selon la position d'origine // l'étape en cours for LI := 1 to Length(LPts) - 2 do begin LPts[LI].x := LPtsTemp[LI].x; LPts[LI].y := LPtsTemp[LI].y + (imgResult.ClientHeight - LPtsTemp[LI].y) * fStep / 100; end; // le polygone est dessiné et rempli 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); imgResult.Repaint; sleep(100 - fSpeed); until fStep = 100; finally LBGRAMask.Free; end; finally LBGRATo.Free; end; finally LBGRAFrom.Free; btnGo.Enabled := True; end; end;
Le code est suffisamment commenté pour en comprendre les principales étapes. La difficulté majeure tient aux extrémités qui doivent à la fois rester immobiles et se déplacer vers le bas : une solution est de les représenter avec deux points aux destins différents !
Une amélioration à apporter à ce code consisterait à faire disparaître la constante numérique C_Points utilisée pour le nombre de points de la courbe. Elle serait alors remplacée, soit par un calcul en fonction de la largeur de l'image, soit par un paramètre fourni par l'utilisateur. Vous avez cependant sans doute remarqué que cette constante n'a été utilisée que pour l'initialisation des tableaux, les formules de calculs lui préférant la fonction Length appliquée à la structure.
Pour le moment, le résultat n'est pas tout à fait celui escompté :
Seconde étape avec spline
Comment adoucir ces affreuses pointes ? Nous connaissons la réponse à cette question puisque les splines répondent par définition à ce type de problème.
Pour les mettre en œuvre, nous allons déclarer un nouveau tableau ouvert de TPointF que nous appellerons LSpline et grâce auquel nous calculerons la nouvelle courbe. De plus, nous remplacerons la ligne de dessin avec FillPolyAntialias par les deux lignes suivantes :
Code pascal : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 LSpline := LBGRAMask.ComputeOpenedSpline(LPts, ssOutside); LBGRAMask.DrawPolyLineAntialias(LSpline, BGRAWhite, 1, BGRAWhite);
La première ligne calcule grâce à la méthode ComputeOpenedSpline les nouveaux points et les affecte à notre nouvelle variable tandis que la seconde dessine la courbe obtenue précédemment grâce à la méthode DrawPolylineAntialias.
Cette fois-ci, voici ce que nous obtenons :
C'est exactement ce que nous cherchions à faire !
La routine complète de dessin est 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
53
54
55
56
57
58
59
60
61
62
63
64
65 procedure TMainForm.btnGoClick(Sender: TObject); // *** dessin *** const C_Points = 20; var LBGRAFrom, LBGRATo, LBGRAMask: TBGRABitmap; LY, LX, LI: Integer; LPts, LPtsTemp, LSpline: 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, ClientHeight, BGRABlack); try fStep := 0; SetLength(LPts, C_Points); SetLength(LPtsTemp, C_Points); LPts[0] := PointF(0, 0); LPtsTemp[1] := PointF(0, 0); LPts[Length(LPts) - 1] := PointF(imgResult.ClientWidth, 0); LPtsTemp[Length(LPts) - 2] := PointF(imgResult.ClientWidth, 0); for LI := 2 to Length(LPts) - 3 do begin LPtsTemp[LI].x := imgResult.ClientWidth / Length(LPts) * LI; if random < 0.6 then LPtsTemp[LI].y := random(imgResult.ClientHeight div 3) else LPtsTemp[Li].y := 0; end; 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); for LI := 1 to Length(LPts) - 2 do begin LPts[LI].x := LPtsTemp[LI].x; LPts[LI].y := LPtsTemp[LI].y + (imgResult.ClientHeight - LPtsTemp[LI].y) * fStep / 100; end; LSpline := LBGRAMask.ComputeOpenedSpline(LPts, ssOutside); LBGRAMask.DrawPolyLineAntialias(LSpline, BGRAWhite, 1, BGRAWhite); LBGRATo.PutImage(0, 0, fBGRATo, dmSet); LBGRATo.ApplyMask(LBGRAMask); LBGRAFrom.PutImage(0, 0, LBGRATo, dmDrawWithTransparency, Opacity); LBGRAFrom.Draw(imgResult.Canvas, 0, 0); imgResult.Repaint; sleep(100 - fSpeed); until fStep = 100; finally LBGRAMask.Free; end; finally LBGRATo.Free; end; finally LBGRAFrom.Free; btnGo.Enabled := True; end; end;
Pour clore ce billet, comme cette transition est assez originale et esthétique, voici un petit film qui la montre en action :
Prochainement, nous reviendrons sur les nouvelles méthodes introduites afin d'élargir le champ de leurs applications.