Pour notre premier projet de la spé NSI dans le lycée Louis Pasteur, on nous à demandé de réaliser une image déssinée en Python. Nous avions libre choix de réaliser le dessin que l’on desirait, on à donc décidé de réaliser un sujet qui nous passione – le Pixel Art. Seulement, déssiner un pixel art en python demande beaucoup de travail et de réflexion.
Pour valoriser encore plus notre travail, nous avons décidé de rendre cette image animée.
C’est pour cela que le prof nous à suggeré d’utiliser les fonctions, et dans la suite de cet article, nous vous présenterons les techniques qui nous ont permi d’optimiser et de réaliser notre animation représentant un Pixel Art d’une nuie étoilée.
Sommaire
Schéma Original
Tout d’abord, Léandre à commencé par schématiser notre idée sur un logiciel de déssin pixelart – Aseprite.
Nous avons décidé de transcrire seulement les rideaux et la fenêtre en python, sinon le travail était trop manuel, long, et ne demandait pas tant de réfléxion.
Reconstitution des rideaux en Python
Commençons par le commencement :
Nous avons défini les constantes suivantes, qui vont nous aider plus tard.
SCREEN_WIDTH = 1280 + 10 SCREEN_HEIGHT = 720 + 10 PIXEL_SIZE = 15.25 SNOWFLAKE_COUNT = 50 SNOWFLAKE_SIZE_RATIO = 0.1 SNOWFLAKE_MOVE_DOWN = -10 STAR_COUNT = 100 WAIT_TIME_MS = 100 TOTAL_GENERATIONS = 100
Puis, nous avons transcrit manuellement le schéma en python à l’aide de boucles for, et d’une fonction spéciale, conçue par Clovis, nommée draw_pixel_symmetric(couleur, x, y)
.
Pourquoi symmetric
? Car cela nous permet de réaliser 2 rideaux en même temps, symétrique à l’axe vertical, en 2 fois moins de lignes !
D’ailleurs, fun fact. Nous avions pris plus d’une heure à trouver comment réaliser cette symétrie. Clovis était parti dans l’écriture d’un algorithme de dessin de pixels sous forme de dictionnaires {"y = 0": ["Couleur du pixel en x = 0", "Couleur du pixel en x = 1", "etc.."], "y 1":["..."]}
avant de trouver une solution plus simple, efficace qui occupe un peu moins de 10 lignes.
def draw_pixel_symmetric(couleur, x, y): # OG PIXEL penup() goto(x, y) pendown() color(couleur) begin_fill() for _ in range(4): forward(PIXEL_SIZE) left(90) end_fill() penup() # SYMMETRIQUE PIXEL goto(-x, y) pendown() begin_fill() for _ in range(4): forward(PIXEL_SIZE) left(90) end_fill() penup()
C’est évident – au lieu d’avoir un seul pixel en x, y
, nous l’avons dupliqué et placé le doublon en -x, y
.
Seulement, si c’était si simple.. Nous avons crée la fonction rideaux()
où se focalise la transcription manuelle. Merci à Léandre d’avoir transcrit l’entièreté du rideau (2h de travail : 1400 lignes de code).
def rideaux(): x = -SCREEN_WIDTH//2 y = -SCREEN_HEIGHT//2 + PIXEL_SIZE for _ in range(2): draw_pixel_symmetric('#9e1b1b',x,y) x = x + PIXEL_SIZE x = -SCREEN_WIDTH//2 y = -SCREEN_HEIGHT//2 + PIXEL_SIZE * 2 draw_pixel_symmetric('#9e2828',x,y) x = x + PIXEL_SIZE draw_pixel_symmetric('#ac3232',x,y) x = x + PIXEL_SIZE # ... Et la suite dont on vous fait part d'abstraction.
rideaux()
sans système de symétrie
rideaux()
avec draw_pixel_symmetric()
. Et voici donc le résultat final. Pas mal non ? Ce n’est seulement que la première étape.
Système de gradient
Le système de gradient, développé par Clovis, prend 5 arguments :
- colors (liste
["couleur 1", "couleur 2"]
) - x_start
- y_start
- x_end
- y_end
Sans oublier une fonction qui converti l’héxadécimal en décimal, pour pouvoir manipuler des couleurs RGB, et assurer une transition des couleurs de manière fluide.
def hex_to_rgb(hex_color): hex_color = hex_color.lstrip('#') return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) def draw_gradient(colors, x_start, y_start, x_end, y_end): penup() setheading(0) # tortue regarde à l'est current_y = y_start total_steps = y_end - y_start start_rgb = hex_to_rgb(colors[0]) end_rgb = hex_to_rgb(colors[1]) while current_y <= y_end: proportion = (current_y - y_start) / total_steps interpolated_rgb = tuple( int(start + (end - start) * proportion) for start, end in zip(start_rgb, end_rgb) ) pencolor(interpolated_rgb) penup() goto(x_start, current_y) pendown() forward(x_end - x_start) current_y += 1
Expliquons la fonction draw_gradient()
plus en détail :
Celle-ci effectue une boucle while
qui s’arrête lorsque current_y
dépasse ou est égal aux coordonnées y de destination (y_end
).
Pour calculer la valeur RGB correspondante à la position de current_y
, nous effectuons une boucle for
qui consiste à construire un tuple contenant les valeurs rouge, vert, et bleu en fonction de start_rgb
, end_rgb
et la valeur qui change selon current_y
: proportion
.
Résultat avec les valeurs hexadécimales ["#4a4c9b", "#050758"]
.
Appel de la fonction complète : draw_gradient(["#4a4c9b", "#050758"], -SCREEN_WIDTH//2, -SCREEN_HEIGHT//2, SCREEN_WIDTH//2, SCREEN_HEIGHT//2)
Les étoiles
Pour pimenter notre gradient, nous avons placé des étoiles à des emplacements aléatoires.
Voici l’algorithme que nous avions développé.
def initialize_positions(count, screen_width, screen_height): # liste avec des tuples dedans PRCQ ON ADORE LES TUPLES !!!!! TOUCHE PAS A MON TUPLE !!! positions = [] for _ in range(count): positions.append((randint(-screen_width // 2, screen_width // 2), randint(-screen_height // 2, screen_height // 2))) return positions def draw_stars(stars): for (x, y) in stars: draw_pixel(2, x, y, '#ffffff') stars = initialize_positions(STAR_COUNT, SCREEN_WIDTH, SCREEN_HEIGHT) draw_stars(stars)
Pour générer les positions aléatoires, on à décidé de créer une fonction initialize_positions(count, screen_width, screen_height)
qui prend en compte les arguments correspondant au montant désiré, la largeur et la hauteur de l’écran. Cette fonction renvoie une liste []
remplie de tuples (x, y)
contenant les positions aléatoires en x et y. Créer cette fonction nous permettra d’optimiser encore plus notre système de flocons dans la prochaine section de cet article.
Puis, une fonction draw_starts(stars)
à été réalisée pour looper dans chaques tuples contenus dans la liste stars
et placer un pixel de taille 2 à l’emplacement correspondant.
Gradient + Etoiles
Gradient + Etoiles + Rideaux
Les compléments des rideaux
Pour ajouter un peu plus de détails, nous avons décidé de rajouter un mur en bas de l’image et un cadre de fenêtre.
De plus, nous avons détaillé les couleurs en rajoutant un système de randomization pour altérner les couleurs marron (claires & foncées). Pour faire cela, il fallait inventer un système pour que chaques pixels ai une variation de marron propre à celui-ci. (Vu qu’il y a une animation plus tard, faire colour_variations_marron[randint(0, 1)]
changerait la couleur du pixel à chaques générations).
colour_variations_marron = [] # initialisation des variations de couleurs une seule fois colour_variations_marron = tuple( tuple("#63352d" if randint(0, 1) == 0 else "#5e3129" for _ in range(SCREEN_HEIGHT)) for _ in range(SCREEN_WIDTH) ) def supplement_rideaux(): x = -SCREEN_WIDTH//2 y = SCREEN_HEIGHT//2 # Barre horizontale for _ in range(44): draw_pixel_symmetric('#4e2b25',x,PIXEL_SIZE * 2.2) x = x + PIXEL_SIZE x = 0 - PIXEL_SIZE * 12 y = SCREEN_HEIGHT//2 # Double Barre Verticale for _ in range(45): color = colour_variations_marron[round(x)][round(y)] draw_pixel_symmetric(color, x, y) draw_pixel_symmetric(color, x - PIXEL_SIZE // 1.5, y) y -= PIXEL_SIZE x = -SCREEN_WIDTH//2 y = -SCREEN_HEIGHT//2 + PIXEL_SIZE * 8 # Barre horizontale au dessus du mur for _ in range(43): draw_pixel_symmetric('#4e2b25',x,y) x = x + PIXEL_SIZE x = -SCREEN_WIDTH//2 y = -SCREEN_HEIGHT//2 # Mur en bas for _ in range(7): for _ in range(22): for _ in range(3): color = colour_variations_marron[round(x)][round(y)] draw_pixel(PIXEL_SIZE,x,y,color) x = x + PIXEL_SIZE draw_pixel(PIXEL_SIZE,x,y,'#8f563b') x = x + PIXEL_SIZE x = -SCREEN_WIDTH//2 y += PIXEL_SIZE
Et voici le résultat !
Les flocons animés
Cette dernière partie était la plus intéressante à faire. Beaucoup de travail et de recherches mais pour un résultat surprenant.
Voici la fonction draw_snowflake()
qui déssine un flocon en x, y
. Le choix d’une couleur est possible grâce à l’argument couleur
. Et la taille de celui-ci est désirable grâce à l’argument size_ratio
. Étonnement, Clovis était celui qui à désinné ce flocon, et à trouvé cela plus compliqué comparé aux autres fonctions développées dans tout le projet (ça à pris du temps et de l’imagination).
def draw_snowflake(size_ratio, og_x, og_y, couleur): color(couleur) x, y = og_x, og_y ps = 7 * size_ratio # pixel(ps, og_x, og_y) goto(og_x - 8*ps, og_y - 8*ps) x, y = pos() left(90) pendown() for _ in range(16): draw_pixel(ps, x, y) x, y = x + ps, y + ps goto(x, y) goto(og_x - 8*ps, og_y + 7*ps) x, y = pos() for _ in range(16): draw_pixel(ps, x, y) x, y = x + ps, y - ps goto(x, y) goto(og_x - ps, og_y + ps * 8) x, y = pos() og_x2, og_y2 = x, y for p in range(18): goto(og_x2, og_y2 - (p * ps)) x, y = pos() for i in range(2): draw_pixel(ps, x + (i * ps), y) x, y = pos() goto(og_x - 8 * ps, og_y- ps) x, y = pos() og_x2, og_y2 = x, y for p in range(16): goto(og_x2 + (p * ps), og_y2) x, y = pos() for i in range(2): draw_pixel(ps, x , y+ (i * ps)) x, y = pos() penup()
‘Quel banger‘ – Léandre quand Clovis à montré son flocon pour la premère fois
Ensuite, passons au système de mouvement des flocons.
def update_snowflake_positions(current_positions): updated_positions = [] for (x, y) in current_positions: new_y = y + SNOWFLAKE_MOVE_DOWN # snowflake pos reset if goes too down if new_y < -SCREEN_HEIGHT // 2: new_y = SCREEN_HEIGHT // 2 new_x = randint(-SCREEN_WIDTH // 2, SCREEN_WIDTH // 2) updated_positions.append((new_x, new_y)) else: updated_positions.append((x, new_y)) return updated_positions snowflakes = initialize_positions(SNOWFLAKE_COUNT, SCREEN_WIDTH, SCREEN_HEIGHT)
Pour commencer, la fonction update_snowflake_positions(current_positions)
prend en compte la liste [(x, y), (x, y)]
contenant les tuples de positions x, y
de chaques flocons. (réf. initialize_positions(count, screen_width, screen_height)
).
Puis initialise une nouvelle liste updated_positions
qui contient une nouvelle version de chaques tuples (x, y)
, cette fois-ci avec une valeur y
différée selon la constante SNOWFLAKE_MOVE_DOWN (new_y = y + SNOWFLAKE_MOVE_DOWN)
.
En plus, nous avons ajouté un système qui replace le flocon en haut de l’écran si les coordonnées y de celui-ci dépasse le point le plus bas de l’écran.
- Point le plus haut de l’écran :
SCREEN_HEIGHT // 2
- Point le plus bas de l’écran :
-SCREEN_HEIGHT // 2
La valeur SCREEN_HEIGHT est divisée par 2, car le module turtle fait en sorte que la position (0, 0) correspond au centre absolu de l’écran.
Enfin, voici la boucle finale permettant une animation.
for generation in range(1, TOTAL_GENERATIONS + 1): # main loop clear() snowflakes = update_snowflake_positions(snowflakes) draw_snowflakes(snowflakes) update() print(f"Generation n'{generation} dessinée :D") time.sleep(WAIT_TIME_MS / 1000) # Converti millisecondes en seconds
Explication des termes :
clear()
permet d’effacer complètement tous les éléments de la fenêtre turtle.update()
permet de mettre à jour la fenêtre, utile pour réaliser des animations.time.sleep(WAIT_TIME_MS / 1000)
permet un temps d’attente entre chaques image exposée. Ici la constante WAIT_TIME_MS est de 100, et la fonction time.sleep() ne prend seulement comme argument des secondes. Il faut donc convertir les millisecondes en secondes, signifiant une division par 1000.
Si TOTAL_GENERATIONS = 1, alors turtle nous renverra une seule image, et non un animé.
Autrement, si TOTAL_GENERATIONS est au dessus de 1, on obtient une animation. Voici un exemple avec 100 :
Pas mal non ?
Sources
Pour réaliser toutes ces fonctions, nous avons utilisé nos propres connaissances, logique, et w3schools qui nous à beaucoup servi pour comprendre certaines opérations et fonctions integrées dans python (comme la fonction zip).
Image et animation finale
Télécharger le projet
Remarque des enseignants : les élèves n’ont pas respecté le nombre maximal de lignes imposé (404) ni la charte de nommage des variables. Cela pose de nombreux problèmes. L’article est néanmoins publié.