#!/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"))