Uhr mit Pygame

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo Leute,
hier eine Uhr, welche ich mit Pygame programmiert habe. Ist bestimmt nicht die eleganteste Lösung. Evtl. werde ich nochmal eine verbesserte Version hochladen :)

Code: Alles auswählen

import pygame
import pygame.freetype
import time

class Number(pygame.sprite.Sprite):
    def __init__(self, digit, size, color, pos_x, pos_y):
        super().__init__()
        self.size = size
        font = pygame.freetype.Font(None, size)
        self.image = digit
        font.render_to(self.image, (0, 0), digit, color)
        self.font = pygame.freetype.Font(None)
        self.rect = self.image.get_rect()
        self.rect.center = (pos_x, pos_y)

    @property
    def image(self):
        return self._image

    @image.setter
    def image(self, digit):
        if len(digit) > 1:
            self._image = pygame.Surface((self.size + 5, self.size - 20))
        else:
            self._image = pygame.Surface((self.size/2, self.size - 20))


class Circle(pygame.sprite.Sprite):
    def __init__(self, radius, color, pos):
        super().__init__()
        self.image = pygame.Surface((radius*2, radius*2))
        pygame.draw.circle(self.image, color, (radius,radius), radius, width=10)
        self.rect = self.image.get_rect()
        self.rect.center = (pos['x'], pos['y'])


def add_circle(center, radius, window):
    circle_group = pygame.sprite.Group()
    circle_white = Circle(radius, (159, 226, 191), center)
    circle_group.add(circle_white)
    circle_group.draw(window)


def add_numbers(center_vector, radius_vector, size, window):
    angle = 360/12
    number_vectors = []
    
    radius_vectors = [radius_vector for vector in range(0, 12)]
    for vector in radius_vectors:
        vector = vector.rotate(int(angle))
        number_vectors.append(center_vector + vector)
        angle += 360/12

    numbers = []
    for i in range(len(number_vectors)):
        numbers.append(Number(str(i+1), size, (255, 127, 80), number_vectors[i].x, number_vectors[i].y))

    numbers_group = pygame.sprite.Group()
    numbers_group.add(numbers)
    numbers_group.draw(window)


def add_second_lines(center_vector, radius, window):
    second_line_end_vector = pygame.math.Vector2(0, -radius+5)
    angle_offset = 360/60

    for angle in range(0, 60):
        if angle % 15 == 0:
            length = -210
            width = 10
        elif angle % 5 == 0:
            length = -210
        else:
            length = -230
            width = 5
            
        second_line_begin_vector = pygame.math.Vector2(0, length)

        second_line_begin_vector2 = second_line_begin_vector.rotate(angle*angle_offset)
        second_line_end_vector2 = second_line_end_vector.rotate(angle*angle_offset)
        pygame.draw.line(window, (159, 226, 191), center_vector + second_line_begin_vector2, center_vector + second_line_end_vector2, width=width)


def add_pointer(type, center_vector, radius, time, window, length=50, width=1, color=(255, 255, 255)):
    if type == "hour":
        angle_offset = 360/12
    elif type == "min":
        angle_offset = 360/60
    elif type == "sec":
        angle_offset = 360/60

    second_line_begin_vector = pygame.math.Vector2(0, 0)
    second_line_end_vector = pygame.math.Vector2(0, -radius + length)
    second_line_begin_vector = second_line_begin_vector.rotate(time*angle_offset)
    second_line_end_vector = second_line_end_vector.rotate(time*angle_offset)
    pygame.draw.line(window, color, center_vector + second_line_begin_vector, center_vector + second_line_end_vector, width=width)


def main():
    pygame.init()
    pygame.freetype.init()

    screen_width = 720
    screen_height = 720

    window = pygame.display.set_mode((screen_width, screen_height))
    pygame.display.set_caption("Clock")

    # constants
    center = {'x' : screen_width/2, 'y': screen_height/2}
    radius = screen_width/2 - 100
    size = 80
    center_vector = pygame.math.Vector2(center['x'], center['y'])
    radius_vector = pygame.math.Vector2(0, -radius - size/1.5)

    clock = pygame.time.Clock()

    run = True
    game_speed = 120
    while run:
        window.fill((0, 0, 0))
        clock.tick(game_speed)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False

        add_circle(center, radius, window)
        add_numbers(center_vector, radius_vector, size, window)
        add_second_lines(center_vector, radius, window)

        hour = time.gmtime().tm_hour + 2
        minute = time.gmtime().tm_min
        second = time.gmtime().tm_sec
        add_pointer("min", center_vector, radius, minute, window, color=(100, 149, 237), width=9)
        add_pointer("hour", center_vector, radius, hour, window, color=(64, 224, 208), length=130, width=3)
        add_pointer("sec", center_vector, radius, second, window, color=(0, 200, 100), width=2)

        pygame.display.update()


if __name__ == '__main__':
    main()
Rainier
User
Beiträge: 19
Registriert: Mittwoch 22. August 2007, 10:03

Da sich, jedenfalls bei mir unter Windoof 10, das Fenster nur per kill schließen lässt, habe ich ein pygame.quit() eingebaut:

Code: Alles auswählen

import pygame
import pygame.freetype
import time, sys

class Number(pygame.sprite.Sprite):
    def __init__(self, digit, size, color, pos_x, pos_y):
        super().__init__()
        self.size = size
        font = pygame.freetype.Font(None, size)
        self.image = digit
        font.render_to(self.image, (0, 0), digit, color)
        self.font = pygame.freetype.Font(None)
        self.rect = self.image.get_rect()
        self.rect.center = (pos_x, pos_y)

    @property
    def image(self):
        return self._image

    @image.setter
    def image(self, digit):
        if len(digit) > 1:
            self._image = pygame.Surface((self.size + 5, self.size - 20))
        else:
            self._image = pygame.Surface((self.size/2, self.size - 20))


class Circle(pygame.sprite.Sprite):
    def __init__(self, radius, color, pos):
        super().__init__()
        self.image = pygame.Surface((radius*2, radius*2))
        pygame.draw.circle(self.image, color, (radius,radius), radius, width=10)
        self.rect = self.image.get_rect()
        self.rect.center = (pos['x'], pos['y'])


def add_circle(center, radius, window):
    circle_group = pygame.sprite.Group()
    circle_white = Circle(radius, (159, 226, 191), center)
    circle_group.add(circle_white)
    circle_group.draw(window)


def add_numbers(center_vector, radius_vector, size, window):
    angle = 360/12
    number_vectors = []
    
    radius_vectors = [radius_vector for vector in range(0, 12)]
    for vector in radius_vectors:
        vector = vector.rotate(int(angle))
        number_vectors.append(center_vector + vector)
        angle += 360/12

    numbers = []
    for i in range(len(number_vectors)):
        numbers.append(Number(str(i+1), size, (255, 127, 80), number_vectors[i].x, number_vectors[i].y))

    numbers_group = pygame.sprite.Group()
    numbers_group.add(numbers)
    numbers_group.draw(window)


