Nachahmung der Freihandzeichnung am Beispiel von RoughJS

RoughJS ist eine kleine (< 9 KB ) JavaScript-Grafikbibliothek, mit der Sie in skizzenhaftem, handgeschriebenem Stil zeichnen können . Es ermöglicht Ihnen, auf <canvas>und mit zu zeichnen SVG. In diesem Beitrag möchte ich die beliebteste Frage zu RoughJS beantworten: Wie funktioniert das?


Ein bisschen Geschichte


Fasziniert von den Bildern handgezeichneter Grafiken, Diagramme und Skizzen fragte ich mich wie ein echter Nerd: Kann ich solche Zeichnungen mit Hilfe eines Codes so genau wie möglich erstellen, um eine Freihandzeichnung zu imitieren und gleichzeitig die Möglichkeit einer Softwareimplementierung beizubehalten? Ich beschloss, mich auf die Grundelemente - Linien, Polygone, Ellipsen und Kurven - zu konzentrieren, um eine ganze Bibliothek von 2D-Grafiken zu erstellen. Darauf basierend können Sie Bibliotheken und Diagramme zum Zeichnen von Diagrammen und Diagrammen erstellen.

Nachdem ich das Problem kurz untersucht hatte, fand ich einen Artikel von Joe Wood und seinen Kollegen namens Sketchy Rendering zur Informationsvisualisierung . Die darin beschriebenen Techniken wurden zur Grundlage der Bibliothek, insbesondere beim Zeichnen von Linien und Ellipsen.

2017 habe ich die erste Version der Bibliothek geschrieben, die nur auf Canvas funktioniert hat. Nachdem ich das Problem gelöst hatte, verlor ich das Interesse daran. Ein Jahr später arbeitete ich viel mit SVG und beschloss, RoughJS an die Arbeit mit SVG anzupassen. Ich habe auch die Struktur der API geändert, um sie einfacher zu gestalten, und mich auf einfache Grundelemente für Vektorgrafiken konzentriert. Ich habe in den Hacker News über Version 2.0 gesprochen und plötzlich hat sie immense Popularität erlangt. 2018 war es der zweitbeliebteste Beitrag von ShowHN .

Seitdem haben andere Leute auf der Basis von RoughJS erstaunlichere Dinge geschaffen, zum Beispiel Excalidraw , Why do Cats & Dogs ... , die RoughViz-Grafikbibliothek .

Lassen Sie uns nun über Algorithmen sprechen ...

Unebenheit


Die grundlegende Grundlage für die Nachahmung handschriftlicher Figuren ist der Zufall. Wenn wir von Hand zeichnen, sind zwei beliebige Formen etwas unterschiedlich. Niemand zeichnet perfekt genau, daher wird jeder räumliche Punkt in RoughJS für zufällige Verschiebungen angepasst. Die Größe der Zufälligkeit wird durch einen numerischen Parameter angegeben roughness.


Stellen Sie sich einen Punkt Aund einen Kreis um ihn herum vor. Ersetzen Sie nun durch einen Azufälligen Punkt innerhalb dieses Kreises. Die Fläche dieses Zufallskreises wird durch den Wert gesteuert roughness.

Linien


Handschriftliche Linien sind niemals gerade und zeigen häufig eine Krümmung in der Biegung ( hier beschrieben ). Wir randomisieren die beiden Endpunkte der Linie basierend auf der Rauheit. Dann wählen wir zwei weitere zufällige Punkte ungefähr in einem Abstand von 50% und 75% vom Ende des Segments aus. Durch Verbinden dieser Punkte der Kurve erhalten wir den Effekt des Biegens .


Beim Zeichnen von Hand bewegen Menschen manchmal den Bleistift entlang der Linie vorwärts und rückwärts. Dies ist entweder erforderlich, um die Linie heller zu machen oder um einfach die Geradheit der Linie zu korrigieren. Es sieht ungefähr so ​​aus:


Um einen skizzenhaften Effekt hinzuzufügen, zeichnet RoughJS zweimal eine Linie. In Zukunft plane ich, diesen Aspekt anpassbarer zu machen.

Schau dir diese Leinwandoberfläche an. Der Rauheitsparameter ändert das Erscheinungsbild der Linien:


Im Originalartikel auf Leinwand können Sie selbst zeichnen.

Beim Zeichnen von Hand werden lange Linien normalerweise weniger gerade und mehr gekrümmt. Das heißt, die Zufälligkeit von Offsets zum Erzeugen eines Effekts ist eine Funktion der Zeilenlänge und des Werts randomness. Die Skalierung dieser Funktion ist jedoch nicht für sehr lange Zeilen geeignet. Zum Beispiel werden in dem Bild unten konzentrische Quadrate mit demselben zufälligen Keim gezeichnet, d.h. Tatsächlich handelt es sich um eine Zufallszahl, jedoch mit einer anderen Skala.


