Files
vitrine-pixels/8x32 PI/outils/generer_exemples.py
T
2026-10-08 15:25:14 +02:00

211 lines
8.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
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"))