Shape ton Widget comme si c’était un SVG

le 17/11/2023 par Rémi Dormoy
Tags: Software Engineering

Dans la majorité des cas, en tant que développeur front, que ce soit sur mobile ou sur du web, nous avons à dessiner des formes classiques. Des rectangles, des cercles, des ovales ou des rectangles avec des bords arrondis. Et les frameworks que nous utilisons nous permettent donc très facilement ces formes.
Et si demain, notre designer nous demande de faire une carte de la forme de la France ? Ou un bouton en forme de cheval ? Certes, ça n’a que peu de chances de pouvoir vous arriver, mais ce serait dommage d’avoir à dire que vous ne savez pas faire. Et puis, où est le fun dans les carrés et les ronds ?

Comprendre comment fonctionnent les SVG

Si vous ne vous sentez pas l’âme d’un artiste, rassurez vous, aucune créativité n’est nécessaire pour la suite de cet article. Nous allons simplement utiliser des images au format svg, et les transformer en Widget.
Le format svg est un langage, basé sur le XML, qui permet de dessiner des images vectorielles. Bien sûr, les possibilités offertes par le format svg dépassent largement le cadre de ce que l’on va utiliser dans cet article, où nous allons simplement nous concentrer sur son élément central : les paths. Si vous souhaitez aller plus loin, ou plus en détail, sur le fonctionnement des svg, je vous conseille la documentation de mozilla, très complète et très bien faite.

Dans la suite de cet article, nous allons utiliser trois fichiers svg différents : une carte de la France Image de la franceun cheval

Image de chevalun fromage Image de fromage

La carte de France

Commençons par regarder le contenu du svg de la carte de france, qui est le plus simple des trois malgré les apparences :

<svg
  xmlns:svg="http://www.w3.org/2000/svg"
  xmlns="http://www.w3.org/2000/svg"
  version="1.0"
  width="507"
  height="520"
  id="svg2">
 <defs
    id="defs4" />
 <path id="France-outline" fill="#86aae0"
    d="M 204.3125,99.211183 L 198.65625,98.054933 L 194.75,95.117433 L 195.71875,89.648683 ...[~15000 charactères]... L 203.40625,99.367433"/>
</svg><br>```

Dans ce fichier, seules deux informations nous intéressent : 
- la __taille__ : 507 de largeur et 520 de hauteur 
- L’attribut __path__, et notamment le fait qu’il n’y ait qu’une seule balise

Regardons de plus près cet attribut path\. Il est composé d’une suite de lettres et de nombres\. C’est une succession d’informations qui nous servent à créer une ligne qui se refermera sur elle\-même pour former notre carte de France\. <br>Les lettres correspondent à un type d’instruction et les nombres à des coordonnées\. <br><br>
- __M__ est la commande __Move To__\. Les nombres suivants seront donc 2, l’abscisse et l’ordonnée\. 
   - M 204\.3125,99\.211183 => déplaçons le curseur jusqu’au point x=204\.3125 et y=99\.211183
   - Lorsque l’on fait un __Move To__ on se déplace mais on ne dessine pas\. Le mouvement du curseur ne traçera pas de ligne et ne sera donc pas pris en compte dans notre __shape__ finale\.


- __L__ est la commande __Line To__\. Les nombres suivants seront également  2, l’abscisse et l’ordonnée\.
   - __L 198\.65625,98\.054933__ => faisons une ligne depuis le curseur actuel jusqu’au point x=198\.65625 et y=98\.054933
   - Cette fois\-ci, le déplacement sera bien pris en compte dans notre __shape__ finale\.

Notre SVG de la carte de France est donc simplement une succession de lignes d’un point à un autre, répété un peu plus de 700 fois\.
### Le cheval

Regardons maintenant le contenu du svg de l’image de cheval : 
```xml<br><?xml version="1.0" standalone="no"?>
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
 width="1280.000000pt" height="957.000000pt" viewBox="0 0 1280.000000 957.000000"
 preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,957.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1678 9550 c-171 -32 -454 -124 -716 -235 -71 -30 -150 -59 -175 -65
