23. Pohybujúce sa obrázky


Obrázkom v nejakej väčšej scéne, ktoré sa animujú, hýbu, spolupracujú navzájom, resp. sa dajú modifikovať prostredníctvom myši alebo klávesnice, sa zvykne hovoriť sprite. V predchádzajúcej prednáške sme do grafickej plochy umiestňovali nejaké animované objekty a už toto boli najjednoduchšie verzie sprite (škriatkov).


Grafická aplikácia so spritami

V dnešnej prednáške sa budeme znovu zaoberať „spritami“, ale pridáme im ďalšiu funkčnosť:

  • okrem toho, že sa grafické objekty v ploche budú nejako animovať, môžu sa automaticky po scéne pohybovať, resp. meniť smer pohybu na okraji plochy

  • objektmi nemusia byť len obrázky vo formáte PhotoImage, ale aj nakreslené útvary, napríklad farebné obdĺžniky

  • objekty budeme môcť pohybovať pomocou myši

  • v niektorých situáciách budeme riešiť aj vzájomnú polohu viacerých objektov


Automatický pohyb

Predpokladajme, že všetky grafické objekty môžu mať určený aj svoj smer (vektor) pohybu, t.j. dvojicu (dx, dy), ktorá pri tikaní časovača mení polohu objektu (x, y) (t.j. posúva objekt). Zároveň zadefinujeme správanie týchto objektov na okraji plochy: objekt sa odrazí od okrajov, podobne ako sa odráža gulečníková guľa od okrajov hracieho stola:

import tkinter
import random
from PIL import Image, ImageTk

class Anim:
    canvas = None
    gsirka = 0
    gvyska = 0

    def __init__(self, obr, x, y, dx=0, dy=0):
        self.x, self.y = x, y
        self.dx, self.dy = dx, dy
        if not isinstance(obr, list):
            obr = [obr]
        self.zoz = obr
        self.faza = 0    # random.randrange(len(obr))
        self.w2, self.h2 = obr[0].width()//2, obr[0].height()//2
        self.id = self.canvas.create_image(x, y, image=obr[0])

    def timer(self):
        if len(self.zoz) > 1:
            self.canvas.itemconfig(self.id, image=self.zoz[self.faza])
            self.faza = (self.faza + 1) % len(self.zoz)
        if self.dx!=0 or self.dy!=0:
            if self.x+self.dx < self.w2: self.dx = abs(self.dx)
            if self.y+self.dy < self.h2: self.dy = abs(self.dy)
            if self.x+self.dx > self.gsirka - self.w2: self.dx = -abs(self.dx)
            if self.y+self.dy > self.gvyska - self.h2: self.dy = -abs(self.dy)
            self.x += self.dx
            self.y += self.dy
            if self.id is not None:
                self.canvas.move(self.id, self.dx, self.dy)

#################################################

class Plocha(tkinter.Canvas):
    def __init__(self, **pparam):
        super().__init__(**pparam)
        self.pack()
        Anim.canvas = self
        Anim.gsirka = int(self['width'])
        Anim.gvyska = int(self['height'])
        self.azoz = []
        self.timer()

    def timer(self):
        for obj in self.azoz:
            obj.timer()
        self.after(100, self.timer)

    def pridaj(self, obj):
        self.azoz.append(obj)

Keďže teraz už nechceme, aby sa nové animované objekty vytvárali pomocou klikania do plochy, vytvorili sme metódu pridaj, pomocou ktorej si bude trieda Plocha pamätať všetky objekty v grafickej ploche a teda sú to tie, ktoré chceme animovať a pohybovať v časovači timer.

Na otestovanie využijeme týchto 21 obrázkov zemegule (zrejme ich budeme musieť rozstrihať):

_images/23_1.png

Trochu upravíme funkciu strihaj z minulej prednášky a otestujeme:

