Ecrire des partitions en Flutter avec CustomPaint
Pour un projet perso je souhaitais dessiner dynamiquement des partitions musicales dans une application Flutter. Voilà un exemple de partition de musique :
La réflexion
J’ai rapidement regardé si il existait un package ou une font pour m’aider mais en réalité je voulais le faire moi même, « à la main ». Je suis donc parti sur la recherche : comment dessiner des formes en Flutter, qui m’a orienté sur le CustomPaint. J’avais déjà réalisé ce genre de dessin avec d’autres technologies donc je me suis lancé pour découvrir ce widget.
Le premier réflexe à avoir lorsqu’on utilise un nouveau composant est naturellement de consulter la documentation officielle. Elle devrait nous apporter les bonnes pratiques à adopter.
La vidéo issue de la série « Flutter of the week » nous présente le widget :
On peut y voir que le principe est d’implémenter un CustomPainter. Attention on a bien 2 objets : le Widget CustomPaint et l’interface CustomPainter. C’est dans le second que nous allons essentiellement travailler.
La documentation officielle du CustomPainter contient une autre vidéo qui rentre un peu plus dans les détails mais qui reste très accessible :
Le principe est en fait très simple mais c’est le dessin que l’on souhaite réaliser qui va apporter plus ou moins de complexité. Il n’est pas nécessaire de manipuler des formules mathématiques complexes mais je recommande d’être à l’aise avec le produit en croix, et les fondamentaux de géométrie peuvent être un plus pour les formes les plus complexes.
Pour mon projet je vais commencer par réaliser une croche. Sur une partition de musique cela ressemble à ça :
En terme de dessin elle comporte 3 parties : un oval à la base (la tête), une barre verticale (hampe ou queue) et un crochet en forme de drapeau sur le haut. J’ai donc 3 éléments à dessiner et à positionner les uns par rapport aux autres.
Le widget pourra avoir des dimensions variables selon son implementation. Voici les règles que je me suis fixées pour dimensionner et positionner les formes :
- La note occupera toute la surface du widget.
- C’est la largeur de la zone de dessin (le canvas) qui va déterminer la taille de la tête et du crochet. Ces deux éléments auront comme largeur la moitié de celle du canvas. Leur hauteur sera calculée proportionnellement. La hampe sera donc toujours au milieu du widget et relira la tête et le crochet.
- Pour l’exercice je ne me souci pas du cas où le widget serait plus large que haut car cela ne serait pas une utilisation logique.
Première forme : la tête
J’ai implémenté un CustomPainter et la méthode paint en particulier où on manipule le canvas fourni en paramètre pour dessiner. Il y des tas de manière de réaliser ce dessin mais ici je souhaite dessiner un oval, lui appliquer une légère rotation et le positionner dans le coin inférieur gauche. Les méthodes de dessin offre la possibilité d’appliquer directement des transformations à l’aide de matrice. Après des essais infructueux, j’ai trouvé plus simple d’appliquer des transformations au canvas. Pour que les transformations ne s’appliquent qu’à la forme souhaité, il faut encadrer les transformations et le dessin par les méthodes canvas.save() au début puis canvas.restore() à la fin.
Voici les étapes :
- J’ai défini une taille de référence : 140 (j’expliquerai l’origine de ce nombre par la suite)
- A partir de cette taille de référence et de la largeur du canvas je détermine le facteur de zoom à appliquer à mon dessin
- Je défini un objet paint pour le rendu.
- Je défini l’angle de rotation en radian
- Je calcule les dimensions de l’oval (le ratio 3/2 me semblait donner un bon rendu)
- J’applique une translation
- J’applique une rotation
- Je dessine mon oval
- J’applique des petites corrections pour obtenir le rendu souhaité
Deuxième forme : la hampe
Sur le premier dessin j’ai eu besoin de quelques essais pour bien comprendre le fonctionnement des transformations. Pour cette deuxième forme se sera beaucoup plus simple et plus rapide. Il est possible de dessiner un trait et de faire varier son épaisseur.
Voici les étapes :
- Je défini un objet paint pour le rendu.
- Je dessine mon trait
- J’applique des petites corrections pour obtenir le rendu souhaité
Troisième forme : le crochet
Ici encore plusieurs méthodes sont possibles. J’aurais pu utiliser une image mais je ne souhaitais pas utiliser de resources/assets. J’ai donc utilisé un Path qui permet de dessiner des formes vectorielles complexes. L’idéal est de se baser sur une image SVG qui nous fournira les tracés à effectuer.
J’ai donc utilisé Figma qui est gratuit pour faire un petit projet. J’ai dessiné une croche en m’appliquant surtout sur le crochet. L’outil « pen » m’a permis de définir les courbes. Si vous n’avez jamais utilisé ce genre d’outils cela demande un temps d’apprentissage pour le prendre en main.
En exportant le crochet au format SVG voici le fichier obtenu :
<svg width=”65" height=”79" viewBox=”0 0 65 79" fill=”none” xmlns=”http://www.w3.org/2000/svg">
<path d=”M6.49999 14C2.99999 8 1.99998 0.5 1.99998 0.5C1.99998 0.5 -1.50002 26.5 1.99998 34C5.49999 41.5 29 42 37 45C45 48 52 51.5 57 63.5C62 75.5 59.721 78.4558 62 78C66 77.2 65 53 51 38.5C37 24 9.99999 20 6.49999 14Z” fill=”#D9D9D9"/>
</svg>
Ce n’est pas très lisible au premier regard mais on y trouve rapidement les informations dont on a besoin. On récupère notamment toutes les coordonnées du path que l’on va pouvoir reprendre dans notre code. Au passage, j’avoue avoir dessiner dans Figma sans faire attention aux dimensions. Le path généré a une dimension de base de 65. Je suis donc parti de cette valeur pour déterminer la largeur de référence de ma croche : 65 pour la tête + 10 pour la hampe + 65 pour le crochet, soit un total de 140.
Il faut ensuite savoir décrypter les caractéristiques du path. Il s’agit d’une série d’actions qui sont normalisées. Les lettres définissent les actions et les nombres qui suivent sont les paramètres.
On peut trouver trois lettres différentes donc trois actions :
- M pour un Move
- C pour une courbe cubique
- Z pour “fermer” la forme
On trouve facilement les méthodes équivalentes de l’objet Path en Dart (j’ai arrondi certaines valeurs) :
- M6.5 14 → moveTo(6.5, 14)
- C9 8 2 0.5 2 0.5 → cubicTo(3, 8, 2, 0.5, 2, 0.5)
Voici les étapes :
- J’utilise le facteur de zoom et l’objet paint définis pour ma première forme
- J’applique une translation
- J’applique le zoom
- Je dessine mon Path
Et voilà le rendu final :
En conclusion, à partir de ce premier exemple j’imagine facilement comment je vais créer les différents symboles d’une partition musicale. J’aurais pu utiliser des images mais le CustomPaint présente deux avantages :
- Le rendu est dynamique : en fonction des dimensions du Canvas je contrôle parfaitement l’adaptation des différents éléments de mon image. Il peut également être paramétrable.
- En tant que développeur, je peux plus facilement travailler le rendu sans devoir faire appel au designer (dans certaines limites, chacun son métier..)
Le repo :