def add_second_lines(center_vector, radius, window):
    second_line_end_vector = pygame.math.Vector2(0, -radius+5)
    angle_offset = 360/60

    for angle in range(0, 60):
        if angle % 15 == 0:
            length = -210
            width = 10
        elif angle % 5 == 0:
            length = -210
        else:
            length = -230
            width = 5
            
        second_line_begin_vector = pygame.math.Vector2(0, length)

        second_line_begin_vector2 = second_line_begin_vector.rotate(angle*angle_offset)
        second_line_end_vector2 = second_line_end_vector.rotate(angle*angle_offset)
        pygame.draw.line(window, (159, 226, 191), center_vector + second_line_begin_vector2, center_vector + second_line_end_vector2, width=width)


def add_pointer(type, center_vector, radius, time, window, length=50, width=1, color=(255, 255, 255)):
    if type == "hour":
        angle_offset = 360/12
    elif type == "min":
        angle_offset = 360/60
    elif type == "sec":
        angle_offset = 360/60

    second_line_begin_vector = pygame.math.Vector2(0, 0)
    second_line_end_vector = pygame.math.Vector2(0, -radius + length)
    second_line_begin_vector = second_line_begin_vector.rotate(time*angle_offset)
    second_line_end_vector = second_line_end_vector.rotate(time*angle_offset)
    pygame.draw.line(window, color, center_vector + second_line_begin_vector, center_vector + second_line_end_vector, width=width)


def main():
    pygame.init()
    pygame.freetype.init()

    screen_width = 720
    screen_height = 720

    window = pygame.display.set_mode((screen_width, screen_height))
    pygame.display.set_caption("Clock")

    # constants
    center = {'x' : screen_width/2, 'y': screen_height/2}
    radius = screen_width/2 - 100
    size = 80
    center_vector = pygame.math.Vector2(center['x'], center['y'])
    radius_vector = pygame.math.Vector2(0, -radius - size/1.5)

    clock = pygame.time.Clock()

    run = True
    game_speed = 120
    while run:
        window.fill((0, 0, 0))
        clock.tick(game_speed)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False

        add_circle(center, radius, window)
        add_numbers(center_vector, radius_vector, size, window)
        add_second_lines(center_vector, radius, window)

        hour = time.gmtime().tm_hour + 2
        minute = time.gmtime().tm_min
        second = time.gmtime().tm_sec
        add_pointer("min", center_vector, radius, minute, window, color=(100, 149, 237), width=9)
        add_pointer("hour", center_vector, radius, hour, window, color=(64, 224, 208), length=130, width=3)
        add_pointer("sec", center_vector, radius, second, window, color=(0, 200, 100), width=2)

        pygame.display.update()

    pygame.quit()
    sys.exit()

if __name__ == '__main__':
    main()
Den Stundenzeiger solltest Du nochmal in Angriff nehmen. Sieht komisch aus, wenn es eine 11:45 Uhr ist und der Zeiger immer noch auf 11 steht. Der müsste etwas mit wandern Richtung nächste Stunde.
Zuletzt geändert von Rainier am Freitag 21. Mai 2021, 10:52, insgesamt 1-mal geändert.
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hi Rainier :) Danke für Deine Rückmeldung ;)

Ich benutze auch Windoof 10 (aber auch nur weil ich es für Computerspiele brauche, ansonsten hätte ich standardmäßig Debian installiert ;)).

Wenn ich das "X" vom Fenster drücke dann schließt es sich bei mir ganz normal da ich das hier eigentlich in meinem main-loop eingebaut habe:

Code: Alles auswählen

for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
Lässt es sich trotzdem nicht schließen?
Rainier
User
Beiträge: 19
Registriert: Mittwoch 22. August 2007, 10:03

Vielleicht liegt es auch an meiner Umgebung. Nicht auszuschließen.

Meine Überarbeitung des Beitrages und Deiner hat sich überschnitten. Daher nochmal:

Den Stundenzeiger solltest Du nochmal in Angriff nehmen. Sieht komisch aus, wenn es 11:45 Uhr ist und der Zeiger immer noch auf 11 steht. Der müsste etwas mit wandern Richtung nächste Stunde.
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Rainier hat geschrieben: Freitag 21. Mai 2021, 10:53 Vielleicht liegt es auch an meiner Umgebung. Nicht auszuschließen.

Meine Überarbeitung des Beitrages und Deiner hat sich überschnitten. Daher nochmal:

Den Stundenzeiger solltest Du nochmal in Angriff nehmen. Sieht komisch aus, wenn es 11:45 Uhr ist und der Zeiger immer noch auf 11 steht. Der müsste etwas mit wandern Richtung nächste Stunde.
Ich habe erst nicht verstanden was du meinst :D Du hast recht, das werde ich auf jedenfall verbessern. Wäre mir gar nicht aufgefallen. Vielen Dank Dir ;)
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Domroon: Anmerkungen zum Code:

Der `freetype.init()`-Aufruf ist im `pygame.init()` schon enthalten. Am Programmende fehlt ein `pygame.quit()`.

Das `center`-Wörterbuch in der Hauptfunktion ist komisch. Warum ist das ein Wörterbuch? Pygame benutzt überall einfach Tupel für Koordinaten. Wenn man unbedingt `x` und `y` haben möchte, wäre das ein eigener Datentyp, also zum Beispiel ein `namedtuple`.

Statt sich `x` und `y` selbst zu berechnen, könnte man auch einfach `window.get_rect().center` verwenden.

`run` kann man sich sparen.

Das mit den Sprites ist so nicht sinnvoll umgesetzt. Es macht keinen Sinn für jeden Frame neue Sprites zu erstellen, die in Spritegruppen zu stecken um sie dann zu zeichnen, und dann alles weg zu werfen, um beim nächsten Frame alles von vorne zu erstellen. Entweder man zeichnet einfach alles neu bei jedem Frame, oder man hebt die Sprites/Gruppen auf. Wobei auch da Sprites und Gruppen für den Kreis und die Zahlen nicht sinnvoll sind, solange man nichts davon bewegen möchte. Statische Zahlen und Kreis würde man auf *ein* Surface, *einmal* am Anfang zeichnen, und das dann immer blitten. Beim Verhältnis von Fenstergrösse und Uhr am besten gleich komplett in der Fenstergrösse. Dann kann man sich das füllen mit der Hintergrundfarbe auch sparen.

Die Attribute `font` und `size` bei `Number` werden nirgends verwendet.

