Simples Script für 2D Animationen

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Hatte in den letzten Tagen mit einfachen Animationen begonnen, ein erstes Script als kleiner Test läuft nun, zumindest mit kleineren Images. Die Hintergrundgrafik hatte für den Test 400 x 260 Pixel und die beiden Kugeln 80 Pixel im Durchmesser.

Ohne deepcopy oder alternativ die Hintergrundgrafik in jeder Runde neu zu öffnen, wurden nur die neuen Positionen zu den bereits vorhandenen hinzugefügt, also bei 100 Durchläufen 99-mal zusätzlich eingefügt, was höchstens interessant aussehen könnte, wenn man eine Umlaufbahn, Bremsspur oder ähnliches gebrauchen könnte. Bin mir nicht sicher, ob deepcopy sehr viel sparsamer ist als neu öffnen, mit größeren Grafiken habe ich es noch nicht probiert?

Als nächstes würde die Frage stehen, wie sich aus den Einzelbildern am günstigsten ein Video erstellen lässt.

Code: Alles auswählen

from tkinter import Tk, Frame, Label, Button, Checkbutton, Radiobutton, IntVar
from PIL import Image, ImageTk
from copy import deepcopy

class SimpleAnimation:
    """Eine Klasse für simple 2D Animationen mit Image-Objekten"""

    def __init__(self, im_bg = None, im_01 = None, im_02 = None):
        self.fenster = Tk()
        self.fenster.title("Simple Animationen")
        self.fenster.wm_iconbitmap("logo.ico")
        self.fenster.config(bg = "#808080")

        # Aufteilung in oberen und unteren Frame mit Buttons usw.
        self.frame_o = Frame(self.fenster, bg = "#808080", padx = 10)
        self.frame_u = Frame(self.fenster, bg = "#6f6352", padx = 10)
        self.fenbild = Label(self.frame_o, bd = 0)
        self.control = Label(
                       self.frame_u, bd = 4, bg = "#000", fg = "#40ff40",
                       width = 20, relief = "sunken", pady = 4)
        self.speichr = IntVar()
        self.checker = Checkbutton(
                       self.frame_u, text = "Mit Speicherung?",
                       bg = "#6f6352", fg = "#e2aa00",
                       variable = self.speichr,
                       command  = self.setze_auswahl)
        self.laufmal = IntVar()
        self.radio01 = Radiobutton(
                       self.frame_u, text = "50", bg = "#6f6352",
                       fg = "#e2aa00", value = 50,
                       variable = self.laufmal,
                       command  = self.setze_auswahl)
        self.radio02 = Radiobutton(
                       self.frame_u, text = "100", bg = "#6f6352",
                       fg = "#e2aa00", value = 100,
                       variable = self.laufmal,
                       command  = self.setze_auswahl)
        self.radio03 = Radiobutton(
                       self.frame_u, text = "150", bg = "#6f6352",
                       fg = "#e2aa00", value = 150,
                       variable = self.laufmal,
                       command  = self.setze_auswahl)
        self.radio04 = Radiobutton(
                       self.frame_u, text = "200", bg = "#6f6352",
                       fg = "#e2aa00", value = 200,
                       variable = self.laufmal,
                       command  = self.setze_auswahl)
        self.starter = Button(
                       self.frame_u, text = "Start",
                       font = ("cambria", 10, "bold"), padx = 12,
                       command = self.bewege_objekte)

        self.frame_o.pack(side = "top")
        self.fenbild.pack(padx = 20, pady = 20)
        self.frame_u.pack(side = "bottom", fill = "x")
        self.control.pack(side = "left", padx = 20, pady = 4)
        self.checker.pack(side = "left", padx =  2, pady = 4)
        self.radio01.pack(side = "left")
        self.radio02.pack(side = "left")
        self.radio03.pack(side = "left")
        self.radio04.pack(side = "left")
        self.radio01.select()
        self.starter.pack(side = "left", padx =  2, pady = 4)

        # Images vor und nach dem Öffnen
        self.iopen_bg = im_bg
        self.iopen_01 = im_01
        self.iopen_02 = im_02
        self.image_bg = None
        self.image_01 = None
        self.image_02 = None
        self.label_bg = None
        self.img_temp = None

        # Positionen bei Start und bei Veränderungen
        self.posx_01 = 0
        self.posy_01 = 0
        self.posx_02 = 0
        self.posy_02 = 0
        self.startwt = 0
        self.endwert = 0
        self.speichg = 0
        self.bildnum = 0

    def main(self, px01 = None, py01 = None, px02 = None, py02 = None):
        if self.iopen_bg != None:
            self.image_bg = Image.open(self.iopen_bg)
        if self.iopen_01 != None:
            self.image_01 = Image.open(self.iopen_01).convert("RGBA")
        if self.iopen_02 != None:
            self.image_02 = Image.open(self.iopen_02).convert("RGBA")

        if px01 != None:
            self.posx_01 = px01
        if py01 != None:
            self.posy_01 = py01
        if px02 != None:
            self.posx_02 = px02
        if py02 != None:
            self.posy_02 = py02

        if self.iopen_bg != None:
            self.positioniere_objekte()

    def positioniere_objekte(self):
        self.img_temp = deepcopy(self.image_bg)
        # Die bewegten Objekte in jeder Runde neu ausrichten.
        if self.iopen_01 != None:
            self.img_temp.paste(
            self.image_01, (self.posx_01,self.posy_01), self.image_01)
            if self.iopen_02 != None:
                self.img_temp.paste(
                self.image_02, (self.posx_02,self.posy_02), self.image_02)

            # Speichern aller Einzelbilder einer Animation
            if self.speichg == 1:
                self.bildnum += 1
                ablage = "temps/temp-{0:03d}.png".format(self.bildnum)
                self.img_temp.save(ablage, "PNG")

        if self.iopen_bg != None:
            self.label_bg = ImageTk.PhotoImage(self.img_temp)
            self.fenbild.config(image = self.label_bg)

    def bewege_objekte(self):
        if self.startwt < self.endwert:
            self.fenster.after(200, self.bewege_objekte)
            self.startwt += 1
            # Berechnung der Bahnen
            self.posy_01 += 1
            self.posy_02 += 1
            if self.startwt < 110:
                self.posx_01 += 1
                self.posx_02 -= 1

            self.positioniere_objekte()
            # Als Anzeige der laufenden Werte
            ausg  = "{0:d} - {1:d} | ".format(self.posx_01,self.posy_01)
            ausg += "{0:d} - {1:d} | ".format(self.posx_02,self.posy_02)
            ausg += "E {0:d}".format(self.endwert)
            self.control["text"] = ausg

    def setze_auswahl(self):
        self.endwert = self.laufmal.get()
        self.speichg = self.speichr.get()