def strihaj(meno_suboru, n, m=1):   # m je pocet riadkov
    obr = Image.open(meno_suboru)
    zoz = []
    sir, vys = obr.width//n, obr.height//m
    x = y = 0
    for i in range(m*n):
        zoz.append(ImageTk.PhotoImage(obr.crop((x, y, x+sir, y+vys))))
        if (i+1) % n == 0:
            x = 0
            y += vys
        else:
            x += sir
    return zoz

p = Plocha(width=800, height=500, bg='green')
zemegula = strihaj('zemegula.png', 7, 3)
for i in range(5):
    x, y = random.randrange(800), random.randrange(500)
    dx, dy = random.randrange(-5, 5), random.randrange(-5, 5)
    p.pridaj(Anim(zemegula, x, y, dx, dy))

Všimnite si, že trieda Plocha je odvodená od tkinter.Canvas a preto hneď prvý príkaz v inicializácii:

super().__init__(**pparam)

vlastne zavolá:

tkinter.Canvas(**pparam)

teda vytvorí grafickú plochu s parametrami, ktoré prišli do inicializácie (napríklad pomenovaný parameter width=800 sa takto prenesie aj do tkinter.Canvas()).

Aby sme mohli pridať aj iný typ spritu (neanimovaný obdĺžnik), vytiehneme z Anim do bázovej triedy Zaklad spoločné časti:

import tkinter
import random
from PIL import Image, ImageTk

class Zaklad:
    canvas = None
    sirka = 0
    vyska = 0
    def __init__(self, x, y, dx=0, dy=0, sirka=0, vyska=0):
        self.x, self.y = x, y
        self.dx, self.dy = dx, dy
        self.w2, self.h2 = sirka//2, vyska//2
        self.id = None

    def timer(self):
        if self.dx!=0 or self.dy!=0:
            if self.x+self.dx < self.w2: self.dx = abs(self.dx)
            if self.y+self.dy < self.h2: self.dy = abs(self.dy)
            if self.x+self.dx > self.sirka - self.w2: self.dx = -abs(self.dx)
            if self.y+self.dy > self.vyska - self.h2: self.dy = -abs(self.dy)
            self.x += self.dx
            self.y += self.dy
            if self.id is not None:
                self.canvas.move(self.id, self.dx, self.dy)

#-----------------------------------------------------------------------

class Ramik(Zaklad):
    def __init__(self, x, y, dx=0, dy=0, sirka=30, vyska=30, farba=''):
        super().__init__(x, y, dx, dy, sirka, vyska)
        if farba == 'random':
            farba = f'#{random.randrange(256**3):06x}'
        self.id = self.canvas.create_rectangle(
            x-self.w2, y-self.h2, x+self.w2, y+self.h2, fill=farba)

#-----------------------------------------------------------------------

class Anim(Zaklad):
    def __init__(self, obr, x, y, dx=0, dy=0):
        if not isinstance(obr, list):
            obr = [obr]
        self.zoz = obr
        self.faza = 0
        super().__init__(x, y, dx, dy, obr[0].width(), obr[0].height())
        self.id = self.canvas.create_image(x, y, image=obr[0])

    def timer(self):
        if len(self.zoz) > 1:
            self.canvas.itemconfig(self.id, image=self.zoz[self.faza])
            self.faza = (self.faza + 1) % len(self.zoz)
        super().timer()

#################################################

class Plocha(tkinter.Canvas):
    def __init__(self, **pparam):
        super().__init__(**pparam)
        self.pack()
        Zaklad.canvas = self
        Zaklad.sirka = int(self['width'])
        Zaklad.vyska = int(self['height'])
        self.azoz = []
        self.timer()

    def timer(self):
        for obj in self.azoz:
            obj.timer()
        self.after(100, self.timer)

    def pridaj(self, obj):
        self.azoz.append(obj)

Teraz otestujeme spolu aj so zemeguľami:

p = Plocha(width=800, height=500, bg='green')
for i in range(10):
    x, y = random.randrange(800), random.randrange(500)
    dx, dy = random.randrange(-2, 2), random.randrange(-2, 2)
    p.pridaj(Ramik(x, y, dx, dy, 60, 40, farba='random'))
