Ein Partikelsystem mit Proceso und PyScript

Proceso
Python
PyScript
Processing
Nature of Code
Creative Coding
Autor:in

Jörg Kantel

Veröffentlichungsdatum

21. Juli 2025

Das kleine Planetensystem mit den rotierenden Kisten, das ich vor etwa einer Woche mit Proceso und PyScript realisierte, erinnerte mich an das Partikelsystem in zwei Stufen, an dem ich mich vor mehr als einem Jahr in microStudio mit Brython versucht hatte. Vor allem, da in der zweiten Stufe die Partikel teilweise ebenfalls rotierende Quadrate waren.

Ähnliches müßte man doch auch in Proceso und PyScript programmieren können, dachte ich mir, vor allem da Nick McIntyre, der Kopf hinter Proceso, verspricht, daß die Vektor-Klasse von Proceso »lovingly borrowed« von Py5, dem Python3-Port von Processing sei. Also habe ich als ersten Schritt erst einmal eine Version mit runden Partikeln erstellt, die noch nicht rotieren.

from proceso import Sketch
from random import uniform, choice

WIDTH, HEIGHT = 640, 360
START_X, START_Y = 450, 70

p5 = Sketch()

colors = [(8, 247, 254), (254, 83, 187), (245, 211, 0),
          (0, 255, 65), (250, 25, 25), (148, 103, 89)]

particles = []

def preload():
    global bg1
    bg1 = p5.load_image("assets/bg1.jpg")  # Load the image

def setup():
    p5.create_canvas(WIDTH, HEIGHT)

def draw():
    p5.image(bg1, 0, 0)
    update_particles()
    for particle in particles:
       particle.display()
    

def update_particles():
    particle = Particle(START_X, START_Y)
    particles.append(particle)
    for particle in reversed(particles):
       if particle.done:
          particles.remove(particle)
    for particle in particles:
       particle.update()
          

class Particle():
  
  def __init__(self, _x, _y):
    self.loc = p5.Vector(_x, _y)
    self.acc = p5.Vector(0, 0.005)
    self.vel = p5.Vector(uniform(-1.0, 1.0), uniform(2.0, -0.5))
    self.col = choice(colors)
    self.lifespan = 255.0
    self.d = uniform(5, 15)
    self.done = False
    
  def update(self):
    self.vel += self.acc
    self.loc += self.vel
    self.lifespan -= uniform(0.5, 1.0)*2.0
    self.alpha = self.lifespan
    if self.lifespan <= 10:
      self.done = True
      
  def display(self):
    p5.fill(self.col[0], self.col[1], self.col[2], self.alpha)
    p5.circle(self.loc.x, self.loc.y, self.d)

p5.run_sketch(preload=preload, setup=setup, draw=draw)

Kern ist die Klasse Particle(), die neben dem Konstruktor die beiden Methoden update() und display() besitzt. Im Hauptprogramm wird innerhalb der draw()-Funktion dann mit update_particles() bei jedem Durchlaufes ein neues Partikel erzeugt und an die Liste particles[] angehängt. Jedes Partikel besitzt nur eine bestimmte Lebensdauer (lifespan). Geht diese zuende, wird das Partikel mit particles.remove(particle) aus der Liste enfernt. Damit es dabei nicht zu einem Schhluckauf beim Durcharbeiten der Liste kommt, wird diese mit

    for particle in reversed(particles):
       if particle.done:
          particles.remove(particle)

rückwärts durchlaufen.

Wie P5.js, die JavaScript-Version von Processing, besitzt auch Proceso eine preload()-Funktion, in der in diesem Skript das Hintergrundbild geladen wird. Die preload()-Funktion sorgt dafür, daß das Skript erst dann setup() aufruft, wenn alle dort zu ladenden Assets tatsächlich geladen sind. Dadurch ist leider die Variable bg1 lokal in preload() und muß explizit als global deklariert werden, damit sie in draw() auch verwendet werden kann.

Ich mag globale Deklarationen ja bekanntlich nicht, aber ich glaube in diesem Fall ist das zu verschmerzen.

Die verwendete, neonbunte Palette ist »MPL Cyberpunk«, die ich hier erstmalig vorgestellt hatte. Sie steht unter der MIT-Lizenz und kann daher auch von Euch verwendet werden.