-29 -8 -52 -22 -66 -40 -11 -16 -55 -54 -96 -86 -42 -31 -108 -93 -148 -138
-39 -45 -76 -82 -83 -84 -7 -2 -60 29 -118 69 -80 54 -110 69 -118 61 -15 -15<br>...[~6000 characteres]...
m10550 -7296 c81 -40 60 -72 -29
-43 -49 17 -72 39 -58 56 16 18 28 17 87 -13z"/>
</g>
</svg><br>```

On peut voir que la structure est identique, mais on y voit apparaître quelques nouveautés :

- Une transformation appliquant une translation et un scale
- Deux nouvelles instructions __c__ et __m__ \(c\-171 \-32 \-454 \-124 \-716 \-235 et  m10550 \-7296\)

Nous regarderons la transformation quand nous passerons à l’implémentation en Flutter mais pour le moment ignorons là\. Concentrons nous sur ces deux instructions : <br>
- __m__ est la commande __Relative Move To__\. Comme la commande __M__, elle prend deux nombres en argument et déplace le curseur sans dessiner\. Mais le fait qu’elle soit en minuscule veut dire que le mouvement est relatif\. On ne va donc pas se déplacer à des coordonnées mais d’une certaine distance\.
   - __m10550 \-7296__ indique qu’il faut se déplacer de 10550 vers la droite et de 7296 vers le haut\.

- __c__ est la commande __Relative Cubic Bezier\.__ Comme __m__ elle est en minuscule et donc relative, et elle prend cette fois 6 arguments que l’on appellera dx1 dy1 x2 y2 et dx3 dy3
   - Nous allons dessiner une [courbe de Bézier](https://developer.mozilla.org/en-US/docs/Glossary/Bezier_curve) du curseur actuel vers une distance de dx3 dy3\. 
   - dx1 dy1 et dx2 dy2 sont les distances vers les deux points de contrôle servant à calculer la courbure

![Graphique expliquant les différents dx et dy](image4.png)

- Le fait qu’il n’y ait pas de nouvelle lettre après le premier __c__ signifie qu’après les 6 premiers nombres correspondant à la première courbe de Bézier, nous devons démarrer une nouvelle courbe avec les 6 nombre suivants, et ainsi de suite, jusqu’à trouver une nouvelle lettre\.


### Le fromage

Passons enfin au dernier SVG, le fromage : 
```xml<br><?xml version="1.0" encoding="iso-8859-1"?>
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"  viewBox="0 0 32 32">
<path<br> id="cheese_1_" d="M30,30.36H13c-0.199,0-0.36-0.161-0.36-0.36c0-1.318-0.921-2.351-2.097-2.351
c-1.213,0-2.201,1.055-2.201,2.351c0,0.199-0.161,0.36-0.36,0.36H2c-0.199,0-0.36-0.161-0.36-0.36v-4.829
...[~1600 characteres]...	
s0.89-0.399,0.89-0.89S23.491,10.61,23,10.61z"/>
</svg><br>```

Deux nouveaux types d’instructions apparaissent : <br>
- __v__ et __H__ sont similaires au __L__ vu plus haut\. Ce sont aussi des __Line To__ mais horizontal ou vertical et qui ne prennent donc qu’un seul argument
   - __v\-4\.829__ trace une droite de 4\.829 vers le haut
   - __H2__ trace une droite horizontale vers l’abscisse 2

- __S__ et __s__ sont des courbes de Béziers comme __c__ mais qui prennent seulement 4 arguments car le premier point de contrôle \(dx1 dy1 pour __c__\) sera calculé à partir de la courbe de Bézier précédente\. __S__ et __s__ doivent donc suivre une instruction __C, c, S__ ou __s__\.


## L’implémentation en Flutter avec ClipPath
Dans la deuxième partie de cet article, nous allons implémenter ces trois fichiers à l’aide du Widget __ClipPath__, qui permet de donner une forme à ses enfants en suivant un __path__\. Pour être précis, il ne dessinera ses enfants qu’à l’intérieur de ce __path__\.  
Il prend la forme suivante :
```dart<br>ClipPath(
  clipper: _FranceClipper(),
  child: Container(color: Colors.black87),
);
<br>class _FranceClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    // ...<br>    // TODO construire le path<br>    // ...
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return false;
  }
}<br>```
### La carte de France
Maintenant que tout est en place, il est temps d’afficher notre premier Widget \! La carte de France étant la plus simple, il sera aussi notre premier\. Pour cela nous allons utiliser deux méthodes de l’objet __Path__ : 

- __moveTo\(x, y\)__ : même principe que pour le svg \(pratique pour nous \!\), on déplace le curseur, mais sans dessiner
- __lineTo\(x, y\)__ : même principe que pour le svg \(la vie est décidément bien faite \!\), on dessine jusqu’aux coordonnées indiquées en arguments

Attention ici à un dernier détail\. Les coordonnées indiquées dans le SVG correspondent à sa taille \(507x520 dans le cas de la France\) qui ne sont pas les mêmes que celles de notre Widget\. Il faudra donc bien penser à les convertir\. <br>Notre SVG ayant plus de 700 __Line To__, nous allons également créer une méthode se chargeant de faire cette conversion pour nous\.
```dart<br> @override
  Path getClip(Size size) {
    final width = size.width;
  final height = size.height;
  final path = Path();   <br>  final void Function(double x, double y) L = (x, y) {
    path.lineTo((x / 507) * width, (y / 520) * height);
  };
  path.moveTo((204.3125 / 507) * width, (99.211183 / 520) * height);
    L(198.65625, 98.054933);
    L(198.65625, 98.054933);
   .... ~ 700 autres lignes similaires<br>  L(203.40625, 99.367433);
    return path;
  }<br>```

*PS : il est évident qu’il y a très probablement une manière d’automatiser l’écriture de ces 700 lignes, en parsant le fichier svg puis en bouclant dessus, mais je n’ai pas eu le temps de le faire\. Et bien que ce soit un petit peu rébarbatif d’écrire ces lignes, c’est aussi très simple, et avec un peu de sélection multiple dans Android Studio, ça va assez vite\.*

![Capture d'écran d'un téléphone avec l'image de la France](image5.png)
Et voilà le résultat \!

### Le cheval
L’objet Path ayant également à disposition les méthodes __relativeLineTo__ et __relativeCubicTo__ à disposition, nous pouvons reprendre pour le cheval le même principe que plus haut. Il faudra également appliquer les transformations inverses de celles décrites dans le SVG.<br>Il faudra donc multiplier par 0.1 les ordonnées et par -0.1 les abscisses, et appliquer une translation de (0, -957) sur les coordonnées absolues.<br>Ce qui nous donne : <br>```dart
class _ChevalClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final width = size.width;
    final height = size.height;
    final path = Path();

    final void Function(double dx, double dy) l = (dx, dy) {
      path.relativeLineTo((dx / 1280) * width * 0.1, (dy / 957) * height * -0.1);
    };
    final void Function(double dx1, double dy1, double dx2, double dy2, double dx3, double dy3) c = (dx1, dy1, dx2, dy2, dx3, dy3) {
      path.relativeCubicTo(
        (dx1 / 1280) * width * 0.1,
        (dy1 / 957) * height * -0.1,
        (dx2 / 1280) * width * 0.1,
        (dy2 / 957) * height * -0.1,
        (dx3 / 1280) * width * 0.1,
        (dy3 / 957) * height * -0.1,
      );
    };
    final void Function(double x, double y) M = (x, y) {
      path.moveTo(
        (x / 1280) * width * 0.1,
        (y / 32) * height * -0.1,
      );
    };
    M(1678, 0);
    c(-171, -32, -454, -124, -716, -235);
    c(-71, -30, -150, -59, -175, -65);
    c(-29, -8, -52, -22, -66, -40);
    ....
  return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return false;
  }
}<br>```
### ![Capture d'écran avec une image de cheval](image6.png)
Et voilà le résultat \!

### Le fromage
Passons maintenant à notre dernière image : le fromage\. Il sera un tout petit peu plus complexe que les autres car les instructions __S__ et __s__ sont dépendantes de l’instruction précédente pour pouvoir être calculées\. Nous aurons besoin de calculer à chaque instruction la position actuelle du curseur, ainsi que de retenir quel était la dernière position du second point de contrôle pour les courbes de Bézier\.<br>Ce qui nous donne : <br>
```dart
 @override
  Path getClip(Size size) {
    final width = size.width;
    final height = size.height;
    final path = Path();
    double lastDx2 = 0;
    double lastDy2 = 0;
    double currentX = 0;
    double currentY = 0;
    final void Function(double a, double b) l = (a, b) {
      currentX = currentX + a;
      currentY = currentY + b;
      path.relativeLineTo((a / 32) * width, (b / 32) * height);
    };
    final void Function(double dx1, double dy1, double dx2, double dy2, double dx3, double dy3) c = (dx1, dy1, dx2, dy2, dx3, dy3) {
      lastDx2 = dx3 - dx2;
      lastDy2 = dy3 - dy2;
      currentX = currentX + dx3;
      currentY = currentY + dy3;
      path.relativeCubicTo(
        (dx1 / 32) * width,
        (dy1 / 32) * height,
        (dx2 / 32) * width,
        (dy2 / 32) * height,
        (dx3 / 32) * width,
        (dy3 / 32) * height,
      );
    };
    final void Function(double dx2, double dy2, double dx3, double dy3) s = (dx2, dy2, dx3, dy3) {
      currentX = currentX + dx3;
      currentY = currentY + dy3;
      path.relativeCubicTo(
        (lastDx2 / 32) * width,
        (lastDy2 / 32) * height,
        (dx2 / 32) * width,
        (dy2 / 32) * height,
        (dx3 / 32) * width,
        (dy3 / 32) * height,
      );
      lastDx2 = dx3 - dx2;
      lastDy2 = dy3 - dy2;
    };
    final void Function(double x2, double y2, double x3, double y3) S = (x2, y2, x3, y3) {
      path.cubicTo(
        ((lastDx2 + currentX) / 32) * width,
        ((lastDy2 + currentY) / 32) * height,
        (x2 / 32) * width,
        (y2 / 32) * height,
        (x3 / 32) * width,
        (y3 / 32) * height,
      );
      lastDx2 = x3 - x2;
      lastDy2 = y3 - y2;
      currentX = x3;
      currentY = y3;
    };
    final void Function(double x, double y) M = (x, y) {
      currentX = x;
      currentY = y;
      path.moveTo(
        (x / 32) * width,
        (y / 32) * height,
      );
    };
  ...
  return path;
  }<br>```

![Capture d'écran avec une image de fromage](image7.png)
Et voilà le résultat \!



## Est\-ce que c’est vraiment utile ?
Pour être tout à fait honnête je ne suis pas sûr que vous ayez l’occasion d’utiliser ça sur un “vrai” projet\. Comme je l’ai dit au début de cet article, nous travaillons quasi exclusivement des formes simples, rectangulaires avec des angles plus ou moins arrondies\. <br>Malgré tout, je retiens plusieurs choses de cette expérience : 

- Dessiner un SVG et un Path en Flutter sont des processus très proches, ce qui peut ouvrir beaucoup de portes le jour où on veut implémenter un design sortant vraiment de l’ordinaire\.
- Flutter est un framework qui rend beaucoup de choses ayant l’air compliquées, assez simples\. Et on trouve toujours une solution à notre problème en creusant un peu\.
- Si pour vos projets personnels vous cherchez une petite dose de fun, n’oubliez pas que nous avons créé des Widgets et pas des images, et que l’on peut mettre ce que l’on veut en enfant : une image, une scrollview, ou même un bouton avec Inkwell




Une photo croppée suivant la forme d’un fromage

![Capture d'écran avec une photo en forme de fromage](image8.png)
<br>Un élément scrollable dans une carte de France
             ![Capture d'écran animé montrant un composant scrollable avec la forme de la France](image9.gif)
<br>Un bouton en forme de cheval 
            ![Capture d'écran d'un bouton en forme de cheval](image10.gif)