Grafikk 1


Forkrav

Sjekk at du har tkinter installert. Dersom du installerte Python som beskrevet på installasjon-siden, skal dette allerede være i orden. Du kan sjekke om tkinter er installert ved å kopiere og kjøre dette programmet lokalt på din maskin:

import tkinter
print(tkinter.TkVersion)

Dersom du får en utskrift (f. eks 8.6), er tkinter installert. Dersom du får ModuleNotFoundError: No module named 'tkinter' eller lignende krasj må du installere tkinter. Det enkleste er da å installere Python på nytt slik som beskrevet på installasjon-siden. Alternativt kan tkinter installeres med ‘pip’ (se guide for å installere moduler i neste steg).

Rammeverket vi skal bruke er basert på tkinter, men i INF100 skal vi kun benytte oss av en svært liten del dette rammeverket, som kalles for Canvas. Derfor kan det å oppsøke dokumentasjonen til tkinter være mer forvirrende enn helpsomt. Om du likevel ønsker å lese dokumentasjonen, anbefaler vi denne skrevet av John W. Shipman ved New Mexico Tech.

Installasjon

For å benytte alle mulighetene som finnes i vårt grafikk-bibliotek, trenger du også å installere et par moduler. Det er ikke strengt tatt nødvendig å installere dem for å jobbe med biblioteket vårt, men noen muligheter vil forsvinne (f. eks. å vise frem bilder), og du vil få meldinger som dette når programmet ditt starter:

**********************************************************
** Cannot import PIL -- it seems you need to install pillow
** This may result in limited functionality or even a runtime error.
**********************************************************

Her er stegene for å installere modulene:

import sys, os
# Dersom tkinter ikke er installert, kan du fjerne kommentar-symbolet
# fra linjene under som omhandler tkinter

# Windows
if (os.name == "nt"):
    print(f"'{sys.executable}' -m pip install pillow")
    print(f"'{sys.executable}' -m pip install requests")
    # print(f"'{sys.executable}' -m pip install tkinter")

# Mac/Linux
elif (os.name == "posix"):
    print(f"sudo '{sys.executable}' -m pip install pillow")
    print(f"sudo '{sys.executable}' -m pip install requests")
    # print(f"sudo '{sys.executable}' -m pip install tkinter")

else:
    print("Ukjent operativsystem")

Du vil få en utskrift, men du har ikke installert modulene enda; du har bare fått vite hvordan de skal installeres. For å installere modulene, kopiér én og én linje fra utskriften og kjør den i terminalen. For Mac er det ofte mulig å bruke terminalen i VSCode til dette. Dersom du bruker Windows eller det resulterer i feilmeldinger, kan du kjøre kommandoene i operativsystemets terminal:

Dersom du blir bedt om passord underveis, benytt passordet du bruker til datamaskinen.

Test at det virker

Et vindu skal åpne seg og vise teksten Hello, Graphics! sammen med en kjent figur.

Et blankt vindu
from uib_inf100_graphics import *

def redraw_all(app, canvas):
    # kode som tegner noe skal plasseres i denne blokken
    # `pass` er en kommando som gjør ingenting; en midlertidig plassholder. 
    pass  # erstatt denne linjen

run_app(width=400, height=200)

Vil åpne et blankt vindu (nøyaktig utseende på rammen vil variere med operativsystem): Et blankt vindu

Koordinatsystemet

Ulikt det vi er vant til fra matematikken på skolen, vokser y-aksen nedover istedet for oppover. Dermed er \((0, 0)\) punktet til venstre øverst på lerretet, mens punktet \((\text{width}, \text{height})\) er punktet til høyre nederst. For et lerret med bredde 400 og høyde på 200, får hjørnene koordinatene under:

Koordinater for hjørnene til et lerret på 400x200

Tegn en strek
from uib_inf100_graphics import *

def redraw_all(app, canvas):
    # create_line(x0, y0, x1, y1, fill='black')
    # tegner en svart strek fra (x0, y0) to (x1, y1)
    canvas.create_line(25, 50, 200, 100, fill='black')

run_app(width=400, height=200)

Tegning av en strek

