Rotierende Kreise

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
rogerb
User
Beiträge: 232
Registriert: Dienstag 26. November 2019, 23:24

Sonntag 27. Juni 2021, 16:35

Inspiriert durch diese Video
https://www.youtube.com/watch?v=pNe6fsaCVtI
gepostet von k-net,
habe ich mal einen Schritt für Schritt Nachbau erstellt.
(Es wird immer nur der Unterschied zum vorherigen Schritt angezeigt)
SCHRITT 1
Zunächst fängt man mit einem Basis pygame an:

Code: Alles auswählen

import sys
import pygame


WIDTH = 800
HEIGHT = 800


def main():
    screen = init()
    game_loop(screen)


def init():
    """Initialisierung von pygame und Anlegen des screen buffers"""
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    return screen


def game_loop(screen):
    """Die game loop in der der zeitliche Ablauf kontrolliert wird"""
    while True:
        process_events()
        pygame.display.update()
        pygame.time.Clock().tick(60)


def process_events():
    """Verarbeitung von Ereignissen"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()


if __name__ == "__main__":
    main()
SCHRITT 2
Als nächstes wird das Konzept der Zeit hinzugefügt:

Code: Alles auswählen

def game_loop(screen):
    """Die game loop in der der zeitliche Ablauf kontrolliert wird"""
    t = 0  # Die Zeit beginnt
    while True:
        process_events()
        pygame.display.update()
        pygame.time.Clock().tick(60)

        # t wird 60 mal pro Sekunde um 0.02 erhöht
        # erhöht/veringert man dies Zahl, läuft die Zeit schneller/langsamer
        t += 0.02
SCHRITT 3
Das Zeichnen findet in einer eigenen Funktion statt:

Code: Alles auswählen


BLACK = (0, 0, 0)

def game_loop(screen):
    """Die game loop in der der zeitliche Ablauf kontrolliert wird"""
    t = 0  # Die Zeit beginnt
    while True:
        process_events()
        draw(screen, t)
        pygame.display.update()
        pygame.time.Clock().tick(60)

        # t wird 60 mal pro Sekunde um 0.02 erhöht
        # erhöht/veringert man dies Zahl, läuft die Zeit schneller/langsamer
        t += 0.02

def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
SCHRITT 4
Um einen roten Kreis in den Hintergrund zubekommen geht es so weiter:

Code: Alles auswählen

CENTERX = WIDTH // 2
CENTERY = WIDTH // 2
OUTERRADIUS = min(WIDTH, HEIGHT) * 0.45
BLACK = (0, 0, 0)
RED = (255, 0, 0)

def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)


def draw_background_circle(screen):
    """Roter Kreis als Hintergrund"""
    pygame.draw.circle(screen, RED, (CENTERX, CENTERY), OUTERRADIUS)
SCHRITT 5
Jetzt kommt erstmal nur eine Linie dazu:

Code: Alles auswählen

import math

def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)
    draw_angled_line(screen, 0)

def draw_angled_line(screen, angle):
    """Zeichnet eine Linie durch den Fenstermittelpunkt unter einem bestimmten Winkel"""
    pygame.draw.line(
        screen,
        BLACK,
        (
            CENTERX - math.cos(angle) * OUTERRADIUS,
            CENTERY - math.sin(angle) * OUTERRADIUS,
        ),
        (
            CENTERX + math.cos(angle) * OUTERRADIUS,
            CENTERY + math.sin(angle) * OUTERRADIUS,
        ),
        2,
    )
SCHRITT 6
Mehrere Linien, alle um den gleichen Winkel zueinander verdreht:

Code: Alles auswählen

COUNT = 8

def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)
    draw_lines(screen)

def draw_lines(screen):
    """Zeichnet mehrere Linien mit gleichem Verdrehungwinkel zueinander"""
    for index in range(COUNT):
        draw_angled_line(screen, math.pi / COUNT * index)
SCHRITT 7
Ein einzelner Kreis:

Code: Alles auswählen

RADIUS = 20
WHITE = (255, 255, 255)

def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)
    draw_lines(screen)
    draw_circle(screen, 0, t)

def draw_circle(screen, angle, t):
    """Zeichnet einen weißen kleinen Kreis in einem bestimmten Winkel"""
    pygame.draw.circle(
        screen,
        WHITE,
        (
            CENTERX + math.cos(angle) * (OUTERRADIUS - RADIUS), # um RADIUS nach innen versetzt
            CENTERY + math.sin(angle) * (OUTERRADIUS - RADIUS),
        ),
        RADIUS,
    )
SCHRITT 8
Jetzt kommt die Animation über die Zeit:

Code: Alles auswählen

def draw_circle(screen, angle, t):
    """Zeichnet einen weißen kleinen Kreis in einem bestimmten Winkel"""
    pygame.draw.circle(
        screen,
        WHITE,
        (
            CENTERX + math.sin(t) * math.cos(angle) * (OUTERRADIUS - RADIUS), # um RADIUS nach innen versetzt
            CENTERY + math.sin(t) * math.sin(angle) * (OUTERRADIUS - RADIUS),
        ),
        RADIUS,
    )
SCHRITT 9
Die Möglichkeit zur Phasenverschiebung wird geschaffen:

Code: Alles auswählen

def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)
    draw_lines(screen)
    draw_circle(screen, 0, t)

def draw_circle(screen, angle, t, shift):
    """Zeichnet einen weißen kleinen Kreis in einem bestimmten Winkel"""
    pygame.draw.circle(
        screen,
        WHITE,
        (
            CENTERX + math.sin(t + shift) * math.cos(angle) * (OUTERRADIUS - RADIUS),
            CENTERY + math.sin(t + shift) * math.sin(angle) * (OUTERRADIUS - RADIUS),
        ),
        RADIUS,
    )
SCHRITT 10
Und schließlich sollen mehrere Kreise gezeichnet werden:

Code: Alles auswählen

def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)
    draw_lines(screen)
    draw_circles(screen, t)

def draw_circles(screen, t):
    for index in range(COUNT):
        draw_circle(screen, math.pi / COUNT * index, t, math.pi / COUNT * index)

Alles zusammen:

Code: Alles auswählen

import sys
import math
import pygame


WIDTH = 800
HEIGHT = 800
CENTERX = WIDTH // 2
CENTERY = WIDTH // 2
OUTERRADIUS = min(WIDTH, HEIGHT) * 0.45
RADIUS = 20
COUNT = 8
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)


def main():
    screen = init()
    game_loop(screen)


def init():
    """Initialisierung von pygame und Anlegen des screen buffers"""
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    return screen


def game_loop(screen):
    """Die game loop in der der zeitliche Ablauf kontrolliert wird"""
    t = 0  # Die Zeit beginnt
    while True:
        process_events()
        draw(screen, t)
        pygame.display.update()
        pygame.time.Clock().tick(60)

        # t wird 60 mal pro Sekunde um 0.02 erhöht
        # erhöht/veringert man dies Zahl, läuft die Zeit schneller/langsamer
        t += 0.02


def process_events():
    """Verarbeitung von Ereignissen"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()


