Imitation de dessin à main levée sur l'exemple de RoughJS

RoughJS est une petite bibliothèque graphique JavaScript (< 9 Ko ) qui vous permet de dessiner dans un style manuscrit sommaire . Il vous permet de dessiner sur <canvas>et avec SVG. Dans cet article, je veux répondre à la question la plus populaire sur RoughJS: comment ça marche?


Un peu d'histoire


Fasciné par les images de graphiques, diagrammes et croquis dessinés à la main, je me suis, comme un vrai nerd, demandé: puis-je créer de tels dessins à l'aide d'un code, puis-je imiter le dessin aussi précisément que possible à la main, tout en conservant la possibilité de mise en œuvre logicielle? J'ai décidé de me concentrer sur les primitives - lignes, polygones, ellipses et courbes - pour créer une bibliothèque entière de graphiques 2D. Sur cette base, vous pouvez créer des bibliothèques et des graphiques pour dessiner des graphiques et des diagrammes.

Après avoir brièvement examiné le problème, j'ai trouvé un article de Joe Wood et ses collègues intitulé Sketchy Rendering pour la visualisation d'informations . Les techniques qui y sont décrites sont devenues la base de la bibliothèque, en particulier pour dessiner des lignes et des ellipses.

En 2017, j'ai écrit la première version de la bibliothèque, qui ne fonctionnait que sur Canvas. Ayant résolu le problème, je m'y suis désintéressé. Un an plus tard, j'ai beaucoup travaillé avec SVG et j'ai décidé d'adapter RoughJS pour qu'il fonctionne avec SVG. J'ai également changé la structure de l'API pour la rendre plus simple et je me suis concentrée sur les primitives graphiques vectorielles simples. J'ai parlé de la version 2.0 sur Hacker News et soudain, elle a gagné en popularité. En 2018, c'était le deuxième poste le plus populaire de ShowHN .

Depuis lors, d'autres personnes ont créé des choses plus étonnantes basées sur RoughJS, par exemple, Excalidraw , Why do Cats & Dogs ... , la bibliothèque graphique roughViz .

Parlons maintenant des algorithmes ...

Inégalité


La base fondamentale de l'imitation de figures manuscrites est le hasard. Lorsque nous dessinons à la main, deux formes différentes seront quelque peu différentes. Personne ne dessine parfaitement avec précision, donc chaque point spatial dans RoughJS est ajusté pour un déplacement aléatoire. L'amplitude du caractère aléatoire est donnée par un paramètre numérique roughness.


Imaginez un point Aet un cercle autour de lui. Remplacez maintenant par un Apoint aléatoire dans ce cercle. L'aire de ce cercle de hasard est contrôlée par la valeur roughness.

Lignes


Les lignes manuscrites ne sont jamais droites et montrent souvent une courbure dans le virage (décrit ici ). Nous randomisons les deux extrémités de la ligne en fonction de la rugosité. Ensuite, nous sélectionnons deux autres points aléatoires approximativement à une distance de 50% et 75% de la fin du segment. En reliant ces points de la courbe, nous obtenons l'effet de flexion .


Lors du dessin à la main, les gens déplacent parfois le crayon vers l'avant et vers l'arrière le long de la ligne. Cela est nécessaire soit pour rendre la ligne plus lumineuse, soit simplement pour corriger la rectitude de la ligne. Cela ressemble à ceci:


Pour ajouter un effet esquissé, RoughJS trace deux fois une ligne. À l'avenir, je prévois de rendre cet aspect plus personnalisable.

Regardez cette surface de toile. Le paramètre de rugosité modifie l'apparence des lignes:


Dans l'article original sur toile, vous pouvez vous dessiner.

Lorsque vous dessinez à la main, les longues lignes deviennent généralement moins droites et plus courbes. Autrement dit, le caractère aléatoire des décalages pour créer un effet est fonction de la longueur et de la valeur de la ligne randomness. Cependant, la mise à l'échelle de cette fonction n'est pas adaptée aux très longues lignes. Par exemple, dans l'image ci-dessous, les carrés concentriques sont dessinés avec la même graine aléatoire, c'est-à-dire en fait, ce n'est qu'une figure aléatoire, mais avec une échelle différente.


Vous remarquerez peut-être que les bords des carrés extérieurs semblent un peu plus inégaux que ceux intérieurs. Par conséquent, j'ai également ajouté un facteur d'amortissement en fonction de la longueur de la ligne. Le coefficient d'atténuation est utilisé comme une fonction de pas à différentes longueurs.


