This commit is contained in:
bach
2026-10-08 15:25:14 +02:00
parent 32e617f67b
commit da4fac3af7
64 changed files with 3921 additions and 0 deletions
+210
View File
@@ -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"))
+316
View File
@@ -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)
+187
View File
@@ -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()