zemegula = strihaj('zemegula.png', 7, 3)
for i in range(5):
    x, y = random.randrange(800), random.randrange(500)
    dx, dy = random.randrange(-5, 5), random.randrange(-5, 5)
    p.pridaj(Anim(zemegula, x, y, dx, dy))

Všimnite si, že trieda Zaklad slúži len ako základná trieda (bázová, super trieda) pre triedy Ramik a Anim. Vďaka tomuto sa zjednoduší inicializácia a aj metóda timer týchto odvodených tried.


Ťahanie objektov myšou

Pridáme metódy:

import tkinter
import random
from PIL import Image, ImageTk

class Zaklad:
    canvas = None
    sirka = 0
    vyska = 0
    def __init__(self, x, y, dx=0, dy=0, sirka=0, vyska=0):
        self.x, self.y = x, y
        self.dx, self.dy = dx, dy
        self.w2, self.h2 = sirka//2, vyska//2
        self.id = None

    def vnutri(self, x, y):
        return abs(self.x-x) <= self.w2 and abs(self.y-y) <= self.h2

    def mouse_move(self, x, y):
        if self.id is not None:
            self.canvas.move(self.id, x-self.x, y-self.y)
        self.x, self.y = x, y

    def mouse_down(self):
        pass

    def mouse_up(self):
        pass

    def timer(self):
        if self.dx!=0 or self.dy!=0:
            if self.x+self.dx < self.w2: self.dx = abs(self.dx)
            if self.y+self.dy < self.h2: self.dy = abs(self.dy)
            if self.x+self.dx > self.sirka - self.w2: self.dx = -abs(self.dx)
            if self.y+self.dy > self.vyska - self.h2: self.dy = -abs(self.dy)
            self.x += self.dx
            self.y += self.dy
            if self.id is not None:
                self.canvas.move(self.id, self.dx, self.dy)

#-----------------------------------------------------------------------

class Ramik(Zaklad):
    def __init__(self, x, y, dx=0, dy=0, sirka=30, vyska=30, farba=''):
        super().__init__(x, y, dx, dy, sirka, vyska)
        if farba == 'random':
            farba = f'#{random.randrange(256**3):06x}'
        self.id = self.canvas.create_rectangle(
            x-self.w2, y-self.h2, x+self.w2, y+self.h2, fill=farba)

    def mouse_down(self):
        self.canvas.itemconfig(self.id, width=3, outline='red')

    def mouse_up(self):
        self.canvas.itemconfig(self.id, width=1, outline='black')

#-----------------------------------------------------------------------

class Anim(Zaklad):
    def __init__(self, obr, x, y, dx=0, dy=0):
        if not isinstance(obr, list):
            obr = [obr]
        self.zoz = obr
        self.faza = 0
        super().__init__(x, y, dx, dy, obr[0].width(), obr[0].height())
        self.id = self.canvas.create_image(x, y, image=obr[0])

    def timer(self):
        if len(self.zoz) > 1:
            self.canvas.itemconfig(self.id, image=self.zoz[self.faza])
            self.faza = (self.faza + 1) % len(self.zoz)
        super().timer()

#################################################

class Plocha(tkinter.Canvas):
    def __init__(self, **pparam):
        super().__init__(**pparam)
        self.pack()
        Zaklad.canvas = self
        Zaklad.sirka = int(self['width'])
        Zaklad.vyska = int(self['height'])
        self.bind('<ButtonPress-1>', self.mouse_down)
        self.bind('<B1-Motion>', self.mouse_move)
        self.bind('<ButtonRelease-1>', self.mouse_up)
        self.azoz = []
        self.tahany = None
        self.timer()

    def timer(self):
        for obj in self.azoz:
            obj.timer()
        self.after(100, self.timer)

    def mouse_down(self, event):
        for obj in reversed(self.azoz):
            if obj.vnutri(event.x, event.y):
                self.tahany = obj
                self.dx, self.dy = event.x - obj.x, event.y - obj.y
                obj.mouse_down()
                return
        self.tahany = None

    def mouse_move(self, event):
        if self.tahany is not None:
            self.tahany.mouse_move(event.x-self.dx, event.y-self.dy)

    def mouse_up(self, event):
        if self.tahany is not None:
            self.tahany.mouse_up()
            self.tahany = None

    def pridaj(self, obj):
        self.azoz.append(obj)

