»The Nature of Code« in Python (Py5): Ein einfaches Partikelsystem (Stage 1)

Py5
Processing
Python
Creative Coding
Nature of Code
Autor:in

Jörg Kantel

Veröffentlichungsdatum

8. August 2023

Ich habe noch gestern meine Ankündigung wahrgemacht und ein einfaches Partikelsystem in Py5 implementiert. Das ging ziemlich flott, nicht nur, weil ich ähnliches schon einmal in Processing.py programmiert, sondern ich auch schon einmal Versuche mit der NodeBox 1 und sogar mit Pythons Schildkröte angestellt hatte. Alle diese Programme basierten auf der Processing (Java)-Version eines Partikelsystems, die Daniel Shiffman in seinem wunderbaren Buch »The Nature of Code im vierten Kapitel (Seiten 143-188) implementiert hatte.

Dadurch fiel mir der Port nach Py5 nicht allzu schwer, dennoch habe ich dabei einiges über Py5 und seine Besonderheiten gelernt. Als erstes: Ich hatte die Klassen Particle() und RectParticle() in eine separate Datei ausgelagert und daher endlich verstanden, welche Probleme seit dem letzten Update mit dem import-Befehl im Py5-Imported Mode nun behoben seien. Das mir die Probleme bisher nicht unterkamen, lag daran, daß ich nur Bibliotheken/Module importiert hatte, die nicht auf Py5 basierten und daher auch nicht den JPype-Übersetzer aufrufen mußten. Meine beiden Partikel-Klassen waren aber Py5-Klassen und mußten daher JPype passieren. Nur: Woher sollte Thonny das wissen?

Die Lösung fand ich in der Py5-Dokmentation: Der Datei muß als »magischer Kommentar« die Zeile

# PY5 IMPORTED MODE CODE

vorangestellt werden (es muß nicht unbedingt die erste Kommentarzeile sein, aber sie sollte vor den eigentlichen Codezeilen stehen). Und dieser »magische Kommentar« ist unabhängig von der Groß- oder Kleinschreibung: # py5 imported mode code funktioniert ebenfalls, auch # py5 Imported Mode code bringt keine Probleme. Ich empfehle aber zur besseren Hervorhebung und um die Zeile von den gewöhnlichen Kommentaren zu unterscheiden, die Großschreibung.

Der zweite Stolperstein war ein seltsamer – denn darüber stolpern vermutlich nur diejenigen, die Processings PDE gewohnt sind. Dieser war es nämlich völlig egal, welcher Tab mit welcher Datei gerade offen war (den Fokus hatte), der Run-Befehl startete immmer die Datei mit dem jeweiligen Hauptprogramm.

Hier zeigt Thonny im Py5-Mode ein anderes Verhalten. Hat man zum Beispiel den Tab mit den Klassen-Definitionen offen, dann versucht der Interpreter, diese Datei zu starten. Und da er durch das »magische Kommando« # PY5 IMPORTED MODE CODE weiß, das dies ein Py5-Programm ist, startet er ohne eine Fehlermeldung ein »leeres« Py5-Programm mit dem Default-Fensterchen (siehe Screenshot).

Eigentlich ist das ein logisches Verhalten, aber glaubt mir, ich habe lange geflucht und den Fehler bei mir und in meinem Code gesucht, bis ich darauf gekommen bin.

Nachdem ich nun diese Hürden überwunden hatte, konnte ich mich endlich an meinem Partikelsystem erfreuen. Es besitzt einen Emitter, der zufällig entweder Scheiben oder Rechtecke (genauer: Quardrate) ausstößt, Diese fallen nach unten, und verblassen dabei, je länger sie leben (im System sind). Ist ihre Lebenszeit abgelaufen, werden sie aus dem System entfernt. Dafür habe ich – in Anlehnung an Daniel Shiffmans Sketch, eine Klasse Particle() entworfen:

class Particle():
    
    def __init__(self, _x, _y):
        self.loc = Py5Vector(_x, _y)
        self.acc = Py5Vector(0, 0.05)
        self.vel = Py5Vector(random(-1.0, 1.0), random(-2.0, 0.0))
        self.col = random_choice(codingtrain)
        self.lifespan = 255.0
        self.d = 20
        
    def run(self):
        self.update()
        self.show()
        
    def update(self):
        self.vel += self.acc
        self.loc += self.vel
        self.lifespan -= random(0.5, 1.0)*2
        
    def show(self):
        stroke(0, self.lifespan)
        fill(self.col[0], self.col[1], self.col[2], self.lifespan)
        circle(self.loc.x, self.loc.y, self.d)
        
    def is_not_alive(self):
        if self.lifespan <= 0:
            return True
        else:
            return False

