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"))