🌍 English
Σε βάθος 🔬

Εργαστήριο Οθόνης

Πώς κάνει το CLUE τον κώδικά σου εικόνες; Ας κάνουμε ζουμ μέχρι μέσα: pixel, συντεταγμένες, χρώματα, στρώσεις, σχέδιο, κίνηση και μνήμη. Κάθε παράδειγμα εδώ δοκιμάστηκε σε αληθινό CLUE.

Μέρος 1

🔍Μια οθόνη είναι ένα πλέγμα από μικροσκοπικά φωτάκια

Η οθόνη του CLUE είναι μια έγχρωμη οθόνη TFT 1,3 ιντσών. Από κοντά είναι ένα πλέγμα από 240 × 240 = 57.600 pixel. Κάθε pixel είναι στην πραγματικότητα 3 ακόμα πιο μικροσκοπικά φωτάκια, κόκκινο, πράσινο και μπλε, τόσο μικρά που τα μάτια σου δεν τα ξεχωρίζουν, οπότε ανακατεύονται σε ένα χρώμα.

🧠Ο εγκέφαλοςΤο τσιπ nRF52840 τρέχει τον κώδικά σου σε Python
〰️Τα καλώδια (SPI)Τα pixel ταξιδεύουν στην οθόνη το ένα μετά το άλλο, πολύ γρήγορα
🎛️Το τσιπ της οθόνης (ST7789)Θυμάται κάθε pixel και το κρατάει αναμμένο
💡Ο οπισθοφωτισμόςΈνα φως πίσω από όλα: αυτό είναι η φωτεινότητα

Στην CircuitPython η οθόνη λέγεται board.DISPLAY. Δεν μιλάς ποτέ ο ίδιος στα καλώδια: ένα σύστημα που λέγεται displayio βρίσκει ποια pixel άλλαξαν και στέλνει μόνο αυτά. Θυμήσου το, είναι το μυστικό για γρήγορη κίνηση!

Μέρος 2

📍Συντεταγμένες: πού είναι ένα pixel;

Κάθε pixel έχει μια διεύθυνση (x, y). Το x μετράει από τα αριστερά, το y μετράει από πάνω. Το πρώτο pixel είναι το (0, 0) στην πάνω αριστερή γωνία, και το τελευταίο είναι το (239, 239) κάτω δεξιά.

🐱 Φίλοι του Scratch, προσοχή!
Στο 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

🎨Χρώματα: ανακατεύοντας φως

Οι οθόνες ανακατεύουν φως, όχι μπογιά, οπότε κόκκινο + πράσινο κάνει κίτρινο, και και τα τρία στο φουλ κάνουν λευκό. Στον κώδικα ένα χρώμα είναι 3 αριθμοί από 0 έως 255: (κόκκινο, πράσινο, μπλε). Θα τα δεις γραμμένα και σε δεκαεξαδικό (hex, με βάση το 16), όπως 0xFF4FA3: δύο ψηφία για το κόκκινο, δύο για το πράσινο, δύο για το μπλε.

 

RGB565:

Αυτό που ζήτησες
Αυτό που δείχνει το CLUE

Το μυστικό: η οθόνη έχει μόνο 65.536 χρώματα

Ο κώδικάς σου μπορεί να ζητήσει 16,7 εκατομμύρια χρώματα (256 × 256 × 256), αλλά η οθόνη του CLUE αποθηκεύει κάθε pixel σε 16 bit: 5 bit για το κόκκινο, 6 bit για το πράσινο και 5 bit για το μπλε. Αυτό λέγεται RGB565. Το πράσινο παίρνει το έξτρα bit γιατί τα ανθρώπινα μάτια ξεχωρίζουν καλύτερα τις αποχρώσεις του πράσινου! Έτσι, χρώματα που μοιάζουν πολύ μπορεί να βγουν ακριβώς ίδια. Κοίτα το κουτί RGB565 από πάνω καθώς μετακινείς ένα ρυθμιστικό κατά 1.

Μέρος 4

🎂displayio: μια τούρτα με στρώσεις

Δεν ζωγραφίζεις κατευθείαν πάνω στην οθόνη. Χτίζεις μια στοίβα από στρώσεις, και το displayio τις ζωγραφίζει με τη σειρά, από κάτω προς τα πάνω, σαν αυτοκόλλητα σε μια σελίδα:

board.DISPLAYη αληθινή οθόνη
root_groupη μία ομάδα που δείχνει η οθόνη
Groupμια στρώση· μπορεί να μετακινηθεί (x, y) και να μεγεθυνθεί (scale)
TileGrid · Label · Shapeπράγματα που βλέπεις
Bitmap + Paletteτα pixel (αριθμοί) + τα βαζάκια με τις μπογιές (χρώματα)

Ένα Bitmap (χάρτης από pixel) μοιάζει με ζωγραφική με αριθμούς: κάθε pixel αποθηκεύει έναν μικρό αριθμό (0, 1, 2…), και η Palette (παλέτα) λέει ποιο χρώμα σημαίνει κάθε αριθμός. Άλλαξε ένα χρώμα της παλέτας και όλα τα pixel με αυτόν τον αριθμό αλλάζουν μαζί. Είναι ένα φτηνό μαγικό κόλπο για αναβοσβήσιμο και σβήσιμο!

Μέρος 5

✏️Ζωγράφισε 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".
Μέρος 6

🔷Σχεδιάζοντας σχήματα: 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 ώστε το γράφημα να μην αλλάζει κλίμακα μόνο του.
Μέρος 7

⚡Κίνηση: γιατί το μικρό είναι γρήγορο

Για να σταλούν και τα 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 καρέ το δευτερόλεπτο.
Μέρος 8

🔄Περιστροφή και χαμήλωμα φωτός

Το 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)
Μέρος 9

🧮Μνήμη: γιατί οι εικόνες έχουν τόσο λίγα χρώματα

Ένα bitmap χρειάζεται αρκετά bit ανά pixel για να χωράει ο μεγαλύτερος αριθμός χρώματος. Περισσότερα χρώματα = περισσότερα bit = περισσότερη μνήμη:

Χρώματα στην παλέταBit ανά pixelΌλη η οθόνη (240 × 240)
217,2 KB
3 – 4214,4 KB
5 – 16428,8 KB
17 – 256857,6 KB
65.536 (κάθε χρώμα RGB565)16115,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 μέσα στον κώδικα.

Μέρος 10

🖼️Εικόνες από αρχεία

Μια εικόνα δεν χρειάζεται να ζει στη μνήμη. Το 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!

🖼️ Άνοιξε το Picture Maker