Lek litt med hvilke argumenter du gir til create_line for å gjøre deg kjent med koordinatsystemet. Tegn mer enn en strek. Kan du lage en fin strekfigur?

Tegn et hyperrektangel
from uib_inf100_graphics import *

def redraw_all(app, canvas):
    # De fire første parameterne x_0, y_0, x_1 og y_1 representerer to
    # motstående hjørner i hyperrektangelet. Tenk på det som hjørnet til
    # venstre øverst etterfulgt av hjørnet til høyre nederst.
    canvas.create_rectangle(100, 50, app.width/2, app.height/2, fill='blue')

run_app(width=400, height=200)

Tegning av et rektangel

Forsøk å endre på størrelsen på vinduet. Forandrer figuren seg? Hvorfor? Tegn en figur med både rektangler og streker. Klarer du å tegne en figur som endrer størrelse når brukeren endrer størrelse på vinduet?

Parametre for tegninger
from uib_inf100_graphics import *

def redraw_all(app, canvas):
    # De fleste tegne-funksjoner lar oss benytte valgfrie parametre
    # for å endre på tegningens utseende. Disse er skrevet på formen
    # parameter_navn=parameter_verdi, og kommer etter de posisjonelle
    # parameterne (som regel koordinater)

    # fill endrer fargen inni figuren
    canvas.create_rectangle(  0,   0, 150, 150, fill='yellow')
    # width endrer tykkelsen på kanten
    canvas.create_rectangle(100,  50, 250, 100, fill='orange', 
                            outline='black', width=5)
    # outline endrer fargen til kanten
    canvas.create_rectangle( 50, 100, 150, 200, fill='green',
                                                outline='red', width=3)
    # width=0 fjerner kanten fullstendig
    canvas.create_rectangle(125,  25, 175, 190, fill='purple', width=0)

run_app(width=400, height=200)

Rektangler med ulike parametre

Tegn andre figurer og tekst
from uib_inf100_graphics import *

def redraw_all(app, canvas):
    # For å tegne ovaler, oppgir vi koordinatene til hyperrektangelet som 
    # omgir ovalen
    canvas.create_oval(100, 50, 300, 150, fill='yellow', outline='black')

    # For polygoner og linjer oppgir vi en rekke med (x, y) -koordinater
    # Linjer må ha to punkter
    canvas.create_line(100, 50, 150, 150, fill='red', width=5)

    # Polygoner må ha 3 eller flere punkter
    canvas.create_polygon(100, 30, 200, 50, 300, 30, 200, 10, 
                          fill='green', width = 5, outline='black')


    # For å skrive tekst, oppgir vi en enkelt koordinat som representerer
    # 'ankeret' til teksten. Vi må også ha selve teksten, og vi kan oppgi
    # font/skriftstørrelse om vi ønsker
    canvas.create_text(200, 100, text='INF100!',
                       fill='purple', font='Helvetica 26 bold underline')

    # Vi kan endre hva slags betydning anker-koordinatet har.
    # Prøv å sette ankeret til en av disse strengene:
    # 'n' 's' 'e' 'w' 'sw' 'nw' 'ne' 'se' 'center'
    canvas.create_text(200, 100, text='Grip dagen!', anchor='sw',
                       fill='darkBlue', font='Times 28 bold italic')

    # Her er en prikk som viser ankeret brukt til begge tekstene over
    canvas.create_oval(200 - 3, 100 - 3, 200 + 3, 100 + 3, fill="white")

run_app(width=400, height=200)

Figurer og tekst

Farger

Et par farger er innebygget, som demonstrert i eksemplene over: 'black' 'white' 'gray' 'red' 'green' 'blue' 'RosyBrown2', samt en hel del andre spenstige farger som du finner i dokumentasjonen til tkinter. Vi er imidlertid ikke begrenset til kun disse fargene.

Piksel