Bei `Number` ist das berechnete `image`-Attribut verwirrend. Man weist eine Zeichenkette zu, bekommt beim Abfragen aber ein `Surface`-Objekt. So etwas sollte man nicht machen.

Das ist auch nicht wirklich robust was da gemacht wird. Du hoffst das die erstellten `Surface`-Objekte auf diese Weise ausreichen werden. Aber warum hoffen wenn man doch einfach `render()` statt `render_to()` verwenden kann und so auf jeden Fall ein `Surface` bekommt, in das der Text hinein passt‽

Die Klasse schrumpft dann komplett auf das hier zusammen:

Code: Alles auswählen

class Number(pygame.sprite.Sprite):
    def __init__(self, digit, size, color, pos_x, pos_y):
        super().__init__()
        font = pygame.freetype.Font(None, size)
        self.image, self.rect = font.render(digit, color)
        self.rect.center = (pos_x, pos_y)
Ich würde da noch `pos_x` und `pos_y` zu einem Argument zusammenfassen.

In `add_numbers()` ist die `radius_vectors`-Liste überflüssig. Da steckt 12 mal der *selbe* Vektor drin.

`center_vector` braucht kein `Vector2` zu sein, da würde ein Koordinatentupel reichen. Und statt `radius_vector` könnte man auch nur den Radius übergeben und das `Vector2`-Objekt in der Funktion erstellen. Das wird ausserhalb ja nirgends verwendet.

In Schleifen wiederholt eine Schrittweite als Gleitkommazahlen addieren ist allgemein problematisch, weil man auf diese Weise Ungenauigkeiten verstärkt. Im Konkreten Fall ist das kein Problem, weil bei der konkreten Division keine Nachkommastellen entstehen, aber üblicherweise multipliziert man die Schrittweite mit dem Schrittindex um dieses Problem auszuschliessen. Dann lässt sich `number_vectors` auch als „list comprehension“ ausdrücken:

Code: Alles auswählen

    radius_vector = pygame.math.Vector2(0, -radius)
    number_vectors = [
        center + radius_vector.rotate(int(360 / 12 * hour))
        for hour in range(1, 13)
    ]
Jetzt macht das aber nicht wirklich Sinn erst diese Liste aus den `hour`-Werten zu erstellen, um dann im nächsten Schritt mit dem „anti-pattern“ ``for i in range(len(number_vectors)):`` über diese Liste zu iterieren und sich aus dem `i` mit ``+ 1`` wieder den `hour`-Wert zu berechnen den man in der ersten Schleife/„list comprension“ schon mal in den Fingern hatte. Wenn man dann noch die `Number`-Klasse weg lässt und direkt zeichnet, bleibt am Ende eine Schleife über die Stunden-Werte in der die Zahlen gezeichnet werden.

Bliebe das hier übrig:

Code: Alles auswählen

def draw_numbers(surface, radius, size):
    radius_vector = pygame.math.Vector2(0, -radius)
    font = pygame.freetype.Font(None, size)
    for hour in range(1, 13):
        image, rect = font.render(str(hour), (255, 127, 80))
        rect.center = surface.get_rect().center + radius_vector.rotate(
            int(360 / 12 * hour)
        )
        surface.blit(image, rect)
Der Name `add_second_lines()` ist ein bisschen irreführend, weil da ja nicht nur Sekunden sondern auch Stunden markiert werden.

Der Name `angle` ist falsch, denn das ist die Minute und nicht der Winkel. Der Winkel ergibt sich erst aus der Multiplikation von Minute und `angle_offset`.

Ich würde das mit den Vektoren anders ausdrücken und die Linien vom Kreis aus eine bestimmte Länge zur Mitte hin zeichnen. Und die Länge dann auch vom Radius abhängig ausdrücken, statt in absoluten Zahlen die von der Mitte aus gesehen werden. Da man nicht direkt am Code ablesen kann was der Radius ist, ist es schwer zu sehen was die 230 und 210 eigentlich bedeuten, also wie lang die Linie letztendlich ist.

Für *einen* Zeitpunkt `gmtime()` *drei mal* aufzurufen ist ein Fehler! Zwischen den Aufrufen vergeht ja auch Zeit und so kann es passieren, dass die Stunde um 2:59:59 ermittelt wird, und die Minute und die Sekunde um 4:00:00 und die Uhr dann eine nahezu eine Stunde falsche Zeit anzeigt.

Das mit dem Stunde +2 funktioniert auch nur solange man bei der analogen Anzeige bleibt. Warum nicht einfach die lokale Zeit direkt ermitteln, dann braucht man sich auch keine Gedanken um Sommer-/Winterzeit zu machen.

Uhrenzeiger heissen auf Englisch nicht „pointer“ sondern „hand“.

Das `type`-Argument verdeckt die eingebaute `type`-Funktion. Da Zeichenketten mit besonderer Bedeutung zu übergeben ist auch unschön. Man könnte da beispielsweise einen `max_value` übergeben. Bei Stunden 12 und bei Minuten und Sekunden 60.

Das `second_line_*` in den Namen ist irreführend weil es ja nicht nur für Sekunden verwendet wird.

Die Funktion kennt den Mittelpunkt und die Länge des Zeigers. Wenn sie dann noch zusätzlich den Radius des Kreises kennen muss, ist was bei der Berechnung ungünstig gelaufen, denn das sollte nicht bekannt sein müssen. Ausserdem ist die Länge hier auch wieder völlig unintuitiv angegeben, denn das ist gar nicht die Länge des Zeigers sondern die Länge die der Zeiger *nicht* belegt. Wovon? Nun die Zahl muss man sich selbst errechnen, aus Werten die ganz woanders stehen.

`begin_vector` ist (0, 0) — egal um was man den rotiert, das bleibt immer (0, 0). Und (0, 0) auf den Mittelpunkt addieren bleibt immer der Mittelpunkt. Das kann also komplett weg.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import datetime as DateTime

import pygame
import pygame.freetype
from pygame.math import Vector2


def draw_circle(surface, radius):
    pygame.draw.circle(
        surface, (159, 226, 191), surface.get_rect().center, radius, 10
    )


def draw_numbers(surface, radius, size):
    font = pygame.freetype.Font(None, size)
    radius_vector = Vector2(0, -radius)
    for hour in range(1, 13):
        image, rect = font.render(str(hour), (255, 127, 80))
        rect.center = surface.get_rect().center + radius_vector.rotate(
            int(360 / 12 * hour)
        )
        surface.blit(image, rect)


def draw_tick_marks(surface, radius):
    center = surface.get_rect().center
    radius_vector = Vector2(0, -radius + 5)
    angle_per_minute = 360 / 60
    for minute in range(0, 60):
        tick_vector = radius_vector.rotate(minute * angle_per_minute)
        line_start = center + tick_vector
        length = radius / 5 if minute % 5 == 0 else radius / 8.5
        pygame.draw.line(
            surface,
            (159, 226, 191),
            line_start,
            line_start + tick_vector.normalize() * -length,
            10 if minute % 15 == 0 else 5,
        )