Ellipses (et cercles)


Prenez une feuille de papier et tracez quelques cercles le plus rapidement possible en un seul mouvement continu. Voici ce que j'ai obtenu:


Notez que les points de début et de fin de la boucle ne correspondent pas toujours. RoughJS essaie d'imiter cela, tout en rendant l'apparence plus complète (la technique est adaptée de l' article giCenter ).

L'algorithme trouve les npoints d'ellipse où il nest déterminé par la taille de l'ellipse. Ensuite, chaque point est randomisé par sa valeur roughness. Ensuite, une courbe est tracée à travers ces points. Pour obtenir l'effet des extrémités déconnectées, les points du deuxième au dernier ne coïncident pas avec le premier point. Au lieu de cela, la courbe relie les deuxième et troisième points.


Une deuxième ellipse est également dessinée afin que la boucle soit plus fermée et ait un effet d'esquisse supplémentaire.

Dans l'article d'origine, vous pouvez dessiner des ellipses sur une surface de canevas interactive. Variez la rugosité et regardez comment la forme change:


Dans le cas du dessin au trait, certains de ces artefacts deviennent plus accentués si une forme est mise à l'échelle à différentes tailles. Dans une ellipse, cet effet est plus visible car le rapport est quadratique. Dans l'image ci-dessous, tous les cercles ont la même forme, mais les extérieurs semblent plus inégaux.


L'algorithme s'ajuste automatiquement en fonction de la taille de la forme, augmentant le nombre de points dans le cercle ( n). Vous trouverez ci-dessous le même ensemble de cercles générés à l'aide du réglage automatique.


Complétant


Les lignes pointillées sont généralement utilisées pour remplir des formes dessinées à la main . Dans le cas des esquisses à main levée, les lignes ne restent pas toujours dans le contour des formes. Ils sont également randomisés. La densité, l'angle, la largeur de ligne peuvent être ajustés.


Les carrés illustrés ci-dessus sont faciles à remplir, mais dans le cas d'autres formes, toutes sortes de problèmes peuvent survenir. Par exemple, les polygones concaves (dans lesquels les angles peuvent dépasser 180 °) causent souvent de tels problèmes:


En fait, l'image ci-dessus est tirée d'un rapport d'erreur dans l'une des versions précédentes de RoughJS. Depuis lors, j'ai mis à jour l'algorithme de remplissage des traits en adaptant la version de la méthode de balayage des chaînes .

L'algorithme de balayage des chaînes peut être utilisé pour remplir n'importe quel polygone. Son principe est de balayer un polygone en utilisant des lignes horizontales (lignes raster). Les lignes raster vont du haut du polygone vers le bas. Pour chaque ligne raster, nous déterminons à quels points la ligne coupe le polygone. Nous construisons ces points d'intersection de gauche à droite.


En passant d'un point à un autre, nous passons du mode remplissage au mode non remplissage; la commutation entre les états se produit lorsque chaque point d'intersection sur la ligne raster se rencontre. Ici, il faut prendre en compte bien plus, en particulier les cas limites et les méthodes d'optimisation de l'analyse; Vous pouvez en savoir plus à ce sujet ici: pixellisation des polygones , ou déployer un spoiler avec un pseudo-code.

Détails de la mise en œuvre de l'algorithme de balayage des chaînes
() .

— (Edge Table, ET), , Ymin. Ymin, Xmin.

— (Active Edge Table, AET), , .

:

interface EdgeTableEntry {
  ymin: number;
  ymax: number;
  x: number; // Initialized to Xmin
  iSlope: number; // Inverse of the slope of the line: 1/m
}

interface ActiveEdgeTableEntry {
  scanlineY: number; // The y value of the scanline
  edge: EdgeTableEntry;
}

, :

1. y y ET. .

2. AET .

3. , AET, ET :

(a) ET y AET , ymin ≤ y.

(b) AET , y = ymax, AET x.

() y, x AET.

(d) y , , .. .

(e) , AET, x y (edge.x = edge.x + edge.iSlope)

Dans l'image ci-dessous (dans l'article original interactif), chaque carré représente un pixel. Vous pouvez déplacer les sommets pour changer le polygone et observer quels pixels seront remplis traditionnellement.


