8x32 PI : interface web, matrice verticale, compatibilité Raspbian 11

- Interface web (serveur_web.py, Flask, gabarits dans web/) : dépôt de GIF,
  PNG animés et séquences de PNG, aperçus animés, ordre de lecture,
  activer / désactiver, corbeille, « jouer maintenant » et « suivante ».
  Service systemd vitrine-web, activé par installer.sh.
- Lecteur : réagit aux demandes de la page web (post-it dans
  /run/vitrine-pixels), écrit l'animation en cours, éteint la matrice
  proprement sur SIGTERM.
- Matrice posée debout : réglage ROTATION (90 = image 8 × 32) dans
  matrice.py ; images couchées tournées automatiquement ; exemples,
  animations codées, aperçus et page web adaptés au format vertical.
- Compatibilité Raspbian 11 Bullseye : partition /boot, Pillow 8.1, Flask 1.1.
- Doc : nouveau guide INTERFACE_WEB.md ; README, GUIDE_KRITA, MODE_VITRINE,
  CODER_EN_PYTHON mis à jour (format 8 × 32, adresses Wi-Fi / câble du Pi).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bach
2026-10-08 16:08:18 +02:00
co-authored by Claude Opus 5.5
parent 98fca33142
commit 2d4b0d5a5f
63 changed files with 1592 additions and 156 deletions
+49 -28
View File
@@ -25,12 +25,15 @@ Pillow, puis les jouer comme des dessins.
import math
import os
import sys
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
sys.path.insert(0, ICI)
from matrice import LARGEUR as L, HAUTEUR as H # noqa: E402 (8 × 32 si debout)
DEBOUT = H > L
def nouvelle_image():
@@ -56,14 +59,13 @@ PETIT_COEUR = [
"...R...",
]
GRAND_COEUR = [
".RRR.RRR.",
"RBBRRRRRR",
"RBRRRRRRR",
"RRRRRRRRR",
".RRRRRRR.",
"..RRRRR..",
"...RRR...",
"....R....",
".RR..RR.",
"RBBRRRRR",
"RBRRRRRR",
"RRRRRRRR",
".RRRRRR.",
"..RRRR..",
"...RR...",
]
@@ -75,7 +77,7 @@ def coeur_qui_bat():
(GRAND_COEUR, 160)]:
im = nouvelle_image()
largeur = len(motif[0])
dessiner(im, motif, (L - largeur) // 2, (H - len(motif)) // 2 + (len(motif) == 6), couleurs)
dessiner(im, motif, (L - largeur) // 2, (H - len(motif)) // 2, couleurs)
images.append(im)
durees.append(duree)
images[0].save(os.path.join(ANIMATIONS, "01_coeur_qui_bat.gif"), save_all=True,
@@ -103,26 +105,43 @@ POLICE = {
}
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
def couleur_de_lettre(i, n):
"""Chaque lettre a sa propre teinte : un petit arc-en-ciel."""
return tuple(int(255 * (0.5 + 0.5 * math.cos(2 * math.pi * (i / n + k / 3))))
for k in range(3))
# 2) On fait glisser une « fenêtre » de 32 pixels sur la bande.
def texte_defilant(texte="VITRINE PIXELS"):
# 1) On écrit tout le texte sur une longue bande :
# - matrice couchée : les lettres côte à côte, la bande défile vers la gauche ;
# - matrice debout : les lettres les unes SOUS les autres (comme une
# enseigne verticale), la bande défile vers le haut.
if DEBOUT:
bande = Image.new("RGB", (L, len(texte) * 8))
for i, c in enumerate(texte):
x0 = (L - len(POLICE[c][0])) // 2 # lettre centrée
for y, ligne in enumerate(POLICE[c]):
for dx, p in enumerate(ligne):
if p == "#":
bande.putpixel((x0 + dx, i * 8 + y), couleur_de_lettre(i, len(texte)))
longueur, fenetre = bande.size[1], H
else:
longueur = sum(len(POLICE[c][0]) + 1 for c in texte)
bande = Image.new("RGB", (longueur, H))
x = 0
for i, c in enumerate(texte):
for y, ligne in enumerate(POLICE[c]):
for dx, p in enumerate(ligne):
if p == "#":
bande.putpixel((x + dx, y), couleur_de_lettre(i, len(texte)))
x += len(POLICE[c][0]) + 1
fenetre = L
# 2) On fait glisser une « fenêtre » de la taille de la matrice sur la bande.
images = []
for decalage in range(-L, largeur_totale + 1):
for decalage in range(-fenetre, longueur + 1):
im = nouvelle_image()
im.paste(bande, (-decalage, 0))
im.paste(bande, (0, -decalage) if DEBOUT else (-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)
@@ -137,7 +156,9 @@ def vague(nb_images=40, ips=20):
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))
# Hauteur de l'eau dans la colonne x : la moitié de la matrice,
# plus ou moins une vague qui se déplace avec « phase ».
hauteur = H / 2 + max(1.5, H * 0.15) * math.sin(2 * math.pi * x / max(L, 16) + 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 :
+1 -1
View File
@@ -416,7 +416,7 @@ def brochage():
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"]),
("#7b3fa0", "1. Dessiner", ["Krita", "document 8 × 32 px", "(vertical), 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)"]),
+15 -7
View File
@@ -39,7 +39,7 @@ from matrice import LARGEUR, HAUTEUR, TABLE_GAMMA, LUMINOSITE, courant_estime_ma
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
MAX_PLANCHE = 24 # nombre maximum d'images sur la planche
def pixel_eteint_sur_matrice(r, v, b):
@@ -137,14 +137,22 @@ def verifier(chemin):
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.
# Planche : toutes les images, numérotées. Matrice couchée : les unes sous
# les autres ; matrice debout : côte à côte, comme une bande dessinée.
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))
if h > l:
planche = Image.new("RGB", (len(montrees) * (l + 8), h + 24), (255, 255, 255))
crayon = ImageDraw.Draw(planche)
for i, im in enumerate(montrees):
planche.paste(im, (i * (l + 8), 24))
crayon.text((i * (l + 8) + l // 2 - 6, 6), str(i + 1), fill=(0, 0, 0))
else:
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="")