def draw_hand(surface, length, width, color, max_value, value):
    center = surface.get_rect().center
    pygame.draw.line(
        surface,
        color,
        center,
        center + Vector2(0, -length).rotate(360 / max_value * value),
        width,
    )


def main():
    pygame.init()
    try:
        window_size = 720

        window = pygame.display.set_mode((window_size, window_size))
        pygame.display.set_caption("Clock")

        radius = min(window.get_rect().center) - 100
        assert radius > 0
        number_size = 80

        background = pygame.Surface(window.get_size())
        draw_circle(background, radius)
        draw_numbers(background, radius + number_size / 1.5, number_size)
        draw_tick_marks(background, radius)

        clock = pygame.time.Clock()
        frames_per_second = 120
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return

            window.blit(background, (0, 0))
            now = DateTime.now()
            draw_hand(window, radius * 0.8, 9, (100, 149, 237), 60, now.minute)
            draw_hand(window, radius * 0.5, 3, (64, 224, 208), 12, now.hour)
            draw_hand(window, radius * 0.8, 1, (0, 200, 100), 60, now.second)

            pygame.display.update()
            clock.tick(frames_per_second)
    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
Was hier jetzt auffällt ist das `draw_hand()` viele Argumente bekommt und drei mal aufgerufen wird, und ein Teil der Argumente die Eigenschaften eines Zeigers beschreiben. Also ein heisser Kandidat für eine Klasse.

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import datetime as DateTime

import pygame
import pygame.freetype
from attr import attrib, attrs
from pygame.math import Vector2


def draw_circle(surface, radius):
    pygame.draw.circle(
        surface, (159, 226, 191), surface.get_rect().center, radius, 10
    )


def draw_numbers(surface, radius, size):
    font = pygame.freetype.Font(None, size)
    radius_vector = Vector2(0, -radius)
    for hour in range(1, 13):
        image, rect = font.render(str(hour), (255, 127, 80))
        rect.center = surface.get_rect().center + radius_vector.rotate(
            int(360 / 12 * hour)
        )
        surface.blit(image, rect)


def draw_tick_marks(surface, radius):
    center = surface.get_rect().center
    radius_vector = Vector2(0, -radius + 5)
    angle_per_minute = 360 / 60
    for minute in range(0, 60):
        tick_vector = radius_vector.rotate(minute * angle_per_minute)
        line_start = center + tick_vector
        length = radius / 5 if minute % 5 == 0 else radius / 8.5
        pygame.draw.line(
            surface,
            (159, 226, 191),
            line_start,
            line_start + tick_vector.normalize() * -length,
            10 if minute % 15 == 0 else 5,
        )


@attrs(frozen=True)
class Hand:
    attribute_name = attrib()
    max_value = attrib()
    length = attrib()
    width = attrib()
    color = attrib()

    def draw(self, surface, time):
        center = surface.get_rect().center
        pygame.draw.line(
            surface,
            self.color,
            center,
            center
            + Vector2(0, -self.length).rotate(
                360 / self.max_value * getattr(time, self.attribute_name)
            ),
            self.width,
        )


def main():
    pygame.init()
    try:
        window_size = 720

        window = pygame.display.set_mode((window_size, window_size))
        pygame.display.set_caption("Clock")

        radius = min(window.get_rect().center) - 100
        assert radius > 0
        number_size = 80

        background = pygame.Surface(window.get_size())
        draw_circle(background, radius)
        draw_numbers(background, radius + number_size / 1.5, number_size)
        draw_tick_marks(background, radius)

        hands = [
            Hand("minute", 60, radius * 0.8, 9, (100, 149, 237)),
            Hand("hour", 12, radius * 0.5, 3, (64, 224, 208)),
            Hand("second", 60, radius * 0.8, 1, (0, 200, 100)),
        ]
        clock = pygame.time.Clock()
        frames_per_second = 120
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return

            window.blit(background, (0, 0))
            now = DateTime.now()
            for hand in hands:
                hand.draw(window, now)

            pygame.display.update()
            clock.tick(frames_per_second)
    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

@__blackjack__:
Uii, dass sind ja eine menge Verbesserungen! ;) Vielen Dank erstmal für Deine Bemühungen ;)
Ich habe mir auch schon einige Gedanken gemacht was man anders machen könnte. Ich werde Deine und meine Verbesserungen mal einfließen lassen und dann denn verbesserten Code auch nochmal hier posten. Am langen Wochenende werde ich leider nichts schaffen :( Ich will schauen, dass ich das Programm nächste Woche anfange zu verbessern :)
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo Leute,
ich habe meine Uhr verbessert und dabei versucht so viel wie möglich von __blackjack__ umzusetzen. Das Modul "attr" habe ich (noch) nicht verstanden. Ich habe es deshalb an dieser Stelle vorerst außen vor gelassen ;)
Das Programm ist nun so geschrieben, dass alle anzeigbaren Elemente in Abhängigkeit des Kreismittelpunktes und der "screen_width" positioniert werden. Außerdem wandert der kleine Zeiger nun auch zwischen den Zahlen, damit z.B. 11:45 oder auch schon 11:30 nicht "merkwürdig" wenn nicht sogar falsch aussieht.

PS:
Wundert euch nicht über die "JSON-artige" globale Variable "PATTERNS". Diese ist für diese zukünftige Implementierung gedacht:
Die zeitlichen Abfolgen in PATTERNS sollen in die Uhr "geladen" werden können (Ich hatte schon etwas implementiert, hat mir aber nicht gefallen und ich habe es prompt gelöscht). Auch habe ich eine selbst gebaute Timer-Klasse verwendet um einen zeitlichen Bezug für meine Animation zu finden. Durch meine Experimente hat sich herausgestellt, dass das ganze aber Mist ist und ich mir ein Konzept überlegen werde wo die Animation in Abhängigkeit der Frames ablaufen soll. Also z.B. sollen die Zahlen 1 und 7, 60-Frames lang "leuchten" (auf color=red) und dann wieder "ausgehen" (color=white). Somit sind ja jegliche zeitliche Abläufe denkbar. Die Animationen sollen sich nicht nur auf die Nummern sondern auch auf die "TickMarks" auswirken, weshalb ich vorsorglich eher objektorientiert an die Sache herangegangen bin. Ein aktualisierter Code folgt, sobald ich etwas ansehnlichen Code fertig habe :lol:

Hier aber nun erstmal der aktuelle Code:

Code: Alles auswählen

from datetime import datetime as DateTime
import pygame
from pygame import Vector2
import pygame.freetype
import time

