Comment créer des graphiques vectoriels sur iOS

introduction

Les ressources graphiques dans le monde numérique sont de deux types de base, raster et vecteur. Les images raster sont essentiellement un tableau rectangulaire d'intensités de pixels. Les graphiques vectoriels, en revanche, sont des représentations mathématiques de formes.

Bien qu'il existe des situations dans lesquelles les images raster sont irremplaçables (photos, par exemple), dans d'autres scénarios, les graphiques vectoriels constituent des substituts performants. Les graphiques vectoriels facilitent la création de ressources graphiques pour plusieurs résolutions d'écran. Au moment de la rédaction de cet article, il existe au moins une demi-douzaine de résolutions d'écran sur la plate-forme iOS..

Une des meilleures choses à propos des graphiques vectoriels est qu’ils peuvent être restitués à n’importe quelle résolution tout en restant parfaitement nets et fluides. C'est pourquoi les polices PostScript et TrueType ont une apparence nette, quel que soit leur agrandissement. Les écrans des smartphones et des ordinateurs étant de nature matricielle, l’image vectorielle doit finalement être restituée à l’affichage sous forme d’image matricielle à la résolution appropriée. Ceci est généralement pris en charge par la bibliothèque graphique de bas niveau et le programmeur n'a pas à s'en soucier..

1. Quand utiliser les graphiques vectoriels?

Examinons quelques scénarios dans lesquels vous devriez envisager d'utiliser des graphiques vectoriels..

Icônes d'application et de menu, éléments d'interface utilisateur

Il y a quelques années, Apple a évité le skeuomorphism dans l'interface utilisateur de ses applications et dans iOS lui-même, au profit de designs audacieux et géométriquement précis. Jetez un coup d'œil aux icônes de l'appareil photo ou de l'application photo, par exemple.

Plus probablement qu'autrement, ils ont été conçus à l'aide d'outils graphiques vectoriels. Les développeurs ont dû suivre et la plupart des applications populaires (non liées au jeu) ont subi une métamorphose complète afin de se conformer à ce paradigme de conception..

Jeux

Les jeux avec des graphismes simples (pensez astéroïdes) ou des thèmes géométriques (on pense à Super Hexagon et Geometry Jump) peuvent avoir leurs sprites rendus à partir de vecteurs. La même chose s'applique aux jeux qui ont des niveaux générés procéduralement.

Images

Images dans lesquelles vous souhaitez injecter une petite quantité aléatoire pour obtenir plusieurs versions de la même forme de base.

2. Courbes de Bézier

Quelles sont les courbes de Bézier? Sans entrer dans les détails de la théorie mathématique, parlons simplement des fonctionnalités utiles aux développeurs..

Degrés de liberté

Les courbes de Bézier sont caractérisées par combien degrés de liberté ils ont. Plus ce degré est élevé, plus la courbe peut incorporer de variation (mais aussi plus elle est mathématiquement complexe).

Degré un Béziers sont des segments de droite. Les courbes de degré deux sont appelées courbes quadruples. Nous nous concentrerons sur les courbes de degré trois (cubiques), car elles offrent un bon compromis entre souplesse et complexité..

Cubic Beziers peut représenter non seulement des courbes lisses simples, mais également des boucles et des cuspides. Plusieurs segments de Bézier cubiques peuvent être raccordés bout à bout pour former des formes plus complexes.

Béziers Cubique

Un Bézier cubique est défini par ses deux points d'extrémité et par deux points de contrôle supplémentaires qui déterminent sa forme. En général, un diplôme n Bézier a (n-1) points de contrôle, sans compter les points finaux.

Une caractéristique intéressante de Béziers cubique est que ces points ont une interprétation visuelle significative. La ligne reliant un point final à son point de contrôle adjacent agit comme une tangente à la courbe au point final. Ce fait est utile pour concevoir des formes. Nous allons exploiter cette propriété plus tard dans le tutoriel.

Transformations Géométriques

En raison de la nature mathématique de ces courbes, vous pouvez facilement leur appliquer des transformations géométriques, telles que la mise à l'échelle, la rotation et la translation, sans perte de fidélité..

L'image suivante montre un échantillon de différentes sortes de formes pouvant être prises par un seul cube de Bézier. Remarquez comment les segments de ligne verte agissent comme des tangentes à la courbe.