objekt00 = "images/background.jpg"
objekt01 = "images/kugel-01.png"
objekt02 = "images/kugel-02.png"

if __name__ == "__main__":
    instanz = SimpleAnimation(objekt00, objekt01, objekt02)
    instanz.main(10,10,308,10)
    instanz.fenster.mainloop()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Melewo: wenn man anfängt, Variablennamen durchzunummerieren, dann will man eigentlich eine andere Datenstruktur (Liste) verwenden. Wie läßt sich Dein Programm auf drei, vier, fünf bewegte Objekte erweitern? Nur sehr schwer und mit großen Risiko, dass irgendwo an den vielen Stellen, wo man etwas ändern muß, ein Fehler einschleicht oder eine Stelle übersehen wird. Apropos Variablennamen: was soll iopen heißen? Oder startwt? speichr und speichg? gibt es auch noch einen speichl? Python kann mit mehr als 7 Buchstaben umgehen, und das solltest Du auch nutzen. In ein paar Tagen weißt Du nicht mehr, was diese kryptischen Abkürzungen bedeutet haben und mußt jedesmal das ganze Programm durchsuchen, was denn damit gemeint sein könnte.

Die ganzen GUI-Elemente an Attribute zu binden, ist unnötig. Eigentlich braucht man bei TK außer ein paar IntVar/StrVar-Objekten fast nichts mehr nach der Initialisierung. Das können also alles lokale Variablen sein. Bilddateiname, Bildobjekt, Bildposition: all das wird in unterschiedlichen Variablen an unterschiedlichen Stellen initialisiert. Das ist sehr unübersichtlich. Warum wird die Position erst in «main» gesetzt, der Bilddateiname aber schon in «__init__»? Die ganze Funktion »main« scheint mir fehl am Platz zu sein.

Auf None wird per »is None« bzw. »is not None« geprüft nicht mit »==« oder »!=«. Sind in » positioniere_objekte« die Einrückungen wirklich so gemeint? Die Prüfung von »iopen_bg« am Ende ist überflüssig, falls die Bedingung nicht erfüllt ist, schon vorher Probleme auftauchen könnten.
Zeile 117: es gibt boolsche Werte True und False in Python, so dass man nicht auf 0 und 1 zurückgreifen muß.
Zeile 128: startwt schein nichts mit start zu tun zu haben, weil sich die Variable ja verändert.
BlackJack

@Melewo: Was das `deepcopy()` angeht, da ist es sicher effizienter die PIL-Methoden zum Kopieren zu verwenden. Also zum Beispiel das man den Hintergrund in das Bildobjekt kopiert, dass für die Anzeige verwendet wird, so wie man das mit den Objekten auch macht.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Gut, das mit dem "is" und "is not" werde ich mir mal merken und das mit auf True und False prüfen ebenfalls. Die Einrückungen habe ich so gemeint, denn wenn iopen_01 nicht offen ist, braucht auch nichts gespeichert werden und auch nicht nach einem weiteren iopen_ gesucht werden und nur noch das iopen_bg geladen werden. Und wenn das auch nicht vorhanden sein sollte, wird ja die Methode als solche nicht angesprochen. Das mit den Bezeichnern und deren Benennung sehe ich nicht ganz so verbissen, meisten notiere ich etwas beim Definieren, wie halt