Nun aber zum zweiten Sketch mit den rotierenden Quadraten. Die Python vom Hintergrundbild des ersten Sketches ist so stolz auf ihre Schöpfung, daß sie ihren Freund, das weiße Kaninchen mit der großen Uhr, mitgenommen hat, damit dieses gebührend das Schauspiel bewundert.

Im Skript selber mußten nur wenige Änderungen vorgenommen werden:

from proceso import Sketch
from random import uniform, choice

WIDTH, HEIGHT = 640, 360
START_X, START_Y = 450, 70

p5 = Sketch()

colors = [(8, 247, 254), (254, 83, 187), (245, 211, 0),
          (0, 255, 65), (250, 25, 25), (148, 103, 89)]

particles = []

def preload():
    global bg2
    bg2 = p5.load_image("assets/bg2.jpg")  # Load the image

def setup():
    p5.create_canvas(WIDTH, HEIGHT)
    p5.rect_mode(p5.CENTER)

def draw():
    p5.image(bg2, 0, 0)
    update_particles()
    for particle in particles:
       particle.display()
    

def update_particles():
    particle = Particle(START_X, START_Y)
    particles.append(particle)
    for particle in reversed(particles):
       if particle.done:
          particles.remove(particle)
    for particle in particles:
       particle.update()
          

class Particle():
  
  def __init__(self, _x, _y):
    self.loc = p5.Vector(_x, _y)
    self.acc = p5.Vector(0, 0.005)
    self.vel = p5.Vector(uniform(-1.0, 1.0), uniform(2.0, -0.5))
    self.col = choice(colors)
    self.angle = 0.0
    self.delta_angle = uniform(-.1, .1)
    self.lifespan = 255.0
    self.d = uniform(5, 15)
    self.done = False
    
  def update(self):
    self.vel += self.acc
    self.loc += self.vel
    self.angle += self.delta_angle
    self.lifespan -= uniform(0.5, 1.0)*2.0
    self.alpha = self.lifespan
    if self.lifespan <= 10:
      self.done = True
      
  def display(self):
    p5.fill(self.col[0], self.col[1], self.col[2], self.alpha)
    p5.push()
    p5.translate(self.loc.x, self.loc.y)
    p5.rotate(self.angle)
    p5.rect(0, 0, self.d, self.d)
    p5.pop()

p5.run_sketch(preload=preload, setup=setup, draw=draw)

Damit die Boxen um ihren eigenen Mittelpunkt rotieren habe ich ihnen im setup() den rect_mode(CENTER) verpasst. Die Rotation selber wird in der Methode display() der Klasse Particle() durchgeführt,

def display(self):
    p5.fill(self.col[0], self.col[1], self.col[2], self.alpha)
    p5.push()
    p5.translate(self.loc.x, self.loc.y)
    p5.rotate(self.angle)
    p5.rect(0, 0, self.d, self.d)
    p5.pop()

die erst einmal mit translate() den Ursprung des Koordinatensystems in den Mittelpunkt des Quadrats legt und dann die Rotation um die eigenen Achse vornimmt. Natürlich muß diese Koordinaten-Transformation mit push() und pop() geklammert werden, damit nach jeder Rotation das Koordinatensystem wieder auf seinen ursprünglichen Zustand zurückgesetzt wird.

Jedes Partikel hat seinen eigenen Rotationswinkel, der mit self.delta_angle = uniform(-.1, .1) im Konstruktor festgelegt wird. Das sind eigentlich alle Änderungen gegenüber dem ersten Sketch.

Verwendete und weiterführende Quellen:

Die Programmierung mit Proceso und PyScript macht vor allem deshalb Spaß, weil man die Ergebnisse wie hier ziemlich schmerzfrei in die eigenen Seiten einbinden kann. Dies wird daher mit Sicherheit nicht das letzte Experiment sein, das ich mit Proceso durchführe. Still digging!


Hintergrundbilder: Planetenbeobachter Stage 1 und Stage 2, erstellt mit OpenArt.ai. Prompt: »Colored Franco-Belgian comic style: A green python with glasses and a rabbit standing side by side on a distant planet, observing the night sky. The rabbit wears a dark blue vest and holds a large pocket watch. A few planets with their moons and gray clouds can be seen in the sky. A planetary base and two spaceships stand in the landscape«. Modell: Flux (Pro), Style: None.