211 lines
8.2 KiB
Python
Executable File
211 lines
8.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
|
||
|
||
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"))
|