Pripravil sem nam zanimiv modul. Imenuje se risar. Čim ga uvozimo, odpre okno, v katerega lahko s funkcijami, ki so v modulu, rišemo črte, kroge, po njem pišemo, vanj vstavljamo slike...

Datoteko risar.py shranimo v direktorij, v katerem je program, ki ga poganjamo, ali pa v direktorij, v katerem se nahaja naš projekt v PyCharmu.

Za uporabo Risarja si boste morali namestiti Qt. Za tole, kar bomo z njim počeli na teh in naslednjih predavanjih, je Qt nekoliko neroden. Izbrali pa smo si ga, ker je relativno preprost, predvsem pa gre za močno orodje za sestavljanje uporabniških vmesnikov (za kar ga bomo čez nekaj časa tudi uporabili).

Modul vsebuje naslednje funkcije

risar.crta(x0, y0, x1, y1, barva=bela, sirina=1)
Nariše črto od (x0, y0) do (x1, y1). Zadnja dva argumenta, barvo in širino črte (v točkah na zaslonu) smemo izpustiti; v tem primeru dobimo belo črto širine ena točka.
risar.tocka(x, y, barva=bela)
Nariše piko na koordinatah (x, y). Če ne določimo barve, bo pika bela.
risar.krog(x, y, r, barva=bela, sirina=1)
Nariše krog s polmerom r in središčem na koordinatah (x, y). Z zadnjima argumentoma je pa tako kot pri črti.
risar.elipsa(x, y, rx, ry, barva=bela, sirina=1)
Nariše elipso s polmeroma rx in ry ter središčem na koordinatah (x, y).
risar.besedilo(x, y, s, barva=bela, velikost=20, pisava="Arial")
Izpiše niz s tako, da je zgornji levi vogal na koordinatah (x, y). Zadnji trije argumenti, ki jih lahko tudi izpustimo, določijo barvo, velikost in pisavo ter imajo privzete vrednosti, kot piše.
risar.slika(x, y, ime)
Naloži sliko iz datoteke z imenom ime in jo postavi tako, da je njen gornji levi vogal na koordinatah (x, y).
risar.barva_ozadja(barva)
Nastavi barvo ozadja
risar.barva(r, g, b)
Sestavi in vrne barvo z danimi deleži rdeče, zelene in modre (argumenti morajo biti med 0 in 255).
risar.nakljucna_barva()
Vrne naključno barvo.
risar.nakljucne_koordinate()
Vrne naključne koordinate kot terko (x, y).
risar.obnovi()
S to funkcijo zahtevamo, da se slika ponovno izriše. Ali se slika obnovi sama od sebe ali je potrebno poklicati obnovi, je odvisno predvsem od tega, na kakšen način (iz kakšnega okolja) poganjamo Python.
risar.obnavljaj
ni funkcija, temveč spremenljivka, ki jo lahko postavimo na True ali False. Privzeta vrednost je False; če jo postavimo na True se bo po vsakem klicu funkcij za risanje poklicala tudi funkcija obnovi. Risanje bo s tem počasnejše, vendar bomo sproti videli, kaj se dogaja s sliko.
risar.cakaj(t)
Ustavi program za t sekund, pri čemer je lahko t tudi necelo število, npr. 0.01 (stotinka sekunde). Funkcijo uporabljajmo namesto time.sleep, ki tule ne bi delovala povsem pravilno.
risar.stoj()
To funkcijo pokličemo, da se program ustavi, ne da bi se zaprl. Če jo izpustimo, bo program le naredil, kar smo mu rekli in okno se bo takoj nato zaprlo. (Zgodba za to funkcijo je sicer nekoliko bolj zapletena - v resnici programa ne ustavi, temveč ga na nek način šele zares požene. A o tem se bomo učili čez dva ali tri tedne.)

Koordinatni sistem je malo drugačen, kot ste ga vajeni: točka (0, 0) ni v spodnjem, temveč v zgornjem levem vogalu, in koordinata y narašča navzdol, ne navzgor - nižje ko gremo, večji je y. Če se zdi to komu nenavadno, naj razmisli, kako šteje vrstice na zaslonu: je prva vrstica čisto zgoraj ali čisto spodaj? No, to je isto. Koordinatni sistemi v računalniku so navadno obrnjeni tako.