PATTERNS = {
        'test_pattern' : {
                        '1' : 
                                { '1' : {'color' : (255, 0, 0), 'duration' : 1}},
                        '2' : 
                                { '1' : {'color' : (255, 0, 0), 'duration' : 1}},
                        '3' : 
                                { '1': {'color' : (255, 0, 0), 'duration' : 1}},
                        '4' : 
                                { '1': {'color' : (255, 0, 0), 'duration' : 1}},
                        '5' : 
                                { '2': {'color' : (255, 0, 0), 'duration' : 1}},
                        '6' : 
                                { '2': {'color' : (255, 0, 0), 'duration' : 1}},
                        '7' : 
                                { '2': {'color' : (255, 0, 0), 'duration' : 1}},
                        '8' : 
                                { '2': {'color' : (255, 0, 0), 'duration' : 1}},
                        '9' : 
                                { '3': {'color' : (255, 0, 0), 'duration' : 1}},
                        '10' : 
                                { '3': {'color' : (255, 0, 0), 'duration' : 1}},
                        '11' : 
                                { '3': {'color' : (255, 0, 0), 'duration' : 1}},
                        '12' : 
                                { '3': {'color' : (255, 0, 0), 'duration' : 1}},
                        'pattern_duration' : 4},

        'test_pattern2' : {
                        '1' : 
                                { '1' : {'color' : (255, 0, 0), 'duration' : 1}},
                        '2' : 
                                { '2' : {'color' : (255, 0, 0), 'duration' : 1}},
                        '3' : 
                                { '3': {'color' : (255, 0, 0), 'duration' : 1}},
                        '4' : 
                                { '4': {'color' : (255, 0, 0), 'duration' : 1}},
                        '5' : 
                                { '5': {'color' : (255, 0, 0), 'duration' : 1}},
                        '6' : 
                                { '6': {'color' : (255, 0, 0), 'duration' : 1}},
                        '7' : 
                                { '7': {'color' : (255, 0, 0), 'duration' : 1}},
                        '8' : 
                                { '8': {'color' : (255, 0, 0), 'duration' : 1}},
                        '9' : 
                                { '9': {'color' : (255, 0, 0), 'duration' : 1}},
                        '10' : 
                                { '10': {'color' : (255, 0, 0), 'duration' : 1}},
                        '11' : 
                                { '11': {'color' : (255, 0, 0), 'duration' : 1}},
                        '12' : 
                                { '12': {'color' : (255, 0, 0), 'duration' : 1}},
                        'pattern_duration' : 13},

        'test_pattern3' : {
                        '1' : 
                                { '1' : {'color' : (255, 0, 0), 'duration' : 1}},
                        '2' : 
                                { '2' : {'color' : (255, 0, 0), 'duration' : 1}},
                        '3' : 
                                { '3': {'color' : (255, 0, 0), 'duration' : 1}},
                        '4' : 
                                { '4': {'color' : (255, 0, 0), 'duration' : 1}},
                        '5' : 
                                { '5': {'color' : (255, 0, 0), 'duration' : 1}},
                        '6' : 
                                { '6': {'color' : (255, 0, 0), 'duration' : 1}},
                        '7' : 
                                { '1': {'color' : (255, 0, 0), 'duration' : 1}},
                        '8' : 
                                { '2': {'color' : (255, 0, 0), 'duration' : 1}},
                        '9' : 
                                { '3': {'color' : (255, 0, 0), 'duration' : 1}},
                        '10' : 
                                { '4': {'color' : (255, 0, 0), 'duration' : 1}},
                        '11' : 
                                { '5': {'color' : (255, 0, 0), 'duration' : 1}},
                        '12' : 
                                { '6': {'color' : (255, 0, 0), 'duration' : 1}},
                        'pattern_duration' : 7}
        }


class PointSightingLine(pygame.sprite.Sprite):
    def __init__(self, pos, width, length, color, radius=0, offset=0, rotate_itself=True):
        super().__init__()
        self.pos = pos
        self.image = pygame.Surface((width, length), pygame.SRCALPHA)
        self.image = self.image.convert_alpha() # work faster with this image
        self.image.fill(color)
        self.image_copy = self.image
        self.move_vector = Vector2(0, -radius - offset)
        self.rect = self.image.get_rect(center=self.pos+self.move_vector)
        self.radius = radius
        self.rotate_itself = rotate_itself

    def rotate(self, angle):
        move_vector = self.move_vector
        move_vector = move_vector.rotate(angle)
        if self.rotate_itself:
                self.image = pygame.transform.rotozoom(self.image_copy, -angle, 1)
        self.rect = self.image.get_rect(center = self.pos + move_vector)


class Hand(PointSightingLine):
    def __init__(self, hand_type, pos, width, length, color, radius=0, offset=0):
        super().__init__(pos, width, length, color, radius, offset)
        self.hand_type = hand_type
        self.angle_per_second = 360/60
        self.angle_per_minute = self.angle_per_second
        self.angle_per_hour = 360/12

    def update(self, now):
        if self.hand_type == "second":
            self.rotate(self.angle_per_second * now.second)
        elif self.hand_type == "minute":
            self.rotate(self.angle_per_minute * now.minute)
        elif self.hand_type == "hour":
            self.rotate(self.angle_per_hour * now.hour + 30 * (now.minute/60))


class Number(PointSightingLine):
    def __init__(self, number, size, pos, color, radius, offset=0, width=0, length=0, rotate_itself=False):
        super().__init__(pos, width, length, color, radius, offset, rotate_itself)
        self.number = number
        self.settings = {}
        self.font = pygame.freetype.Font(None, size)
        self.color = color
        self.image, self.rect = self.font.render(self.number, self.color)
        self.image_copy = self.image
        self.rect = self.image.get_rect(center=pos + self.move_vector)


def draw_circle(radius, screen_width, surface):
    pygame.draw.circle(surface, (0, 100, 200), surface.get_rect().center, radius, int(screen_width/100))


def generate_numbers(radius, numbers_group, size, surface):
    angel_per_number = 360/12
    for i in range(1, 13):
        number = Number(str(i), size, surface.get_rect().center, (0, 100, 120), radius=radius, offset=50)
        number.rotate(i * angel_per_number)
        numbers_group.add(number)


def generate_tick_marks(radius, screen_width, tick_mark_group, surface):
    angle_per_minute = 360/60
    
    for second in range(0, 60):
        tick_length = screen_width/36
        tick_width = screen_width/400
        offset = -screen_width/48
        if second % 15 == 0:
            tick_length = screen_width/18
            tick_width = screen_width/80
            offset = -screen_width/28
        if second % 5 == 0 and not second % 15 == 0:
            tick_length = screen_width/18  
            tick_width = screen_width/160
            offset = -screen_width/28
        tick = PointSightingLine(surface.get_rect().center, tick_width, tick_length, (0, 100, 200), radius=radius, offset=offset)
        tick.rotate(second*angle_per_minute)
        tick_mark_group.add(tick)