3. Core Graphics et le UIBezierPath Classe

Sur iOS et OS X, les graphiques vectoriels sont implémentés à l'aide de la bibliothèque Core Graphics basée sur C. UIKit / Cocoa est construit sur le dessus, ce qui ajoute un vernis d'orientation d'objet. Le bourreau de travail est le UIBezierPath classe (NSBezierPath sur OS X), une implémentation d’une courbe de Bézier mathématique.

le UIBezierPath classe prend en charge les courbes de Bézier de degré un (segments de droite), deux (courbes quad) et trois (courbes cubiques).

Par programme, un UIBezierPath L’objet peut être construit pièce par pièce en y ajoutant de nouveaux composants (sous-chemins). Pour faciliter cela, le UIBezierPath objet garde la trace de la currentPoint propriété. Chaque fois que vous ajoutez un nouveau segment de chemin, le dernier point du segment ajouté devient le point actuel. Tout dessin supplémentaire que vous faites commence généralement à ce stade. Vous pouvez explicitement déplacer ce point vers un emplacement souhaité.

La classe a des méthodes pratiques pour créer des formes couramment utilisées, telles que des arcs et des cercles, des rectangles (arrondis), etc. En interne, ces formes ont été construites en reliant plusieurs sous-chemins..

Le chemin global peut être une forme ouverte ou fermée. Il peut même être auto-intersecté ou avoir plusieurs composants fermés.

4. Commencer

Ce didacticiel a pour objectif d’examiner de manière approfondie la génération de graphiques vectoriels. Mais même si vous êtes un développeur expérimenté qui n'a pas utilisé Core Graphics ou UIBezierPath avant, vous devriez pouvoir suivre. Si vous débutez dans ce domaine, je vous recommande de parcourir la UIBezierPath référence de classe (et les fonctions Core Graphics sous-jacentes) si vous ne l'êtes pas déjà. Nous ne pouvons exercer qu'un nombre limité de fonctionnalités de l'API dans un seul tutoriel.

Assez parlé. Commençons à coder. Dans la suite de ce didacticiel, je présenterai deux scénarios dans lesquels les graphiques vectoriels sont l’outil idéal à utiliser..

Lancez Xcode, créez un nouveau terrain de jeu et configurez la plate-forme sur iOS. Incidemment, les terrains de jeu Xcode sont une autre raison pour laquelle travailler avec des graphiques vectoriels est maintenant amusant. Vous pouvez modifier votre code et obtenir un retour visuel instantané. Notez que vous devriez utiliser la dernière version stable de Xcode, qui est 7.2 au moment de l'écriture..

Scénario 1: Créer des formes de nuage

Nous aimerions générer des images de nuages ​​qui adhèrent à une forme de nuage de base tout en ayant un caractère aléatoire afin que chaque nuage ait un aspect différent. La conception de base sur laquelle je me suis fixé est une forme composée, définie par plusieurs cercles de rayons aléatoires centrés sur un trajet elliptique de taille aléatoire (dans les limites appropriées)..

Pour clarifier, voici à quoi ressemble l’objet global si nous effleurons le chemin du vecteur au lieu de le remplir.

Si votre géométrie est un peu rouillée, cette image de Wikipedia montre à quoi ressemble une ellipse..

Quelques fonctions utilitaires

Commençons par écrire quelques fonctions d’aide.

"import javascript UIKit

func randomInt (inférieur inférieur: Int, supérieur: Int) -> Int assert (inférieur < upper) return lower + Int(arc4random_uniform(UInt32(upper - lower)))

