Willkommen zum zweiten Teil meines Javascript-Canvas-Tutorials! Die Programmierung meiner fraktalen, ornamentalen Blumenranke tritt nun in die entscheidende Phase. Mit der animierten Sinuskurve habe ich im ersten Teil bereits die Grundform der Hauptranke definiert. Im nächsten Schritt muss die rekursive Verzweigung der Ranke implementiert werden.
Rotation und Translation
Die Verzweigung der Ranken funktioniert unter der Voraussetzung, dass Sinuskurven beliebig rotiert und translatiert (d.h. verschoben) werden können.
Beispiel 1
Eine Ranke muss entlang der x- und y-Achse jeweils um einen bestimmten Betrag ("Offset") verschoben werden können. Ich speichere die Offsets in den Variablen "x_trans" und "y_trans". Sie werden in den Parametern jeder Canvas-Zeichenfunktion, wie z.B. moveTo, lineTo oder quadraticCurveTo auf die x- und y-Koordinaten aufaddiert. Man kann den Punkt (x_trans, y_trans) auch als neuen Koordinatenursprung der Ranke auffassen.
Beispiel 2
Eine Sinuskurve muss um einen bestimmten Winkel w rotiert werden können. Ich speichere den gewünschten Rotationswinkel in einer Variable "rot". Die Koordinaten "x" und "y" müssen gemäß der Vorschrift zur Rotation eines Koordinatensystems neu berechnet werden. Die rotierten Koordinaten nenne ich "x_rot" und "y_rot". Es gilt: x_rot = x * cos(w) + y * sin(w) y_rot = -x * sin(w) + y * cos(w)
Die Winkelangabe w erfolgt in Bogenmaß. Eine Rotation um 0.2 Bogenmaß entspricht ca. 11.5°. Die Umrechnung von Winkelmaß (°) in Bogenmaß können Sie z.B. mit einem Online-Rechner vornehmen.
Beispiel 3
Das kleine Blatt (vgl. Teil 1 des Tutorials) soll wieder an das Ende der Ranke gesetzt werden. Damit die Richtung des Blattes stimmt, müssen alle Start-, End- und Stützpunkte der beiden quadratischen Kurven auf die gleiche Weise rotiert und translatiert werden wie die Punkte der Sinuskurve.
Um den Code übersichtlich zu halten, lagere ich die Umrechnung der Koordinaten ab jetzt in eine Funktion rotate aus:
Eine objektorientierte Sinuskurve
Unsere Sinuskurve hat einen Zustand, der durch die Werte der Eigenschaften "freq", "amp", "x_trans", "y_trans" und "rot" bestimmt wird. Sie besitzt auch ein Verhalten, denn sie ist in der Lage, ihren Graph zu zeichnen (Funktion draw). Bald wird weiteres Verhalten hinzugefügt, nämlich die Fähigkeit zur Vermehrung, um Kind-Sinuskurven zu erzeugen.
Die Kind-Sinuskurven besitzen andere Zustände, d.h. die Werte der oben genannten Eigenschaften weichen von der "Mutter" ab. Sie besitzen z.B. eine andere Rotation und Translation. Das Verhalten (Zeichnen und Vermehrung) bleibt dagegen gleich. Damit mein Programm in der Lage ist, die vielen verschiedenen erzeugten Sinuskurven zu verwalten, möchte ich das Konzept der Objektorientierung zu Rate ziehen. Ich werde die Eigenschaften und Methoden einer Sinuskurve in einer Klasse kapseln. Von dieser Klasse können dann zur Laufzeit der Animation beliebig viele Instanzen (oder Objekte) erzeugt werden, die sich hinsichtlich der konkreten Werte ihrer Eigenschaften unterscheiden.
Beispiel 4
In diesem Beispiel sehen Sie die objektorientierte Variante der Sinuskurve. Mit "var Sinus = {...}" definiere ich die Klasse (oder anders ausgedrückt: den Objekt-Prototyp) "Sinus". In der Klasse werden die benötigten Eigenschaften und Methoden angelegt. Die Eigenschaften werden mit Standardwerten vorbelegt. Weiter unten erzeuge ich mittels "Object.create(Sinus)" ein Objekt von der Klasse "Sinus". Die etwas eigenwillige Funktionsverschachtelung innerhalb von setInterval ist auf ein hier diskutiertes Problem zurückzuführen. Kurz gesagt: Wenn ich nur "setInterval(hauptranke.draw, 5)" schreibe, kann ich nicht innerhalb von draw mittels "this." auf die Objekteigenschaften zugreifen.
Beispiel 5
In Zukunft wird nicht mehr nur eine, sondern eine Vielzahl von Sinusobjekten vorliegen, deren Graphen alle im Takt von 5ms aktualisiert werden müssen. Um das Programm auf die Verarbeitung mehrerer Objekte vorzubereiten, lege ich ein Array "ranken" an, welches neben der Hauptranke zukünftig alle durch Vermehrung generierten Kind-Ranken aufnehmen wird. Zudem implementiere ich eine Funktion update, die für alle im Array enthaltenen Ranken die draw-Funktion aufruft. Die update--Funktion wird nun alle 5ms ausgeführt.
Die Verzweigung der Sinuskurve
Endlich ist es soweit: Die Abspaltung neuer Ranken von der Hauptranke kann implementiert werden. Zunächst lege ich fest, dass zwei Abspaltungen stattfinden, sobald der x-Wert 150 erreicht hat. Die Abspaltung habe ich in der Funktion breed implementiert. Sie erzeugt zwei neue Sinus-Objekte, die im Verhältnis zur Hauptranke um 51° nach rechts bzw. nach links geneigt sind (+/-0.9 Bogenmaß). Ihr Koordinatenursprung (definiert durch x_trans und y_trans, vgl. Beispiel 1) entspricht den (rotierten und translatierten) Koordinaten, an denen sich der Graph der Mutterranke gerade befindet. Damit ist sichergestellt, dass der Ursprung der beiden Kindranken exakt auf dem Graph der Mutterranke liegt.
Eine Veränderung gibt es jetzt beim Abbruch der Animation: Sobald sich die x-Koordinate oder die y-Koordinate einer Ranke in der Nähe des oberen, unteren, rechten oder linken Canvas-Randes befindet, wird ein Blatt gezeichnet und der Schalter "finished" auf den Wert "true" gesetzt. Die Funktion clearInterval sollte im Gegensatz zu den vorigen Beispielen an dieser Stelle nicht mehr aufgerufen werden, denn der Abschluss einer einzelnen Ranke bedeutet noch nicht das Ende der ganzen Animation. Die Animation wird nun stattdessen in der update-Funktion beendet: Hier werden nur die Ranken weitergezeichnet, bei denen die Variable "finished" den Wert "false" besitzt. Wenn bei allen Ranken "finished" auf "true" steht, wird clearInterval aufgerufen.
Beispiel 6
Beispiel 7
In diesem Beispiel habe ich noch zwei Anpassungen vorgenommen:
Die Abspaltung erfolgt nun in Intervallen von 150 Pixeln. Die Bedingung "if(x==150)" habe ich dazu in "if(x%150==0)" umgeschrieben (% ist der Modulo-Operator), d.h. jedesmal wenn der Rest bei der Division von x durch 150 den Wert 0 ergibt, erfolgt eine Verzweigung.
Jede Ranke hat nun eine mit der Variable "x_end" vorgeschriebene maximale Länge. Die Ranke wird beendet, sobald einer der Canvas-Ränder erreicht ist oder sobald die x-Koordinate den Wert von "x_end" erreicht hat (this.x>this.x_end). Damit das Ornament natürlicher aussieht, reduziere ich den Wert für "x_end" von Generation zu Generation um einen bestimmten Faktor. Im Code sehen Sie z.B. child1.x_end = this.x_end * 0.7 * ... (der Parameter 0.7 kann variiert werden). Dadurch sind die Kind-Ranken immer kürzer als die jeweilige Mutterranke.
Kindranken, die am Ende der Mutterranke stehen, sollen etwas kürzer sein als Kindranken, die am Anfang stehen. Insgesamt soll dadurch der Eindruck entstehen, dass das Ornament zum Ende hin spitz zuläuft - ähnlich wie eine Pflanze, die nach oben hin immer schmaler wird. Die Länge der Kindranken soll also in Abhängigkeit von der Position entlang der Mutterranke abnehmen. Die Position berechne ich als prozentualen Anteil this.x/this.x_end. Da ich eine exponentielle Abnahme bevorzuge, multipliziere ich "x_end" mit dem Faktor Math.exp(-1.5*this.x / this.x_end). Der Parameter 1.5 steuert, wie schnell die Länge abnimmt und kann variiert werden.
Beispiel 8
Experimentieren Sie mit den Einstellungen, um das Ornament abzuwandeln! Im nächsten Beispiel habe ich folgende Modifikationen vorgenommen:
Amplitude ("amp") mit dem Wert 10 (vorher 25)
Aufruf der Funktion breed alle 50 Pixel (vorher 150).
Neuberechnung von "x_end" in der Funktion breed mit den Faktoren 0.5 und 1.7 (vorher 0.7 und 1.5)
Beispiel 9
Experimentieren Sie mit den Einstellungen, um das Ornament abzuwandeln! Das nächste Beispiel enthält die Modifikationen von Beispiel 8 + folgende Änderung:
Neigung der Kindranken um +/-0.4 (vorher 0.9)
Das Resultat auf CodePen
Das Resultat dieses Tutorials stelle ich auch auf CodePen zur Verfügung:
Mit Animationen in Javascript Canvas habe ich mich in den letzten Monaten schon mehrfach beschäftigt. Erst vor Kurzem bin ich dabei auf den Bereich der "Transformationen" in der Canvas-Bibliothek aufmerksam geworden. Dazu gehören u.a. die Funktionen
save()restore()translate()rotate()scale()
Mehr Informationen: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Transformat...
Voriges Jahr habe ich bereits einen Beitrag über die Programmierung eines animierten Sternenhimmels mit Javascript Canvas geschrieben. Die Programmierung war zwar mathematisch interessant, aber recht aufwändig, da Canvas von Haus aus keine dreidimensionalen Koord...
Vor ein paar Monaten habe ich diesen animierten Tunnel programmiert:
See the Pen Simple 3d tunnel with curves by Anna Prenzel (@blaustern_fotografie) on CodePen.
Hintergrund: Vorher hatte ich (auf Anregung meiner Mama) den animierten Sternenhimmel programmiert, d...
Die Intros alter Star-Trek-Serien zeigen einen virtuellen Flug durch den Weltraum:
Ich mag diese Animation, denn sie ist einfach und trotzdem effektvoll. So etwas möchte ich mit Javascript Canvas programmieren. In diesem Tutorial erkläre ich Schritt f...