Dabei habe ich dann den dritten Stolperstein überwinden müssen: is_dead ist in Py5 ein reserviertes Wort, daher habe ich die Methode is_not_alive() genannt.

Die Klasse RectParticle() erbt von Particle. Daher waren im Constructor nur der rect_mode(CENTER) (wird für die Rotation der Quadrate gebraucht) und der Rotationswinkel zusätzlich nötig:

class RectParticle(Particle):
    
    def __init__(self, _x, _y):
        Particle.__init__(self, _x, _y)
        rect_mode(CENTER)
        self.rota = PI/3

Und die Methode show() mußte – damit die Quadrate rotieren – komplett überschrieben werden. Hier kam die von mir heiß geliebte (weil in meinem Augen »pythonischere«) Sprachkonstruktion mit dem with-Statement zum Einsatz:

    def show(self):
        stroke(0, self.lifespan)
        fill(self.col[0], self.col[1], self.col[2], self.lifespan)
        with push_matrix():
            translate(self.loc.x, self.loc.y)
            rotate(self.rota)
            rect(0, 0, 20, 20)
        self.rota += random(0.02, .10)

Als zusätzliche Reminiszenz an Daniel Shiffman und seinem Coding Train habe ich die Partikel noch mit der Coding Train Farbpalette eingefärbt.

Nun zum kompletten Programm, damit Ihr auch alles nachlesen, nachvollziehen, nachprogrammieren und weiterentwickeln könnt. Das Hauptprogramm (particles01.py) ist – dank der Partikelklassen – von erfrischender Kürze:

from particles import Particle, RectParticle

WIDTH, HEIGHT = 500, 500
START_X, START_Y = 250, 70

particles = []

def setup():
    size(WIDTH, HEIGHT)
    # window_move(1400, 30)
    window_title("Partikelsystem 1")
    
def draw():
    background(49, 197, 244)   # Hellblau
    change = random(10)
    if change <= 5:
        particles.append(Particle(START_X, START_Y))
    else:
        particles.append(RectParticle(START_X, START_Y))
    for i in range(len(particles) - 1, 0, -1):
        particles[i].run()
        if particles[i].is_not_alive():
            particles.pop(i)

Die (auskommentierte) Zeile window_move(1400, 30) ist ein Hack, der das Ausgabefenster auf meinen zweiten Monitor positioniert. Ihr solltet sie daher nur verwenden, wenn Ihr ebenfalls einen zweiten Bildschirm besitzt und dessen Pixelkoordinaten kennt.

Etwas fetter ist dann schon die Datei particles.py mit den beiden Klassen:

# PY5 IMPORTED MODE CODE

# Coding Train Farbpalette
codingtrain = [(240, 80, 37), (248, 158, 80), (248, 239, 34) , (240, 99, 164),
               (146, 82, 161), (129, 122, 198), (98, 199, 119)]

class Particle():
    
    def __init__(self, _x, _y):
        self.loc = Py5Vector(_x, _y)
        self.acc = Py5Vector(0, 0.05)
        self.vel = Py5Vector(random(-1.0, 1.0), random(-2.0, 0.0))
        self.col = random_choice(codingtrain)
        self.lifespan = 255.0
        self.d = 20
        
    def run(self):
        self.update()
        self.show()
        
    def update(self):
        self.vel += self.acc
        self.loc += self.vel
        self.lifespan -= random(0.5, 1.0)*2
        
    def show(self):
        stroke(0, self.lifespan)
        fill(self.col[0], self.col[1], self.col[2], self.lifespan)
        circle(self.loc.x, self.loc.y, self.d)
        
    def is_not_alive(self):
        if self.lifespan <= 0:
            return True
        else:
            return False
        
class RectParticle(Particle):
    
    def __init__(self, _x, _y):
        Particle.__init__(self, _x, _y)
        rect_mode(CENTER)
        self.rota = PI/3
    
    def show(self):
        stroke(0, self.lifespan)
        fill(self.col[0], self.col[1], self.col[2], self.lifespan)
        with push_matrix():
            translate(self.loc.x, self.loc.y)
            rotate(self.rota)
            rect(0, 0, 20, 20)
        self.rota += random(0.02, .10)

Auf meinem betagten MacBook Pro (von 2012) läuft der Sketch gerade noch in erträglicher Geschwindigkeit. Wer über potentere Hardware verfügt, kann ja mal versuchen, den lifespan der Partikel zu verlängern.

Natürlich sind die Dateien auch in meinem GitHub-Repositorium abgelegt (particles01.py und particles.py). Ich möchte die Experimente gerne noch ein wenig fortführen. Also seid auf weitere Beiträge zu Py5 gespannt. Still digging!