Möglicherweise stellen Sie fest, dass die Kanten der äußeren Quadrate etwas unebener aussehen als die der inneren. Daher habe ich auch einen Dämpfungsfaktor in Abhängigkeit von der Leitungslänge hinzugefügt. Der Dämpfungskoeffizient wird bei verschiedenen Längen als Sprungfunktion verwendet.


Ellipsen (und Kreise)


Nehmen Sie ein Blatt Papier und zeichnen Sie so schnell wie möglich einige Kreise in einer kontinuierlichen Bewegung. Folgendes habe ich bekommen:


Beachten Sie, dass Start- und Endpunkt der Schleife nicht immer übereinstimmen. RoughJS versucht dies nachzuahmen und gleichzeitig das Erscheinungsbild zu vervollständigen (die Technik wurde aus dem giCenter-Artikel übernommen ).

Der Algorithmus findet die nEllipsenpunkte, an denen sie ndurch die Größe der Ellipse bestimmt werden. Dann wird jeder Punkt nach seinem Wert randomisiert roughness. Dann wird eine Kurve durch diese Punkte gezogen. Um die Wirkung der getrennten Enden zu erzielen, stimmen die Punkte vom zweiten bis zum letzten nicht mit dem ersten Punkt überein. Stattdessen verbindet die Kurve den zweiten und dritten Punkt.


Eine zweite Ellipse wird ebenfalls gezeichnet, damit die Schleife geschlossener ist und einen zusätzlichen Skizziereffekt hat.

Im Originalartikel können Sie Ellipsen auf einer interaktiven Leinwandoberfläche zeichnen. Variieren Sie die Rauheit und beobachten Sie, wie sich die Form ändert:


Beim Strichzeichnen werden einige dieser Artefakte stärker hervorgehoben, wenn eine bestimmte Form auf unterschiedliche Größen skaliert wird. In einer Ellipse macht sich dieser Effekt stärker bemerkbar, da das Verhältnis quadratisch ist. Im Bild unten haben alle Kreise die gleiche Form, aber die äußeren sehen ungleichmäßiger aus.


Der Algorithmus passt sich automatisch an die Größe der Form an und erhöht die Anzahl der Punkte im Kreis ( n). Unten finden Sie die gleichen Kreise, die mit der automatischen Abstimmung erstellt wurden.


Ausfüllen


Gepunktete Linien werden normalerweise verwendet, um handgezeichnete Formen auszufüllen . Bei Freihandskizzen bleiben die Linien nicht immer innerhalb des Umrisses der Formen. Sie sind auch randomisiert. Dichte, Winkel, Linienbreite können eingestellt werden.


Die oben gezeigten Quadrate sind leicht zu füllen, aber bei anderen Formen können alle Arten von Problemen auftreten. Beispielsweise verursachen konkave Polygone (in denen Winkel 180 ° überschreiten können) häufig solche Probleme:


Das obige Bild stammt aus einem Fehlerbericht in einer der vorherigen Versionen von RoughJS. Seitdem habe ich den Strichfüllungsalgorithmus aktualisiert, indem ich die Version der String-Scan-Methode angepasst habe .

Der String-Scan-Algorithmus kann verwendet werden, um ein beliebiges Polygon zu füllen. Das Prinzip besteht darin, ein Polygon mit horizontalen Linien (Rasterlinien) zu scannen. Rasterlinien verlaufen vom oberen Rand des Polygons nach unten. Für jede Rasterlinie bestimmen wir, an welchen Punkten sich die Linie mit dem Polygon schneidet. Wir bauen diese Schnittpunkte von links nach rechts.


Wenn wir von Punkt zu Punkt gehen, wechseln wir vom Füllmodus in den Nichtfüllmodus. Das Umschalten zwischen Zuständen erfolgt, wenn sich jeder Schnittpunkt auf der Rasterlinie trifft. Hier muss viel mehr berücksichtigt werden, insbesondere Grenzfälle und Scanoptimierungsmethoden; Weitere Informationen hierzu finden Sie hier: Polygone rastern oder einen Spoiler mit Pseudocode bereitstellen.

Details zur Implementierung des String-Scan-Algorithmus
() .

— (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)

Im Bild unten (im Originalartikel interaktiv) bezeichnet jedes Quadrat ein Pixel. Sie können die Scheitelpunkte verschieben, um das Polygon zu ändern und zu beobachten, welche Pixel traditionell gefüllt werden.


Beim Ausfüllen von Strichen wird das Inkrement von Rasterlinien in Schritten in Abhängigkeit von der gegebenen Dichte der Strichlinien ausgeführt, und jede Linie wird unter Verwendung des oben beschriebenen Algorithmus gezeichnet.