def generate_hands(radius, hands_group, screen_width, surface):
    offset = -screen_width/4
    second_hand = Hand("second", (surface.get_rect().center), screen_width/400, screen_width/3, (0, 200, 200), radius=radius, offset=offset)
    minute_hand = Hand("minute", (surface.get_rect().center), screen_width/80, screen_width/3, (0, 150, 150), radius=radius, offset=offset)
    hour_hand = Hand("hour", (surface.get_rect().center), screen_width/80, screen_width/5, (0, 100, 150), radius=radius, offset=offset-screen_width/15)
    hands_group.add(minute_hand, hour_hand, second_hand)


def main():
    pygame.init()
    try:
        screen_width = 720

        window = pygame.display.set_mode((screen_width, screen_width))
        pygame.display.set_caption("Clock")

        background = pygame.Surface(window.get_size())
        
        # circle
        radius = min(window.get_rect().center) - screen_width/8
        assert radius > 0
        draw_circle(radius, screen_width, background)

        # hands
        hands_group = pygame.sprite.Group()
        generate_hands(radius, hands_group, screen_width, window)

        # numbers
        number_size = screen_width/12                                                                
        numbers_group = pygame.sprite.Group()
        generate_numbers(radius, numbers_group, number_size, window)

        # tick marks
        tick_mark_group = pygame.sprite.Group()
        generate_tick_marks(radius, screen_width, tick_mark_group, background)

        clock = pygame.time.Clock()
        fps = 120
        while True:
            window.blit(background, (0, 0))

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return

            tick_mark_group.draw(window)        

            now = DateTime.now()
            hands_group.update(now)
            hands_group.draw(window)
            
            numbers_group.draw(window)

            pygame.display.update()
            clock.tick(fps)
    finally:
        pygame.quit()


if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wenn du etwas durchnummerierst als Schluessel in einem Woerterbuch, dann willst du Listen.
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

__deets__ hat geschrieben: Freitag 28. Mai 2021, 21:02 Wenn du etwas durchnummerierst als Schluessel in einem Woerterbuch, dann willst du Listen.
Oh ja, das macht Sinn ;) Danke für Deine Anmerkung. Aber mal schauen ob ich die Struktur überhaupt so lasse. Ich probiere erstmal ein bisschen mit den Farben der Nummern herum und schaue wie ich diese in Abhängigkeit der Frames bekomme ;)
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Hallo Domroon,

schönes Projekt!
Nur eine Frage: Mir war die assertion aufgefallen:

Code: Alles auswählen

radius = min(window.get_rect().center) - screen_width/8
assert radius > 0
Unter welchen Umständen kann der Radius denn überhaupt Null bzw. negativ werden?
Möglicherweise kann man man den Radius ja auch noch mit einem sinnvollen Wert korrigieren bevor man den Kreis zeichnet?
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

rogerb hat geschrieben: Freitag 28. Mai 2021, 21:27 Hallo Domroon,

schönes Projekt!
Nur eine Frage: Mir war die assertion aufgefallen:

Code: Alles auswählen

radius = min(window.get_rect().center) - screen_width/8
assert radius > 0
Unter welchen Umständen kann der Radius denn überhaupt Null bzw. negativ werden?
Möglicherweise kann man man den Radius ja auch noch mit einem sinnvollen Wert korrigieren bevor man den Kreis zeichnet?
Vielen Dank! :wink:
Die Idee mit der assertion habe ich von __blackjack__s korrigiertem Code meiner ersten Version. Die Intention dahinter verstehe ich auch nicht so ganz, jetzt wo du es sagst ;) Ich vermute einfach mal, dass hier schon vor gedacht wurde. Es könnte ja in Zukunft passieren, dass "radius" eine Benutzereingabe wird.. Aber keine Ahnung ob ich da richtig liege ;)
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Mit Benutzereingabe hat es nix zu tun. Im Original Code mit dem assert stand

Code: Alles auswählen

radius  = min(window.get_rect().center) - 100
Und das kann bei einem Fenster kleiner als 101 Pixel in eine Richtung eben kleiner-gleich 0 werden.
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo Leute,
ich habe nun Animationen für die Nummern implementiert. Diese Animationen können in der Funktion "load_animations()" in beliebiger Reihenfolge sortiert werden und Parameter wie Farbe und bestimmte Zeiten können angepasst werden.

Folgende Animationen sind möglich:
- "schneller werdende Umrundung"
- "Umrundung"
- "harter Farbwechsel"
- "ausfüllen des Kreises nach und nach"
- "einfaden(fade_in)" -> funktioniert momentan noch nicht
- "Wechsel zwischen zwei Farben"
- "blinken"
- "Kreis füllen und nach und nach löschen"

Hier der Code:

Code: Alles auswählen

from datetime import datetime as DateTime
import pygame
from pygame import Vector2
import pygame.freetype
import math


COLORS = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255),
          (255, 128, 0), (51, 255, 255), (255, 0, 119)]


class PointSightingLine(pygame.sprite.Sprite):
    def __init__(self, pos, width, length, color, radius=0, offset=0, rotate_itself=True):
        super().__init__()
        self.pos = pos
        self.image = pygame.Surface((width, length), pygame.SRCALPHA)
        self.image = self.image.convert_alpha() # work faster with this image
        self.image.fill(color)
        self.image_copy = self.image
        self.move_vector = Vector2(0, -radius - offset)
        self.rect = self.image.get_rect(center=self.pos+self.move_vector)
        self.radius = radius
        self.rotate_itself = rotate_itself

    def rotate(self, angle):
        move_vector = self.move_vector
        move_vector = move_vector.rotate(angle)
        if self.rotate_itself:
                self.image = pygame.transform.rotozoom(self.image_copy, -angle, 1)
        self.rect = self.image.get_rect(center = self.pos + move_vector)


class Hand(PointSightingLine):
    def __init__(self, hand_type, pos, width, length, color, radius=0, offset=0):
        super().__init__(pos, width, length, color, radius, offset)
        self.hand_type = hand_type
        self.angle_per_second = 360/60
        self.angle_per_minute = self.angle_per_second
        self.angle_per_hour = 360/12

    def update(self, now):
        if self.hand_type == "second":
            self.rotate(self.angle_per_second * now.second)
        elif self.hand_type == "minute":
            self.rotate(self.angle_per_minute * now.minute)
        elif self.hand_type == "hour":
            self.rotate(self.angle_per_hour * now.hour + 30 * (now.minute/60))