func circle (au centre: CGPoint, rayon: CGFloat) -> UIBezierPath return UIBezierPath (arcCenter: centre, rayon: rayon, 0Angle: 0, finAngle: CGFloat (2 * M_PI), dans le sens des aiguilles d'une montre: vrai) "

le aléatoire (inférieur: supérieur :) fonction utilise le intégré arc4random_uniform () fonction pour générer des nombres aléatoires dans la plage inférieur et (supérieur-1). le cercle (au: centre :) La fonction génère un chemin de Bézier, représentant un cercle avec un centre et rayon.

Générer des points et des chemins

Essayons maintenant de générer les points le long du chemin elliptique. Une ellipse centrée à l'origine du système de coordonnées et dont les axes sont alignés le long des axes de coordonnées a une forme mathématique particulièrement simple ressemblant à ceci.

P (r, θ) = (a cos (θ), b sin (θ))

Nous assignons des valeurs aléatoires aux longueurs de ses axes majeur et mineur afin que la forme ressemble à un nuage, plus allongé horizontalement que verticalement.

Nous utilisons le foulée() fonction pour générer des angles régulièrement espacés autour du cercle, puis utiliser carte() générer des points régulièrement espacés sur l'ellipse en utilisant l'expression mathématique ci-dessus.

"javascript let a = Double (randomInt (inférieur: 70, supérieur: 100)) let b = Double (randomInt (inférieur: 10, supérieur: 35)) let ndiv = 12 comme Double

let points = (0.0) .stride (to: 1.0, by: 1 / ndiv) .map CGPoint (x: a * cos (2 * M_PI * $ 0), y: b * sin (2 * M_PI * $ 0))  "

Nous générons la «masse» centrale du nuage en joignant les points le long du trajet elliptique. Si nous ne le faisons pas, nous aurons un grand vide au centre.

"javascript let path = UIBezierPath () path.moveToPoint (points [0])

pour point en points [1…

path.closePath () "

Notez que le chemin exact n'a pas d'importance, car nous allons le remplir et non le tracer. Cela signifie qu'il ne sera pas distinguable des cercles.

Pour générer les cercles, nous avons d’abord choisi de manière heuristique une plage pour les rayons de cercle aléatoires. Le fait que nous développions cela dans un terrain de jeu m'a aidé à jouer avec les valeurs jusqu'à obtenir un résultat satisfaisant..

"javascript laissez minRadius = (Int) (M_PI * a / ndiv) laissez maxRadius = minRadius + 25

pour point en points [0…

chemin"

Prévisualisation du résultat

Vous pouvez afficher le résultat en cliquant sur l'icône "œil" dans le panneau des résultats à droite, sur la même ligne que l'instruction "chemin"..

Touches finales

Comment pouvons-nous rasteriser cela pour obtenir le résultat final? Nous avons besoin de ce qu’on appelle un «contexte graphique» pour tracer les chemins. Dans notre cas, nous allons dessiner dans une image (un UIImage exemple). C'est à ce stade que vous devez définir plusieurs paramètres spécifiant le rendu du chemin final, tels que les couleurs et les largeurs de trait. Enfin, vous caressez ou remplissez votre chemin (ou les deux). Dans notre cas, nous voulons que nos nuages ​​soient blancs et nous voulons seulement les remplir.

Intégrons ce code dans une fonction afin de générer autant de nuages ​​que nous le souhaitons. Et pendant que nous y sommes, nous allons écrire du code pour dessiner quelques nuages ​​aléatoires sur un fond bleu (représentant le ciel) et pour dessiner tout cela dans la vue en direct du terrain de jeu..

Voici le code final:

"import javascript import uikit XCPlayground

func generateRandomCloud () -> UIImage

func randomInt (inférieur inférieur: Int, supérieur: Int) -> Int assert (inférieur < upper) return lower + Int(arc4random_uniform(UInt32(upper - lower)))  func circle(at center: CGPoint, radius: CGFloat) -> UIBezierPath return UIBezierPath (arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat (2 * M_PI), dans le sens horaire: true) let a = Double (randomInt (lower: 70, upper: 100)) let b = Double (randomInt (lower: 10, upper: 35)) let ndiv = 12 as Double let points = (0.0) .stride (to: 1.0, by: 1 / ndiv) .map CGPoint (x: a * cos (2 * M_PI * $ 0), y: b * sin (2 * M_PI * $ 0)) let path = UIBezierPath () path.moveToPoint (points [0]) pour le point aux points [1…  

Voir la classe: UIView override func drawRect (rect: CGRect) let ctx = UIGraphicsGetCurrentContext () UIColor.blueColor (). setFill () CGContextFillRect (ctx, rect)

 let cloud1 = generateRandomCloud (). CGImage let cloud2 = generateRandomCloud (). CGImage let cloud3 = generateRandomCloud (). CGImage CGContextDrawImage (ctx, CGRect (x: 20, y: 20, largeur: CGImageGetWidth (cloud1), CGGRage, hauteur: CGImage )), cloud1) CGContextDrawImage (ctx, CGRect (x: 300, y: 100, width: CGImageGetWidth (cloud2), hauteur: CGImageGetHeight (cloud2)), cloud2) CGContextDrawImage (ctx, CGRect (x: 50, y: 200, largeur: CGImageGetWidth (cloud3), hauteur: CGImageGetHeight (cloud3)), cloud3) 

XCPlaygroundPage.currentPage.liveView = Afficher (cadre: CGRectMake (0, 0, 600, 800))

"

Et voici à quoi ressemble le résultat final:

Les silhouttes des nuages ​​apparaissent un peu flous dans l'image ci-dessus, mais il s'agit simplement d'un artefact de redimensionnement. La vraie image de sortie est nette.

Pour le voir dans votre propre terrain de jeu, assurez-vous que le Assistant rédacteur est ouvert. Sélectionner Montrer l'assistant éditeur du Vue menu.

Scénario 2: Génération de pièces de puzzle

Les pièces de puzzle ont généralement un «cadre» carré, chaque bord étant soit plat, avec une languette arrondie faisant saillie vers l'extérieur, soit une fente de la même forme permettant d'insérer une languette d'une pièce adjacente. Voici une section d'un puzzle typique.

Variations accommodantes avec des graphiques vectoriels

Si vous développiez une application de puzzle, vous voudriez utiliser un masque en forme de pièce de puzzle pour segmenter l'image représentant le puzzle. Vous pouvez choisir des masques raster prégénérés livrés avec l'application, mais vous devez inclure plusieurs variantes pour s'adapter à toutes les variations de forme possibles des quatre bords..

Avec les graphiques vectoriels, vous pouvez générer le masque pour tout type de pièce à la volée. De plus, il serait plus facile d’accepter d’autres variantes, comme si vous vouliez des pièces rectangulaires ou obliques (au lieu de pièces carrées)..

Conception de la limite de pièce de puzzle

Comment concevons-nous réellement la pièce du casse-tête, c'est-à-dire comment pouvons-nous déterminer comment placer nos points de contrôle pour générer un chemin de Bézier qui ressemble à l'onglet incurvé?

Rappelez-vous la propriété de tangence utile de Béziers cubique dont j'ai parlé plus tôt. Vous pouvez commencer par dessiner une approximation de la forme désirée, en la divisant en segments en estimant le nombre de segments cubiques dont vous aurez besoin (connaître le type de formes qu'un segment cubique peut accueillir), puis en dessinant des tangentes à ces segments. où vous pouvez placer vos points de contrôle. Voici un diagramme expliquant de quoi je parle.

Relier la forme aux points de contrôle de la courbe de Bézier

J'ai déterminé que pour représenter la forme de l'onglet, quatre segments de Bézier feraient parfaitement l'affaire:

  • deux représentant les segments de droite aux deux extrémités de la forme
  • deux représentant les segments en forme de S représentant l'onglet au centre

Remarquez les segments de ligne pointillés verts et jaunes agissant comme des tangentes aux segments en forme de S, ce qui m'a aidé à estimer l'emplacement des points de contrôle. Notez également que j'ai visualisé le morceau comme ayant une longueur d'une unité, c'est pourquoi toutes les coordonnées sont des fractions de un. J'aurais facilement pu faire que ma courbe ait, par exemple, une longueur de 100 points (échelonnant les points de contrôle par un facteur de 100). L'indépendance de résolution des graphiques vectoriels signifie qu'il ne s'agit pas d'un problème.

Enfin, j’ai utilisé Béziers cubiques même pour les segments de lignes droites uniquement pour des raisons de commodité, afin que le code puisse être écrit de manière plus concise et uniforme..

J'ai sauté le dessin des points de contrôle des segments droits dans le diagramme pour éviter l'encombrement. Bien sûr, un cube de Bézier représentant une ligne a simplement les points d'extrémité et les points de contrôle tous situés le long de la ligne elle-même..

Le fait que vous développiez ceci dans un terrain de jeu signifie que vous pouvez facilement «modifier» les valeurs des points de contrôle pour trouver une forme qui vous plaise et obtenir un retour instantané..

Commencer

Commençons. Vous pouvez utiliser le même terrain de jeu qu'auparavant en y ajoutant une nouvelle page. Choisir Nouveau> Page de terrain de jeu du Fichier menu ou créer un nouveau terrain de jeu si vous préférez.

Remplacez tout code de la nouvelle page par ce qui suit:

"import javascript UIKit

let outie_coords: [(x: CGFloat, y: CGFloat)] = [(1.0 / 9, 0), (2.0 / 9, 0), (1.0 / 3, 0), (37.0 / 60, 0), (1.0 / 6, 1,0 / 3), (1,0 / 2, 1,0 / 3), (5,0 / 6, 1,0 / 3), (23,0 / 60, 0), (2,0 / 3, 0), (7,0 / 9, 0 ), (8.0 / 9, 0), (1.0, 0)]

let size: CGFloat = 100 let outie_points = outie_coords.map CGPointApplyAffineTransform (CGPointMake ($ 0.x, $ 0.y), CGAffineTransformMakeScale (taille, taille))

let path = UIBezierPath () path.moveToPoint (CGPointZero)

pour i dans 0.stride (via: outie_points.count - 3, de: 3) chemin.addCurveToPoint (outie_points [i + 2], controlPoint1: outie_points [i], controlPoint2: outie_points [i + 1])

chemin"

Génération des quatre côtés à l'aide de transformations géométriques

Notez que nous avons décidé de rendre notre chemin de 100 points de long en appliquant une transformation de redimensionnement aux points..

Nous voyons le résultat suivant en utilisant la fonctionnalité «Quick Look»:

Jusqu'ici tout va bien. Comment pouvons-nous générer quatre côtés de la pièce de puzzle? La réponse est (comme vous pouvez le deviner) en utilisant des transformations géométriques. En appliquant une rotation de 90 degrés suivie d’une translation appropriée à chemin ci-dessus, nous pouvons facilement générer le reste des côtés.

Mise en garde: un problème de remplissage intérieur

Il y a une mise en garde ici, malheureusement. La transformation ne joindra pas automatiquement les segments individuels ensemble. Bien que la silhouette de notre puzzle ait belle apparence, son intérieur ne sera pas rempli et nous aurons des problèmes pour l'utiliser comme masque. Nous pouvons observer cela dans la cour de récréation. Ajoutez le code suivant:

"javascript let transform = CGAffineTransformTranslate (CGAffineTransformMakeRotation (CGFloat (-M_PI / 2)), 0, taille)

laissez temppath = path.copy () comme! UIBezierPath

let foursided = UIBezierPath () pour i dans 0… 3 temppath.applyTransform (transformation) foursided.appendPath (temppath)

à quatre pattes "

Quick Look nous montre ce qui suit:

Remarquez comment l'intérieur de la pièce n'est pas ombré, indiquant qu'il n'a pas été rempli.

Vous pouvez trouver les commandes de dessin utilisées pour construire un complexe UIBezierPath en examinant ses debugDescription propriété dans la cour de récréation.

Résoudre le problème de remplissage

Transformations géométriques sur UIBezierPath fonctionne assez bien pour le cas d'utilisation courant, c'est-à-dire lorsque vous avez déjà une forme fermée ou que la forme que vous transformez est intrinsèquement ouverte et que vous souhaitez en générer des versions transformées géométriquement. Notre cas d'utilisation est différent. Le chemin agit comme un sous-chemin dans une forme plus large que nous construisons et dont nous voulons remplir l'intérieur. C'est un peu plus compliqué.

Une approche serait de jouer avec les éléments internes du chemin (en utilisant le CGPathApply () fonction de l’API graphique principale) et relier manuellement les segments pour former une seule forme fermée et correctement remplie.

Mais cette option me semble un peu féroce et c'est pourquoi j'ai opté pour une approche différente. Nous appliquons d’abord les transformations géométriques aux points eux-mêmes, via CGPointApplyAffineTransform () fonction, en appliquant exactement la même transformation que nous avons tenté d’utiliser il ya un instant. Nous utilisons ensuite les points transformés pour créer le sous-chemin, qui est ajouté à la forme générale. À la fin du didacticiel, nous verrons un exemple où nous pouvons appliquer correctement une transformation géométrique au chemin de Bézier..

Génération des variations de bord de pièce

Comment générer l'onglet “innie”? Nous pourrions appliquer une transformation géométrique à nouveau, avec un facteur d'échelle négatif dans la direction y (en inversant sa forme), mais j'ai choisi de le faire manuellement en inversant simplement les coordonnées y des points outie_points.

En ce qui concerne l'onglet à bord plat, alors que j'aurais pu simplement utiliser un segment de ligne droite pour le représenter, afin d'éviter de devoir spécialiser le code pour des cas distincts, je règle simplement la coordonnée y de chaque point dans outie_points à zéro. Cela nous donne:

javascript let innie_points = outie_points.map CGPointMake ($ 0.x, - $ 0.y) let flat_points = outie_points.map CGPointMake ($ 0.x, 0)

À titre d’exercice, vous pouvez générer des courbes de Bézier à partir de ces bords et les afficher à l’aide de Quick Look..

Vous en savez maintenant assez pour que je vous blitz avec tout le code, ce qui lie tout ensemble dans une seule fonction.

Remplacez tout le contenu de la page de terrain de jeu par ce qui suit:

"import javascript import uikit XCPlayground

enum Edge cas Outie cas Innie cas Flat

func jigsawPieceMaker (taille taille: CGFloat, bords: [Edge]) -> UIBezierPath

func incrementalPathBuilder (firstPoint: CGPoint) -> ([CGPoint]) -> UIBezierPath let path = UIBezierPath () path.moveToPoint (firstPoint) renvoie points dans assert (points.count% 3 == 0) pour i dans 0. stride (via: points.count - 3, par: 3) path.addCurveToPoint (points [i + 2], controlPoint1: points [i], controlPoint2: points [i + 1]) chemin de retour laissez outie_coords: [(x: CGFloat, y: CGFloat)] = [/ * (0, 0), * / (1.0 / 9, 0), (2.0 / 9, 0), (1.0 / 3, 0), (37.0 / 60,0), (1,0 / 6, 1,0 / 3), (1,0 / 2, 1,0 / 3), (5,0 / 6, 1,0 / 3), (23,0 / 60, 0), (2,0 / 3, 0) , (7.0 / 9, 0), (8.0 / 9, 0), (1.0, 0)] let outie_points = outie_coords.map CGPointApplyAffineTransform (CGPointMake ($ 0.x, $ 0.y), CGAffineTransformMakeScale (taille, taille))  let innie_points = outie_points.map CGPointMake ($ 0.x, - $ 0.y) let flat_points = outie_points.map CGPointMake ($ 0.x, 0) var shapeDict: [Edge: [CGPoint]] = [.Outie : outie_points, .Innie: innie_points, .Flat: flat_points] laissez transformer = CGAffineTransformTranslate (CGAffineTransformMakeRotation (CGFl oat (-M_PI / 2)), 0, taille) let path_builder = incrementalPathBuilder (CGPointZero) chemin var: UIBezierPath! pour une arête dans des arêtes path = path_builder (shapeDict [arête]!) pour (e, points) dans shapeDict let tr_pts = pts.map CGPointApplyAffineTransform ($ 0, transformation) shapeDict [e] = tr_pts path.closePath ( ) chemin de retour  

let piece1 = jigsawPieceMaker (taille: 100, bords: [.Innie, .Outie, .Flat, .Innie])

let piece2 = jigsawPieceMaker (taille: 100, bords: [.Innie, .Innie, .Innie, .Innie]. piece2.applyTransform (CGAffineTransformMakeRotation (CGFloat (M_PI / 3))

"

J'aimerais préciser quelques points supplémentaires dans le code:

  • Nous utilisons un enum définir les différentes formes de bord. Nous stockons les points dans un dictionnaire qui utilise les valeurs d'énumération comme clés.
  • Nous assemblons les sous-chemins (constitués de chaque bord de la forme de pièce de puzzle à quatre côtés) dans le incrementalPathBuilder (_) fonction, définie en interne à la jigsawPieceMaker (taille: bords :) une fonction.
  • Maintenant que la pièce de puzzle est correctement remplie, comme nous pouvons le voir dans la sortie Aperçu rapide, nous pouvons utiliser le applyTransform (_ :) méthode pour appliquer une transformation géométrique à la forme. Par exemple, j'ai appliqué une rotation de 60 degrés à la deuxième pièce.

Conclusion

J'espère vous avoir convaincu que la capacité de générer par programme des graphiques vectoriels peut être une compétence utile à posséder dans votre arsenal. J'espère que vous serez inspiré pour penser (et coder) à d'autres applications intéressantes pour les graphiques vectoriels que vous pouvez incorporer dans vos propres applications.