Was du hier lernst
- Komplexe Zahlen in Python
- Iteration und Abbruchbedingung
- Fluchtzeit-Algorithmus
- Farbpaletten
- Pixel mit bytearray
- Maus-Ereignisse
So entsteht das Apfelmännchen
Das Bild zeigt keine gezeichnete Form, sondern das Ergebnis einer Rechnung – für jeden einzelnen Pixel. Jeder Pixel steht für eine Zahl c. Mit dieser Zahl spielt das Programm immer wieder dieselbe Formel durch und schaut, was passiert. Aus der Antwort wird eine Farbe.
1. Eine Formel, immer wieder
Der Kern ist die Funktion mandelbrot. Sie beginnt mit z = 0 und rechnet dann wiederholt z = z * z + c:
def mandelbrot(c, max_iter):
z = 0
for n in range(max_iter):
z = z * z + c
if abs(z) > 2:
return n
return max_iter
Für manche c bleibt z für immer klein, für andere wächst es explosionsartig. abs(z) > 2 erkennt die Flucht, und n sagt, nach wie vielen Schritten sie passiert ist. Diese Schrittzahl heißt Fluchtzeit. Punkte, die nach max_iter Schritten noch nicht geflohen sind, zählen zur Mandelbrot-Menge.
Warum gerade 2?
Sobald der Betrag von z größer als 2 ist, wächst er mit jedem weiteren Schritt garantiert weiter – der Punkt kommt nie zurück. Man muss also nicht ewig weiterrechnen: Der Kreis mit Radius 2 ist die Grenze, ab der die Entscheidung feststeht.
2. Komplexe Zahlen sind in Python eingebaut
c und z sind komplexe Zahlen: Zahlen mit einem Realteil und einem Imaginärteil, die man sich als Punkt in einer Ebene vorstellen kann. Viele Sprachen brauchen dafür eine Bibliothek, Python kann es von Haus aus:
>>> c = complex(-0.5, 0.5) # oder: -0.5 + 0.5j
>>> c * c
-0.5j
>>> abs(3 + 4j)
5.0
Deshalb sieht die Formel im Code genauso aus wie in einem Mathebuch. abs(z) ist der Abstand vom Nullpunkt.
3. Vom Pixel zur komplexen Zahl
Die Leinwand hat 640 × 400 Pixel, der interessante Teil der Ebene ist aber nur etwa 3,2 Einheiten breit. step ist die Breite eines Pixels in der Ebene; die Bildmitte entspricht center:
step = width / WIDTH
x0 = center.real - WIDTH / 2 * step
...
complex(x0 + px * step, ci)
Zoomen heißt nur: width wird kleiner. Die gleichen 640 Pixel decken dann einen winzigeren Ausschnitt ab – und zeigen mehr Details.
4. Farben aus Schrittzahlen
Für jede mögliche Schrittzahl legt make_palette vorab eine Farbe fest, als vier Bytes für Rot, Grün, Blau und Deckkraft. hsv() aus c4f liefert Regenbogenfarben: Der Farbton läuft mit der Schrittzahl mehrmals durch das Spektrum, die Helligkeit steigt. Die Punkte der Menge selbst werden schwarz.
Die Palette vorher zu berechnen spart viel Zeit: Beim Zeichnen ist jede Farbe nur noch ein Nachschlagen in einer Liste – bei 256 000 Pixeln macht das einen großen Unterschied.
5. Zeile für Zeile auf die Leinwand
Jede Bildzeile wird als bytearray gesammelt und mit einem einzigen Aufruf gezeichnet:
row = bytearray()
for px in range(WIDTH):
row += palette[mandelbrot(complex(x0 + px * step, ci), max_iter)]
screen.pixels(0, py, WIDTH, 1, row)
Alle zehn Zeilen gibt das Programm dem Browser mit await screen.frame(None) kurz die Kontrolle. So siehst du, wie das Bild von oben nach unten wächst, und ein Klick wird sofort bemerkt – dann beginnt die Rechnung mit dem neuen Ausschnitt von vorn.
6. Hineinzoomen
Ein Klick macht den angeklickten Punkt zur neuen Bildmitte und teilt die Breite durch drei. Je tiefer du zoomst, desto mehr Schritte braucht es, um Innen und Außen zu unterscheiden. Deshalb steigt max_iter mit dem Zoom:
zoom = 3.2 / width
max_iter = int(60 + 30 * math.log(max(zoom, 1), 3))
In der Konsole steht nach jedem Bild, wie stark vergrößert ist, wo die Mitte liegt und wie lange die Rechnung gedauert hat.
Verwandt: die Julia-Menge
Im Buch berechnet Kapitel 11 eine Julia-Menge. Der Unterschied ist klein: Dort bleibt c fest, und der Pixel bestimmt den Startwert von z. Unter »Probier mal« kannst du dieses Programm mit einer Zeile in eine Julia-Menge verwandeln.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01Direkt ins Seepferdchental
Zwischen dem großen »Kopf« und dem »Rumpf« der Menge liegt ein Tal voller Spiralen, die an Seepferdchen erinnern. Der neue Startpunkt fliegt direkt hin – rund 270-fach vergrößert. Die Zahl der Schritte wächst mit dem Zoom automatisch mit – deshalb braucht dieses Bild einige Sekunden.
- START_CENTER = complex(-0.65, 0.0) + START_CENTER = complex(-0.7436, 0.1318) - START_WIDTH = 3.2 + START_WIDTH = 0.012 -
02Feuer statt Eis
Die Farben entstehen in
make_paletteaus dem Farbton-Modell HSV. Ein kleiner Farbtonbereich um Rot und Gelb, dafür steigende Helligkeit, ergibt ein glühendes Fraktal.- r, g, b = hsv(0.66 + 3 * t, 0.8, min(1.0, 4 * t + 0.15)) + r, g, b = hsv(0.0 + 0.16 * t, 1.0 - 0.6 * t, min(1.0, 3 * t)) -
03Aus Mandelbrot wird Julia
Vertausche die Rollen: Starte nicht bei
z = 0mit dem Pixel alsc, sondern beim Pixel alsz– und haltecfest. Genau so entsteht eine Julia-Menge. Jedescergibt eine andere;-0.8 + 0.156jist eine besonders schöne.- z = 0 + z, c = c, complex(-0.8, 0.156) - START_CENTER = complex(-0.65, 0.0) + START_CENTER = complex(0.0, 0.0) -
04Mehr Rechenschritte, schärfere Ränder
Mit mehr Schritten pro Pixel werden die feinen Fäden am Rand der Menge sichtbar – das Bild braucht dafür länger. Beobachte in der Konsole, wie sich die Rechenzeit verändert.
- max_iter = int(60 + 30 * math.log(max(zoom, 1), 3)) + max_iter = int(250 + 60 * math.log(max(zoom, 1), 3)) -
05Zehnfach-Zoom
Jeder Klick vergrößert jetzt zehnfach statt dreifach. Nach sechs Klicks bist du bei einer Million. Nach etwa 13 Klicks stößt du an die Grenzen der Gleitkommazahlen: Das Bild wird pixelig, weil benachbarte Pixel dieselbe Zahl bekommen.
- ZOOM_STEP = 3 + ZOOM_STEP = 10
Hinter den Kulissen
Dieses Beispiel steht so nicht im Buch. Dort geht es in Kapitel 11 unter »Was ist Chaos?« um Fraktale, berechnet wird eine Julia-Menge. In den Quelltexten zum Buch lag außerdem eine sehr schnelle, aber schwer lesbare Mandelbrot-Version mit der Bibliothek NumPy. Das Programm hier ist neu geschrieben, im Stil des Buchs: reines Python, kurze Funktionen, sprechende Namen.
Warum ohne NumPy? Pyodide könnte NumPy laden, das wären aber mehrere Megabyte zusätzlicher Download. Und die Schleife in reinem Python zeigt viel deutlicher, was eigentlich passiert. Der Preis ist Geschwindigkeit: 256 000 Pixel mit jeweils bis zu 60 Schritten sind einige Millionen Rechenoperationen. Im Browser dauert ein Bild deshalb ein bis zwei Sekunden, tiefe Zooms länger.
Warum Zeile für Zeile? Ein Browser zeigt nichts an, solange ein Programm ununterbrochen rechnet. Das await screen.frame(None) nach jeweils zehn Zeilen macht die Rechnung sichtbar – und hält die Seite bedienbar. Python läuft dabei in einem eigenen Hintergrund-Thread (einem Web Worker), sodass du auch während einer langen Rechnung scrollen kannst.
Und die Genauigkeit? Python rechnet mit 64-Bit-Gleitkommazahlen, etwa 16 Stellen genau. Das reicht für Vergrößerungen bis etwa 10¹³. Danach bekommen benachbarte Pixel dieselbe Zahl, und das Bild zerfällt in Blöcke. Für tiefere Zooms bräuchte man Zahlen mit beliebiger Genauigkeit, zum Beispiel aus dem Modul decimal – dann dauert jedes Bild allerdings sehr viel länger.