Poznámky:

  • v triede Plocha sme zviazali tri metódy so zodpovedajúcimi udalosťami od myši:

    • mouse_down - zatlačenie ľavého tlačidla myši

    • mouse_move - ťahanie so zatlačeným ľavým tlačidlom myši

    • mouse_up - pustenie ľavého tlačidla myši

  • pridali sme sem aj ďalší atribút self.tahany, v ktorom si pamätáme, ktorý z objektov je momentálne ťahaný

  • všimnite si, že v metóde mouse_down prechádzame všetky objekty v grafickej ploche ale v opačnom poradí (reversed), ako boli vytvorené, teda prechádzame najprv tie, ktoré vznikli ako posledné a teda sú na vrchu všetkých starších; vďaka tomuto, ak klikneme na pozíciu, pod ktorou sa nachádza naraz viac objektov, táto metóda zoberie ten z nich, ktorý je navrchu

Otestujeme rovnako ako v predchádzajúcej verzii.


Akcie pri pustení myši

Budeme riešiť takúto úlohu:

  • len niektoré objekty budú ťahateľné (self.moze_tahat = True), iné nie

  • každý objekt si bude pamätať svoje domovské miesto (kde sa narodil) v atribútoch self.x0 a self.y0 a metóda self.domov() ho tam presťahuje

  • okrem ťahateľných obrázkov zadefinujeme niekoľko statických (neťahateľných) rámikov, ktoré budú slúžiť ako cieľové políčka pre ťahateľné objekty

  • do metódy mouse_up() v triede Plocha pridáme mechanizmus, ktorý dovolí pustiť ťahateľný objekt len vo vnútri cieľového políčka, inak ho automaticky pošle domov (metóda domov())

  • všetkým objektom sme zrušili atribúty self.dx a self.dy, vďaka ktorým ich vedel timer automaticky posúvať - teraz bude timer len animovať prípadné animované obrázky

import tkinter
import random
from PIL import Image, ImageTk

class Zaklad:
    canvas = None

    def __init__(self, x, y, sirka=0, vyska=0):
        self.x, self.y = self.x0, self.y0 = x, y
        self.w2, self.h2 = sirka//2, vyska//2
        self.id = None
        self.moze_tahat = True
        self.vnom = None

    def vnutri(self, x, y):
        return abs(self.x-x) <= self.w2 and abs(self.y-y) <= self.h2

    def mouse_move(self, x, y):
        if self.id is not None:
            self.canvas.move(self.id, x-self.x, y-self.y)
        self.x, self.y = x, y

    def mouse_down(self):
        pass

    def mouse_up(self):
        pass

    def timer(self):
        pass

    def domov(self):
        self.mouse_move(self.x0, self.y0)

#-----------------------------------------------------------------------

class Ramik(Zaklad):
    def __init__(self, x, y, sirka=30, vyska=30, farba=''):
        super().__init__(x, y, sirka, vyska)
        if farba == 'random':
            farba = f'#{random.randrange(256**3):06x}'
        self.id = self.canvas.create_rectangle(
            x-self.w2, y-self.h2, x+self.w2, y+self.h2, fill=farba)

    def mouse_down(self):
        self.canvas.itemconfig(self.id, width=3, outline='red')

    def mouse_up(self):
        self.canvas.itemconfig(self.id, width=1, outline='black')

#-----------------------------------------------------------------------