class Number(PointSightingLine):
    def __init__(self, number, size, pos, color, radius, offset=0, width=0, length=0, rotate_itself=False):
        super().__init__(pos, width, length, color, radius, offset, rotate_itself)
        self.pos = pos
        self.number = number
        self.settings = {}
        self.font = pygame.freetype.Font(None, size)
        self.color = color
        self.image, self.rect = self.font.render(self.number, self.color)
        self.image_copy = self.image
        self.rect = self.image.get_rect(center=pos + self.move_vector)
    
    def change_color(self, color):
        self.font.render_to(self.image, (0, 0), self.number, color)
        self.font.bgcolor = (0, 0, 0)


class Animations:
    def __init__(self, segments):
        self.segments = segments
        self.total_necessary_frames = self._calculate_total_necessary_frames()
        self.frames_list = self._save_segment_frames()
        self.current_segment = 1
        self.past_frames = self.frames_list[0]
        self.segments_frames = 0

    def _calculate_total_necessary_frames(self):
        total_necessary_frames = 0
        for segment in self.segments:
            total_necessary_frames += segment.necessary_frames

        return total_necessary_frames

    # save the frames from all segments in a list
    def _save_segment_frames(self):
        frames_list = []
        for segment in self.segments:
            frames_list.append(segment.necessary_frames)

        return frames_list

    def update(self, current_frame):
        self.segments_frames += int(current_frame/current_frame)
        self.segments[self.current_segment-1].update()
        if self.segments_frames == self.total_necessary_frames:
            self.segments_frames = 0
            self.current_segment = 1
            self.past_frames = self.frames_list[0]

        if self.segments_frames == self.past_frames:
            self.past_frames += self.frames_list[self.current_segment]
            self.current_segment +=1


class Segment:
    def __init__(self, animation_elements, pattern, color=(50, 50, 50), elements=[], time_in_ms=1000, fps=120):
        self.animation_elements = animation_elements
        self.color = color
        self.elements = elements
        self.basic_status_color = (50, 50, 50)
        self.necessary_frames = int(fps * (time_in_ms/1000))
        self.pattern = pattern
        self.test = 0
        self.frame = 1
        self.r = 50
        self.g = 50
        self.b = 50
        self.color_counter = 0

    def set_color(self):
        for element in self.elements:
            self.animation_elements[element].change_color(self.color)
            if self.frame == self.necessary_frames:
                self.animation_elements[element].change_color(self.basic_status_color)

    def do_nothing(self):
        pass

    def fade_out_pattern(self, number, time_in_ms):
        pass

    # not working for now
    def fade_in(self):
        start_color=(50, 50, 50)
        end_color=(255, 255, 255)

        r_increasing_value =  end_color[0] - start_color[0]
        g_increasing_value =  end_color[1] - start_color[1]
        b_increasing_value =  end_color[2] - start_color[2]

        r_increase_per_frame = round(r_increasing_value / self.necessary_frames)
        g_increase_per_frame = round(g_increasing_value / self.necessary_frames)
        b_increase_per_frame = round(b_increasing_value / self.necessary_frames)

        r_calculated_end_color = r_increase_per_frame * self.necessary_frames + end_color[0]
        g_calculated_end_color = g_increase_per_frame * self.necessary_frames + end_color[1]
        b_calculated_end_color = b_increase_per_frame * self.necessary_frames + end_color[2]

        too_much_r = r_calculated_end_color - end_color[0]
        too_much_g = g_calculated_end_color - end_color[1]
        too_much_b = b_calculated_end_color - end_color[2]

        # if the calculated end_color is higher than the specified
        if self.frame % (too_much_r/5):
            r_increase_per_frame = 0
        
        if self.frame % (too_much_g/5):
            g_increase_per_frame = 0

        if self.frame % (too_much_b/5):
            b_increase_per_frame = 0

        self.r += r_increase_per_frame
        self.g += g_increase_per_frame
        self.b += b_increase_per_frame

        for element in self.elements:
            self.animation_elements[element].change_color(self.color)
            self.color = (self.r, self.g, self.b)

        if self.r == 254:
            for element in self.elements:
                self.animation_elements[element].change_color(self.color)
                self.color = start_color

    def permanent_color(self):
        for number in self.elements:
            self.animation_elements[number].change_color(self.color)

    def update(self):
        if self.pattern == "set_color":
            self.set_color()
        elif self.pattern == "do_nothing":
            self.do_nothing() 
        elif self.pattern == "permanent_color":
            self.permanent_color()
        elif self.pattern == "fade_in":
            self.fade_in()

        self.frame += 1
        if self.frame == self.necessary_frames + 1:
            self.frame = 1


class AnimationGenerator:
    def __init__(self, animation_elements):
        self.animation_elements = animation_elements

    def raising_circling_num(self, rounds_per_ms_1, rounds_per_ms_2, number_of_steps, color=(255, 255, 255)):
        segments = []

        element_time = rounds_per_ms_1/12
        half_time = rounds_per_ms_2/2
        step_time = half_time/(number_of_steps-1)

        for j in range(0, number_of_steps):
            for i in range(0, 12):
                segments.append(Segment(self.animation_elements, "set_color", color=color, elements=[i], time_in_ms=element_time))

            element_time += step_time/12

        return segments

    def circling_num(self, rounds, color=(255, 255, 255), clockwise=True, ms_per_num=100):
        segments = []
        for j in range(0, rounds):
            if clockwise:
                for i in range(0, 12):
                    segments.append(Segment(self.animation_elements, "set_color", color=color, elements=[i], time_in_ms=ms_per_num))
            else:
                for i in range(11, -1, -1):
                    segments.append(Segment(self.animation_elements, "set_color", color=color, elements=[i], time_in_ms=ms_per_num))

        return segments

    def hard_color_change(self, elements=[0], time_ins_ms=500):
        segments = []
        for i in range(0, len(COLORS)):
            segments.append(Segment(self.animation_elements, "set_color", color=COLORS[i], elements=elements, time_in_ms=time_ins_ms))

        return segments

    def fill_circle_gradually(self, color=(255, 255, 255), ms_per_num=70, clockwise=True):
        segments = []
        for j in range(0, 12):
            if clockwise:
                for i in range(0, 12-j-1):
                    segments.append(Segment(self.animation_elements, "set_color", color=color, elements=[i], time_in_ms=ms_per_num))
                segments.append(Segment(self.animation_elements, "permanent_color", color=color, elements=[12-j-1], time_in_ms=300))
            else:
                pass

        return segments

    # not working for now
    def fade_in(self, elements=[0]):
        segments = []
        segments.append(Segment(self.animation_elements, "fade_in", elements=elements, time_in_ms=1000))
        #segments.append(Segment(self.animation_elements, "set_color", color=(50, 50, 50), elements=elements, time_in_ms=20))

        return segments

    def swap_between_two(self, color_1=(255, 0, 0), color_2=(0, 255, 0), rounds=10):
        segments = []
        elements = []
        elements_2 = []
        for i in range(0, 11, 2):
            elements.append(i)

        for i in range(1, 10, 2):
            elements_2.append(i)
            elements_2.append(11)   

        for i in range(0, rounds):
            segments.append(Segment(self.animation_elements, "set_color", color=color_1, elements=elements, time_in_ms=200))
            segments.append(Segment(self.animation_elements, "set_color", color=color_2, elements=elements_2, time_in_ms=200))

        return segments

    def blink(self, count, color=(255, 255, 255), glow_time=200, pause_time=200):
        segments = []
        for i in range(0, count):
            segments.append(Segment(self.animation_elements, "set_color", color=color, elements=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], time_in_ms=glow_time))
            segments.append(Segment(self.animation_elements, "do_nothing", time_in_ms=pause_time))

        return segments

    def fill_circle_erase(self, color=(255, 255, 255), ms_per_num=10):
        segments = []
        segments.append(Segment(self.animation_elements, "permanent_color", color=color, elements=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]))
        self.circling_num(1, ms_per_num=10)

        return segments