I en LED-skjerm (som er en vanlig dataskjerm) tegnes bildet på skjermen ved at hver enkelt piksel (liten prikk på skjermen) får en bestemt farge. Inne i selve skjermen sitter det tre lamper inne i hver piksel: en rød lampe, en grønn lampe og en blå lampe. Når alle tre lampene lyser med maksimal intensitet, ser vi hvitt lys komme ut av pikselen. Dersom ingen av lampene lyser, er pikselen svart. Alle fargene skjermen kan produsere, blir laget av en kombinasjon av lysintensiteter i de tre pikslene.

Hvis man zoomer inn svært tett på dataskjermen, kan man skimte at en hvit piksel ikke faktisk er helt hvit, men består egentlig av en rød, grønn og blå lampe ved siden av hverandre som lyser. Her er et bilde jeg har tatt av musenpekeren min på skjermen:

Bilde av musepekeren

Om vi zoomer litt inn på bildet, kan vi skimte at hver piksel består av tre lamper: en rød, en grønn og en blå.

Bilde av musepekeren

På grunn av regler beskrevet i fysikken, vil en blanding av røde, grønne og blå lyssignaler se ut som det er hvitt.

Når man kjøper en LED-skjerm på butikken, finnes det ulike fargedypder eller man får oppgitt antall farger skjermen kan vise. Denne spesifikasjonen bestemmes av i hvor mange “trinn” man kan justere intensiteten til hver av de fargede lampene i en piksel. Det har lenge vært vanlig at man bruker 256 slike trinn. En farge i dette systemet kan derfor sees på som tre tall (r, g, b), der hver av r, g og b er et tall mellom 0 og 255.

Selv om nyere og dyre skjermer teknisk sett kan ha flere trinn, bruker som regel software som ikke er rettet spesielt mot high-end bildebehandling fremdeles dette systemet som standard.

Alle farger har en RGB-verdi. I tabellen ser vi at hver farge har en gitt styrke av rød (R), grønn (G) og blå (B), som er et tall mellom 0 og 255. Denne RGB-verdien kan også skrives i heksadesimalt format (se kolonnen Hex), hvor de to første tegnene etter hashtag reprsenterer styrken på rød, de to neste representerer grønn, og de to siste representerer blå sin styrke.

FargeRGBHexKallenavn
 
000#000000black
 
25500#ff0000red
 
02550#00ff00green1
 
00255#0000ffblue
 
2552550#ffff00yellow
 
0255255#00ffffcyan
 
2550255#ff00ffmagenta
 
255255255#ffffffwhite
 
128128128#808080gray
 
12800#800000maroon
 
2551400#ff8c00dark orange
 
224227206#e0e3ce-
 
248249245#f8f9f5-

For flere farger, se for eksempel listen over farger (A-F) på Wikipedia, eller prøv RGB-kalkulatoren til w3schools.com.

Vårt rammeverk for grafikk kan tolke alle RGB-verdier skrevet i hex-format. For eksempel:

from uib_inf100_graphics import *

def redraw_all(app, canvas):
    # Fargen "Absolute Zero" har RGB -verdi #0048BA
    # Fargen "Acid green" har RGB -verdi #B0BF1A
    # https://en.wikipedia.org/wiki/List_of_colors:_A%E2%80%93F
    canvas.create_oval(100, 50, 300, 150,
                       fill='#0048BA', outline='#B0BF1A', width=10)


run_app(width=400, height=200)

Spesielle farger

Når vi sier at farger er repsentert heksadesimalt, sikter vi til 16-tallsystemet (det heksadesimale tallsystemet). Det er i dette formatet vi vanligvis representerer RGB-farger. Strengen begynner med hashtag, som egentlig bare er for å formidle at resten av strengen representerer en farge representert som (tre) hexadesimale tall. Så følger det seks tegn, hvor de to første representerer intensiteten til den røde lampen, de to neste representerer intensiteten til den grønne lampen, og de to siste representerer intensiteten til den blå lampen.

Der vårt vanlige tallsystem, titallsystemet, har 10 ulike tallsymboler (0123456789), har det heksadesimale tallsystemet 16 tallsymboler (0123456789abcdef). I totallsystemet, også kalt det binære tallsystemet, har vi kun to tallsymboler (0 og 1). Måten vi teller på er den samme uansett tallsystem; for å øke et tall med én, øker vi det bakerste sifferet med én – helt til det ikke er flere tallsymboler igjen å øke med. På dette tidspunktet må man øke neste posisjon i tallet med én i stedet for, og samtidig gå tilbake til 0 på énerplassen.