class Anim(Zaklad):
    def __init__(self, obr, x, y):
        if not isinstance(obr, list):
            obr = [obr]
        self.zoz = obr
        self.faza = 0
        super().__init__(x, y, obr[0].width(), obr[0].height())
        self.id = self.canvas.create_image(x, y, image=obr[0])

    def timer(self):
        if len(self.zoz) > 1:
            self.canvas.itemconfig(self.id, image=self.zoz[self.faza])
            self.faza = (self.faza + 1) % len(self.zoz)

#################################################

class Plocha(tkinter.Canvas):
    def __init__(self, **pparam):
        super().__init__(**pparam)
        self.pack()
        Zaklad.canvas = self
        Zaklad.sirka = int(self['width'])
        Zaklad.vyska = int(self['height'])
        self.bind('<ButtonPress-1>', self.mouse_down)
        self.bind('<B1-Motion>', self.mouse_move)
        self.bind('<ButtonRelease-1>', self.mouse_up)
        self.azoz = []
        self.tahany = None
        self.ciele = []
        self.timer()

    def timer(self):
        for obj in self.azoz:
            obj.timer()
        self.after(100, self.timer)

    def mouse_down(self, event):
        for obj in reversed(self.azoz):
            if obj.moze_tahat and obj.vnutri(event.x, event.y):
                self.tahany = obj
                self.dx, self.dy = event.x - obj.x, event.y - obj.y
                obj.mouse_down()
                return
        self.tahany = None

    def mouse_move(self, event):
        if self.tahany is not None:
            self.tahany.mouse_move(event.x-self.dx, event.y-self.dy)

    def mouse_up(self, event):
        if self.tahany is not None:
            kto = self.tahany
            kto.mouse_up()

            for ciel in self.ciele:
                if ciel.vnom is kto:
                    ciel.vnom = None
            for ciel in self.ciele:
                if ciel.vnutri(kto.x, kto.y):
                    if ciel.vnom is not None:
                        ciel.vnom.domov()
                    ciel.vnom = kto
                    kto.mouse_move(ciel.x, ciel.y)
                    break
            else:
                kto.domov()

            self.tahany = None

    def pridaj(self, obj):
        self.azoz.append(obj)

Pri testovaní použijeme tieto obrázky číslic (súbor 'cislice.png' v priečinku obrazky.zip):

_images/23_2.png

Všimnite si, že 8 rámikov nie sú ťahateľné, ale môžu slúžiť ako cieľové pozície pre iné objekty:

p = Plocha(width=800, height=500, bg='green')
for i in range(8):
    r = Ramik(i*80+50, 200, 70, 70)
    r.moze_tahat = False
    p.pridaj(r)
    p.ciele.append(r)

zoz = strihaj('cislice.png', 10)
for i in range(10):
    p.pridaj(Anim(zoz[i], i*50+50, 100))

Cvičenia

tréning testu

  • úlohy riešte na papieri bez počítača