Spodnja desna točka ima koordinati (risar.maxX-1, risar.maxY-1).

Kot argumente, s katerimi sporočamo barvo, lahko uporabimo bodisi to, kar vrne funkcija risar.barva bodisi konstante risar.bela, risar.crna, risar.rdeca, risar.zelena, risar.modra, risar.vijolicna, risar.rumena, risar.siva, risar.rjava. Vse te barve so zbrane tudi v terki risar.barve.

Črte

Ko si človek pripravi tak imeniten modul, si seveda ne more kaj, da ne bi takoj narisal slike, na kateri je sto naključnih pisanih črt različnih debelin. Ne bo imel veliko dela.

import risar
from random import randint

for i in range(100):
    x0, y0 = risar.nakljucne_koordinate()
    x1, y1 = risar.nakljucne_koordinate()
    barva = risar.barva(randint(0, 255), randint(0, 255), randint(0, 255))
    sirina = randint(2, 20)
    risar.crta(x0, y0, x1, y1, barva, sirina)
risar.stoj()

V njem se ne dogaja nič zapletenega: naložimo modul risar, iz modula random pa funkcijo randint, ki nam žreba naključna cela števila znotraj podanega intervala. Nato si stokrat izberemo začetne in končne koordinate, barvo in širino ter narišemo črto.

Program je preprost, veličina umetnosti pa nesporna.

Besede

Začutili smo umetniško žilico; škoda bi bilo ne nadaljevati v tem slogu. Napišimo funkcijo, ki razkosa besedilo, ki ga podamo kot argument, na besede in jih v pisanih barvah razmeče po sliki.

import risar
from random import randint

def besedici(besedilo):
    for beseda in besedilo.split():
        risar.besedilo(
            randint(0, 750), randint(0, 400), beseda, risar.nakljucna_barva(),
            velikost=randint(30, 80), pisava="Calibri")

besedici(open("krst .txt").read())
risar.stoj()

Rezultat je gotovo vreden diplome na ALUO. ;)

Za doktorat pa je morda potrebne še nekaj multikulturnosti.

Da, da, to bi moralo zadoščati. ;)

Face

Isto bi lahko naredili tudi s slikami - recimo slikami, ki so jih študenti, ki poslušajo Programiranje 1, naložili v Moodle in jih imamo slučajno na disku v poddirektoriju "slike" (da se boste lahko igrali z njimi sem vam jih zapakiral v tale arhiv; gre za predlanske slike, saj zdaj slike niso v modi in je ni naložil skoraj nihče).

for fn in os.listdir("slike"):
    risar.slika(randint(0, risar.maxX-100), randint(0, risar.maxY-100),
                "slike/"+fn)

Ponovno gre brez vsakega dvoma za umetniško stvaritev, morda celo inštalacijo. Še več, ni vrag, da ne gre za virtualno inštalacijo. No, tega ne vem, a dasiravno je razpored slik naključen, je globoki simbolizem nastale slikovne kompozicije, ki poudarja brezčasno stalnost človeškega čutenja v odnosu do presežnega, razumljiv celo popolnim analfabetom umetnosti.

Trikotnik Sierpinskega

V svojem iskanju lepote se zatecimo tja, kjer je je največ (morda to vseeno ni ALUO): v matematiko. Poiščimo jo tako, da znotraj trikotnika narišemo obrnjen trikotnik, s čimer ga razdelimo na štiri trikotnike.

Potem to ponovimo vse skupaj na treh "zunanjih" trikotnikih. Kako "ponovimo vse skupaj"? Pač tako, da tudi v vsakega od teh treh trikotnikov vrišemo narobe obrnjen trikotnik in potem ponovimo vse skupaj.

Ponovimo vse skupaj? Da, v vsakem od po treh (zdaj skupaj že devetih) novonastalih narišemo narobe obrnjen trikotnik in potem ponovimo vse skupaj.

Pa znamo to sprogramirati? I, seveda. Da nam bo lažje, najprej napišimo funkcijo, ki nariše trikotnik med točkami A, B in C, podanimi s koordinatami Ax, Ay, Bx, By, Cx, Cy.

