Files
vitrine-pixels/8x32 PI/outils/generer_exemples.py
T
bachandClaude Opus 5.5 2d4b0d5a5f 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>
2026-10-08 16:08:18 +02:00

232 lines
9.2 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
import sys
from PIL import Image
ICI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ANIMATIONS = os.path.join(ICI, "animations")
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():
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 = [
".RR..RR.",
"RBBRRRRR",
"RBRRRRRR",
"RRRRRRRR",
".RRRRRR.",
"..RRRR..",
"...RR...",
]
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, 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 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))
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(-fenetre, longueur + 1):
im = nouvelle_image()
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)
# --- 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 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 :
# 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"))