def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)
    draw_lines(screen)
    draw_circles(screen, t)


def draw_background_circle(screen):
    """Roter Kreis als Hintergrund"""
    pygame.draw.circle(screen, RED, (CENTERX, CENTERY), OUTERRADIUS)


def draw_circles(screen, t):
    for index in range(COUNT):
        draw_circle(screen, math.pi / COUNT * index, t, math.pi / COUNT * index)


def draw_circle(screen, angle, t, shift):
    """Zeichnet einen weißen kleinen Kreis in einem bestimmten Winkel"""
    pygame.draw.circle(
        screen,
        WHITE,
        (
            CENTERX + math.sin(t + shift) * math.cos(angle) * (OUTERRADIUS - RADIUS),
            CENTERY + math.sin(t + shift) * math.sin(angle) * (OUTERRADIUS - RADIUS),
        ),
        RADIUS,
    )


def draw_lines(screen):
    """Zeichnet mehrere Linien mit gleichem Verdrehungwinkel zueinander"""
    for index in range(COUNT):
        draw_angled_line(screen, math.pi / COUNT * index)


def draw_angled_line(screen, angle):
    """Zeichnet eine Linie durch den Fenstermittelpunkt unter einem bestimmten Winkel"""
    pygame.draw.line(
        screen,
        BLACK,
        (
            CENTERX - math.cos(angle) * OUTERRADIUS,
            CENTERY - math.sin(angle) * OUTERRADIUS,
        ),
        (
            CENTERX + math.cos(angle) * OUTERRADIUS,
            CENTERY + math.sin(angle) * OUTERRADIUS,
        ),
        2,
    )


