- 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>
232 lines
9.2 KiB
Python
Executable File
232 lines
9.2 KiB
Python
Executable File
#!/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"))
|