Lors du remplissage de traits, l'incrément de lignes raster est effectué par incréments en fonction de la densité donnée de lignes de traits, et chaque ligne est dessinée en utilisant l'algorithme décrit ci-dessus.

Cependant, cet algorithme concerne les lignes raster horizontales. Pour implémenter différents angles de traits, l'algorithme fait d'abord pivoter la forme elle-même de l'angle de traits souhaité. Ensuite, les lignes raster de la figure pivotée sont calculées. De plus, les lignes calculées retournent à l'angle des traits dans la direction opposée.


Pas seulement pour remplir des traits


RoughJS prend également en charge d'autres styles de remplissage, mais ils sont tous dérivés du même algorithme de hachurage. Le hachurage croisé consiste à dessiner des lignes en pointillés sous un angle angle, puis d'autres lignes sous un angle angle + 90°. Zigzag cherche à connecter une ligne pointillée avec la précédente. Pour obtenir un motif de points , tracez de petits cercles le long des lignes en pointillés.


Les courbes


Tout dans RoughJS est normalisé aux courbes - lignes, polygones, ellipses, etc. Par conséquent, le développement naturel de cette idée est de créer une courbe d'esquisse. Dans RoughJS, nous passons un ensemble de points à une courbe, après quoi nous utilisons l' approximation de la courbe pour les convertir en courbes de Bézier cubiques .

Chaque courbe de Bézier a deux points d'extrémité et deux points de contrôle. En les randomisant sur la base roughness, vous pouvez également créer des courbes «manuscrites».


Remplissage de courbe


Cependant, le processus inverse est nécessaire pour remplir les courbes. Au lieu de tout normaliser en une courbe, la courbe se normalise en un polygone. Après avoir obtenu le polygone, vous pouvez utiliser l'algorithme de balayage de ligne pour remplir la forme courbe.

Vous pouvez échantillonner des points sur la courbe avec la fréquence souhaitée en utilisant l' équation de la courbe de Bézier cubique .


Si nous utilisons la fréquence d'échantillonnage, qui dépend de la densité des traits, nous obtenons suffisamment de points pour remplir la figure. Mais ce n'est pas particulièrement efficace. Si une partie de la courbe est nette, nous avons besoin de plus de points. Si une partie de la courbe est presque droite, alors moins de points sont nécessaires. Une solution peut être de déterminer la courbure / lissage de la courbe. Si elle est très courbe, nous divisons la courbe en deux courbes plus petites. S'il est lisse, nous le considérerons simplement comme une ligne droite.

Le lissage de la courbe est calculé en utilisant la méthode décrite dans ce post . La valeur de lissage est comparée à la valeur de tolérance, après quoi une décision est prise de diviser ou non la courbe.

Voici la même courbe avec un niveau de tolérance de 0,7:


Sur la seule base de la tolérance, l'algorithme fournit suffisamment de points pour représenter la courbe. Cependant, cela ne vous permet pas de vous débarrasser efficacement des points optionnels. Cela aidera le deuxième paramètre appelé distance . Pour réduire le nombre de points dans cette méthode, l'algorithme Ramer-Douglas-Pecker est utilisé .

Ce qui suit montre les points générés avec des valeurs de distance, égale à 0.15, 0.75, 1.5et 3.0.


En fonction de la rugosité de la forme, vous pouvez définir la valeur de distance appropriée . Après avoir reçu tous les sommets du polygone, nous pouvons magnifiquement remplir les formes courbes:


Circuits SVG


Les contours SVG sont un outil très puissant qui peut être utilisé pour créer toutes sortes d'images époustouflantes, mais pour cette raison, il est assez difficile de les utiliser.

RoughJS analyse le chemin et le normalise en seulement trois opérations: Move , Line et Cubic Curve . ( chemin-analyseur-de-données ). Après la normalisation, la figure peut être dessinée en utilisant les méthodes ci-dessus pour tracer des lignes et des courbes.

Le package de points sur chemin combine la normalisation des chemins et l'échantillonnage des points de courbe pour calculer les points de chemin correspondants.

Voici un exemple de calcul de points pour un chemin M240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100:


Un autre exemple SVG que j'aime montrer est la carte muette des États-Unis:


Essayez RoughJS


Consultez le site Web ou le référentiel sur Github ou la documentation de l' API . Suivez Twitter @RoughLib pour des informations sur le projet .

All Articles