if __name__ == "__main__":
    main()

Weitere Schritte:
Variieren der Anzahl der Kreise, Animationsgeschwindigkeit, Hintergrundlinien ein/ausschalten, ...
rogerb
User
Beiträge: 232
Registriert: Dienstag 26. November 2019, 23:24

Sonntag 27. Juni 2021, 16:50

Übrigens:

Um diesen Animationseffekt zu erhalten müssen Phasenverschiebung und Rotationswinkel übereinstimmen.

Code: Alles auswählen

def draw_circles(screen, t):
    for index in range(COUNT):
        draw_circle(screen, math.pi / COUNT * index, t, math.pi / COUNT * index)
Oder:

Code: Alles auswählen

def draw_circles(screen, t):
    for index in range(COUNT):
        distortion_factor = 1
        rotation_angle = phase_shift = math.pi / COUNT * index
        draw_circle(screen, rotation_angle, t, phase_shift * distortion_factor)
Verwendet man für distortionfactor andere Werte (..., 1/4, 1/3, 1/2, 1, 2, 3, 4, ...) entstehen unterschiedliche Effekte
__deets__
User
Beiträge: 10055
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 27. Juni 2021, 16:55

Nettes Programm, aber so leider bei mir nicht lauffaehig. Es fehlte der main-guard, und es gab eine Reihe von Typfehlern, weil floats benutzt wurden, pygame aber ein int wollte.

Nachtrag: hast du den __main__-Guard erst nur vergessen, oder ich den nicht kopiert? Die ints waren aber ein Problem bei meinem pygame.

Ich habe mir dann noch erlaubt meine urspruengliche Formel zur Frequenz zu nutzen, und die Zeit, die tatsaechlich verflossen ist, zu messen. Es ist ein klassischer Fehler anzunehmen, dass Warteperioden aequidistant sind. Das Clock.tick ist an sich schon gut, aber sollte es mal nicht rechtzeitig zurueckkehren, dann verliert man die Synchronisation.

Leicht ueberarbeiteter Code:

Code: Alles auswählen

import sys
import math
import pygame
import time

WIDTH = 800
HEIGHT = 800
CENTERX = WIDTH // 2
CENTERY = WIDTH // 2
OUTERRADIUS = int(min(WIDTH, HEIGHT) * 0.45)
RADIUS = 20
COUNT = 8
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
# revolutions of the circle in Hz
F = .3


def main():
    screen = init()
    game_loop(screen)


def init():
    """Initialisierung von pygame und Anlegen des screen buffers"""
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    return screen


def game_loop(screen):
    """Die game loop in der der zeitliche Ablauf kontrolliert wird"""
    clock = pygame.time.Clock()
    while True:
        process_events()
        draw(screen, time.monotonic())
        pygame.display.update()
        clock.tick(60)

        # t wird 60 mal pro Sekunde um 0.02 erhöht
        # erhöht/veringert man dies Zahl, läuft die Zeit schneller/langsamer


