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