Záverečný test z Programovania (1) 2018/2019


  1. Do funkcie zisti() sme pridali riadok s kontrolným výpisom. Čo vypíše?

    def zisti(zoz):
        if len(zoz) <= 1:
            return True
        stred = len(zoz) // 2
        print(zoz[stred-1], zoz[stred], zoz[stred-1] <= zoz[stred])
        if zoz[stred-1] > zoz[stred]:
            return False
        return zisti(zoz[:stred]) and zisti(zoz[stred:])
    
    print(zisti([0, 1, 2, 3, 4, 6, 5, 7, 8, 9]))
    

  1. Rekurzívnu funkciu vypis_rek() by sme chceli prepísať na nerekurzívnu verziu. Doplň chýbajúce riadky:

    def vypis_rek(n):
        if n >= 1:
            print(n, end=' ')
            vypis_rek(n - 1)
            print(n, end=' ')
        else:
            print('*', end=' ')
    
    def vypis_nerek(n):
        p = 0
        while n >= 1:
            ________________
            print(n, end=' ')
            ________________
        print('*', end=' ')
        while p > 0:
            ________________
            print(n, end=' ')
            ________________
    

  1. Dopíš chýbajúce časti do funkcie pocet(), ktorá zistí počet výskytov nejakej hodnoty v dvojrozmernom zozname tab:

    def pocet(tab, hodnota):
        vysl = 0
        for i in range(________________):
    
            for j in range(__________________):
    
                if ________________:
    
                    _______________
        return vysl
    

  1. Zisti, čo vráti funkcia vyrob():

    def vyrob(n):
        vysl = [0] * n
        riadok = [1]
        for i in range(n):
            vysl[i] = riadok
            riadok = riadok + [riadok[-1] * 2]
        return vysl
    
    print(vyrob(5))
    

  1. Zadefinovali sme triedu Subor, v ktorej metóda __init__(meno_suboru) vytvorí nový prázdny súbor, pripis(text) pridá na koniec súboru nový riadok so zadaným textom a vypis() vypíše momentálny obsah súboru. Oprav chyby:

    class Subor:
        def __init__(self, meno_suboru):
            with open(self.meno, 'w'):
                self.meno = meno_suboru
    
        def pripis(self, text):
            with open(self.meno, 'a') as subor:
                subor.write(self.text + '\n')
    
        def vypis(self):
            with open(self.meno) as subor:
                for i in range(len(subor)):
                    print(subor[i], end='')
    

  1. Máme takto definovanú triedu Body. Zisti, čo sa vypíše:

    class Body:
        def __init__(self):
            self.body = ''
    
        def pridaj(self):
            self.body += 'a'
            return self
    
        def uber(self):
            self.body += 'b'
            return self
    
        def kolko(self):
            print(self.body.count('a') - self.body.count('b'))
            return self
    
    b = Body().uber().pridaj().pridaj().kolko().pridaj().uber().kolko()
    print(b.body)
    

  1. Máme zadefinovať triedu Kruh, pomocou ktorej sa budú reprezentovať kruhy. Pri kruhoch nás budú zaujímať len polomer, obsah a obvod. Trieda bude používať jediný (inštančný) atribút obvod a ako konštantu pi použite triedny atribút pi. Dopíšte chýbajúce časti:

    class Kruh:
        pi = 3.14159
    
        def __init__(self, r):
            self.obvod = ____________________
    
        def __str__(self):                           # vráti Kruh(polomer)
            return f'Kruh({________________})'
    
        def obsah(self):
            return _____________________
    
        def obvod(self):
            return self.obvod
    
        def polomer(self):
            return ________________________
    
        def kopia(self):
            return ________________________
    

  1. Funkcia najcastejsie() má zistiť, ktoré slovo v textovom súbore (v riadkoch sú slová oddelené medzerami) sa vyskytuje najčastejšie. Dopíšte vyznačené časti:

    def najcastejsie(meno_suboru):
        with open(meno_suboru) as subor:
            slovnik = {}
            for slovo in ____________________:
                try:
                    ______________________
                except KeyError:
                    ______________________
            slovo = None
            for kluc, hodnota in ___________________:
                if slovo is None or ______________________:
                    slovo = _________________
        return slovo
    

  1. Dopíš funkciu zip(p1, p2), ktorá z dvoch postupností rovnakých dĺžok vytvorí zoznam zodpovedajúcich dvojíc, t.j. zoznam, v ktorom prvým prvkom bude dvojica prvých prvkov postupností, druhým prvkom dvojica druhých prvkov, … atď.

    def zip(p1, p2):
        return [__________________________________________________________]
    
    >>> zip('python', [2, 3, 5, 7, 11, 13])
    [('p', 2), ('y', 3), ('t', 5), ('h', 7), ('o', 11), ('n', 13)]
    

  1. Zisti, čo vráti nasledovné volanie funkcie vsetky():

    def vsetky(*post):
        mn = set()
        for i in post:
            for j in post:
                if i < j:
                    mn.add((i, j))
        print(mn)
        return [{*p} for p in mn]
    
    print(vsetky(*[2, 5, 11, 5, 7]))