def trikotnik(Ax, Ay, Bx, By, Cx, Cy):
    risar.crta(Ax, Ay, Bx, By)
    risar.crta(Bx, By, Cx, Cy)
    risar.crta(Cx, Cy, Ax, Ay)

Takole smo narisali črto od A do B, od B do C in od C do A. Zdaj pa narišimo zunanji trikotnik.

Ax, Ay =  10, 475
Bx, By = 537, 475
Cx, Cy = 271,  10
trikotnik(Ax, Ay, Bx, By, Cx, Cy)

Do sem je preprosto. Naprej pa tudi, saj se rekurzije ne bojimo. Koordinate notranjega trikotnika izračunamo, kot kaže slika - notranje točke so pač na polovici med zunanjimi. Napisati moramo funkcijo za razdeljevanje trikotnika, ki dobi tri točke (oglišča trikotnika) in naredi tole:

  • znotraj trikotnika, ki ga opisujejo te točke, izriše obrnjeni trikotnik,
  • za vsakega od treh zunanjih trikotnikov pokliče funkcijo za razdeljevanje trikotnika.

    def sierpinski(Ax, Ay, Bx, By, Cx, Cy):
        ABx, ABy = (Ax + Bx) / 2, (Ay + By) / 2
        BCx, BCy = (Bx + Cx) / 2, (By + Cy) / 2
        CAx, CAy = (Cx + Ax) / 2, (Cy + Ay) / 2
        trikotnik(ABx, ABy,   BCx, BCy,   CAx, CAy)
        sierpinski(Ax, Ay,     ABx, ABy,   CAx, CAy)
        sierpinski(ABx, ABy,   Bx, By,     BCx, BCy)
        sierpinski(CAx, CAy,   BCx, BCy,   Cx, Cy)
    

Samo še en detajl moramo rešiti: gornja reč se nikoli ne ustavi. Funkcija riše manjše in manjše in manjše trikotnike, v neskončnost. Da to preprečimo, bomo dodali še en argument, ki bo povedal, kolikokrat še želimo razdeliti trikotnik. Po vsaki delitvi, pri vsakem rekurzivnem klicu bomo ta parameter zmanjšali: ko funkcija, ki ji je rečeno, da je potrebno trikotnik razdeliti še štirikrat, kliče samo sebe, si bo rekla, da ga je treba zdaj pa samo še trikrat. Ko ga je potrebno ničkrat, funkcija ne bo naredila ničesar več.

def sierpinski(Ax, Ay, Bx, By, Cx, Cy, n):
    if n == 0:
      return
    ABx, ABy = (Ax + Bx) / 2, (Ay + By) / 2
    BCx, BCy = (Bx + Cx) / 2, (By + Cy) / 2
    CAx, CAy = (Cx + Ax) / 2, (Cy + Ay) / 2
    trikotnik( ABx, ABy,   BCx, BCy,   CAx, CAy)
    sierpinski(Ax, Ay,     ABx, ABy,   CAx, CAy,  n-1)
    sierpinski(ABx, ABy,   Bx, By,     BCx, BCy,  n-1)
    sierpinski(CAx, CAy,   BCx, BCy,   Cx, Cy,  n-1)

In pokličemo:

trikotnik(Ax, Ay, Bx, By, Cx, Cy)
sierpinski(Ax, Ay, Bx, By, Cx, Cy, 6)

Lepo, ne?

Kar smo dobili je zelo znana reč, tudi ime ima: trikotnik Sierpinskega.

Naloga za frajerje (da ne boste izgubljali časa samo z grdimi programi v eni vrstici): napiši funkcijo tako, da bo narisala najprej veliki obrnjeni trikotnik, nato vse tri manjše obrnjene trikotnike, nato še vseh 9 še manjših ... in tako naprej. Funkcija pa naj bo še vedno rekurzivna (z malo iteracije od zunaj (tole je bil namig)).

Omahljivi vojaki