def draw_circle(radius, screen_width, surface):
    pygame.draw.circle(surface, (0, 100, 200), surface.get_rect().center, radius, int(screen_width/100))


def generate_numbers(radius, numbers_group, size, surface):
    angel_per_number = 360/12
    basic_status_color = (50, 50, 50)
    for i in range(1, 13):
        number = Number(str(i), size, surface.get_rect().center, basic_status_color, radius=radius, offset=50)
        number.rotate(i * angel_per_number)
        numbers_group.add(number)


def generate_tick_marks(radius, screen_width, tick_mark_group, surface):
    angle_per_minute = 360/60
    
    for second in range(0, 60):
        tick_length = screen_width/36
        tick_width = screen_width/400
        offset = -screen_width/48
        if second % 15 == 0:
            tick_length = screen_width/18
            tick_width = screen_width/80
            offset = -screen_width/28
        if second % 5 == 0 and not second % 15 == 0:
            tick_length = screen_width/18  
            tick_width = screen_width/160
            offset = -screen_width/28
        tick = PointSightingLine(surface.get_rect().center, tick_width, tick_length, (0, 100, 200), radius=radius, offset=offset)
        tick.rotate(second*angle_per_minute)
        tick_mark_group.add(tick)


def generate_hands(radius, hands_group, screen_width, surface):
    offset = -screen_width/4
    second_hand = Hand("second", (surface.get_rect().center), screen_width/400, screen_width/3, (0, 200, 200), radius=radius, offset=offset)
    minute_hand = Hand("minute", (surface.get_rect().center), screen_width/80, screen_width/3, (0, 150, 150), radius=radius, offset=offset)
    hour_hand = Hand("hour", (surface.get_rect().center), screen_width/80, screen_width/5, (0, 100, 150), radius=radius, offset=offset-screen_width/15)
    hands_group.add(minute_hand, hour_hand, second_hand)


def load_animations(numbers_group):

    animation_elements = numbers_group.sprites()

    animation_generator = AnimationGenerator(animation_elements)

    # animations
    raising_circling_num_red = animation_generator.raising_circling_num(250, 1000, 10, color=(255, 0, 0))
    raising_circling_num_green = animation_generator.raising_circling_num(250, 1000, 10, color=(0, 255, 0))
    raising_circling_num_blue = animation_generator.raising_circling_num(250, 1000, 10, color=(0, 0, 255))

    circling_num_counter_clockwise = animation_generator.circling_num(2, color=(0, 255, 0), clockwise=False)
    circling_num_clockwise = animation_generator.circling_num(2, color=(0, 255, 0))

    color_change_1 = animation_generator.hard_color_change(elements=[0, 2, 4, 6, 8, 10], time_ins_ms=250)
    color_change_2 = animation_generator.hard_color_change(elements= [1, 3, 5, 7, 9, 11], time_ins_ms=150)

    fill_circle_white = animation_generator.fill_circle_gradually()
    fill_circle_blue = animation_generator.fill_circle_gradually(color=(0, 0, 255), ms_per_num=50)

    swap_between_two_1 = animation_generator.swap_between_two()
    swap_between_two_2 = animation_generator.swap_between_two(color_1=(255, 255, 255), color_2=(0, 0, 255))

    blink_white = animation_generator.blink(5)
    blink_green = animation_generator.blink(5, color=(0, 255, 0))

    fill_circle_erase = animation_generator.fill_circle_erase(color=(255, 0, 0))

    # animation groups
    raising_group = [raising_circling_num_red, raising_circling_num_green, raising_circling_num_blue]
    circling_group = [circling_num_counter_clockwise, circling_num_clockwise]
    color_change_group = [color_change_1, color_change_2]
    fill_group = [fill_circle_white, fill_circle_blue]
    swap_group = [swap_between_two_1, swap_between_two_2]
    blink_group = [blink_white, blink_green]
    circle_erase_group = [fill_circle_erase]

    # add animations from animation_groups to animations_list
    animation_groups = [circle_erase_group, fill_group, swap_group, blink_group, color_change_group, raising_group, circling_group]

    animations_list = []
    for animation_group in animation_groups:
        for animation in animation_group:
            animations_list.append(animation)

    segment_list = []

    for animation in animations_list:
        for segment in animation:
            segment_list.append(segment)

    return segment_list


def main():
    pygame.init()
    try:
        screen_width = 720

        window = pygame.display.set_mode((screen_width, screen_width))
        pygame.display.set_caption("Clock")

        background = pygame.Surface(window.get_size())
        
        # circle
        radius = min(window.get_rect().center) - screen_width/8
        assert radius > 0
        draw_circle(radius, screen_width, background)

        # hands
        hands_group = pygame.sprite.Group()
        generate_hands(radius, hands_group, screen_width, window)

        # numbers
        number_size = screen_width/12                                                                
        numbers_group = pygame.sprite.Group()
        generate_numbers(radius, numbers_group, number_size, window)

        # tick marks
        tick_mark_group = pygame.sprite.Group()
        generate_tick_marks(radius, screen_width, tick_mark_group, background)
        
        animations = Animations(load_animations(numbers_group))

        clock = pygame.time.Clock()
        fps = 120
        frame = 0
        while True:
            #window.fill((0, 0, 0))
            frame += 1
            window.blit(background, (0, 0))

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return

            tick_mark_group.draw(window)        

            now = DateTime.now()
            hands_group.update(now)
            hands_group.draw(window)
            
            numbers_group.update(frame)
            numbers_group.draw(window)

            animations.update(frame)

            pygame.display.update()
            clock.tick(fps)
            if frame == fps:
                frame = 0
    finally:
        pygame.quit()


if __name__ == '__main__':
    main()
Antworten