Voir le flux RSS

Blog de Gilles Vasseur - Pascal et compagnie

Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines

Noter ce billet
par , 14/04/2018 à 20h10 (377 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é :

Nom : spline1.png
Affichages : 41
Taille : 5,3 Ko

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

Nom : bspline1.png
Affichages : 44
Taille : 329,2 Ko

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 :

Nom : bspline2.png
Affichages : 48
Taille : 329,0 Ko

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.

Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog Viadeo Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog Twitter Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog Google Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog Facebook Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog Digg Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog Delicious Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog MySpace Envoyer le billet « Les transitions entre images sous Lazarus avec BGRABitmap (XVII) - Utilisation des splines » dans le blog Yahoo

Mis à jour 20/04/2018 à 16h12 par gvasseur58

Catégories
Programmation , Free Pascal , Lazarus , Graphisme

Commentaires

  1. Avatar de circular17
    • |
    • permalink
    Bonjour !

    Cela fait plaisir de voir une jolie utilisation de BGRABitmap

    Les splines sont pratiques effectivement, il suffit de donner des points et hop on a une courbe.

    La bibliothèque permet aussi de faire des courbes de Bézier. Pour cela, le plus pratique est sans doute d'utiliser BGRAPath. En instantiant un object TBGRAPath, on peut ensuite lui donner des instructions de dessin à la façon d'HTML5 ou bien le définir avec une chaine SVG. Exemple de programme :
    https://github.com/bgrabitmap/bgrabi...pline_main.pas

    Egalement, il est possible de dessiner un SVG, la lecture de ce format ayant été amélioré récemment :
    https://forum.lazarus.freepascal.org...24239.345.html

    Donc par exemple il serait possible de créer le masque avec un éditeur vectoriel (une forme blanche par exemple pour spécifier des pixels à garder) et utiliser cela dans une transition.

    Cordialement,

    Johann
  2. Avatar de gvasseur58
    • |
    • permalink
    Bonjour Johann,

    Il m'aura fallu du temps avant de me lancer dans l'exploration de ta bibliothèque, mais c'est fait . Je suis loin d'en avoir fait le tour et les billets du blog montrent le chemin que je parcours peu à peu. J'ai une démonstration du composant qui va paraître [EDIT : voir billet XIX] sous forme de vidéo.

    Je vais tester les solutions que tu proposes, en particulier celle du SVG...

    A bientôt,

    Gilles

    PS : j'ai rencontré quelques difficultés avec la méthode resample. Je n'avais pas compris qu'elle créait une instance de TBGRABitmap, ce qui m'a valu des fuites de mémoire (détectées à cause d'un manque de mémoire sur un Linux Ubuntu dans une machine virtuelle).