Code: Alles auswählen

iopen_    # für Image.open
speichg   # für Speicherung
und dann weiß man auch noch nach 3 Jahren, was man sich dabei dachte und für was die gut sind. Aber ja, das Script ist verbesserungswürdig, sehe ich ein, war ja erst einmal nicht mehr als etwas zum Üben und um zu hören, was man besser machen könnte.
BlackJack

@Melewo: Das mit den Bezeichnern solltest Du aber ”verbissenner” sehen. Quelltext wird hauptsächlich von Menschen gelesen und deshalb ist es wichtig das für Menschen möglichst lesbar und verständlich zu schreiben. Und wenn Du einen kurzen Kommentar brauchst um zu verstehen was ein Name bedeutet, warum dann nicht gleich den Namen richtig wählen? Dann braucht man keinen Kommentar, und vor allem steht dann an jeder Stelle im Quelltext der verständliche Name und man muss nicht nach einem Kommentar suchen.

Wobei die Kommentare in diesem Fall auch nicht besser sind. Was soll denn das `Image.open` bedeuten? Das ist doch völlig egal wie ein Bild zustande gekommen ist, wichtig ist doch nur das es ein Bild ist. Und selbst das ist eine zweitrangige Information, denn wirklich wichtig ist was das Bild im Kontext des Programms bedeutet. Die anderen beiden Informationen, das es ein Bild ist und wie das mal zustande kam, kann man notfalls auch noch aus dem Quelltext lesen.

Bei `Speicherung` ist auch der Kommentar ziemlich nichtssagend.

Du scheinst dafür sehr viel Wert auf untereinander ausgerichtete ``=``-Zeichen zu legen, was so überhaupt nichts verständlicher macht, aber einiges umständlicher wenn man den Quelltext verändern möchte. Und in Diffs dann die tatsächlichen inhaltlichen Änderungen hinter kosmetischen Änderungen versteckt.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Eigentlich schreibe ich nicht immer für Menschen. Bei meinen umfangreicheren PHP-Scripts, bei denen kann ich recht gut damit leben, dass die niemand von innen kennt und niemand weiß, wo die liegen. Gut, dieses Script werde ich voraussichtlich noch einmal überarbeiten, dass mit dem is not hatte ich schon geändert. Dann noch etwas gesucht und als mögliche Aussage fand ich dieses Ergebnis vom Translator fürs allgemeine Verständnis hilfreich:
Die Anweisung 'is' wird für die Objektidentität verwendet, sie prüft, ob sich Objekte auf dieselbe Instanz beziehen (gleiche Adresse im Speicher).
Und die '==' Aussage bezieht sich auf Gleichheit (gleicher Wert).
https://stackoverflow.com/questions/265 ... d-foo-none

Ergibt als Erklärung einen Sinn für mich, falls die so richtig sein sollte.

Dass man eigentlich außer IntVar und StrVar bei Tk nichts oder zumindest nicht vielmehr zu initialisieren brauchte, was sich nur auf die Gestaltung eines Widgets bezieht, da sind mir bisher mehr Beispiele über den Weg gelaufen, wo ziemlich viel initialisiert war. Nun sind mir erst einige Beispiele aufgefallen, wo sparsamer damit umgegangen wurde. Da werde ich bei Gelegenheit noch einige Tests machen, wie es sich am günstigsten umsetzen lässt. Nicht nur was dieses Script hier anbelangt, so allgemein denke ich, bei diesem aber auch.
BlackJack

@Melewo: Man schreibt immer für Menschen, oder bist Du selbst keiner? ;-) Der Fremde der den Quelltext lesen können muss ist man doch auch selbst wenn man ein paar Monate nicht in den Quelltext geschaut hat.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Dieses Unverständnis, was ich da gemacht habe, welche Variable für was steht und was ich mir dabei dachte, liegt glücklicherweise bisher nur bei einem JS-Script vor. Wollte es vor einigen Wochen überarbeiten, gab aber nach einigen Stunden auf. Die Notizen mit einer nicht gerade kurzen Formel aus der Wikipedia hatte ich zwar in einem Sammelordner abgeheftet, nur mich nun erneut wieder durchzuwursteln, welche var für welchen Wert steht, da gab ich nach einigen Stunden auf und verschob es auf spätere Zeiten.

Gut, ich bin heute zu der Einsicht gelangt, ich habe noch reichlich viel zu lernen. Wenn das Script zumindest für den privaten Gebrauch etwas werden soll, müssen es drei Fenster sein, erste Vorstellungen sind seit heute vorhanden. Das Fenster, in dem die Vorschau läuft, sollte auch nur für die Vorschau bleiben.
Antworten