Dieser Algorithmus gilt jedoch für horizontale Rasterlinien. Um verschiedene Strichwinkel zu implementieren, dreht der Algorithmus zuerst die Form selbst um den gewünschten Strichwinkel. Dann werden die Rasterlinien für die gedrehte Figur berechnet. Ferner drehen sich die berechneten Linien in die entgegengesetzte Richtung zurück zum Winkel der Striche.


Nicht nur Striche ausfüllen


RoughJS unterstützt auch andere Füllstile, die jedoch alle vom gleichen Schraffuralgorithmus abgeleitet sind. Kreuzschraffur besteht darin, gestrichelte Linien in einem Winkel angleund dann weitere Linien in einem Winkel zu zeichnen angle + 90°. Zickzack versucht, eine gestrichelte Linie mit der vorherigen zu verbinden. Um ein Punktmuster zu erhalten , zeichnen Sie kleine Kreise entlang der gestrichelten Linien.


Die Kurven


Alles in RoughJS ist auf Kurven normalisiert - Linien, Polygone, Ellipsen usw. Daher besteht die natürliche Entwicklung dieser Idee darin, eine Skizzenkurve zu erstellen. In RoughJS übergeben wir eine Reihe von Punkten an eine Kurve. Anschließend konvertieren wir sie mithilfe der Kurvennäherung in kubische Bezier- Kurven .

Jede Bezier-Kurve hat zwei Endpunkte und zwei Kontrollpunkte. Indem Sie sie auf der Basis randomisieren roughness, können Sie auf ähnliche Weise „handgeschriebene“ Kurven erstellen.


Kurvenfüllung


Der umgekehrte Prozess ist jedoch erforderlich, um die Kurven zu füllen. Anstatt alles auf eine Kurve zu normalisieren, wird die Kurve auf ein Polygon normalisiert. Nachdem Sie das Polygon erhalten haben, können Sie den Linienscan-Algorithmus verwenden, um die gekrümmte Form zu füllen.

Mit der Gleichung der kubischen Bezier-Kurve können Sie Punkte auf der Kurve mit der gewünschten Frequenz abtasten .


Wenn wir die Abtastfrequenz verwenden, die von der Dichte der Striche abhängt, erhalten wir genügend Punkte, um die Figur zu füllen. Dies ist jedoch nicht besonders effektiv. Wenn ein Teil der Kurve scharf ist, brauchen wir mehr Punkte. Wenn ein Teil der Kurve fast gerade ist, werden weniger Punkte benötigt. Eine Lösung kann darin bestehen, die Krümmung / Glätte der Kurve zu bestimmen . Wenn es sehr gekrümmt ist, teilen wir die Kurve in zwei kleinere Kurven. Wenn es glatt ist, betrachten wir es einfach als gerade Linie.

Die Glätte der Kurve wird mit der in diesem Beitrag beschriebenen Methode berechnet . Der Glättungswert wird mit dem Toleranzwert verglichen, wonach entschieden wird, ob die Kurve geteilt werden soll oder nicht.

Hier ist die gleiche Kurve mit einem Toleranzniveau von 0,7:


Allein aufgrund der Toleranz liefert der Algorithmus genügend Punkte, um die Kurve darzustellen. Es erlaubt Ihnen jedoch nicht, optionale Punkte effektiv loszuwerden. Dies hilft dem zweiten Parameter namens distance . Um die Anzahl der Punkte bei dieser Methode zu verringern, wird der Ramer-Douglas-Pecker-Algorithmus verwendet .

Im folgenden werden die erzeugten Punkte mit Werten von Entfernung gleich 0.15, 0.75, 1.5und 3.0.


Basierend auf der Rauheit der Form können Sie den entsprechenden Abstandswert einstellen . Nachdem wir alle Eckpunkte des Polygons erhalten haben, können wir die gekrümmten Formen wunderschön füllen:


SVG-Schaltungen


SVG-Konturen sind ein sehr leistungsfähiges Werkzeug, mit dem alle Arten von atemberaubenden Bildern erstellt werden können. Aus diesem Grund ist es jedoch schwierig, mit ihnen zu arbeiten.

RoughJS analysiert den Pfad und normalisiert ihn in nur drei Operationen: Verschieben , Linie und Kubische Kurve . ( Pfad-Daten-Parser ). Nach der Normalisierung kann die Figur mit den oben beschriebenen Methoden zum Zeichnen von Linien und Kurven gezeichnet werden.

Das Points-on-Path- Paket kombiniert die Normalisierung von Pfaden und die Abtastung von Kurvenpunkten, um die entsprechenden Pfadpunkte zu berechnen.

Das Folgende ist ein Beispiel für eine Punktberechnung für einen Pfad 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:


Ein weiteres Beispiel SVG , dass ich zu zeigen Liebe ist die Kontur Karte der Vereinigten Staaten:


Versuchen Sie es mit RoughJS


Überprüfen Sie die Website oder das Repository auf Github oder die API-Dokumentation . Folgen Sie Twitter @RoughLib für Projektinformationen .

All Articles