Was du hier lernst
- Komplexe Zahlen
- Iteration
- Farben aus Zahlen berechnen
- Pixel spaltenweise zeichnen
- Ereignisschleife
- if __name__ == "__main__"
Die Erklärung stammt – leicht überarbeitet für Python 3 – aus dem Buch „Coding for Fun mit Python“ von Lars Heppert, Kapitel 11, Abschnitte 11.5 und 11.6.
Ein etwas schönerer Schongang
Mit dem Wissen über Fraktale aus dem Abschnitt »Was ist Chaos?« entwickeln wir jetzt einen Bildschirmschoner, der Fraktale auf den Bildschirm zaubert. Im Buch zeichnet er wieder mit pygame, im Browser übernimmt das die kleine Zeichen-API c4f. Der Einfachheit halber verwenden wir denselben Code wie beim Julia-Programm und nehmen nur ein paar Anpassungen für die Ausgabe auf dem Bildschirm vor.

Die Vorbereitung
Wir beginnen mit der Initialisierung. Ganz oben stehen die Größe des Bildschirms und die Zahl der Rechenschritte, die ein Punkt höchstens durchlaufen darf:
SCREEN_WIDTH = 800; SCREEN_HEIGHT = 600
max_iteration = 70
Im Buch ruft init() zuerst pygame.init() auf – ohne diesen Aufruf lässt sich pygame nicht verwenden – und legt dann mit pygame.display.set_mode() die Fläche an, auf die wir zeichnen. Der Parameter pygame.FULLSCREEN sorgt, wie der Name schon vermuten lässt, für die Darstellung im Vollbildmodus. Eine letzte Zeile macht den Mauszeiger unsichtbar: Sie setzt einen Zeiger, dessen Pixel alle durchsichtig sind. Im Browser legt Screen die Leinwand an, und für den Vollbildmodus gibt es einen Knopf über der Leinwand:
def init():
global screen
screen = Screen(SCREEN_WIDTH, SCREEN_HEIGHT, background=(255, 255, 255),
title="Fraktal-Bildschirmschoner")
Die Hauptschleife
Kommen wir gleich zur Hauptschleife des Programms:
async def main():
# die Hauptschleife des Programms, welche sich um die
# Ereignisbehandlung kümmert und die Zeichenroutine aufruft
global scale
await update_screen()
while True:
for event in screen.events():
if event.type == QUIT or \
(event.type == KEYDOWN and event.key == K_ESCAPE):
return
await sleep(0.05) # pygame.time.delay(50)
Zuerst löscht update_screen() den Bildschirm und ruft die Funktion zum Zeichnen des Fraktals auf. Die while-Schleife dient danach nur noch als Ereignisschleife: Sie wartet darauf, dass jemand das Programm mit der Taste Esc beendet, und schaut alle 50 Millisekunden nach. update_screen() ist schnell erklärt:
async def update_screen():
# hier wird der Bildschirm gelöscht, so dass dieser zunächst
# komplett weiß ist, danach wird das Fraktal durch Aufruf von
# draw_field gezeichnet
screen.fill((255, 255, 255))
await draw_field(screen)
Die Funktionen sind hier mit async markiert und werden mit await aufgerufen. So kann der Browser zwischendurch zeichnen, und du siehst das Fraktal Spalte für Spalte entstehen – dazu mehr unter »Vom Buch in den Browser«.
Das Fraktal zeichnen
Die Funktion draw_field() ist fast genauso aufgebaut wie das Julia-Programm, das ein Fraktal in eine Grafikdatei schreibt:
c = -0.767793511962608 + 0.0952946560332372j
for x in range(SCREEN_WIDTH):
column = bytearray()
for y in range(SCREEN_HEIGHT):
re = (x * 2.0 / SCREEN_WIDTH) - 1.0
im = (y * 2.0 / SCREEN_HEIGHT) - 1.0
z = re + im * 1j
for i in range(max_iteration):
if abs(z) > 2.0:
break
z = z * z + c
r, g, b = set_color(i)
column += bytes((r, g, b, 255))
Für jeden Punkt des Bildschirms wird eine komplexe Zahl z gebildet und immer wieder mit z · z + c weitergerechnet, bis ihr Betrag größer als 2 wird oder die 70 Schritte aufgebraucht sind. Neu sind nur zwei Dinge: Die Ausgabe landet auf dem Bildschirm statt in einer Datei, und die Farbe bestimmt die Funktion set_color() aus der Zahl der Schritte. Die Ausgabe auf dem Bildschirm bedarf sicher keines weiteren Kommentars, deshalb wenden wir uns direkt der Farbwahl zu.
Farben nach Gefühl
def set_color(i):
# hier wird eine hübsche Farbkombination gewählt, so dass die
# Fraktale nicht nur in einer Farbe mit Farbabstufung erscheinen –
# hier gibt es viel Spielraum für eigene Versuche, schöne Farben
# zu erhalten
if i == max_iteration:
col = (0, 0, 0)
return col
else:
c = (i * 15)
g = 255
b = 255
r = 255 - c
if r < 0:
r = 0
g = 255 * 2 - c
if g < 0:
g = 0
b = 255 * 3 - c
if b < 0:
r = g = b = 0
return (r, g, b)
Die einzelnen Farbanteile sind hier eher nach Gefühl als nach einer festen Regel bestimmt – deshalb ist das die ideale Stelle, um ein wenig mit der Implementierung zu spielen. Rechnen wir einmal nach, was passiert. c wächst mit jedem Schritt um 15. Ein Punkt, der sofort entkommt, bekommt (255, 255, 255), also Weiß. Solange c unter 255 liegt, wird nur der Rotanteil kleiner: Die Farbe wandert von Weiß nach Türkis. Danach verschwindet der Grünanteil, und aus Türkis wird Blau. Zuletzt geht auch das Blau zurück, und ab 52 Schritten ist der Punkt schwarz. So entsteht ein Farbverlauf aus drei Etappen: Weiß, Türkis, Blau, Schwarz. Punkte weit weg von der Menge leuchten hell, am Rand der Menge wird es blau, und die Menge selbst ist schwarz.
Genau hingeschaut
Die Abfrage if i == max_iteration greift nie. range(max_iteration) zählt nur bis 69, also ist i nach der Schleife höchstens 69 und nie 70. Aufgefallen ist das damals nicht, weil die Farbrechnung ab 52 Schritten ohnehin Schwarz liefert – das Ergebnis stimmt also trotzdem, nur aus einem anderen Grund. Sobald du eine andere Farbwahl ausprobierst, wird der Fehler sichtbar. Die Variante »Feuer statt Eis« unter »Probier mal« zeigt es und korrigiert die Abfrage.
Das Modul ausführbar machen
Um das Ganze auch zu starten, fehlen nur noch die letzten Zeilen:
if __name__ == "__main__":
init()
await main()
Die Bedingung in der ersten Zeile sorgt dafür, dass das Programm nur startet, wenn das Modul direkt aufgerufen wird. Wird es dagegen nur importiert – etwa weil ein anderes Programm set_color wiederverwenden möchte –, passiert beim Import nichts. Das ist in der Regel genau das, was man will.
Schon ganz nett, was fehlt denn noch?
In diesem Kapitel hast du am meisten Spielraum für eigene Erweiterungen, denn hier ist nur der Grundstein für einen Bildschirmschoner gelegt.
Als Nächstes könnten zum Beispiel wechselnde Parameter auf dem Programm stehen, denn immer dasselbe Fraktal gibt einen schlechten Bildschirmschoner ab. Die Anpassung ist leicht: Du änderst die Parameter des Fraktals in gewissen Abständen, holst das Zeichnen in die Hauptschleife, in der bisher nur die Ereignisse behandelt werden, und verlängerst dort die Wartezeit. Die Variante »Wechselnde Fraktale« macht genau das.
Etwas schwieriger, aber nicht weniger interessant ist es, die Berechnung auf mehrere Threads zu verteilen. Damit ließe sich die Rechenzeit stark verkürzen, und das Programm könnte während der Berechnung weiter auf Eingaben reagieren. Heute würde man in Python dafür eher mehrere Prozesse nehmen, etwa mit concurrent.futures, oder die Rechnung mit NumPy für alle Punkte auf einmal ausführen. Im Browser läuft Python übrigens schon in einem eigenen Hintergrund-Thread, einem Web Worker – deshalb bleibt die Seite bedienbar, während das Fraktal entsteht.
Außerdem fehlt noch die Umwandlung in eine ausführbare Datei, falls du unter Windows programmierst. Die unter Windows üblichen Bildschirmschoner sind ganz normale Programme, die nur die Endung .scr tragen. Beim Start bekommen sie zusätzlich Parameter mit, die verraten, ob der Benutzer Einstellungen vornehmen möchte oder ob der Schoner gerade starten soll. Wie man das damals und heute macht, steht unter »Vom Buch in den Browser«.
Falls du gar keinen Bildschirmschoner bauen möchtest, sondern lieber in das Fraktal hineinzoomen willst, ist das natürlich auch eine mögliche Ausbaustufe – probier die Variante »Hineinzoomen per Klick«. Tiefer geht es auf der Seite zur Mandelbrot-Menge.
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.
-
01Wechselnde Fraktale
Immer dasselbe Fraktal gibt einen schlechten Bildschirmschoner ab – das schreibe ich im Buch selbst. Die Lösung steht gleich dabei: Die Parameter in gewissen Abständen ändern und das Zeichnen in die Hauptschleife holen. Hier kommt alle vier Sekunden eine neue Konstante
can die Reihe, und jede ergibt eine völlig andere Julia-Menge.- max_iteration = 70 + max_iteration = 70 + constants = [-0.767793511962608 + 0.0952946560332372j, -0.8 + 0.156j, + 0.285 + 0.01j, -0.4 + 0.6j, -0.70176 - 0.3842j] + number = 0 # welches Fraktal gerade zu sehen ist - c = -0.767793511962608 + 0.0952946560332372j + c = constants[number % len(constants)] - global scale + global scale, number + waited = 0 - await sleep(0.05) # pygame.time.delay(50) + await sleep(0.05) # pygame.time.delay(50) + waited += 0.05 + if waited > 4: # alle vier Sekunden ein neues Fraktal + waited = 0 + number += 1 + await update_screen() -
02Hineinzoomen per Klick
Die zweite Idee aus dem Buch: Statt eines Bildschirmschoners ein Zoom ins Fraktal. Jeder Klick macht den angeklickten Punkt zur neuen Bildmitte und halbiert den Ausschnitt – die Variable
scale, die im Listing noch ungenutzt herumstand, bekommt endlich eine Aufgabe. Damit auch tief im Fraktal Farbe bleibt, gibt es pro Zoom 30 Rechenschritte mehr, die Farben wiederholen sich alle 51 Stufen, und das Schwarz der Menge wird richtig erkannt (mehr dazu in der Variante »Feuer statt Eis«).- from c4f import Screen, sleep, QUIT, KEYDOWN, K_ESCAPE + from c4f import Screen, sleep, QUIT, KEYDOWN, K_ESCAPE, MOUSEDOWN - scale = 3.0 / (SCREEN_HEIGHT * 500.0) # im Listing noch ungenutzt + scale = 1.0 # halbe Breite des Ausschnitts + center = 0 + 0j # Mitte des Ausschnitts - re = (x * 2.0 / SCREEN_WIDTH) - 1.0 + re = center.real + ((x * 2.0 / SCREEN_WIDTH) - 1.0) * scale - im = (y * 2.0 / SCREEN_HEIGHT) - 1.0 + im = center.imag + ((y * 2.0 / SCREEN_HEIGHT) - 1.0) * scale - global scale + global scale, center, max_iteration - return + return + if event.type == MOUSEDOWN: + # der angeklickte Punkt wird die neue Mitte, der Ausschnitt halb so groß + center += complex(((event.x * 2.0 / SCREEN_WIDTH) - 1.0) * scale, + ((event.y * 2.0 / SCREEN_HEIGHT) - 1.0) * scale) + scale /= 2 + max_iteration += 30 + await update_screen() - if i == max_iteration: + if i == max_iteration - 1: - c = (i * 15) + c = (i * 15) % 765 # die Farben wiederholen sich -
03Das richtige Seitenverhältnis
Das Listing presst den Bereich von −1 bis +1 in beiden Richtungen auf 800 × 600 Punkte. Waagerecht ist ein Punkt dadurch breiter als senkrecht, und das Fraktal wirkt um ein Drittel in die Breite gezogen. Mit dem Faktor 800/600 haben beide Achsen denselben Maßstab: Die Spiralen werden rund, und links und rechts kommt mehr von der Menge ins Bild.
- re = (x * 2.0 / SCREEN_WIDTH) - 1.0 + re = ((x * 2.0 / SCREEN_WIDTH) - 1.0) * SCREEN_WIDTH / SCREEN_HEIGHT -
04Feuer statt Eis – und ein Fehler weniger
Eine andere Farbwahl: Schwarz über Rot und Gelb bis Weiß, wie Glut. Dabei fällt ein kleiner Fehler auf.
iwird nie gleichmax_iteration, dennrange(70)endet bei 69 – die Abfrage für das Schwarz der Menge greift also nie. Im Original merkt man das nicht, weil die Farbrechnung ab Stufe 52 ohnehin Schwarz liefert. Bei der neuen Farbwahl würde die Menge weiß, deshalb prüft diese Variante aufmax_iteration - 1.- if i == max_iteration: + if i == max_iteration - 1: # der letzte Durchlauf: der Punkt gehört zur Menge - c = (i * 15) - g = 255 - b = 255 - r = 255 - c - if r < 0: - r = 0 - g = 255 * 2 - c - if g < 0: - g = 0 - b = 255 * 3 - c - if b < 0: - r = g = b = 0 - return (r, g, b) + c = (i * 15) + r = min(255, c) # erst rot … + g = min(255, max(0, c - 255)) # … dann gelb … + b = min(255, max(0, c - 510)) # … und schließlich weiß + return (r, g, b) -
05Mandelbrot statt Julia
Dieselbe Rechnung, anders verteilt: Bei der Mandelbrot-Menge ist jeder Bildpunkt selbst das
c, undzstartet immer bei 0. Der Ausschnitt wandert nach links, weil die Menge zwischen −2 und 0,5 liegt. Die Mandelbrot-Menge ist so etwas wie die Landkarte aller Julia-Mengen: Liegtcin ihr, ist die Julia-Menge zusammenhängend, liegt es draußen, zerfällt sie zu Staub.- re = (x * 2.0 / SCREEN_WIDTH) - 1.0 + re = (x * 3.0 / SCREEN_WIDTH) - 2.2 - im = (y * 2.0 / SCREEN_HEIGHT) - 1.0 + im = (y * 2.25 / SCREEN_HEIGHT) - 1.125 - z = re + im * 1j + c = re + im * 1j # jetzt ist der Punkt selbst das c … + z = 0 # … und z startet immer bei 0
Vom Buch in den Browser
Der Quelltext dieses Bildschirmschoners liegt nicht im Quellarchiv zum Buch – er ist hier aus den gedruckten Listings 11.12 bis 11.17 zusammengesetzt. Aufbau, Namen und Rechnung sind die aus dem Buch; die Funktionen stehen in derselben Reihenfolge wie dort.
| Im Buch (pygame) | Hier (c4f) |
|---|---|
pygame.init() und pygame.display.set_mode((800, 600), pygame.FULLSCREEN) |
Screen(800, 600) und der Vollbild-Knopf |
pygame.mouse.set_cursor((8,8), (0,0), (0,)*(64/8), (0,)*(64/8)) |
entfällt – im Browser bleibt der Mauszeiger sichtbar |
event = pygame.event.poll() |
for event in screen.events(): |
pygame.time.delay(50) |
await sleep(0.05) |
pygame.draw.line(scr, set_color(i), (x, y), (x, y)) für jeden Punkt |
eine Spalte Pixel sammeln und mit scr.pixels(...) zeichnen |
pygame.display.flip() |
await scr.frame(None) alle zehn Spalten |
Warum spaltenweise? Im Buch zeichnet das Programm jeden der 480 000 Punkte als Linie von einem Punkt zu sich selbst. Das geht mit pygame, im Browser wäre aber jeder Punkt ein eigener Zeichenbefehl. Deshalb sammelt draw_field die Farben einer ganzen Spalte in einem bytearray – vier Bytes pro Punkt für Rot, Grün, Blau und Deckkraft – und zeichnet die Spalte auf einmal. Weil das Bild so nach und nach erscheint, sind main, update_screen und draw_field hier async.
Python 3. In Python 2 war 64/8 eine ganzzahlige Division mit dem Ergebnis 8; in Python 3 ergibt / immer eine Kommazahl, (0,) * 8.0 wäre ein Fehler. Richtig hieße es heute 64 // 8. Die Zeile entfällt im Browser aber ohnehin. Die Kommentare sind in Umlauten geschrieben, und der Kommentar in update_screen sagt jetzt »weiß« statt »schwarz« – gefüllt wurde schon im Buch mit (255, 255, 255).
Ein Bildschirmschoner für Windows – damals und heute. In den Quelltexten zum Buch liegt dafür ein kleines Setup-Skript für py2exe (screenSaver/createExe.py):
from distutils.core import setup
import py2exe
setup(console=['screenSaver.py'])
python createExe.py py2exe machte daraus eine .exe, die man nur in .scr umbenennen musste. py2exe und distutils gehören heute zur Geschichte – distutils ist seit Python 3.12 gar nicht mehr dabei. Der heutige Weg ist PyInstaller: pyinstaller --onefile --noconsole schoner.py erzeugt eine einzelne .exe, die du in schoner.scr umbenennst. Windows ruft einen Bildschirmschoner mit /s zum Starten, mit /c für den Einstellungsdialog und mit /p für die kleine Vorschau auf; ein richtiger Schoner wertet diese Parameter über sys.argv aus und beendet sich bei der ersten Mausbewegung – mit dem Trick aus dem Farbbalken-Schoner, der die erste Sekunde abwartet.