def process_events():
    """Verarbeitung von Ereignissen"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()


def draw(screen, t):
    """Alles was gezeichnet werden soll, wird von hier gesteuert"""
    screen.fill(BLACK)
    draw_background_circle(screen)
    draw_lines(screen)
    draw_circles(screen, t)


def draw_background_circle(screen):
    """Roter Kreis als Hintergrund"""
    pygame.draw.circle(screen, RED, (CENTERX, CENTERY), OUTERRADIUS)


def draw_circles(screen, t):
    for index in range(COUNT):
        draw_circle(screen, math.pi / COUNT * index, t, math.pi / COUNT * index)


def draw_circle(screen, angle, t, shift):
    """Zeichnet einen weißen kleinen Kreis in einem bestimmten Winkel"""
    r_i = math.sin(F * math.pi * 2 * t + shift)
    pygame.draw.circle(
        screen,
        WHITE,
        (
            int(CENTERX + r_i * math.cos(angle) * (OUTERRADIUS - RADIUS)),
            int(CENTERY + r_i * math.sin(angle) * (OUTERRADIUS - RADIUS)),
        ),
        RADIUS,
    )


def draw_lines(screen):
    """Zeichnet mehrere Linien mit gleichem Verdrehungwinkel zueinander"""
    for index in range(COUNT):
        draw_angled_line(screen, math.pi / COUNT * index)


def draw_angled_line(screen, angle):
    """Zeichnet eine Linie durch den Fenstermittelpunkt unter einem bestimmten Winkel"""
    pygame.draw.line(
        screen,
        BLACK,
        (
            CENTERX - math.cos(angle) * OUTERRADIUS,
            CENTERY - math.sin(angle) * OUTERRADIUS,
        ),
        (
            CENTERX + math.cos(angle) * OUTERRADIUS,
            CENTERY + math.sin(angle) * OUTERRADIUS,
        ),
        2,
    )

if __name__ == '__main__':
    main()
rogerb
User
Beiträge: 232
Registriert: Dienstag 26. November 2019, 23:24

Sonntag 27. Juni 2021, 17:17

Nachtrag: hast du den __main__-Guard erst nur vergessen, oder ich den nicht kopiert? Die ints waren aber ein Problem bei meinem pygame.
main-guard war von Anfang an drin (Siehe Schritt 1)

Es ist ein klassischer Fehler anzunehmen, dass Warteperioden aequidistant sind
Richtig. Um es anschaulicher zu gestallten, hatte ich mich für das Hochzählen von 't' entschieden. An time.monotonic() hatte ich ehrlich gesagt auch nicht gedacht.

Ich habe mir dann noch erlaubt meine urspruengliche Formel zur Frequenz zu nutzen, und die Zeit, die tatsaechlich verflossen ist, zu messen.
Auch ein guter Punkt. F * math.pi * 2 * t sind bis auf 't' alle konstant. Man kann sie daher grundsätzlich in 't' zusammenfassen (Skalierung der Zeit). Aber anschaulicher ist es sicher die explizite Verwendung der Größen.
Sirius3
User
Beiträge: 14773
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 27. Juni 2021, 17:27

Irgendwo mitten im Code ein sys.exit zu haben ist sehr unschön, weil Code zum Aufräumen nicht funktioniert.
Bei dir auch relativ einfach zu vermeiden:

Code: Alles auswählen

def game_loop(screen):
    """Die game loop in der der zeitliche Ablauf kontrolliert wird"""
    clock = pygame.time.Clock()
    while process_events():
        draw(screen, time.monotonic())
        pygame.display.update()
        clock.tick(60)

def process_events():
    """Verarbeitung von Ereignissen"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            return False
    return True
rogerb
User
Beiträge: 232
Registriert: Dienstag 26. November 2019, 23:24

Sonntag 27. Juni 2021, 20:14

Sirius3 hat geschrieben:
Sonntag 27. Juni 2021, 17:27
Irgendwo mitten im Code ein sys.exit zu haben ist sehr unschön, weil Code zum Aufräumen nicht funktioniert.
Tja, Guter Punkt! Da ich noch nicht viel mit pygame gemacht hatte, habe ich einfach das Intro Tutorial kopiert und nicht weiter drüber nachgedacht.
https://www.pygame.org/docs/tut/PygameIntro.html

Man sollte alles hinterfragen.
Antworten