Was du hier lernst
- Farbverläufe berechnen
- map und lambda
- Klassen mit privaten Attributen
- Bewegung und Abprallen
- Refactoring: von Funktionen zur Klasse
- Animationsschleife
So funktioniert der Bildschirmschoner
Bildschirmschoner stammen aus der Zeit der Röhrenmonitore: Ein Bild, das stundenlang unverändert blieb, konnte sich dauerhaft in die Leuchtschicht einbrennen. Bewegte Bilder verhinderten das. Heute braucht kein Bildschirm mehr diesen Schutz – schön anzusehen sind die Animationen trotzdem. Dieser Klassiker lässt drei farbige Balken auf und ab gleiten.
1. Ein Balken aus vielen Linien
Ein Balken ist 50 Pixel hoch und besteht aus 50 waagerechten Linien, jede einen Pixel dick. Jede Linie bekommt eine etwas andere Helligkeit – von oben nach unten erst heller, dann wieder dunkler. Genau dieser Verlauf lässt den Balken rund wirken, wie eine Röhre, auf die Licht fällt.
factor = int(math.floor(255.0 / self.__height))
for i in range(0, self.__height):
if i < self.__height / 2.0:
brightness = i * factor
else:
brightness = 255 - i * factor
factor ist die Schrittweite: 255 geteilt durch 50 ergibt abgerundet 5. In der oberen Hälfte wächst die Helligkeit also um 5 pro Linie, in der unteren Hälfte sinkt sie wieder.
2. Farbe mal Helligkeit
Welche Farbe ein Balken hat, legt ein Tupel mit drei Anteilen fest: (0, 0, 1) heißt nur Blau, (0, 1, 0) nur Grün, (1, 0, 0) nur Rot. Für jede Linie wird jeder Anteil mit der Helligkeit multipliziert:
color = tuple(map(lambda x: x * brightness, self.__color))
lambda x: x * brightness ist eine kleine Funktion ohne Namen. map wendet sie auf jedes Element des Tupels an, tuple(...) sammelt die Ergebnisse. Aus (0, 0, 1) wird bei Helligkeit 100 also (0, 0, 100) – ein mittleres Blau. Mischfarben entstehen von allein: (1, 1, 0) wird zu Gelb.
Warum die Balken nie ganz hell werden
Rechne nach: Die obere Hälfte endet bei Linie 24 mit 24 × 5 = 120. Die untere Hälfte beginnt bei Linie 25 mit 255 − 25 × 5 = 130. Heller als 130 von 255 wird ein Balken also nie, und in der Mitte springt die Helligkeit von 120 auf 130. Das fällt kaum auf, erklärt aber, warum die Balken eher gedämpft leuchten. Unter »Probier mal« findest du eine Rechnung, die bis 255 geht.
3. Hin und her
Jeder Balken merkt sich seine Position yPos und seine Richtung direction: +1 heißt nach unten, −1 nach oben. Bei jedem Bild wandert der Balken um einen Pixel. Stößt er an den Rand, kehrt sich die Richtung um:
def move(self):
self.__yPos += self.__direction
if self.__yPos + barHeight > self.__windowHeight or self.__yPos < 0:
self.__direction *= -1
self.__drawBar()
Die Multiplikation mit −1 ist ein beliebter Trick: Aus +1 wird −1 und umgekehrt, ohne dass das Programm fragen muss, in welche Richtung der Balken gerade fährt.
4. Aufräumen mit einer Klasse
Im Buch entsteht das Programm in zwei Schritten. Die erste Fassung kommt ohne Klasse aus: Für jeden Balken gibt es zwei Variablen (y1, dir1, y2, dir2 …), und die Hauptschleife ruft für jeden Balken Zeichnen und Bewegen einzeln auf. Das funktioniert, wird aber mit jedem weiteren Balken unübersichtlicher.
Die zweite Fassung bündelt alles, was zu einem Balken gehört, in der Klasse Bar: Position, Höhe, Breite, Farbe und Richtung. Nach außen bietet sie nur move() an, das Zeichnen erledigt die private Methode __drawBar(). Die Hauptschleife schrumpft auf drei Zeilen:
bar1.move()
bar2.move()
bar3.move()
Diesen Umbau nennt man Refactoring: Das Programm tut danach genau dasselbe, ist aber leichter zu lesen und zu erweitern. Ein vierter Balken ist jetzt eine einzige Zeile.
Eine kleine Unsauberkeit
move() prüft den unteren Rand mit der globalen Variable barHeight statt mit der eigenen Höhe self.__height. Das geht gut, solange alle Balken 50 Pixel hoch sind. Ein Balken mit anderer Höhe würde falsch abprallen – die Variante »Unterschiedlich dicke Balken« zeigt, wie es richtig geht.
5. Die Hauptschleife und der Ein-Sekunden-Trick
Ein Bildschirmschoner soll bei der ersten Berührung verschwinden. handleEvents beendet die Schleife deshalb bei jedem Tastendruck oder Klick – aber erst nach der ersten Sekunde:
if time() - start > 1000:
if event.type == KEYDOWN:
running = False
Der Grund: Wer den Schoner von Hand startet, bewegt dabei meist noch die Maus oder hat gerade eine Taste gedrückt. Ohne die Wartezeit würde das Programm sofort wieder enden. await screen.frame(maxfps) zeigt jedes Bild an und begrenzt die Schleife auf 100 Durchläufe pro Sekunde.
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.
-
01Volle Leuchtkraft
Im Original werden die Balken nie richtig hell: Die Helligkeit steigt nur bis 120, springt in der Mitte auf 130 und fällt dann wieder. Diese Rechnung misst stattdessen den Abstand zur Balkenmitte – 0 in der Mitte, 1 am Rand – und macht daraus einen gleichmäßigen Verlauf von 0 bis 255.
- if i < self.__height / 2.0: - # hier wird die Helligkeit erhöht - brightness = i * factor - else: - # hier wird die Helligkeit reduziert - brightness = 255 - i * factor + # Abstand zur Balkenmitte: 0 in der Mitte, 1 am Rand + distance = abs(i - self.__height / 2.0) / (self.__height / 2.0) + brightness = int(255 * (1 - distance)) -
02Sechs Balken in Mischfarben
Eine Farbe ist ein Tupel aus drei Anteilen für Rot, Grün und Blau.
(1, 1, 0)mischt Rot und Grün zu Gelb,(0, 1, 1)ergibt Türkis,(1, 0, 1)Magenta. Dank der Klasse sind drei neue Balken je eine Zeile – und ein weiterer Aufruf vonmove().- bar3 = Bar(400, barHeight, winWidth, winHeight, (1, 0, 0)) + bar3 = Bar(400, barHeight, winWidth, winHeight, (1, 0, 0)) + bar4 = Bar(100, barHeight, winWidth, winHeight, (1, 1, 0)) + bar5 = Bar(300, barHeight, winWidth, winHeight, (0, 1, 1)) + bar6 = Bar(500, barHeight, winWidth, winHeight, (1, 0, 1)) - bar3.move() + bar3.move() + bar4.move() + bar5.move() + bar6.move() -
03Unterschiedlich dicke Balken
Hier bekommt jeder Balken eine eigene Höhe. Damit das funktioniert, muss
move()die Höhe des Balkens selbst verwenden statt der globalen VariablebarHeight– sonst würde der dicke grüne Balken 70 Pixel tief unter den unteren Rand rutschen, bevor er umkehrt.- if self.__yPos + barHeight > self.__windowHeight or self.__yPos < 0: + if self.__yPos + self.__height > self.__windowHeight or self.__yPos < 0: - bar2 = Bar(200, barHeight, winWidth, winHeight, (0, 1, 0)) + bar2 = Bar(200, 120, winWidth, winHeight, (0, 1, 0)) - bar3 = Bar(400, barHeight, winWidth, winHeight, (1, 0, 0)) + bar3 = Bar(400, 20, winWidth, winHeight, (1, 0, 0)) -
04Durchscheinende Balken
Ein viertes Element im Farbtupel ist die Deckkraft von 0 (unsichtbar) bis 255 (deckend). Mit 160 werden die Balken halb durchsichtig: Wo sie sich überkreuzen, mischen sich ihre Farben.
- color = tuple(map(lambda x: x * brightness, self.__color)) + color = tuple(map(lambda x: x * brightness, self.__color)) + (160,) -
05Mehr Tempo
Jeder Balken wandert pro Bild um
directionPixel. Mit 3 statt 1 sind die Balken dreimal so schnell. Weil das Abprallen nur prüft, ob der Rand schon überschritten ist, ragen sie dabei für ein Bild ein kleines Stück über den Rand hinaus.- self.__direction = 1 + self.__direction = 3
Vom Buch in den Browser
Im Buch läuft der Bildschirmschoner mit pygame im Vollbildmodus. Hier zeichnet er auf die Leinwand im Browser; mit dem Vollbild-Knopf in der Werkzeugleiste bekommst du trotzdem den ganzen Bildschirm. Die Klasse Bar und die Hauptschleife sind aus der zweiten Fassung (colorBars_V2.py) übernommen.
| Im Buch (pygame) | Hier (c4f) |
|---|---|
pygame.display.set_mode((800, 600), pygame.FULLSCREEN) |
Screen(800, 600) und der Vollbild-Knopf |
pygame.draw.line(screen, farbe, start, ende) |
screen.line(start, ende, farbe) |
clock.tick(maxfps) + pygame.display.flip() |
await screen.frame(maxfps) |
pygame.time.get_ticks() |
time() aus c4f |
pygame.event.poll() – ein Ereignis pro Durchlauf |
screen.events() – alle Ereignisse seit dem letzten Bild |
map in Python 3. In Python 2 lieferte map direkt eine Liste. In Python 3 liefert es einen sogenannten Iterator, der die Werte erst berechnet, wenn jemand sie abruft. Deshalb steht tuple(...) darum herum: So entsteht wie früher eine fertige Farbe aus drei Zahlen.
Beenden. Das Original endete auch bei jeder Mausbewegung. Im Browser würde dann schon das Überfahren der Seite mit dem Mauszeiger den Schoner beenden. Deshalb reagiert er hier nur auf Tastendrücke und Klicks – nach der ersten Sekunde, wie im Buch. Außerdem gibt es den Knopf »Stopp«.
Die Klasse verwendet – wie im Original – die globalen Variablen screen und barHeight. Für ein kleines Programm ist das in Ordnung, sauberer wäre es, beides beim Anlegen eines Balkens zu übergeben. Die erste Fassung ohne Klasse (colorBars_V1.py) kannst du unten ebenfalls aufklappen.
Original aus dem Buch ansehen colorBars_V2.py · Python 2
import pygame, math
start = pygame.time.get_ticks()
class Bar(object):
def __init__(self, yStartPos, barHeight, barWidth, windowHeight, colorChoice):
self.__yPos = yStartPos
self.__height = barHeight
self.__width = barWidth
self.__windowHeight = windowHeight
self.__color = colorChoice
self.__direction = 1
# berechnet einen schoenen Farbverlauf fuer die angegebenen Farbwerte
# jede Farbkomponente wird hierbei proportional zum Farbwert eingerechnet
def __drawBar(self):
factor = int(math.floor(255.0 / self.__height))
for i in range(0, self.__height):
if i < self.__height/2.0:
# hier wird die Helligkeit erhoeht
brightness = i*factor
else:
# hier wird die Helligkeit reduziert
brightness = 255-i*factor
color = map(lambda x: x*brightness, self.__color)
pygame.draw.line(screen, color, (0, self.__yPos+i), (self.__width, self.__yPos+i))
# prueft ob ein Rand erreicht worden ist und passt die Bewegungsrichtung an
def move(self):
self.__yPos += self.__direction
if self.__yPos + barHeight > self.__windowHeight or self.__yPos < 0:
self.__direction *= -1
self.__drawBar()
def handleEvents(running):
event = pygame.event.poll()
if pygame.time.get_ticks() - start > 1000:
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
running = False
elif event.type == pygame.MOUSEMOTION:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
running = False
return running
# here are the start positions and directions for each bar
barHeight = 50
winHeight = 600
winWidth = 800
bar1 = Bar( 0, barHeight, winWidth, winHeight, (0, 0, 1))
bar2 = Bar(200, barHeight, winWidth, winHeight, (0, 1, 0))
bar3 = Bar(400, barHeight, winWidth, winHeight, (1, 0, 0))
running = True
screen = pygame.display.set_mode((winWidth, winHeight), pygame.FULLSCREEN);
clock = pygame.time.Clock()
maxfps = 100
while running:
clock.tick(maxfps)
running = handleEvents(running)
screen.fill((0, 0, 0))
bar1.move()
bar2.move()
bar3.move()
pygame.display.flip()
Original aus dem Buch ansehen colorBars_V1.py · Python 2
import pygame, math
start = pygame.time.get_ticks()
# berechnet einen schoenen Farbverlauf fuer die angegebenen Farbwerte
# jede Farbkomponente wird hierbei proportional zum Farbwert eingerechnet
def drawBar(yStartPos, barHeight, barWidth, colorChoice):
factor = int(math.floor(255.0 / barHeight))
for i in range(0, barHeight):
if i < barHeight/2.0:
# hier wird die Helligkeit erhoeht
brightness = i*factor
else:
# hier wird die Helligkeit reduziert
brightness = 255-i*factor
color = map(lambda x: x*brightness, colorChoice)
pygame.draw.line(screen, color, (0, yStartPos+i), (barWidth, yStartPos+i))
# this checks if the border is reached and changes the direction if neccessary
def moveAndCheckDirection(direction, yPosition, barHeight, windowHeight):
yPosition += direction
if yPosition + barHeight > windowHeight or yPosition < 0:
direction *= -1
return direction, yPosition
def handleEvents(running):
event = pygame.event.poll()
if pygame.time.get_ticks() - start > 1000:
print start
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
running = False
elif event.type == pygame.MOUSEMOTION:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
running = False
return running
# here are the start positions and directions for each bar
y1 = 0
dir1 = 1
y2 = 200
dir2 = 1
y3 = 400
dir3 = 1
running = True
barHeight = 50
winHeight = 600
winWidth = 800
screen = pygame.display.set_mode((winWidth, winHeight), pygame.FULLSCREEN);
clock = pygame.time.Clock()
maxfps = 100
while running:
clock.tick(maxfps)
running = handleEvents(running)
screen.fill((0, 0, 0))
drawBar(y1, barHeight, winWidth, (0, 0, 1))
drawBar(y2, barHeight, winWidth, (0, 1, 0))
drawBar(y3, barHeight, winWidth, (1, 0, 0))
dir1, y1 = moveAndCheckDirection(dir1, y1, barHeight, winHeight)
dir2, y2 = moveAndCheckDirection(dir2, y2, barHeight, winHeight)
dir3, y3 = moveAndCheckDirection(dir3, y3, barHeight, winHeight)
pygame.display.flip()