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:
@@ -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 :
|
||||
|
||||
@@ -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)"]),
|
||||
|
||||
@@ -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="")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user