8x32 PI
This commit is contained in:
Executable
+210
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
=============================================================================
|
||||
GÉNÉRER LES ANIMATIONS D'EXEMPLE (et la palette Krita)
|
||||
Workshop « Vitrine pixels »
|
||||
=============================================================================
|
||||
|
||||
Fabrique, dans le dossier « animations », un exemple de chaque format que
|
||||
le lecteur sait jouer :
|
||||
|
||||
01_coeur_qui_bat.gif un GIF animé (comme un export Krita en GIF)
|
||||
02_texte_defilant.gif un texte qui défile, écrit avec une police 5 × 7
|
||||
03_vague/ une SÉQUENCE de PNG numérotés + images_par_seconde.txt
|
||||
(comme un export Krita en « séquence d'images »)
|
||||
04_balle.png un PNG animé (APNG)
|
||||
|
||||
Et la palette Krita « krita/palette_vitrine_pixels.gpl ».
|
||||
|
||||
Usage :
|
||||
python3 outils/generer_exemples.py
|
||||
|
||||
C'est aussi un exemple : on peut FABRIQUER des images par le code avec
|
||||
Pillow, puis les jouer comme des dessins.
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ICI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ANIMATIONS = os.path.join(ICI, "animations")
|
||||
L, H = 32, 8
|
||||
|
||||
|
||||
def nouvelle_image():
|
||||
return Image.new("RGB", (L, H))
|
||||
|
||||
|
||||
def dessiner(image, motif, x0, y0, couleurs):
|
||||
"""Dessine un motif texte (une lettre = une couleur, « . » = rien)."""
|
||||
for y, ligne in enumerate(motif):
|
||||
for x, lettre in enumerate(ligne):
|
||||
if lettre in couleurs and 0 <= x0 + x < L and 0 <= y0 + y < H:
|
||||
image.putpixel((x0 + x, y0 + y), couleurs[lettre])
|
||||
|
||||
|
||||
# --- 01 : cœur qui bat (GIF) -------------------------------------------------
|
||||
|
||||
PETIT_COEUR = [
|
||||
".RR.RR.",
|
||||
"RBRRRRR",
|
||||
"RRRRRRR",
|
||||
".RRRRR.",
|
||||
"..RRR..",
|
||||
"...R...",
|
||||
]
|
||||
GRAND_COEUR = [
|
||||
".RRR.RRR.",
|
||||
"RBBRRRRRR",
|
||||
"RBRRRRRRR",
|
||||
"RRRRRRRRR",
|
||||
".RRRRRRR.",
|
||||
"..RRRRR..",
|
||||
"...RRR...",
|
||||
"....R....",
|
||||
]
|
||||
|
||||
|
||||
def coeur_qui_bat():
|
||||
couleurs = {"R": (230, 0, 40), "B": (255, 170, 190)}
|
||||
images, durees = [], []
|
||||
# petit → grand → petit → pause : le rythme d'un battement « boum-boum »
|
||||
for motif, duree in [(PETIT_COEUR, 400), (GRAND_COEUR, 120), (PETIT_COEUR, 120),
|
||||
(GRAND_COEUR, 160)]:
|
||||
im = nouvelle_image()
|
||||
largeur = len(motif[0])
|
||||
dessiner(im, motif, (L - largeur) // 2, (H - len(motif)) // 2 + (len(motif) == 6), couleurs)
|
||||
images.append(im)
|
||||
durees.append(duree)
|
||||
images[0].save(os.path.join(ANIMATIONS, "01_coeur_qui_bat.gif"), save_all=True,
|
||||
append_images=images[1:], duration=durees, loop=0)
|
||||
|
||||
|
||||
# --- 02 : texte défilant (GIF) -----------------------------------------------
|
||||
|
||||
# Police 5 × 7 : chaque lettre est dessinée avec des « # ».
|
||||
POLICE = {
|
||||
"A": [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
|
||||
"D": ["####.", "#...#", "#...#", "#...#", "#...#", "#...#", "####."],
|
||||
"E": ["#####", "#....", "#....", "####.", "#....", "#....", "#####"],
|
||||
"H": ["#...#", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
|
||||
"I": ["###", ".#.", ".#.", ".#.", ".#.", ".#.", "###"],
|
||||
"L": ["#....", "#....", "#....", "#....", "#....", "#....", "#####"],
|
||||
"N": ["#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "#...#"],
|
||||
"P": ["####.", "#...#", "#...#", "####.", "#....", "#....", "#...."],
|
||||
"R": ["####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#"],
|
||||
"S": [".####", "#....", "#....", ".###.", "....#", "....#", "####."],
|
||||
"T": ["#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."],
|
||||
"V": ["#...#", "#...#", "#...#", "#...#", "#...#", ".#.#.", "..#.."],
|
||||
"X": ["#...#", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "#...#"],
|
||||
" ": ["...", "...", "...", "...", "...", "...", "..."],
|
||||
}
|
||||
|
||||
|
||||
def texte_defilant(texte="VITRINE PIXELS"):
|
||||
# 1) On écrit tout le texte sur une longue bande.
|
||||
largeur_totale = sum(len(POLICE[c][0]) + 1 for c in texte)
|
||||
bande = Image.new("RGB", (largeur_totale, H))
|
||||
x = 0
|
||||
for i, c in enumerate(texte):
|
||||
# chaque lettre a sa propre teinte : un petit arc-en-ciel
|
||||
r, v, b = [int(255 * (0.5 + 0.5 * math.cos(2 * math.pi * (i / len(texte) + k / 3))))
|
||||
for k in range(3)]
|
||||
for y, ligne in enumerate(POLICE[c]):
|
||||
for dx, p in enumerate(ligne):
|
||||
if p == "#":
|
||||
bande.putpixel((x + dx, y), (r, v, b))
|
||||
x += len(POLICE[c][0]) + 1
|
||||
|
||||
# 2) On fait glisser une « fenêtre » de 32 pixels sur la bande.
|
||||
images = []
|
||||
for decalage in range(-L, largeur_totale + 1):
|
||||
im = nouvelle_image()
|
||||
im.paste(bande, (-decalage, 0))
|
||||
images.append(im)
|
||||
images[0].save(os.path.join(ANIMATIONS, "02_texte_defilant.gif"), save_all=True,
|
||||
append_images=images[1:], duration=60, loop=0)
|
||||
|
||||
|
||||
# --- 03 : vague (séquence de PNG) --------------------------------------------
|
||||
|
||||
def vague(nb_images=40, ips=20):
|
||||
dossier = os.path.join(ANIMATIONS, "03_vague")
|
||||
os.makedirs(dossier, exist_ok=True)
|
||||
for n in range(nb_images):
|
||||
im = nouvelle_image()
|
||||
phase = 2 * math.pi * n / nb_images
|
||||
for x in range(L):
|
||||
hauteur = (H - 1) / 2 * (1 + math.sin(2 * math.pi * x / L + phase))
|
||||
for y in range(H):
|
||||
if H - 1 - y <= hauteur: # sous la crête de la vague
|
||||
# 3 bleus seulement, du plus clair (crête) au plus profond :
|
||||
# un rendu « pixel art » franc, sans dégradé flou.
|
||||
profondeur = min(2, int(hauteur - (H - 1 - y)))
|
||||
im.putpixel((x, y), [(120, 220, 255), (0, 120, 255), (0, 30, 160)][profondeur])
|
||||
# Même nommage que Krita : nom_0000.png, nom_0001.png…
|
||||
im.save(os.path.join(dossier, f"vague_{n:04d}.png"))
|
||||
with open(os.path.join(dossier, "images_par_seconde.txt"), "w") as f:
|
||||
f.write(f"{ips}\n")
|
||||
|
||||
|
||||
# --- 04 : balle qui rebondit (APNG) ------------------------------------------
|
||||
|
||||
def balle(nb_images=48):
|
||||
images = []
|
||||
for n in range(nb_images):
|
||||
im = nouvelle_image()
|
||||
t = n / nb_images
|
||||
x = round((L - 2) * (1 - abs(2 * t - 1))) # aller-retour
|
||||
y = round((H - 2) * (1 - abs(math.sin(4 * math.pi * t)))) # rebonds
|
||||
for dx in (0, 1):
|
||||
for dy in (0, 1):
|
||||
im.putpixel((x + dx, y + dy), (255, 150, 0))
|
||||
images.append(im)
|
||||
images[0].save(os.path.join(ANIMATIONS, "04_balle.png"), save_all=True,
|
||||
append_images=images[1:], duration=40, loop=0)
|
||||
|
||||
|
||||
# --- Palette Krita -----------------------------------------------------------
|
||||
|
||||
def palette_krita():
|
||||
"""Palette au format GIMP (.gpl), que Krita sait importer.
|
||||
Des couleurs SATURÉES, qui rendent bien sur des LED, en 3 intensités,
|
||||
plus les pastels (mélange avec du blanc) et le noir/blanc."""
|
||||
teintes = [("Rouge", (255, 0, 0)), ("Orange", (255, 90, 0)), ("Jaune", (255, 200, 0)),
|
||||
("Vert", (0, 255, 0)), ("Cyan", (0, 255, 200)), ("Bleu", (0, 60, 255)),
|
||||
("Violet", (140, 0, 255)), ("Rose", (255, 0, 110))]
|
||||
lignes = ["GIMP Palette", "Name: Vitrine pixels (LED)", "Columns: 8", "#"]
|
||||
|
||||
def ajouter(c, nom):
|
||||
lignes.append(f"{c[0]:3d} {c[1]:3d} {c[2]:3d}\t{nom}")
|
||||
|
||||
for nom, c in teintes:
|
||||
ajouter(c, nom)
|
||||
for nom, c in teintes: # 60 % : valeur minimum conseillée pour les « moyens »
|
||||
ajouter(tuple(round(k * 0.6) for k in c), nom + " moyen")
|
||||
for nom, c in teintes: # 35 % : la plus sombre qui reste bien visible
|
||||
ajouter(tuple(round(k * 0.35) for k in c), nom + " sombre")
|
||||
for nom, c in teintes:
|
||||
ajouter(tuple(round(k + (255 - k) * 0.55) for k in c), nom + " pastel")
|
||||
for nom, c in [("Noir (éteint)", (0, 0, 0)), ("Gris sombre", (90, 90, 90)),
|
||||
("Gris", (160, 160, 160)), ("Blanc chaud", (255, 200, 140)),
|
||||
("Blanc", (255, 255, 255))]:
|
||||
ajouter(c, nom)
|
||||
|
||||
os.makedirs(os.path.join(ICI, "krita"), exist_ok=True)
|
||||
with open(os.path.join(ICI, "krita", "palette_vitrine_pixels.gpl"), "w") as f:
|
||||
f.write("\n".join(lignes) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.makedirs(ANIMATIONS, exist_ok=True)
|
||||
coeur_qui_bat()
|
||||
texte_defilant()
|
||||
vague()
|
||||
balle()
|
||||
palette_krita()
|
||||
print("Exemples créés dans", ANIMATIONS)
|
||||
print("Palette créée dans", os.path.join(ICI, "krita"))
|
||||
Executable
+316
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dessine les schémas du dossier « schemas » :
|
||||
- branchement.svg : Raspberry Pi + 74AHCT125 + matrice + alimentation
|
||||
- brochage_74ahct125.svg : la puce adaptatrice de niveau, patte par patte
|
||||
- chaine_krita_matrice.svg : le trajet d'une animation, de Krita à la matrice
|
||||
Usage : python3 outils/schemas.py"""
|
||||
import os
|
||||
|
||||
ICI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SORTIE = os.path.join(ICI, "schemas")
|
||||
|
||||
ROUGE, NOIR, BLANC_FIL, VERT = "#d62828", "#1a1a1a", "#bdbdbd", "#2a9d3a"
|
||||
JAUNE_GPIO = "#e0a800"
|
||||
|
||||
|
||||
class Svg:
|
||||
def __init__(self, l, h, titre):
|
||||
self.l, self.h = l, h
|
||||
self.out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{l}" height="{h}" '
|
||||
f'viewBox="0 0 {l} {h}" font-family="Helvetica, Arial, sans-serif">',
|
||||
f' <title>{titre}</title>',
|
||||
' <defs><marker id="fleche" viewBox="0 0 10 10" refX="8" refY="5" '
|
||||
'markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" '
|
||||
'fill="context-stroke"/></marker>'
|
||||
'<marker id="flecheg" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" '
|
||||
'markerHeight="6" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#555"/></marker>'
|
||||
'<marker id="flechec" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" '
|
||||
'markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#cfcfcf"/></marker>'
|
||||
'</defs>',
|
||||
f' <rect width="{l}" height="{h}" fill="#ffffff"/>']
|
||||
|
||||
def add(self, s):
|
||||
self.out.append(" " + s)
|
||||
|
||||
def rect(self, x, y, w, h, fill, rx=0, stroke=None, sw=1):
|
||||
st = f' stroke="{stroke}" stroke-width="{sw}"' if stroke else ""
|
||||
self.add(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{fill}"{st}/>')
|
||||
|
||||
def texte(self, x, y, s, taille=13, c="#333", ancre="start", gras=False, mono=False):
|
||||
g = ' font-weight="bold"' if gras else ""
|
||||
m = ' font-family="monospace"' if mono else ""
|
||||
self.add(f'<text x="{x}" y="{y}" font-size="{taille}" fill="{c}" '
|
||||
f'text-anchor="{ancre}"{g}{m}>{s}</text>')
|
||||
|
||||
def fil(self, d, c, w=5):
|
||||
self.add(f'<path d="{d}" stroke="{c}" stroke-width="{w}" fill="none" '
|
||||
'stroke-linecap="round" stroke-linejoin="round"/>')
|
||||
|
||||
def point(self, x, y, c, r=5):
|
||||
self.add(f'<circle cx="{x}" cy="{y}" r="{r}" fill="{c}"/>')
|
||||
|
||||
def save(self, nom):
|
||||
self.out.append("</svg>")
|
||||
with open(os.path.join(SORTIE, nom), "w") as f:
|
||||
f.write("\n".join(self.out) + "\n")
|
||||
|
||||
|
||||
def hop_h(x_croise, sens=1, r=7):
|
||||
"""Petit pont (demi-cercle) pour un fil horizontal qui en enjambe un autre.
|
||||
sens = 1 : on va vers la droite ; -1 : vers la gauche."""
|
||||
return (f"H{x_croise - sens * r} a{r} {r} 0 0 {1 if sens == 1 else 0} "
|
||||
f"{2 * r * sens} 0")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 1. BRANCHEMENT
|
||||
# =============================================================================
|
||||
|
||||
def branchement():
|
||||
s = Svg(1200, 940, "Branchement matrice WS2812B 8×32, Raspberry Pi, 74AHCT125 "
|
||||
"et alimentation d'établi")
|
||||
|
||||
# ---------- matrice (vue de dos) ----------
|
||||
s.texte(60, 38, "Matrice WS2812B 8 × 32 — vue de dos (côté fils)", 20, "#222", gras=True)
|
||||
s.add('<pattern id="pix" x="60" y="55" width="30.625" height="30.625" '
|
||||
'patternUnits="userSpaceOnUse"><rect x="9" y="9" width="12" height="12" rx="1" '
|
||||
'fill="#2b2b2b"/></pattern>')
|
||||
s.rect(60, 55, 980, 245, "#141414", 6)
|
||||
s.rect(60, 55, 980, 245, "url(#pix)", 6)
|
||||
s.add('<line x1="760" y1="150" x2="340" y2="150" stroke="#cfcfcf" stroke-width="3" '
|
||||
'marker-end="url(#flechec)"/>')
|
||||
s.texte(550, 135, "les données entrent par DIN et traversent les 256 LED", 15, "#e6e6e6", "middle")
|
||||
for x, t in [(110, "5V"), (150, "GND"), (194, "DOUT"), (490, "5V"), (550, "GND"),
|
||||
(880, "5V"), (910, "GND"), (940, "DIN")]:
|
||||
s.texte(x, 268, t, 12, "#cfcfcf", "middle")
|
||||
|
||||
# ---------- sortie DOUT ----------
|
||||
for x, c in [(110, ROUGE), (150, BLANC_FIL), (190, VERT)]:
|
||||
s.fil(f"M{x} 282 V340", c)
|
||||
s.rect(96, 340, 108, 24, "#222", 3)
|
||||
s.texte(150, 390, "Sortie DOUT : ne rien brancher", 14, "#555", "middle")
|
||||
s.texte(150, 408, "(sert à chaîner un 2e panneau)", 13, "#888", "middle")
|
||||
|
||||
# ---------- alimentation de la matrice (fils du milieu) ----------
|
||||
s.fil("M490 282 V440 H530 V586", ROUGE, 6)
|
||||
s.fil("M550 282 V420 H590 V586", NOIR, 6)
|
||||
s.add('<line x1="490" y1="352" x2="506" y2="352" stroke="#777" stroke-width="2"/>')
|
||||
s.add('<line x1="534" y1="352" x2="550" y2="352" stroke="#777" stroke-width="2"/>')
|
||||
s.rect(506, 340, 28, 24, "#1f4e9c", 5)
|
||||
s.rect(524, 340, 10, 24, "#c9d6ee")
|
||||
s.texte(529, 356, "−", 13, "#1f4e9c", "middle", True)
|
||||
s.texte(474, 340, "Condensateur 1000 µF", 14, "#1f4e9c", "end", True)
|
||||
s.texte(474, 358, "bande « − » côté GND (fil noir)", 13, "#1f4e9c", "end")
|
||||
s.texte(474, 375, "patte + côté 5V (fil rouge)", 13, "#1f4e9c", "end")
|
||||
|
||||
# ---------- alimentation d'établi ----------
|
||||
s.rect(300, 500, 330, 210, "#e9ecef", 10, "#adb5bd", 2)
|
||||
s.texte(465, 526, "Alimentation d'établi", 16, "#333", "middle", True)
|
||||
s.rect(318, 540, 160, 96, "#1d2b1d", 4)
|
||||
s.texte(398, 578, "5.00 V", 28, "#7dff7d", "middle", True, True)
|
||||
s.texte(398, 618, "3.00 A", 20, "#ffb347", "middle", mono=True)
|
||||
for x, t in [(345, "tension"), (420, "courant")]:
|
||||
s.add(f'<circle cx="{x}" cy="672" r="16" fill="#666"/>')
|
||||
s.texte(x, 702, t, 11, "#555", "middle")
|
||||
s.add(f'<circle cx="530" cy="600" r="13" fill="{ROUGE}" stroke="#7a0f0f" stroke-width="2"/>')
|
||||
s.add(f'<circle cx="590" cy="600" r="13" fill="{NOIR}" stroke="#000" stroke-width="2"/>')
|
||||
s.texte(530, 637, "+", 15, ROUGE, "middle", True)
|
||||
s.texte(590, 637, "−", 15, NOIR, "middle", True)
|
||||
s.texte(560, 660, "bornes de", 12, "#555", "middle")
|
||||
s.texte(560, 675, "sortie", 12, "#555", "middle")
|
||||
|
||||
# ---------- entrée DIN + fiche ----------
|
||||
for x, c in [(880, ROUGE), (910, BLANC_FIL), (940, VERT)]:
|
||||
s.fil(f"M{x} 282 V340", c)
|
||||
s.rect(866, 340, 88, 22, "#222", 3)
|
||||
s.rect(870, 364, 80, 22, "#3a3a3a", 3)
|
||||
s.texte(966, 358, "connecteur DIN de la matrice", 13, "#555")
|
||||
s.texte(966, 381, "+ fiche fournie (rouge / blanc / vert)", 13, "#555")
|
||||
|
||||
# ---------- plaque d'essai + 74AHCT125 ----------
|
||||
s.rect(700, 425, 440, 190, "#f6f1e3", 8, "#d9cfb2", 2)
|
||||
s.texte(1132, 608, "plaque d'essai", 12, "#a8997a", "end")
|
||||
|
||||
def broche_bas(n): # pattes 1 à 7 (rangée du bas), de gauche à droite
|
||||
return 815 + (n - 1) * 28
|
||||
|
||||
def broche_haut(n): # pattes 14 à 8 (rangée du haut), de gauche à droite
|
||||
return 815 + (14 - n) * 28
|
||||
|
||||
# fils de la fiche vers la plaque
|
||||
s.fil(f"M880 386 V444 H{broche_haut(14)} V462", ROUGE) # 5V → VCC (14)
|
||||
s.fil(f"M910 386 V454 {hop_h(broche_haut(14), -1)} H780 V585", BLANC_FIL) # GND → rail
|
||||
s.fil(f"M940 386 V444 H1080 V470", VERT) # DIN → résistance
|
||||
s.fil(f"M1080 510 V552 {hop_h(broche_bas(7), -1)} H{broche_bas(3)} V528", VERT)
|
||||
# rail de masse
|
||||
s.fil(f"M780 585 {hop_h(broche_bas(2), 1)} H{broche_bas(7)}", NOIR, 4)
|
||||
s.fil(f"M{broche_bas(1)} 528 V585", NOIR, 4) # 1OE → GND
|
||||
s.fil(f"M{broche_bas(7)} 528 V585", NOIR, 4) # GND (7)
|
||||
for x in (780, broche_bas(1), broche_bas(7)):
|
||||
s.point(x, 585, NOIR)
|
||||
|
||||
# la puce
|
||||
s.rect(805, 470, 192, 50, "#222", 4)
|
||||
s.add('<path d="M805 485 a10 10 0 0 1 0 20" fill="#555"/>')
|
||||
s.texte(901, 500, "74AHCT125", 15, "#eee", "middle", True)
|
||||
for n in range(1, 8):
|
||||
s.rect(broche_bas(n) - 5, 520, 10, 8, "#b8b8b8")
|
||||
s.texte(broche_bas(n), 515, str(n), 9, "#bbb", "middle")
|
||||
for n in range(8, 15):
|
||||
s.rect(broche_haut(n) - 5, 462, 10, 8, "#b8b8b8")
|
||||
s.texte(broche_haut(n), 482, str(n), 9, "#bbb", "middle")
|
||||
s.texte(broche_bas(1) - 10, 545, "1OE", 11, "#333", "end", True)
|
||||
s.texte(broche_bas(2) + 6, 545, "1A", 11, JAUNE_GPIO, "start", True)
|
||||
s.texte(broche_bas(3) + 6, 545, "1Y", 11, VERT, "start", True)
|
||||
s.texte(broche_bas(7) + 8, 545, "GND", 11, "#333", "start", True)
|
||||
|
||||
# résistance 330 Ω
|
||||
s.rect(1071, 470, 18, 40, "#e8d3a8", 6, "#a88a52")
|
||||
for y, c in [(478, "#e67e22"), (486, "#e67e22"), (494, "#8b4513")]:
|
||||
s.rect(1071, y, 18, 4, c)
|
||||
s.rect(1071, 502, 18, 3, "#c9a227")
|
||||
s.texte(1098, 486, "330 Ω", 13, "#7a5a1e", gras=True)
|
||||
|
||||
s.texte(966, 404, "fil rouge de la fiche : il alimente", 12, ROUGE, gras=True)
|
||||
s.texte(966, 418, "la puce (patte 14 = VCC, 5 V)", 12, ROUGE, gras=True)
|
||||
|
||||
# ---------- Raspberry Pi ----------
|
||||
s.rect(700, 645, 440, 230, "#1f7a3a", 12)
|
||||
s.rect(750, 655, 336, 42, "#143d20", 3)
|
||||
x0 = broche_bas(2) - 80 # x de la patte 2 → la patte 12 tombe sous « 1A »
|
||||
for i in range(20):
|
||||
for rang, y in ((0, 666), (1, 684)): # rangée du bord : pattes paires
|
||||
n = 2 * i + 2 if rang == 0 else 2 * i + 1
|
||||
couleur = "#d4af37"
|
||||
if n in (2, 4):
|
||||
couleur = ROUGE
|
||||
elif n in (6, 14):
|
||||
couleur = NOIR
|
||||
elif n == 12:
|
||||
couleur = JAUNE_GPIO
|
||||
s.rect(x0 + i * 16 - 5, y - 5, 10, 10, couleur, 1)
|
||||
s.texte(x0 - 9, 670, "2", 9, "#cde", "end")
|
||||
s.texte(x0 - 9, 688, "1", 9, "#cde", "end")
|
||||
s.texte(x0 + 19 * 16 + 9, 670, "40", 9, "#cde")
|
||||
s.texte(x0 + 19 * 16 + 9, 688, "39", 9, "#cde")
|
||||
|
||||
# GPIO 18 (patte 12) → 1A (patte 2)
|
||||
s.fil(f"M{broche_bas(2)} 528 V661", JAUNE_GPIO)
|
||||
# GND (patte 6) ← rail
|
||||
s.fil(f"M780 585 V630 H{x0 + 32} V661", NOIR, 4)
|
||||
|
||||
s.texte(920, 770, "Raspberry Pi", 24, "#fff", "middle", True)
|
||||
s.texte(920, 794, "3B+, 4 ou Zero 2 W (pas le Pi 5)", 14, "#d8f0dd", "middle")
|
||||
s.texte(x0 + 80, 712, "patte 12 = GPIO 18", 12, "#ffe08a", "start", True)
|
||||
s.texte(x0 + 30, 712, "patte 6 = GND", 12, "#fff", "end", True)
|
||||
s.add(f'<path d="M{x0 + 8} 833 L{x0 + 24} 849 M{x0 + 24} 833 L{x0 + 8} 849" '
|
||||
f'stroke="#ffb3b3" stroke-width="3"/>')
|
||||
s.texte(x0 + 34, 846, "pattes 2 et 4 (5 V) : NE PAS relier à la matrice", 12, "#ffd6d6")
|
||||
|
||||
# alimentation USB du Pi
|
||||
s.rect(1140, 790, 18, 34, "#b8b8b8", 3)
|
||||
s.fil("M1158 807 H1180 V900", "#555")
|
||||
s.texte(1170, 918, "bloc secteur USB du Pi", 13, "#333", "end")
|
||||
s.texte(1170, 935, "(5 V, 2,5 A ou plus)", 12, "#777", "end")
|
||||
|
||||
# ---------- légende ----------
|
||||
s.rect(40, 470, 236, 300, "#f8f9fa", 8, "#dee2e6")
|
||||
s.texte(56, 496, "Code couleur des fils", 15, "#222", gras=True)
|
||||
for i, (c, t) in enumerate([(ROUGE, "rouge = + 5 V"), (NOIR, "noir = GND (masse)"),
|
||||
(BLANC_FIL, "blanc = GND (masse)"), (VERT, "vert = données 5 V"),
|
||||
(JAUNE_GPIO, "jaune = données 3,3 V")]):
|
||||
y = 520 + i * 28
|
||||
s.add(f'<line x1="56" y1="{y}" x2="90" y2="{y}" stroke="{c}" stroke-width="6" '
|
||||
'stroke-linecap="round"/>')
|
||||
s.texte(100, y + 5, t, 14)
|
||||
s.texte(56, 670, "Tous les GND sont reliés :", 13, "#555")
|
||||
s.texte(56, 688, "Pi ↔ puce ↔ fil blanc ↔ matrice", 13, "#555")
|
||||
s.texte(56, 706, "↔ fil noir ↔ borne − de l'alim.", 13, "#555")
|
||||
s.texte(56, 730, "Alim. réglée sur 5,0 V maxi !", 13, "#b00020", gras=True)
|
||||
s.texte(56, 752, "Un pont = les fils se croisent", 12, "#777")
|
||||
s.texte(56, 766, "sans se toucher.", 12, "#777")
|
||||
|
||||
s.save("branchement.svg")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 2. BROCHAGE DU 74AHCT125
|
||||
# =============================================================================
|
||||
|
||||
def brochage():
|
||||
s = Svg(820, 470, "Brochage du 74AHCT125 (vu de dessus)")
|
||||
s.texte(410, 36, "74AHCT125 vu de DESSUS (encoche à gauche)", 20, "#222", "middle", True)
|
||||
s.rect(250, 150, 320, 110, "#222", 6)
|
||||
s.add('<path d="M250 185 a20 20 0 0 1 0 40" fill="#555"/>')
|
||||
s.add('<circle cx="275" cy="240" r="6" fill="#555"/>')
|
||||
s.texte(410, 212, "74AHCT125", 24, "#eee", "middle", True)
|
||||
|
||||
bas = {1: ("1OE", "→ GND", "#333"), 2: ("1A", "← GPIO 18", JAUNE_GPIO),
|
||||
3: ("1Y", "→ 330 Ω → DIN", VERT), 4: ("2OE", "", "#aaa"), 5: ("2A", "", "#aaa"),
|
||||
6: ("2Y", "", "#aaa"), 7: ("GND", "→ GND", "#333")}
|
||||
haut = {14: ("VCC", "← 5 V", ROUGE), 13: ("4OE", "", "#aaa"), 12: ("4A", "", "#aaa"),
|
||||
11: ("4Y", "", "#aaa"), 10: ("3OE", "", "#aaa"), 9: ("3A", "", "#aaa"),
|
||||
8: ("3Y", "", "#aaa")}
|
||||
for n, (nom, role, c) in bas.items():
|
||||
x = 280 + (n - 1) * 43
|
||||
s.rect(x - 8, 260, 16, 26, "#b8b8b8")
|
||||
s.texte(x, 252, str(n), 12, "#bbb", "middle")
|
||||
s.texte(x, 306, nom, 14, c, "middle", True)
|
||||
if role:
|
||||
y = 338 + {1: 0, 2: 22, 3: 44, 7: 0}[n] * 1
|
||||
s.add(f'<line x1="{x}" y1="312" x2="{x}" y2="{y - 13}" stroke="{c}" stroke-width="1.5"/>')
|
||||
s.texte(x, y, role, 13, c, "middle", True)
|
||||
for n, (nom, role, c) in haut.items():
|
||||
x = 280 + (14 - n) * 43
|
||||
s.rect(x - 8, 124, 16, 26, "#b8b8b8")
|
||||
s.texte(x, 168, str(n), 12, "#bbb", "middle")
|
||||
s.texte(x, 114, nom, 14, c, "middle", True)
|
||||
if role:
|
||||
s.texte(x, 92, role, 13, c, "middle", True)
|
||||
|
||||
s.texte(40, 445, "On n'utilise qu'une des 4 « portes » (la n° 1). Pattes en gris : non branchées "
|
||||
"(on peut relier 4, 10 et 13 au 5 V pour les désactiver).", 13, "#555")
|
||||
s.save("brochage_74ahct125.svg")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 3. DE KRITA À LA MATRICE
|
||||
# =============================================================================
|
||||
|
||||
def chaine():
|
||||
s = Svg(1200, 330, "De Krita à la matrice : le trajet d'une animation")
|
||||
etapes = [
|
||||
("#7b3fa0", "1. Dessiner", ["Krita", "document 32 × 8 px", "pixel art, sans flou"]),
|
||||
("#b5418f", "2. Exporter", ["GIF animé", "ou séquence de PNG", "(Rendu de l'animation)"]),
|
||||
("#c9682a", "3. Vérifier", ["verifier_animation.py", "+ simulateur", "sur l'ordinateur"]),
|
||||
("#b08900", "4. Déposer", ["dossier « animations »", "carte SD ou réseau", "(01_, 02_… = ordre)"]),
|
||||
("#1f7a3a", "5. Lire", ["Raspberry Pi", "lecteur.py : gamma,", "luminosité, serpentin"]),
|
||||
("#2f6fc4", "6. Afficher", ["74AHCT125 (3,3 → 5 V)", "puis 256 LED", "WS2812B"]),
|
||||
]
|
||||
larg, ecart = 170, 28
|
||||
for i, (c, titre, lignes) in enumerate(etapes):
|
||||
x = 30 + i * (larg + ecart)
|
||||
s.rect(x, 60, larg, 190, "#ffffff", 12, c, 3)
|
||||
s.rect(x, 60, larg, 46, c, 12)
|
||||
s.rect(x, 90, larg, 16, c)
|
||||
s.texte(x + larg / 2, 90, titre, 18, "#fff", "middle", True)
|
||||
for j, t in enumerate(lignes):
|
||||
s.texte(x + larg / 2, 140 + j * 26, t, 14 if j == 0 else 13,
|
||||
"#222" if j == 0 else "#555", "middle", j == 0)
|
||||
if i < len(etapes) - 1:
|
||||
s.add(f'<line x1="{x + larg + 4}" y1="155" x2="{x + larg + ecart - 4}" y2="155" '
|
||||
'stroke="#555" stroke-width="3" marker-end="url(#flecheg)"/>')
|
||||
s.texte(30, 36, "De Krita à la matrice", 22, "#222", gras=True)
|
||||
s.add('<path d="M115 270 V292 H1003 V270" stroke="#aaa" stroke-width="2" fill="none" '
|
||||
'stroke-dasharray="6 5"/>')
|
||||
s.texte(559, 316, "Étapes 1 à 4 : l'équipe design, sur n'importe quel ordinateur. "
|
||||
"Étapes 5 et 6 : automatiques, dans la vitrine.", 14, "#555", "middle")
|
||||
s.save("chaine_krita_matrice.svg")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.makedirs(SORTIE, exist_ok=True)
|
||||
branchement()
|
||||
brochage()
|
||||
chaine()
|
||||
print("Schémas écrits dans", SORTIE)
|
||||
Executable
+187
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
=============================================================================
|
||||
VÉRIFIER UNE ANIMATION avant de l'envoyer sur la matrice
|
||||
Workshop « Vitrine pixels »
|
||||
=============================================================================
|
||||
|
||||
Pour l'équipe design : ce programme examine un export de Krita et dit
|
||||
s'il est prêt pour la matrice. Il fonctionne sur n'importe quel ordinateur
|
||||
(il suffit de Python et de Pillow), sans la matrice.
|
||||
|
||||
Usage :
|
||||
python3 outils/verifier_animation.py mon_animation.gif
|
||||
python3 outils/verifier_animation.py dossier_de_png/
|
||||
python3 outils/verifier_animation.py animations/ (tout le dossier)
|
||||
|
||||
Il affiche un petit rapport :
|
||||
- taille de l'image, nombre d'images, durée, vitesse ;
|
||||
- nombre de couleurs (beaucoup de couleurs = souvent de l'anticrénelage,
|
||||
ces pixels « flous » qui adoucissent les bords dans Krita) ;
|
||||
- pixels trop sombres, qui seront ÉTEINTS sur la matrice ;
|
||||
- courant consommé, pour vérifier que l'alimentation suit.
|
||||
|
||||
Et il fabrique, dans le dossier « apercus », deux APERÇUS « façon LED » :
|
||||
- mon_animation_apercu.png : planche avec toutes les images ;
|
||||
- mon_animation_apercu.gif : l'animation agrandie, à partager.
|
||||
(Pas dans le dossier des animations : le lecteur les jouerait aussi !)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# On réutilise le code du lecteur et du moteur (dossier parent).
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import lecteur # noqa: E402
|
||||
from matrice import LARGEUR, HAUTEUR, TABLE_GAMMA, LUMINOSITE, courant_estime_ma, COURANT_MAX_MA, pixels_de # noqa: E402
|
||||
|
||||
DOSSIER_APERCUS = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "apercus")
|
||||
TAILLE_LED = 14 # taille d'un pixel dans l'aperçu, en points
|
||||
MAX_PLANCHE = 48 # nombre maximum d'images sur la planche
|
||||
|
||||
|
||||
def pixel_eteint_sur_matrice(r, v, b):
|
||||
"""Vrai si ce pixel, allumé dans l'image, sera éteint sur la matrice
|
||||
(une fois la correction gamma et la luminosité appliquées)."""
|
||||
if (r, v, b) == (0, 0, 0):
|
||||
return False
|
||||
return all(TABLE_GAMMA[c] * LUMINOSITE // 255 == 0 for c in (r, v, b))
|
||||
|
||||
|
||||
def dessiner_leds(image, taille=TAILLE_LED):
|
||||
"""Agrandit une image 32 × 8 en dessinant un rond par pixel."""
|
||||
sortie = Image.new("RGB", (LARGEUR * taille, HAUTEUR * taille), (12, 12, 12))
|
||||
crayon = ImageDraw.Draw(sortie)
|
||||
for y in range(HAUTEUR):
|
||||
for x in range(LARGEUR):
|
||||
couleur = image.getpixel((x, y))
|
||||
if couleur == (0, 0, 0):
|
||||
couleur = (30, 30, 30) # LED éteinte : gris très sombre
|
||||
marge = max(1, taille // 7)
|
||||
crayon.ellipse((x * taille + marge, y * taille + marge,
|
||||
(x + 1) * taille - marge, (y + 1) * taille - marge), fill=couleur)
|
||||
return sortie
|
||||
|
||||
|
||||
def verifier(chemin):
|
||||
nom = os.path.basename(os.path.normpath(chemin))
|
||||
print("=" * 70)
|
||||
print(nom)
|
||||
print("=" * 70)
|
||||
|
||||
# Taille d'origine (avant adaptation par le lecteur).
|
||||
if os.path.isdir(chemin):
|
||||
fichiers = lecteur.images_du_dossier(chemin)
|
||||
if not fichiers:
|
||||
print(" ✗ aucun fichier image dans ce dossier")
|
||||
return
|
||||
with Image.open(fichiers[0]) as premiere:
|
||||
taille = premiere.size
|
||||
print(f" Type : séquence de {len(fichiers)} fichiers "
|
||||
f"({lecteur.lire_vitesse(chemin):g} images/s)")
|
||||
else:
|
||||
with Image.open(chemin) as fichier:
|
||||
taille = fichier.size
|
||||
print(f" Type : {fichier.format}, "
|
||||
f"{'animé' if getattr(fichier, 'is_animated', False) else 'image fixe'}")
|
||||
|
||||
etapes = lecteur.charger(chemin)
|
||||
duree = sum(d for _, d in etapes)
|
||||
|
||||
# --- Taille ---
|
||||
if taille == (LARGEUR, HAUTEUR):
|
||||
print(f" Taille : {taille[0]} × {taille[1]} ✓")
|
||||
elif taille[0] % LARGEUR == 0 and taille[0] // LARGEUR == taille[1] // HAUTEUR \
|
||||
and taille[1] % HAUTEUR == 0:
|
||||
print(f" Taille : {taille[0]} × {taille[1]} ✓ (agrandissement × "
|
||||
f"{taille[0] // LARGEUR}, sera réduit)")
|
||||
else:
|
||||
print(f" Taille : {taille[0]} × {taille[1]} ⚠ attendu {LARGEUR} × {HAUTEUR} "
|
||||
"(l'image sera centrée et coupée)")
|
||||
|
||||
# --- Durée ---
|
||||
if len(etapes) > 1:
|
||||
print(f" Images : {len(etapes)}, durée {duree:.2f} s "
|
||||
f"(≈ {len(etapes) / duree:.1f} images/s)")
|
||||
else:
|
||||
print(" Images : 1 (image fixe)")
|
||||
|
||||
# --- Couleurs ---
|
||||
couleurs = set()
|
||||
trop_sombres = 0
|
||||
for image, _ in etapes:
|
||||
for c in pixels_de(image):
|
||||
couleurs.add(c)
|
||||
for c in couleurs:
|
||||
if pixel_eteint_sur_matrice(*c):
|
||||
trop_sombres += 1
|
||||
alerte = " ⚠ beaucoup ! anticrénelage ou dégradé flou ?" if len(couleurs) > 64 else " ✓"
|
||||
print(f" Couleurs : {len(couleurs)} différentes{alerte}")
|
||||
if trop_sombres:
|
||||
print(f" Trop sombre : {trop_sombres} couleur(s) seront ÉTEINTES sur la matrice "
|
||||
f"(à luminosité {LUMINOSITE}) ⚠")
|
||||
else:
|
||||
print(" Trop sombre : aucune ✓")
|
||||
|
||||
# --- Courant ---
|
||||
courants = [courant_estime_ma(image) for image, _ in etapes]
|
||||
maxi = max(courants)
|
||||
moyen = sum(courants) / len(courants)
|
||||
limite = " ⚠ limité par le logiciel (image assombrie)" if maxi >= COURANT_MAX_MA - 1 else " ✓"
|
||||
print(f" Courant : moyen {moyen / 1000:.2f} A, maximum {maxi / 1000:.2f} A{limite}")
|
||||
|
||||
# --- Aperçus ---
|
||||
os.makedirs(DOSSIER_APERCUS, exist_ok=True)
|
||||
base = os.path.join(DOSSIER_APERCUS, os.path.splitext(nom)[0] + "_apercu")
|
||||
images = [dessiner_leds(image) for image, _ in etapes]
|
||||
|
||||
# Planche : les images les unes sous les autres, numérotées.
|
||||
montrees = images[:MAX_PLANCHE]
|
||||
l, h = images[0].size
|
||||
planche = Image.new("RGB", (l + 50, len(montrees) * (h + 6)), (255, 255, 255))
|
||||
crayon = ImageDraw.Draw(planche)
|
||||
for i, im in enumerate(montrees):
|
||||
planche.paste(im, (50, i * (h + 6)))
|
||||
crayon.text((6, i * (h + 6) + h // 2 - 6), str(i + 1), fill=(0, 0, 0))
|
||||
planche.save(base + ".png")
|
||||
print(f" Aperçu : {base}.png", end="")
|
||||
|
||||
if len(images) > 1:
|
||||
durees = [max(20, round(d * 1000)) for _, d in etapes]
|
||||
images[0].save(base + ".gif", save_all=True, append_images=images[1:],
|
||||
duration=durees, loop=0)
|
||||
print(f" et {base}.gif")
|
||||
else:
|
||||
print()
|
||||
|
||||
|
||||
def est_sequence(dossier):
|
||||
"""Vrai si le dossier ne contient que des PNG (et aucun sous-dossier)."""
|
||||
noms = [n for n in os.listdir(dossier) if not lecteur.est_cache(n)
|
||||
and n != "images_par_seconde.txt"]
|
||||
return bool(noms) and all(n.lower().endswith(".png") and
|
||||
os.path.isfile(os.path.join(dossier, n)) for n in noms)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
for chemin in sys.argv[1:]:
|
||||
# Un dossier peut être une SÉQUENCE (que des PNG, pas de sous-dossier)
|
||||
# ou un dossier d'animations ENTIER : on vérifie alors chaque élément.
|
||||
if os.path.isdir(chemin) and not est_sequence(chemin):
|
||||
cibles = lecteur.lister_animations(chemin)
|
||||
else:
|
||||
cibles = [chemin]
|
||||
for cible in cibles:
|
||||
try:
|
||||
verifier(cible)
|
||||
except Exception as erreur:
|
||||
print(f" ✗ impossible de lire {cible} : {erreur}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user