Trije hudobni desetniki poveljujejo veliki četi vojakov. Desetniki se postavijo v oglišča trikotnika in eden od njih zavpije "k meni"! Vojaki se poženejo proti njemu, a ko pridejo ravno na pol poti, eden od desetnikov (morda isti, morda kateri drugi), zavpije "k meni". Spet, čim pridejo na pol poti do njega, eden od desetnikov (naključno izbrani), zavpije "k meni". Tedaj eden od vojakov omaga, se zgrudi sredi polja, ostali pa tečejo. Na pol poti spet dobijo nov ukaz, eden od vojakov omahne ... in tako naprej. Če bi narisali polje, na katerem potekajo te "vojaške vaje" in vsako mesto, kjer je omahnil kak vojak, označili s piko: kaj bi dobili?

import risar
from random import randint

desetnik = [(10, 475), (537, 475), (273, 10)]
ceta_x = randint(0, 600)
ceta_y = randint(0, 600)

for i in range(50000):
    tuli = randint(0, 2)
    kje_tuli = desetnik[tuli]
    ceta_x = (ceta_x + kje_tuli[0]) / 2
    ceta_y = (ceta_y + kje_tuli[1]) / 2
    risar.tocka(ceta_x, ceta_y, risar.bela)
    if i < 500 or i < 10000 and i % 100==0:
        risar.cakaj(0.01)

V desetnik smo shranili mesta, kjer stojijo desetniki, ceta_x in ceta_y pa sta trenutni koordinati čete. Nato petdeset tisočkrat ponovimo tole: izberemo, kateri desetnik bo zatulil, ničti, prvi ali drugi. Iz seznama desetnik preberemo, koordinate tulečega desetnika. Nato izračunamo, kje je pol poti med trenutnim položajem čete (katere koordinati sta ceta_x in ceta_y) in desetnikom (čigar koordinati sta kje_tuli[0] in kje_tuli[1]). Na tem mestu narišemo piko. Po petdeset tisoč ponovitvah in petdeset tisoč omahnjenih vojakih dobimo takšnole sliko

Čudno, a ni?