For å skille tallsystemene fra hverandre, bruker vi 0b som prefiks for tall i totallsystemet, og 0x for tall i det heksadesimale tallsystemet. Dette er også noe python forstår:

# Oppgi verdier i ulike tallsystemer
x_dec = 100
x_bin = 0b100
x_hex = 0x100

# Print skriver ut tallet i 10-tallsystemet uansett
print(x_dec) # 100
print(x_bin) # 4 -- 100 i totallsystemet (altså 0b100) er fire
print(x_hex) # 256 -- 100 i det heksadesimal (altså 0x100) er 256
print()

# Konvertere tall til streng med str(), bin() og hex()
print("x_dec i titallsystemet:", str(x_dec))
print("x_bin i binærsystemet:", bin(x_bin))
print("x_hex i heksadesimal:", hex(x_hex))
print("255 i heksadesimal:", hex(255))
print()

# Konvertere tall til streng uten prefikser med f-strenger
print(f"x_dec i titallsystemet: {x_dec}")
print(f"x_bin i binærsystemet: {x_bin:b}") # Merk  :b
print(f"x_hex i heksadesimal: {x_hex:x}")  # Merk  :x
print(f"255 i heksadesimal: {255:x}")

La oss telle til 0x100 i heksadesimal, og sammenligne med titallsystemet og totallsystemet:

print(f"{'Decimal':>7}{'Binary':>13}{'Hex':>7}")

for x in range(0x100 + 1):
    print(f"{x:>7}{bin(x):>13}{hex(x):>7}")

For å konvertere en en RGB-verdi basert på tallverdier mellom 0 og 255 til en RGB-steng basert på hex, kan du kopiere og bruke denne funksjonen:

def rgb_hexstring(r, g, b):
    """ Given the integer RGB values (0-255) for a color, return its 
    hexadecimal RGB string repersentation."""
    return f"#{r:02x}{g:02x}{b:02x}"

Ikke tenk for mye på hva :02x betyr i funksjonen over. Men for de nysgjerrige betyr det at tallet skal representeres i sin heksadesimale form (pga. x) og skal skrives med minst to siffer med ledende 0’er hvis nødvendig (pga. 02).

Eksempel på bruk:

from uib_inf100_graphics import *

def rgb_hexstring(r, g, b):
    """ Given the integer RGB values (0-255) for a color, return its 
    hexadecimal RGB string repersentation."""
    return f"#{r:02x}{g:02x}{b:02x}"

def mix_colors(color1, color2, fraction_color1):
    """ Mixing two colors represented as tuples of rgb integer values.
    The third parameter, fraction_color1, must be a floating point
    number between 0 and 1 (inclusive), which represents the mixing
    ratio. If the fraction is e.g. 0.25, then the mixing ratio is 
    25 to 75, which means that the returned color will be closer
    to color2 than to color1."""
    r1, g1, b1 = color1
    r2, g2, b2 = color2
    fraction_color2 = 1 - fraction_color1

    r = round(r1 * fraction_color1 + r2 * fraction_color2)
    g = round(g1 * fraction_color1 + g2 * fraction_color2)
    b = round(b1 * fraction_color1 + b2 * fraction_color2)
    return (r, g, b)

def redraw_all(app, canvas):
    col1 = (0xFF, 0xAA, 0x1D) # Bright yellow, det samme som (255, 170, 29)
    col2 = (0xB0, 0xBF, 0x1A) # Acid green, kan også skrives (176, 191, 26)

    for i in range(100):
        frac1 = (99 - i)/99   # Når i=0 blir frac1=1, når i=99 blir frac1=0
        r, g, b = mix_colors(col1, col2, frac1)
        color = rgb_hexstring(r, g, b)

        x_left = app.width/100 * i
        x_right = x_left + app.width/100
        canvas.create_rectangle(x_left, 0, x_right, app.height,     
                                fill=color, width=0)

run_app(width=400, height=200)

Illustrasjon av koden over