Εργαστήριο Οθόνης
Πώς κάνει το CLUE τον κώδικά σου εικόνες; Ας κάνουμε ζουμ μέχρι μέσα: pixel, συντεταγμένες, χρώματα, στρώσεις, σχέδιο, κίνηση και μνήμη. Κάθε παράδειγμα εδώ δοκιμάστηκε σε αληθινό CLUE.
🔍Μια οθόνη είναι ένα πλέγμα από μικροσκοπικά φωτάκια
Η οθόνη του CLUE είναι μια έγχρωμη οθόνη TFT 1,3 ιντσών. Από κοντά είναι ένα πλέγμα από 240 × 240 = 57.600 pixel. Κάθε pixel είναι στην πραγματικότητα 3 ακόμα πιο μικροσκοπικά φωτάκια, κόκκινο, πράσινο και μπλε, τόσο μικρά που τα μάτια σου δεν τα ξεχωρίζουν, οπότε ανακατεύονται σε ένα χρώμα.
Στην CircuitPython η οθόνη λέγεται board.DISPLAY. Δεν μιλάς ποτέ ο ίδιος στα καλώδια: ένα σύστημα που λέγεται displayio βρίσκει ποια pixel άλλαξαν και στέλνει μόνο αυτά. Θυμήσου το, είναι το μυστικό για γρήγορη κίνηση!
📍Συντεταγμένες: πού είναι ένα pixel;
Κάθε pixel έχει μια διεύθυνση (x, y). Το x μετράει από τα αριστερά, το y μετράει από πάνω. Το πρώτο pixel είναι το (0, 0) στην πάνω αριστερή γωνία, και το τελευταίο είναι το (239, 239) κάτω δεξιά.
Στο Scratch, το (0, 0) είναι στη μέση της σκηνής και το y πηγαίνει προς τα πάνω. Στο CLUE, το (0, 0) είναι στην πάνω αριστερή γωνία και το y πηγαίνει προς τα κάτω. Για να μετατρέψεις:
clue_x = scratch_x + 120 και clue_y = 120 - scratch_y.Δοκίμασέ το: κούνησε το ποντίκι σου (ή το δάχτυλό σου) πάνω από την οθόνη από κάτω.
CLUE: x = ?, y = ?
Scratch: x = ?, y = ?
Οι κόκκινες γραμμές είναι ανά 40 pixel. Η μέση της οθόνης είναι το (120, 120).
🎨Χρώματα: ανακατεύοντας φως
Οι οθόνες ανακατεύουν φως, όχι μπογιά, οπότε κόκκινο + πράσινο κάνει κίτρινο, και και τα τρία στο φουλ κάνουν λευκό. Στον κώδικα ένα χρώμα είναι 3 αριθμοί από 0 έως 255: (κόκκινο, πράσινο, μπλε). Θα τα δεις γραμμένα και σε δεκαεξαδικό (hex, με βάση το 16), όπως 0xFF4FA3: δύο ψηφία για το κόκκινο, δύο για το πράσινο, δύο για το μπλε.
RGB565:
Το μυστικό: η οθόνη έχει μόνο 65.536 χρώματα
Ο κώδικάς σου μπορεί να ζητήσει 16,7 εκατομμύρια χρώματα (256 × 256 × 256), αλλά η οθόνη του CLUE αποθηκεύει κάθε pixel σε 16 bit: 5 bit για το κόκκινο, 6 bit για το πράσινο και 5 bit για το μπλε. Αυτό λέγεται RGB565. Το πράσινο παίρνει το έξτρα bit γιατί τα ανθρώπινα μάτια ξεχωρίζουν καλύτερα τις αποχρώσεις του πράσινου! Έτσι, χρώματα που μοιάζουν πολύ μπορεί να βγουν ακριβώς ίδια. Κοίτα το κουτί RGB565 από πάνω καθώς μετακινείς ένα ρυθμιστικό κατά 1.
🎂displayio: μια τούρτα με στρώσεις
Δεν ζωγραφίζεις κατευθείαν πάνω στην οθόνη. Χτίζεις μια στοίβα από στρώσεις, και το displayio τις ζωγραφίζει με τη σειρά, από κάτω προς τα πάνω, σαν αυτοκόλλητα σε μια σελίδα:
Ένα Bitmap (χάρτης από pixel) μοιάζει με ζωγραφική με αριθμούς: κάθε pixel αποθηκεύει έναν μικρό αριθμό (0, 1, 2…), και η Palette (παλέτα) λέει ποιο χρώμα σημαίνει κάθε αριθμός. Άλλαξε ένα χρώμα της παλέτας και όλα τα pixel με αυτόν τον αριθμό αλλάζουν μαζί. Είναι ένα φτηνό μαγικό κόλπο για αναβοσβήσιμο και σβήσιμο!
✏️Ζωγράφισε pixel μόνος σου
Εδώ είναι όλη η τούρτα με τις στρώσεις σε κώδικα. Το bitmap[x, y] = αριθμός χρωματίζει ΕΝΑ pixel.
import random
import board
import displayio
bitmap = displayio.Bitmap(240, 240, 4) # 240 x 240 pixel, μέχρι 4 χρώματα
palette = displayio.Palette(4) # τα 4 βαζάκια με μπογιά
palette[0] = 0x000022 # 0 = νυχτερινός ουρανός
palette[1] = 0xFFFFFF # 1 = λευκό
palette[2] = 0xFFD23F # 2 = κίτρινο
palette[3] = 0xFF4FA3 # 3 = ροζ
group = displayio.Group()
group.append(displayio.TileGrid(bitmap, pixel_shader=palette))
board.DISPLAY.root_group = group
bitmap[120, 120] = 3 # ΕΝΑ ροζ pixel στη μέση
for i in range(300): # 300 τυχαία αστέρια
x = random.randint(0, 239)
y = random.randint(0, 239)
bitmap[x, y] = random.choice([1, 2])
for x in range(240): # μια κίτρινη γραμμή στο κάτω μέρος
bitmap[x, 200] = 2
while True:
pass
Κόλπο με ζουμ: pixel art
Ένα Group μπορεί να μεγεθύνει (scale) ό,τι έχει μέσα του. Ζωγράφισε μια μικροσκοπική εικόνα 8 × 8 από μια λίστα με strings, και μετά μεγάλωσέ την 30 φορές ώστε να γεμίσει την οθόνη:
import board
import displayio
ART = [
"..####..",
".#....#.",
"#.#..#.#",
"#......#",
"#.#..#.#",
"#..##..#",
".#....#.",
"..####..",
]
palette = displayio.Palette(2)
palette[0] = 0xFFD23F # "." = κίτρινο
palette[1] = 0x1B1340 # "#" = σκούρο μελάνι
bitmap = displayio.Bitmap(8, 8, 2)
for y, row in enumerate(ART): # το enumerate δίνει τη θέση ΚΑΙ το στοιχείο
for x, ch in enumerate(row):
if ch == "#":
bitmap[x, y] = 1
group = displayio.Group(scale=30) # κάθε pixel γίνεται ένα τετράγωνο 30 x 30!
group.append(displayio.TileGrid(bitmap, pixel_shader=palette))
board.DISPLAY.root_group = group
while True:
pass
Ζωγράφισε το δικό σου sprite 8 × 8 (μια καρδιά, ένα φαντασματάκι, έναν space invader…). Πρόσθεσε ένα τρίτο χρώμα: κάνε την παλέτα μήκους 3 και χρησιμοποίησε κι άλλο γράμμα, όπως το
"o".🔷Σχεδιάζοντας σχήματα: 3 εργαλειοθήκες
Το να ζωγραφίζεις ένα pixel τη φορά είναι αργό για μεγάλα σχήματα. Υπάρχουν τρεις εργαλειοθήκες που σχεδιάζουν σχήματα για σένα:
| Εργαλειοθήκη | Εγκατάσταση; | Ιδανική για |
|---|---|---|
bitmaptools | Ενσωματωμένη ✅ | Σχέδιο μέσα σε ένα bitmap: γραμμές, κύκλους, κουτιά, πολύγωνα, γέμισμα με κουβά μπογιάς |
vectorio | Ενσωματωμένη ✅ | Σχήματα που κινούνται: κύκλους, ορθογώνια, πολύγωνα. Γρήγορη και θέλει λίγη μνήμη |
adafruit_display_shapes | Βιβλιοθήκη 📁 | Εύκολα σχήματα με περίγραμμα: Rect, RoundRect, Circle, Triangle, Line, Polygon, και γραφήματα Sparkline |
bitmaptools: σχέδιο μέσα σε ένα bitmap
import array
import board
import displayio
import bitmaptools
bitmap = displayio.Bitmap(240, 240, 5)
palette = displayio.Palette(5)
palette[0] = 0x000000 # μαύρο
palette[1] = 0xFF4FA3 # ροζ
palette[2] = 0x22D3EE # κυανό
palette[3] = 0xFFD23F # κίτρινο
palette[4] = 0x3EE07A # πράσινο
group = displayio.Group()
group.append(displayio.TileGrid(bitmap, pixel_shader=palette))
board.DISPLAY.root_group = group
bitmaptools.fill_region(bitmap, 20, 20, 100, 100, 1) # κουτί: x1, y1, x2, y2, χρώμα
bitmaptools.draw_line(bitmap, 0, 239, 239, 0, 2) # γραμμή από γωνία σε γωνία
bitmaptools.draw_circle(bitmap, 170, 70, 40, 3) # κύκλος: x, y, ακτίνα, χρώμα
bitmaptools.boundary_fill(bitmap, 170, 70, 3, 0) # κουβάς μπογιάς! γέμισε το εσωτερικό
xs = array.array("h", [40, 120, 80]) # οι γωνίες ενός τριγώνου
ys = array.array("h", [220, 220, 150])
bitmaptools.draw_polygon(bitmap, xs, ys, 4)
while True:
pass
Το array.array("h", [...]) είναι μια συμπαγής λίστα με ακέραιους αριθμούς, και αυτό θέλει το draw_polygon για τις θέσεις των γωνιών.
vectorio: σχήματα που κινούνται (μπάλα που αναπηδά!)
Κάθε σχήμα του vectorio έχει δικά του x και y. Άλλαξέ τα και το σχήμα κινείται. Το displayio ξαναζωγραφίζει μόνο τη μικρή περιοχή γύρω από τη μπάλα.
import time
import board
import displayio
import vectorio
sky = displayio.Palette(1)
sky[0] = 0x1B1340
pink = displayio.Palette(1)
pink[0] = 0xFF4FA3
group = displayio.Group()
group.append(vectorio.Rectangle(pixel_shader=sky, width=240, height=240, x=0, y=0))
ball = vectorio.Circle(pixel_shader=pink, radius=15, x=120, y=120)
group.append(ball)
board.DISPLAY.root_group = group
dx, dy = 3, 2 # πόσο κινείται η μπάλα σε κάθε βήμα
while True:
ball.x += dx
ball.y += dy
if ball.x < 15 or ball.x > 225: # χτύπησε σε πλαϊνό τοίχο; αναπήδα!
dx = -dx
if ball.y < 15 or ball.y > 225: # χτύπησε πάνω ή κάτω; αναπήδα!
dy = -dy
time.sleep(0.01)
adafruit_display_shapes: σχήματα με περίγραμμα
Αυτή είναι βιβλιοθήκη. Αντέγραψε τον φάκελο adafruit_display_shapes από το bundle με τις βιβλιοθήκες στο CIRCUITPY/lib, όπως ακριβώς στο Μέρος B της Εγκατάστασης.
import board
import displayio
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.roundrect import RoundRect
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.triangle import Triangle
from adafruit_display_shapes.line import Line
group = displayio.Group()
group.append(Rect(0, 0, 240, 240, fill=0x1B1340)) # φόντο
group.append(RoundRect(20, 20, 200, 60, 15, fill=0xFFD23F, outline=0xFFFFFF, stroke=3))
group.append(Circle(60, 150, 40, fill=0xFF4FA3, outline=0xFFFFFF)) # x, y, ακτίνα
group.append(Triangle(130, 200, 180, 110, 230, 200, fill=0x22D3EE)) # 3 γωνίες
group.append(Line(0, 225, 239, 225, 0x3EE07A)) # x0, y0, x1, y1, χρώμα
board.DISPLAY.root_group = group
while True:
pass
Ζωγράφισε ένα σπίτι: ένα Rect για τους τοίχους, μια στέγη Triangle, μια πόρτα RoundRect και έναν ήλιο Circle.
Ζωντανά δεδομένα σε γράφημα: Sparkline
Ένα Sparkline είναι ένα γράφημα γραμμής που κυλάει: κάθε add_value() προσθέτει ένα σημείο στα δεξιά. Κάνε λίγη φασαρία!
import board
import displayio
import terminalio
from adafruit_display_text import label
from adafruit_display_shapes.sparkline import Sparkline
from adafruit_clue import clue
graph = Sparkline(width=220, height=170, max_items=50, x=10, y=60, color=0x3EE07A)
title = label.Label(terminalio.FONT, text="Sound level", scale=2, x=10, y=20)
value = label.Label(terminalio.FONT, text="", scale=2, x=10, y=45, color=0xFFD23F)
group = displayio.Group()
group.append(graph)
group.append(title)
group.append(value)
board.DISPLAY.root_group = group
while True:
level = clue.sound_level
graph.add_value(level) # πρόσθεσε ένα σημείο· το πιο παλιό φεύγει από την οθόνη
value.text = f"{level:.0f}"
Κάνε γράφημα το
clue.proximity ή το z του επιταχυνσιόμετρου. Δοκίμασε y_min=0, y_max=255 στο Sparkline ώστε το γράφημα να μην αλλάζει κλίμακα μόνο του.⚡Κίνηση: γιατί το μικρό είναι γρήγορο
Για να σταλούν και τα 57.600 pixel μέσα από τα καλώδια χρειάζεται χρόνος. Το displayio είναι έξυπνο: στέλνει μόνο το κομμάτι που άλλαξε. Αυτό το πρόγραμμα το μετράει. Κλείσε το auto_refresh και κάλεσε μόνος σου το display.refresh() για να ελέγχεις ακριβώς πότε ανανεώνεται η οθόνη:
import time
import board
import displayio
import vectorio
display = board.DISPLAY
display.auto_refresh = False # ΕΜΕΙΣ αποφασίζουμε πότε ανανεώνεται η οθόνη
bg = displayio.Palette(1)
bg[0] = 0x000000
fg = displayio.Palette(1)
fg[0] = 0xFF4FA3
group = displayio.Group()
back = vectorio.Rectangle(pixel_shader=bg, width=240, height=240, x=0, y=0)
ball = vectorio.Circle(pixel_shader=fg, radius=10, x=20, y=120)
group.append(back)
group.append(ball)
display.root_group = group
start = time.monotonic()
for i in range(100): # κίνησε κάτι ΜΙΚΡΟ
ball.x = 20 + i * 2
display.refresh()
small = 100 / (time.monotonic() - start)
start = time.monotonic()
for i in range(10): # άλλαξε ΟΛΗ την οθόνη
bg[0] = 0x000000 if i % 2 else 0x222266
display.refresh()
full = 10 / (time.monotonic() - start)
print(f"small ball: {small:.0f} frames per second")
print(f"whole screen: {full:.1f} frames per second")
display.auto_refresh = True
Στο δικό μας CLUE τύπωσε:
small ball: 159 frames per second
whole screen: 3.4 frames per second
Κίνησε μικρά πράγματα πάνω σε ένα φόντο που μένει ακίνητο. Αν αλλάζεις όλη την οθόνη μαζί (μια φωτογραφία σε όλη την οθόνη, ένα νέο χρώμα φόντου), είναι περίπου 50 φορές πιο αργό. Γι' αυτό τα κινούμενα GIF από το Picture Maker παίζουν με περίπου 3 καρέ το δευτερόλεπτο.
🔄Περιστροφή και χαμήλωμα φωτός
Το display.rotation γυρίζει τα πάντα κατά 0, 90, 180 ή 270 μοίρες (τέλειο αν το CLUE σου είναι στερεωμένο στο πλάι). Το display.brightness ρυθμίζει τον οπισθοφωτισμό από 0.0 (σβηστός) έως 1.0 (στο φουλ), και το χαμήλωμα γλιτώνει μπαταρία!
import time
import board
from adafruit_clue import clue
display = board.DISPLAY
screen = clue.simple_text_display(title="Turn me!", text_scale=3)
screen[1].text = "Hello!"
screen.show()
for angle in (90, 180, 270, 0): # γύρνα την εικόνα
display.rotation = angle
print("rotation", display.rotation)
time.sleep(1)
for level in (0.5, 0.1, 1.0): # χαμήλωσε τον οπισθοφωτισμό
display.brightness = level
print("brightness", display.brightness)
time.sleep(1)
🧮Μνήμη: γιατί οι εικόνες έχουν τόσο λίγα χρώματα
Ένα bitmap χρειάζεται αρκετά bit ανά pixel για να χωράει ο μεγαλύτερος αριθμός χρώματος. Περισσότερα χρώματα = περισσότερα bit = περισσότερη μνήμη:
| Χρώματα στην παλέτα | Bit ανά pixel | Όλη η οθόνη (240 × 240) |
|---|---|---|
| 2 | 1 | 7,2 KB |
| 3 – 4 | 2 | 14,4 KB |
| 5 – 16 | 4 | 28,8 KB |
| 17 – 256 | 8 | 57,6 KB |
| 65.536 (κάθε χρώμα RGB565) | 16 | 115,2 KB 😱 πολύ μεγάλο! |
Πόση μνήμη έχει το CLUE για το πρόγραμμά σου; Ας τη μετρήσουμε:
import gc
import displayio
gc.collect()
before = gc.mem_free()
print("free memory:", before, "bytes")
for colors in (2, 4, 16, 256):
gc.collect()
start = gc.mem_free()
bitmap = displayio.Bitmap(240, 240, colors)
gc.collect()
print(colors, "colors cost", start - gc.mem_free(), "bytes")
del bitmap
Το δικό μας CLUE τύπωσε free memory: 129952 bytes (περίπου 127 KB), και το κόστος ταίριαξε με τον πίνακα: 8.064 bytes για 2 χρώματα, 16.432 για 4, 28.720 για 16 και 59.440 για 256.
Το ίδιο το
from adafruit_clue import clue τρώει περίπου 52 KB, πάνω από το ένα τρίτο της μνήμης! Προγράμματα που μόνο ζωγραφίζουν εικόνες καλύτερα να το παραλείπουν και να χρησιμοποιούν κατευθείαν το board.DISPLAY. Αυτό κάνει και το Picture Maker.Εικόνες με 17+ χρώματα σε πλήρες μέγεθος χωράνε, αλλά στο τσακ. Γι' αυτό το Picture Maker χρησιμοποιεί κόλπα: λιγότερα χρώματα, ένα μικρότερο bitmap μεγεθυσμένο με το scale, και στρίμωγμα (συμπίεση) των δεδομένων των pixel μέσα στον κώδικα.
🖼️Εικόνες από αρχεία
Μια εικόνα δεν χρειάζεται να ζει στη μνήμη. Το displayio.OnDiskBitmap διαβάζει ένα αρχείο .bmp κατευθείαν από τον δίσκο CIRCUITPY την ώρα που ζωγραφίζει. Δεν θέλει σχεδόν καθόλου μνήμη, αλλά είναι πιο αργό (μια εικόνα σε όλη την οθόνη θέλει περίπου 2 δευτερόλεπτα για να εμφανιστεί στο δικό μας CLUE).
import board
import displayio
picture = displayio.OnDiskBitmap("/picture.bmp")
tile = displayio.TileGrid(picture, pixel_shader=picture.pixel_shader)
group = displayio.Group()
group.append(tile)
board.DISPLAY.root_group = group
while True:
pass
Πού θα βρεις ένα .bmp στο μέγεθος του CLUE; Φτιάξε ένα με το Picture Maker!