(Zakaj je ta slika točno taka, kot je, smo povedali na predavanjih. Če koga zanima več o tem, pa si bo ogledal IFS fraktale.

Kaj vrača risar

Pri opisu modula risar sem zamolčal en detajl: funkcije vračajo rezultate. Kar v resnici naredijo te funkcije, je tole: sestavijo grafični objekt - črto, besedilo, sliko, karkoli že -, ga postavijo v okno (v resnici: postavijo ga na nekaj, čemur bomo nekoč morda rekli canvas) in ga vrnejo.

Če hočemo spremljati spodnje, moramo odpreti Python iz terminala, če delamo s PyCharmom, pa postaviti risar.obnavljaj na True.

>>> import risar
>>> risar.obnavljaj = True   # ce nismo v konzoli!
>>> slika = risar.slika(370, 90, "8578.jpg")
>>> slika
<PyQt4.QtGui.QGraphicsPixmapItem object at 0x0248CB70>
>>> print slika
<PyQt4.QtGui.QGraphicsPixmapItem object at 0x0248CB70>

Spremenljivka slika zdaj predstavlja narisani objekt, sliko iz datoteke "8578.jpg", ki smo jo postavili na koordinate (370, 90). Objekta ni mogoče izpisati, ne brez printa ne z njim nam o sebi ne pove ničesar. To nam ni novo; nekatere reči, recimo številke, nize, sezname, je mogoče izpisati, drugih, na primer datotek, funkcij in modulov, ne moremo in če jih poskušamo, izvemo le, za kakšen tip spremenljivke gre.

>>> import math
>>> math.sqrt
<built-in function sqrt>
>>> math
<module 'math' (built-in)>
>>> risar
<module 'risar' from 'risar.pyc'>

O objektu, ki mu tule pravimo slika, torej lahko izvemo le, da je tipa PyQt4.QtGui.QGraphicsPixmapItem. Ime tipa je čudno - doslej smo bili vajeni imen, kot so int ali list ali bool. To je pač malo daljše.

Podobno je z vsem drugim, kar dobimo od risarja, na primer s črtami in besedilom:

>>> crta = risar.crta(10, 10, 20, 20)
>>> crta
<PyQt4.QtGui.QGraphicsLineItem object at 0x0248CF60>
>>> beseda = risar.besedilo(10, 10, "tralala")
>>> beseda
<PyQt4.QtGui.QGraphicsTextItem object at 0x024B1270>

Vidimo, da so različnih tipov: slike so QGraphicsPixmapItem (tisto spredaj je pravzaprav le ime nekega modula), črte so QGraphicsLineItem in besedila QGraphicsTextItem.

Čeprav objekta ne moremo izpisati, pa moremo z njim početi kaj drugega. Lahko ga, recimo, vprašamo po njegovih koordinatah: temu sta namenjeni metodi x() in y().

>>> slika.x()
370.0
>>> slika.y()
90.0

Objekt-sliko lahko tudi prestavljamo naokrog.

>>> slika.setPos(375, 90)

Ali pa kar vozimo naokrog

>>> for i in range(0, 700, 5):
...     slika.setPos(i, 88)
...     risar.cakaj(0.05)

Ob tem ste morda opazili nekaj utripanja. V resnici tale modul ni namenjen ravno animacijam; sliko brez utripanja lažje dobimo z neko povsem drugo knjižnico, PyGame.

Spomnimo se programa, ki je naključno razmetal slike študentov po sliki. Nadežujmo jih.

face = []
for fn in os.listdir("slike"):
    faca = risar.slika(randint(0, risar.maxX-100), randint(0, risar.maxY-100),
                       "slike/" + fn)
    face.append(faca)

for i in range(500):
    for faca in face:
        faca.setPos(faca.x(), faca.y() + 1)
    risar.cakaj(0.01)

Vse sestavljene slike zdaj brižljivo shranjujemo v seznam face. Ko so razmetane, petstokrat naredimo tole: gremo čez vse obraze (for face in face) ter vsakemu obrazu nastavimo nove koordinate, ki so enake starim, le y je za eno večji.

Morbidno. Študenti s tem programom "zatonejo". Veliko lepše bi bilo, če bi padali z različnimi hitrostmi (saj študenti tudi v resnici ne padejo vsi hkrati) in se potem znova pojavljali na vrhu. Takole:

face = []
hitrosti = []
for fn in os.listdir("slike"):
    faca = risar.slika(randint(0, risar.maxX-100), randint(0, risar.maxY-100),
                       "slike/" + fn)
    face.append(faca)
    hitrosti.append(2 + random() * 3)

for i in range(5000):
    for i in range(len(face)):
        faca = face[i]
        hitrost = hitrosti[i]
        faca.setPos(faca.x(), faca.y()+hitrost)
        if faca.y() > risar.maxY:
            faca.setPos(randint(0, risar.maxX - 100), -100)
            hitrosti[i] = 0.5 + random() * 5
    risar.cakaj(0.01)

Poleg seznama fac imamo zdaj še seznam hitrosti; vsakič, ko dodamo obraz, dodamo tudi njegovo hitrost. Zanko zdaj spremenimo: namesto for faca in face: imamo for i in range(len(face)):, da lahko iz seznamov privlečemo obraze in hitrosti, z faca = face[i] in hitrost = hitrosti[i]. Obraza zdaj ne spuščamo več za 1, temveč za hitrost.

Poleg tega smo dodali še en pogoj: če je koordinata y presegla največjo (obraz je potonil), postavimo obraz na naključno mesto na vrhu slike (oziroma tik nad njo -100; 100 je višina slike) ter damo objektu novo začetno hitrost.

Če stvar obrnemo malo drugače, pa bodo študenti poskakovali gor in dol: pogoj zamenjamo z

if not (0 < faca.y() < risar.maxY - 100):
    hitrosti[i] = -hitrosti[i]

Vsakič, ko se nekdo zaleti v gornji ali spodnji rob (torej: ko se ne nahaja več znotraj robov, se obrne - njegova hitrost se spremeni iz pozitivne v negativno ali obratno.

Vendar še nismo končali s temi neumnostmi: naj face letijo povsod naokrog, v vse smeri, se odbijajo od vseh robov. Namesto seznama hitrosti bomo imeli zdaj hitrosti v smeri x in y (vx in vy). V začetku bosta obe naključni. V vsakem koraku povečamo koordinato x za vx in y za vy. Ko se faca znajde izven intervala [0, maxX], to pomeni, da se je zaletela bodisi v levo bodisi v desno steno in ji je potrebno zamenjati hitrost v smeri x (če je šla prej levo (in se zaletela v levo steno) bo šla zdaj v desno in obratno). Podobno naredimo še v smeri y. Celoten program je takšen:

import risar, os
from random import randint, random

face = []
vx = []
vy = []
for fn in os.listdir("slike"):
    faca = risar.slika(randint(0, risar.maxX-100), randint(0, risar.maxY-100),
                       "slike/"+fn)
    face.append(faca)
    vx.append(2+random() * 3)
    vy.append(2+random() * 3)

for i in range(5000):
    for i in range(len(face)):
        faca = face[i]
        faca.setPos(faca.x() + vx[i], faca.y() + vy[i])
        if not (0 < faca.x() < risar.maxX - 35):
            vx[i] = -vx[i]
        if not (0 < faca.y() < risar.maxY - 35):
            vy[i] = -vy[i]
    risar.cakaj(0.01)

Preden pripeljemo stvar, kamor pelje (do objektnega programiranja!), samo še pokažimo, kako močno je, kar se skriva v ozadju. Dodajmo še nekaj fac, ki niso face, temveč besede in črte.

import risar, os
from random import randint, random

face = []
vx = []
vy = []
for fn in os.listdir("slike"):
    faca = risar.slika(randint(0, risar.maxX - 100), randint(0, risar.maxY - 100),
                       "slike/" + fn)
    face.append(faca)
    vx.append(2 + random() * 3)
    vy.append(2 + random() * 3)

besedilo = """Valjhun sin Kajtimara boj krvavi
ze dolgo bije za krscansko vero
z Avreljem Droh se vec mu v bran ne stavi
koncano njuno je in marsiktero
zivljenje kri po Kranji Korotanji
prelita napolnila bi jezero"""
for beseda in besedilo.split():
    faca = risar.besedilo(randint(0, 750), randint(0, 400),
                          beseda, risar.nakljucna_barva(),
                          velikost=randint(15, 50), pisava="Calibri")
    face.append(faca)
    vx.append(2 + random() * 3)
    vy.append(2 + random() * 3)

for i in range(10):
    x0, y0 = randint(0, risar.maxX), randint(0, risar.maxY)
    x1, y1 = randint(0, risar.maxX), randint(0, risar.maxY)
    barva = risar.barva(randint(0, 255), randint(0, 255), randint(0, 255))
    sirina = randint(2, 20)
    crta = risar.crta(x0, y0, x1, y1, barva, sirina)
    face.append(crta)
    vx.append(2 + random() * 3)
    vy.append(2 + random() * 3)

for i in range(5000):
    for i in range(len(face)):
        faca = face[i]
        faca.setPos(faca.x() + vx[i], faca.y() + vy[i])
        if not (0 < faca.x() < risar.maxX - 35):
            vx[i] = -vx[i]
        if not (0 < faca.y() < risar.maxY - 35):
            vy[i] = -vy[i]
    risar.cakaj(0.01)

Odpustimo si, da program ni podoben ničemur; no, ni res, program je podoben špagetu, koda v njem se ponavlja in očitno bi bilo lepše uporabiti funkcije. Program bomo skrajšali na nov, večini še neznan način. Zanimivo je nekaj drugega: zadnji konec programa, zanka, ki premika in odbija objekte naokrog, deluje ne glede na to, kakšne objekte dobi. Iz poprejšjnega preskušanja vemo: objekti, ki smo jih nametali v face so tipov QGraphicsPixmapItem, QGraphicsLineItem in QGraphicsTextItem. Ko torej rečemo faca = face[i], vsebuje faca objekt enega od teh treh tipov. V nadaljevanju kličemo metode faca.x, faca.y in faca.setPos. Ker imajo vsi trije tipi premorejo te tri metode (in ker pri vseh pomenijo isto), lahko vse lepo deluje.

Last modified: Sunday, 9 January 2022, 7:03 PM