Blog

Page d’exemple

Art

Un coucher de soleil à la Seyne-sur-Mer

Dans ce premier projet de première spécialité NSI, intitulé Art génératif, nous allons expliquer comment nous avons réalisé un programme représentant un coucher de soleil inspiré des paysages de La Seyne-sur-Mer, avec les célèbres rochers « Les Deux Frères ». Ce projet nous a permis d’explorer différents concepts de programmation en Python, en utilisant notamment les modules turtle et random pour donner un paysage le plus réaliste possible.

Origines de la Seyne-sur-Mer

La Seyne-sur-Mer est une commune française située dans le département du Var, en région Provence-Alpes-Côte d’Azur. Elle compte environ 65 000 habitants, ce qui en fait la deuxième ville la plus peuplée du Var, après Toulon.

Parmi ses nombreuses zones touristiques, on trouve les « Deux Frères », nom donné à deux rochers emblématiques de la côte maritime, émergeant à la pointe du Cap Sicié et visibles depuis les plages. Le nom « Deux Frères » provient d’une légende locale.

Le Projet

Pour réaliser ce projet, nous avons décidé de créer un paysage inspiré des couchers de soleil de La Seyne-sur-Mer. L’objectif était d’utiliser le plus de concepts vus en classe, tels que les modules Turtle et Random, ainsi que l’utilisation de boucles. De plus, pour donner un effet plus naturel, la position et la taille des nuages et des étoiles sont générées de façon aléatoire.

Structure du script

Notre script est divisé en plusieurs parties, chacune ayant une fonction distincte représentant un élément du paysage (dégradé, soleil, étoiles, nuages et les Deux Frères). Ces fonctions sont appelées à la fin dans un ordre spécifique pour créer l’image finale.

Analyse du script

L’analyse du script se déroule en plusieurs étapes.

1. Tout d’abord, nous importons les modules nécessaires, notamment le module Turtle pour le dessin graphique et le module Random pour générer des valeurs aléatoires.

from turtle import *
from random import randint

2. Fonction dégradé de couleur

# Fonction dégradé de couleur
def degrade(couleur_debut, couleur_fin, etapes, decalage_y, effet_vague=False):
    r_diff = couleur_fin[0] - couleur_debut[0]
    g_diff = couleur_fin[1] - couleur_debut[1]
    b_diff = couleur_fin[2] - couleur_debut[2]
    penup()
    goto(-640, decalage_y)
    pendown()

Cette fonction degrade prend quatre arguments : couleur_debutcouleur_finetapes, et decalage_y. Elle calcule la différence entre les valeurs RGB des couleurs de début et de fin, ce qui est nécessaire pour créer un dégradé. Ensuite, elle utilise penup() pour lever le stylo afin de ne pas dessiner pendant le déplacement, et goto() pour se positionner sur l’écran. pendown() est ensuite utilisé pour recommencer à dessiner. Cette partie est essentielle pour établir la base du dégradé.

3. Boucle pour réaliser le dégradé

    for i in range(etapes):
        if effet_vague:
            # Effet  vague si activé
            valeur_vague = int(30 * (1 + (i % 20) / 20.0))
            couleur_actuelle = (
                int(couleur_debut[0] + (r_diff * i / etapes)),
                int(couleur_debut[1] + (g_diff * i / etapes)),
                int(couleur_debut[2] + (b_diff * i / etapes + valeur_vague)),
            )
        else:
            # Couleur effet de vague desactivé 
            couleur_actuelle = (
                int(couleur_debut[0] + (r_diff * i / etapes)),
                int(couleur_debut[1] + (g_diff * i / etapes)),
                int(couleur_debut[2] + (b_diff * i / etapes)),
            )

Cette partie de la fonction utilise une boucle pour parcourir le nombre d’étapes spécifié. Dans la première branche de la condition if, si effet_vague est activé, un effet de vague est appliqué à la couleur actuelle en ajoutant une variation à la composante bleue. La couleur actuelle est ensuite calculée en ajoutant une autre couleur à celle de départ.

Dans le bloc else, qui s’exécute lorsque l’effet de vague est désactivé, la couleur actuelle est calculée sans cet effet.

4. La limite des valeurs pour les couleurs

        # Limite des couleurs (valeur compris entre 0 et 255) 
        couleur_actuelle = (
            min(255, max(0, couleur_actuelle[0])),
            min(255, max(0, couleur_actuelle[1])),
            min(255, max(0, couleur_actuelle[2])),
        )

Cette partie du script assure que les valeurs RGB de couleur_actuelle restent comprises entre 0 et 255, comme le montre l’utilisation de min et max. Cela permet de générer des couleurs valides.

5. Les lignes du dégradé

        pencolor(couleur_actuelle)
        forward(1280)
        goto(-640, decalage_y - (360 / etapes) * (i + 1))

Ici, la couleur du stylo est définie sur couleur_actuelle avec pencolor(couleur_actuelle). Ensuite, le curseur Turtle se déplace en avant sur 1280 pixels (forward(1280)), puis se repositionne pour dessiner la prochaine ligne du dégradé en fonction de l’étape actuelle.

6. Le soleil

# Fonction pour réaliser  le soleil
def soleil(position, rayon, couleur):
    penup()
    goto(position[0], position[1] - rayon)
    pendown()
    pencolor(couleur)
    fillcolor(couleur)
    begin_fill()
    setheading(90)
    circle(rayon, 180)
    end_fill()

Il n’y a pas de coucher de soleil sans le soleil. Nous avons donc créé la fonction soleil, qui dessine un demi-cercle à une position donnée et le remplit d’une couleur spécifiée.

7. Les étoiles

# Fonction pour réaliser  des étoiles dans le ciel disposer aléatoirement 
def etoiles():
    pensize(1)
    for _ in range(125):
        penup()
        x, y = randint(-700, 700), randint(0, 700)
        goto(x, y)
        pendown()
        couleur_etoile = (randint(230, 255), randint(230, 255), randint(230, 255))
        pencolor(couleur_etoile)
        dot(randint(1, 2))

Pour réaliser les étoiles, nous les avons disposées de façon aléatoire dans la partie supérieure, représentant le ciel. Nous avons également varié leur taille de manière aléatoire entre 1 et 2, afin d’ajouter un effet plus naturel au dessin. Ce processus pour les étoiles est réalisé 125 fois (for _ in range(125):)

8. Les nuages

# Fonction pour dessiner des nuages 
def nuages():
    penup()
    pencolor((200, 200, 200))  # Gris pastel
    for _ in range(15):
        x, y = randint(-600, 600), randint(100, 300)
        for _ in range(randint(5, 10)):
            offset_x = x + randint(-30, 30)
            offset_y = y + randint(-10, 10)
            goto(offset_x, offset_y)
            pendown()
            dot(randint(30, 60))
            penup()
            nouvelle_ombre = randint(190, 220)
            pencolor((nouvelle_ombre, nouvelle_ombre, nouvelle_ombre))
            for _ in range(randint(5, 10)):
                sous_offset_x = offset_x + randint(-20, 20)
                sous_offset_y = offset_y + randint(-10, 10)
                goto(sous_offset_x, sous_offset_y)
                pendown()
                dot(randint(10, 20))
                penup()

Pour réaliser les nuages, nous nous sommes inspirés d’une technique de dessin apprise en cours d’arts plastiques, consistant à dessiner plusieurs petits ronds et à les remplir avec trois nuances de gris différents. Les nuages sont disposés aléatoirement dans le ciel, avec des nuances de gris pastel, afin de rester cohérents avec le reste des couleurs du dégradé. Ils sont représentés par plusieurs points (dots) positionnés légèrement décalés pour créer une texture.

9. « Les Deux Frères »

# Fonction pour dessiner les "Deux Frères"
def les_deux_freres():
    penup()
    goto(200, 0)  # Position initiale pour les Deux Frères

    # Petit triangle à gauche
    fillcolor((140, 130, 120))
    begin_fill()
    pendown()
    setheading(60)
    forward(40)
    right(120)
    forward(40)
    right(120)
    forward(40)
    end_fill()
    penup()

    # Premier grand rocher
    goto(230, 0)
    fillcolor((120, 110, 100))
    begin_fill()
    pendown()
    setheading(60)
    forward(110)
    right(120)
    forward(110)
    right(120)
    forward(110)
    end_fill()
    penup()

    # Deuxième grand rocher 
    goto(310, 0)
    fillcolor((100, 90, 80))
    begin_fill()
    pendown()
    setheading(60)
    forward(120)
    right(120)
    forward(120)
    right(120)
    forward(120)
    end_fill()

Pour dessiner les Deux Frères, nous avons utilisé des triangles, représentant de manière stylisée les rochers. La couleur choisie est un gris sombre pour les rochers, afin de contraster avec le ciel. La position et l’orientation sont ajustées pour bien les intégrer dans le paysage.

10. Configuration de la fenêtre

# Configuration de la fenêtre Turtle
setup(1280, 720, 0, 0)
colormode(255)
speed(0)

Dans cette étapes du code nous délimitons la fenêtre afin d’avoir tous les éléments du décor dans la fenêtre afin de tous les apercevoir.

11. Execution des fonctions

# Création du coucher de soleil (océan + ciel)
degrade((0, 0, 255), (255, 255, 102), 720, 360)
degrade((255, 255, 102), (0, 0, 255), 720, 0, effet_vague=True)

# Dessin du soleil, des étoiles, des nuages et des îlots des Deux Frères
soleil((100, 100), 100, (255, 255, 102))
etoiles()
nuages()
les_deux_freres()
done()

Ici nous faisons appelle à tout les fonctions afin de réaliser le dessin.

Les difficultés rencontrées

Lors des nombreux tests réalisés, nous avons rencontré plusieurs difficultés, notamment avec les valeurs RGB et les fonctions dégradé et nuages, en raison de la complexité de leur création et des formules de calcul utilisées.

Source

Pour mener à bien ce projet, nous avons eu recours à nos connaissances personnelles ainsi qu’à Internet. Nous avons utilisé des sites comme NumWorks.com et Python.org pour apprendre de nouvelles choses, et nsi.xyz pour consulter les anciens projets afin de comprendre les attentes.

Télécharger le .py

L’image finale

Après beaucoup de tests et d’ajustements, voici le rendu final de notre projet que vous trouverez ci-dessous, résultat de tout le travail que nous y avons investi.

Projets

Linkin Park

Linkin Park est un groupe de rock et de métal américain formé en 1996. Il est actuellement composé de Emily Armstrong au chant à la suite du décès de Chester Bennington, Colin Brittain à la batterie à la suite du départ de Rob Bourdon, Mike Shinoda au chant(rap), à la guitare rythmique et au clavier, Brad Delson à la guitare solo, Dave Farrell à la guitare basse et Joe Hahn aux platines, effets et mixages.

Le fond

C’est un dégradé allant du blanc au noir grâce à une boucle for qui dessine les traits de couleurs au fur et à mesure. J’ai choisis ces teintes car le symbole et le nom du groupe sont habituellement représentés en noir avec un fond blanc ou inversement.

from turtle import *
colormode(255)
Screen()
setup(800, 600)
penup()
colormode(255)
speed(10)
goto(-400, -300)
pendown()

for i in range(600):
    pencolor(255 - round(i / (600 / 255)), 255 - round(i / (600 / 255)), 255 - round(i / (600 / 255)))
    forward(800)
    goto(-400,-300+i)

Le logo du groupe Linkin Park a beaucoup changé au cours du temps et cette forme est sûrement ma préférée. Chaque angle de l’hexagone est censé symboliser un des membres de la bande (six au total). Pour le codage du logo ; il est composé d’un goto(x,y) pour définir où la figure commencera. Ensuite, la fonction circle() me servira deux fois pour les deux arc de cercle et enfin je ferais tous les angles à l’aide des fonction setheading() pour définir l’angle de direction du trait et forward() pour définir la longueur de ce trait.

pencolor(209,209,209)
pensize(10)
penup()
goto(0,250)
pendown()
circle(-100,-170)

setheading(70)
forward(145)

setheading(115)
forward(-90)

setheading(-180)
forward(108)

setheading(70)
forward(153)

setheading(0)
circle(-100,190)

L’écriture de Linkin

Pour écrire « Linkin » j’ai d’abord définit le i avec la fonction def pour le réutiliser dans d’autre lettres comme la barre du L, du N, du K et les deux barres du dernier N. J’ai ensuite utilisé les fonctions goto(x,y) pour commencer de nouvelles lettres aux coordonnées voulues, setheading() et forward() comme pour le logo. Mais j’ai aussi commencé à appliquer les fonction begin_fill() et end_fill() qui permette de remplir une portion de dessin de la couleur définit par fillcolor(r,g,b) tant que tous les pointes sont reliés.

penup()
goto(-240,20)
pendown()
# linkin park
# def i
def i(r,g,b,pensize):
    pencolor(r,g,b)
    fillcolor(r,g,b)
    
    begin_fill()
    
    setheading(0)
    forward(30)
            
    setheading(-90)
    forward(100)
    
    setheading(-180)
    forward(30)
    
    setheading(90)
    forward(100)
    
    end_fill()

# L
i(60,60,60,pensize(5))

setheading(-90)
forward(70)

fillcolor(60,60,60)
begin_fill()

setheading(0)
forward(70)

setheading(-90)
forward(30)

setheading(-180)
forward(70)

setheading(90)
forward(30)

end_fill()

penup()
goto(-155,20)
pendown()
# I
i(60,60,60,pensize(5))

penup()
goto(-110,20)
pendown()
#N
i(60,60,60,pensize(5))

penup()
goto(-80,20)
pendown()

begin_fill()

setheading(-55)
forward(60)

setheading(90)
forward(49)

setheading(0)
forward(30)

setheading(-90)
forward(100)

forward(50)

setheading(125)
forward(111)

setheading(90)
forward(59)

end_fill()

penup()
goto(-2,20)
pendown()
# K
i(60,60,60,pensize(5))

penup()
goto(28,-20)
pendown()

begin_fill()

setheading(50)
forward(52)

setheading(0)
forward(40)

setheading(-130)
forward(65)

setheading(-52)
forward(65)

setheading(-180)
forward(40)

setheading(130)
forward(52)

setheading(90)
forward(30)

end_fill()

penup()
goto(114,20)
pendown()
# I
i(60,60,60,pensize(5))

penup()
goto(158,20)
pendown()
# N
i(60,60,60,pensize(5))

penup()
goto(188,20)
pendown()

begin_fill()

setheading(-55)
forward(60)

setheading(90)
forward(49)

setheading(-90)
forward(100)

setheading(125)
forward(60)

setheading(90)
forward(50)

end_fill()

penup()
goto(222,20)
pendown()

i(60,60,60,pensize(5))

L’écriture de Park

Pour l’écriture de « Park », j’ai procédé de la même manière qu’avec « Linkin » : j’ai crée une fonction avec def qui a la même forme que la fonction i mais avec des mesures plus grandes et qui s’appelle donc I(i majuscule). J’ai réutilisé les fonctions circle() pour les boucles du P et du R ; c’est d’ailleurs le même bout de code grâce aux fonctions setheading() et forward() (on aurait été de changer toutes les données avec des goto(x,y). Le K, lui, est le même que le petit k mais avec des mesures plus grandes. Pour l’intérieur du A j’ai fais un triangles avec les fonctions setheading() et forward() puis j’ai essayé de le remplis avec begin_fill() et end_fill() de la même couleur que le fond.

penup()
goto(-240,-90)
pendown()
# park
# def I
def I(r,g,b,pensize):
    pencolor(r,g,b)
    fillcolor(r,g,b)
    
    begin_fill()
    
    setheading(0)
    forward(40)
            
    setheading(-90)
    forward(130)
    
    setheading(-180)
    forward(40)
    
    setheading(90)
    forward(130)
    
    end_fill()

# P
I(0,0,0,pensize(5))

penup()
goto(-200,-90)
pendown()

begin_fill()

setheading(0)
forward(30)
circle(-40,180)
setheading(-180)
forward(30)

setheading(90)
forward(29)
setheading(0)
forward(10)
circle(12,180)
setheading(-180)
forward(10)
setheading(90)
forward(27)

end_fill()

penup()
goto(-138,-90)
pendown()
# A

begin_fill()

setheading(0)
forward(85)

setheading(-75)
forward(135)

setheading(-180)
forward(45)

setheading(112)
forward(20)

setheading(-180)
forward(26)

setheading(-115)
forward(20)

setheading(-180)
forward(45)

setheading(70)
forward(105)

setheading(130)
forward(41)

end_fill()

penup()
goto(-91,-180)
pensize(2)
pencolor(219,219,219)
fillcolor(219,219,219)
pendown()

begin_fill()

setheading(0)
forward(15)

setheading(101)
forward(40)

setheading(-101)
forward(40)

end_fill()

penup()
goto(-2,-90)
pencolor(0,0,0)
fillcolor(0,0,0)
pendown()
# R

I(0,0,0,pensize(5))

penup()
goto(38,-90)
pendown()

begin_fill()

setheading(0)
forward(30)
circle(-40,180)
setheading(-180)
forward(30)

setheading(90)
forward(29)
setheading(0)
forward(10)
circle(12,180)
setheading(-180)
forward(10)
setheading(90)
forward(27)

end_fill()

penup()
goto(70,-165)
pendown()

begin_fill()

setheading(-60)
forward(67)

setheading(-90)
forward(60)

setheading(122)
forward(130)

setheading(90)
forward(26)

setheading(0)
forward(27)

end_fill()

penup()
goto(120,-90)
pendown()
# K

I(0,0,0,pensize(5))

penup()
goto(130,-175)
pendown()

begin_fill()

setheading(50)
forward(110)

setheading(0)
forward(50)

setheading(-130)
forward(80)

setheading(-52)
forward(80)

setheading(-180)
forward(50)

setheading(130)
forward(115)

setheading(90)
forward(30)

end_fill()

Image finale

Télécharger le script en .py :

Voici le script complet !

Projets

Une nuit étoilée

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.

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é.

Art

De l’Op art généré par un code python

L’Op Art, ou art optique est un mouvement artistique qui joue avec notre perception visuelle. Son origine remonte aux années 1920 mais il connaît un véritable essor dans les années 1960 avec des artistes comme Victor Vasarely et Bridget Riley. Ils utilisent notamment les contrastes de couleurs, la perspective, la répétition et l’alternance de formes géométriques, pour créer l’illusion de mouvement et de profondeur. Dans cet article, nous allons vous présenter une œuvre inspirée de ce mouvement et entièrement réalisée à l’aide d’un code écrit en langage Python avec le module Turtle.

L’inspiration de départ

Le problème avec les illusions d’optique est que ce que l’on voit, n’est pas la réalité. La première difficulté rencontrée a donc été de trouver un modèle avec des explications pour réaliser la figure. Après de nombreuses recherches, nous avons fini par trouver un programme de construction d’un couloir en damier sur le site d’une classe de CM2 (https://la-gazette-des-cm2-de-ducerceau.sitew.fr/Illusions_d_optique.qB.htm) dans le cadre d’un travail sur Victor Vasarely. Nous l’avons adapté pour respecter les dimensions de l’image finale (forme rectangulaire et non carrée).

Le programme de construction

Le programme de construction nous a permis d’identifier les différentes étapes à programmer :

  • Partager les côtés du rectangle en intervalles de même longueur ;
  • Tracer les segments qui relient deux points symétriques par rapport à l’origine ;
  • Tracer les rectangles qui diminuent régulièrement en taille.
  • Pour apporter notre touche personnelle, nous avons choisi de « peindre » les murs, le sol et le plafond du « couloir » avec des dégradés de couleurs.

Les variables modifiables seront la longueur, la largeur, les couleurs, le nombre d’intervalles, la taille des intervalles, le nombre de rectangles.

Programmation des couleurs

Nous avons commencé par les gradients de couleur en utilisant la méthode expliquée par Eric Schrafstetter dans sa vidéo « l’ART génératif, partie 1 – Transformations affines. »

Explication pour le « mur droit » :

  • La tortue se déplace en abscisse de 0 à L/2. à chaque pas, elle va tracer un segment en couleur de bas en haut (cap = 90°).
  • Pour une abscisse u donnée, ce segment part d’une diagonale et arrive sur l’autre. Donc on a créé les fonctions diag1 et diag2 qui retournent les ordonnées sur chaque diagonale.

def diag1(u): 
    return largeur/longueur * u
def diag2(u):
    return - largeur/longueur * u
  • Entre 0 et L/2, la couleur varie de col_dep (couleur de départ = violet) à col_arr (couleur d’arrivée = jaune).
  • Comme la couleur ne varie pas sur le même intervalle que l’abscisse de la tortue, on utilise une fonction affine qui transforme un point de l’intervalle [0 ; L/2] en un point de l’intervalle [col_dep ; col_arr] :

Voici la fonction codée en Python :

def T(u, a1, a2, b1, b2): 
    return (b2 - b1)/(a2 - a1) * (u - a1) + b1
  • Comme les dégradés ne sont pas dans le même sens ni de la même couleur suivant les triangles, nous avons créé une fonction « triangle » qui dépend de l’orientation de la tortue (cap) et des couleurs de départ et d’arrivée :
def triangle (cap, col_dep, col_arr): 
    colormode(255) # pour pouvoir utiliser le mode RVB
    rvb = [0,0,0]
    pensize(2)
    u_fin= trunc(longueur/2)
    for u in range(u_fin):
        for k in range(3):
            rvb[k] = T(u, 0, u_fin, col_dep[k], col_arr[k])
        pencolor((trunc(rvb[0]), trunc(rvb[1]), trunc(rvb[2])))
        penup()
        if cap == 90 :
            goto(u,diag2(u))
            pendown()
            goto(u,diag1(u))
        elif cap == 180 :
            goto(u,diag1(u))
            pendown()
            goto(-u,diag2(-u))
        elif cap == 270 :
            goto(-u,diag2(-u))
            pendown()
            goto(-u,diag1(-u))
        elif cap == 0 :
            goto(-u,diag1(-u))
            pendown()
            goto(u,diag2(u))

Programmation des « diagonales »

Pour simplifier, nous avons appelé aussi « diagonales », les segments qui relient un point et son symétrique par rapport à l’origine.

La tortue part de (-L :2 ; -l /2) à gauche, fait sa diagonale, revient à gauche en se décalant d’un intervalle vers le haut, fait sa diagonale, …

On procède de la même manière pour les diagonales de haut en bas, en partant de (-L/2 ; l/2).

On a donc d’abord créé une fonction « diag » qui sert à relier un point et son symétrique :

def diag(u, v):
    penup()
    goto(u, v)
    pendown()
    goto(-u, -v)
    penup()

Puis, nous avons programmé deux boucles, pour tracer les  « diagonales gauche-droite » puis les « diagonales haut-bas » :

for i in range(nb_int2):
    diag(-longueur / 2, -largeur/2 + i*taille_int2)
for j in range(nb_int1):
    diag(-longueur/2 + j*taille_int1, largeur / 2)

Programmation des rectangles

Nous avons codé une fonction « rectangle » où la tortue part du coin bas à gauche et tourne dans le sens inverse des aiguilles d’une montre :

def rectangle(L): # fonction permettant de tracer un rectangle
    penup()
    goto(-L/2, diag1(-L/2))
    pendown()
    goto(L/2,diag2(L/2))
    goto(L/2,diag1(L/2))
    goto(-L/2,diag2(-L/2))
    goto(-L/2, diag1(-L/2))

Pour faire diminuer la taille des rectangles de manière régulière, nous avons décidé d’appliquer un pourcentage de diminution de p% à la longueur (p est une variable globale qui peut être modifiée) :

t = float(longueur) 
while t > 10 :
    rectangle(t)
    t = t - t*p #la longueur diminue de p % à chaque itération

Pour l’extrémité du couloir, nous avons tracé un rectangle noir pour lequel nous avons essayé plusieurs dimensions et nous avons finalement choisi 160 pour des raisons esthétiques :

# rectangle final
fillcolor('black')
begin_fill()
rectangle(160)
end_fill()

Le programme zippé

Projets

Décor Urbain

Plongeons dans un univers qui évoque sans le savoir pourquoi une nostalgie avec cette scène inspirée de ces soirs où après être passer une soirée avec ses amis il est temps de retourner à sa voiture dans la nuit et l’humidité. Au premier plan, une voiture au design minimaliste évoque les lignes classiques d’une BMW M3 e30, garée devant un bâtiment aux néons vibrants. Sous un ciel étoilé, l’ensemble évoque une esthétique des années 80, où le contraste entre l’ombre et les couleurs vives nous transporte dans une ambiance intemporelle et nostalgique.

Inspirations

Pour créer nôtre image, celle-ci nous a beaucoup servit de repère, notamment dans les perspectives.

source : @initial_clip (instagram)

Cette voiture est celle que l’on a représentée, une BMW M3 e30. Mise en vente dans le milieu des années 80

Résultat Final

Pour générer cette image le script est découper en plusieurs parties :

  • Mise en place du script
  • L’arrière plan
  • Le bâtiment
  • La voiture
  • L’enregistrement de l’image

Mise en place du script

import turtle
import random
try:
    from PIL import Image
    pillow_installed = True
except:
    print("Oops! - ModuleNotFoundError: No module named 'PIL' - RTFM :")
    print("https://nsi.xyz/py2png")
    pillow_installed = False
titre = "Décor Urbain"
turtle.title(titre)
screen = turtle.Screen()
screen.setup(width=1920, height=1080)
turtle.speed(150000)
turtle.ht()
turtle.colormode(255)

On commence le code en important tout nos modules nécessaires : Turtle, Random et Pillow.

import turtle
import random
try:
    from PIL import Image
    pillow_installed = True
except:
    print("Oops! - ModuleNotFoundError: No module named 'PIL' - RTFM :")
    print("https://nsi.xyz/py2png")
    pillow_installed = False
titre = "Décor Urbain"
turtle.title(titre)
screen = turtle.Screen()
screen.setup(width=1920, height=1080)
turtle.speed(150000)
turtle.ht()
turtle.colormode(255)

On continue avec la mise en place du titre de la fenêtre, des dimensions de l’écran, la vitesse du curseur, la disparition du curseur et le système de couleur mit en rgb.

L’arrière plan

On rentre dans le vif du sujet avec l’arrière plan de l’image. Qui comporte le ciel étoilé et la route avec ses trottoirs.

# Fonction pour dessiner le fond avec un dégradé
def fond():
    rciel, gciel, bciel = 176, 196, 205  # Couleur initiale du ciel
    hauteur = -351
    turtle.penup()
    turtle.goto(-960, hauteur)
    turtle.pendown()

    # Boucle pour créer un dégradé de couleur vers le haut
    while hauteur < 540:
        turtle.pencolor(int(rciel), int(gciel), int(bciel))
        turtle.forward(1920)
        hauteur += 1
        turtle.goto(-960, hauteur)
        bciel = max(100, bciel - 205 / 540)  # Ajuste la composante bleue
        rciel = max(0, rciel - 176 / 540)    # Ajuste la composante rouge
        gciel = max(0, gciel - 196 / 540)    # Ajuste la composante verte

# Fonction pour dessiner des étoiles aléatoirement
def etoiles():
    for i in range(150):
        turtle.penup()
        x = random.randint(-960, 960)
        y = random.randint(-351, 540)
        turtle.goto(x, y)
        turtle.pendown()
        turtle.dot(random.randint(1, 8), "white")
        
# Dessiner la route en perspective
def route():
    turtle.pensize(7)
    
    turtle.begin_fill()
    turtle.color("black","Slate Grey")
    turtle.penup()
    turtle.goto(-428,-81)
    turtle.pendown()
    turtle.goto(-928,-81)
    turtle.goto(-928,-411)
    turtle.goto(-428,-81)
    turtle.end_fill()
    
    turtle.begin_fill()
    turtle.color("black")
    turtle.penup()
    turtle.goto(-928,-411)
    turtle.pendown()
    turtle.goto(-428,-81)
    turtle.goto(1200,-300)
    turtle.goto(315,-900)
    turtle.goto(-928,-411)
    turtle.end_fill()

    turtle.color("white")
    turtle.penup()
    turtle.goto(-775,-411)
    turtle.pendown()
    turtle.goto(-275,-81)
    
    turtle.color("Slate Grey")
    turtle.begin_fill()
    turtle.penup()
    turtle.goto(315,-410)
    turtle.pendown()
    turtle.goto(-500,-330)
    turtle.goto(0,0)
    turtle.goto(1000,-350)
    turtle.goto(315,-410)
    turtle.end_fill()
    
# Appelle les fonctions pour dessiner le fond, les étoiles et la route
fond(),etoiles(),route()

Le ciel :

# Fonction pour dessiner le fond avec un dégradé
def fond():
    rciel, gciel, bciel = 176, 196, 205  # Couleur initiale du ciel
    hauteur = -351
    turtle.penup()
    turtle.goto(-960, hauteur)
    turtle.pendown()

    # Boucle pour créer un dégradé de couleur vers le haut
    while hauteur < 540:
        turtle.pencolor(int(rciel), int(gciel), int(bciel))
        turtle.forward(1920)
        hauteur += 1
        turtle.goto(-960, hauteur)
        bciel = max(100, bciel - 205 / 540)  # Ajuste la composante bleue
        rciel = max(0, rciel - 176 / 540)    # Ajuste la composante rouge
        gciel = max(0, gciel - 196 / 540)    # Ajuste la composante verte

.Le ciel de l’image est en grand dégradé qui vire d’un gris-bleu à un bleu nuit.
Pour ce faire on défini la couleur initiales du ciel, avec sa quantité de rouge, vert et bleu. On détermine ensuite la hauteur où démarre le dégradé avec la variable « hauteur ». On déplace le curseur en -960 et à la hauteur minimale ici -351.
On démarre une boucle qui prend fin lorsque hauteur atteint 540, soit la hauteur maximale souhaitée. La couleur y est défini sur les valeurs actuelles de rouge, de vert et de bleu, on fait avancer le curseur de 1920, la largeur de la fenêtre. On ajoute un à la hauteur et déplace le curseur de nouveaux en -960 quant aux abscisses et à la nouvelle hauteur. La couleur est ensuite modifier en soustrayant la quantité initiale de la couleur divisé par le nombre de traits faits. Ce qui va logiquement amené la valeur à 0 au bouts de toute les itérations. Mais voulant du bleu et non du noir en couleur finale en instaure une limite avec la fonction max() qui est définie sur 100.

Les étoiles :

# Fonction pour dessiner des étoiles aléatoirement
def etoiles():
    for i in range(150):
        turtle.penup()
        x = random.randint(-960, 960)
        y = random.randint(-351, 540)
        turtle.goto(x, y)
        turtle.pendown()
        turtle.dot(random.randint(1, 8), "white")

Pour faire les étoiles on défini le nombre d’étoiles que l’on veut mettre ici c’est défini en argument de range(), que est défini sur 150 itérations. On défini une variable x qui comporte une valeur entière aléatoire entre -960 et 960, de même pour la variable y cette fois entre -351 et 540. On déplace le curseur à ces valeurs qui servent de coordonnées. Et on y fait un point d’un diamètre aléatoire entre 1 et 8 et de couleur blanche : range()

La génération aléatoire permet d’avoir parfois la formations de constellation non calculées. Cela donne je trouve une belle authenticité à l’image.

La route :

# Dessiner la route en perspective
def route():
    turtle.pensize(7)
    
    #Tottoir gauche
    turtle.begin_fill()
    turtle.color("black","Slate Grey")
    turtle.penup()
    turtle.goto(-428,-81)
    turtle.pendown()
    turtle.goto(-928,-81)
    turtle.goto(-928,-411)
    turtle.goto(-428,-81)
    turtle.end_fill()
    
    #Route
    turtle.begin_fill()
    turtle.color("black")
    turtle.penup()
    turtle.goto(-928,-411)
    turtle.pendown()
    turtle.goto(-428,-81)
    turtle.goto(1200,-300)
    turtle.goto(315,-900)
    turtle.goto(-928,-411)
    turtle.end_fill()

    turtle.color("white")
    turtle.penup()
    turtle.goto(-775,-411)
    turtle.pendown()
    turtle.goto(-275,-81)
    
    #Troittoir droit
    turtle.color("Slate Grey")
    turtle.begin_fill()
    turtle.penup()
    turtle.goto(315,-410)
    turtle.pendown()
    turtle.goto(-500,-330)
    turtle.goto(0,0)
    turtle.goto(1000,-350)
    turtle.goto(315,-410)
    turtle.end_fill()

Le code là assez répétitif, on trace un polygone que l’on rempli de couleur à des fonctions turtle.begin_fill() qui déterminer le début du remplissage et turtle.end_fill() qui détermine la fin du remplissage. On fait ceci pour trois polygones, un qui va faire le trottoir gauche, un qui va faire la route, et l’autre qui va s’installer devant la route et va servir de trottoir droit. Après le traçage du polygone pour la route on trace une ligne simple qui sert de ligne blanche pour ajouter du réalisme à la route.

Les polygones et traits paraissent pas propres, et avec des coordonnées douteuses mais seront par la suites recouverts de façons à ce que cela soit joli.

Appel des fonctions :

# Appelle les fonctions pour dessiner le fond, les étoiles et la route
fond(),etoiles(),route()

On appelle finalement les trois fonctions.

Le bâtiment

#fonction pour convertir les points de mon dessin en des points sur python
def point(x,y): 
    l = (1920 * x) / 14,5
    h = (1080 * y) / 8
    return l,h

pen = turtle.Turtle() 
pen.penup() 
pen.pensize(3)
pen.speed(15000)
pen.ht()
# Liste des coordonnées pour les points du bâtiment 

def batiment():
    coords = [ (315, 445),(-398, 324),(-398, -284),(315, -351), (398, -351), (973, -324), (973, 324),  (315, 445), ] 
    pen.fillcolor("light gray")
    pen.penup()
    pen.goto(coords[0])          
    pen.pendown()
    pen.begin_fill()
    for x, y in coords[1:]:
        pen.goto(x, y)
    pen.end_fill()
# Dessiner la ligne centrale 
    pen.penup() 
    pen.goto(315, 445) # Retour au sommet
    pen.pendown() 
    pen.goto(315, -351) # Descend jusqu'au bas du bâtiment
    return

def pare_soleil():
    coords = [(-398, 27),  (315, 54), (315, -108),(-438, -81), (-398, 27), (-110, 37.5),(-157, -90.5),(108, -101.2),(151,47.25),(315,54),(274,-108),]
    pen.penup()
    pen.goto(coords[0]) # Aller au premier point de la liste
    pen.pendown() 
    pen.fillcolor("#FFD700")# colorier le pare soleil côté gauche
    pen.penup()
    pen.goto(coords[0])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[5])
    pen.goto(coords[6])
    pen.goto(coords[3])
    pen.end_fill()

    pen.fillcolor("#FAFAD2")# colorier le pare soleil côté droit
    pen.penup()
    pen.goto(coords[5])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[1])
    pen.goto(coords[2])
    pen.goto(coords[6])
    pen.end_fill()

    pen.fillcolor("#FFFFF0")# colorier le pare soleil sur l'extrémité de droite pour donné un effet de 3D
    pen.penup()
    pen.goto(coords[1])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[1])
    pen.goto(coords[2])
    pen.goto(coords[10])
    pen.goto(coords[1])
    pen.end_fill()

    pen.color("black")          # Couleur du contour
    pen.pensize(5)              # Épaisseur du contour
    pen.penup()# Début du remplissage de la figure
    pen.goto(coords[0])          # Aller au premier point de la liste des coordonnées
    pen.pendown()
# Dessiner la figure en reliant les points
    for x, y in coords[1:]:
        pen.goto(x, y)
    pen.penup()
    return
             
def contour_fenetre():
    coords = [ (-370, 162),(185, 216),(209, 202),(209,53),(185,52),(185, 201.5),(185, 216),(185, 201.5),(-350, 148),(-350, 31),(-370, 30),(-370, 162),(-350, 148),(-370, 162),(-112,187),(-112,38),(-82.5,39),(-82.5,189),(-62.5,175),(-62.5,41),]
    pen.pensize(3)
    pen.penup()
    pen.goto(coords[0]) # Aller au premier point de la liste des coordonnées
    pen.pendown() 
    for x, y in coords[1:]: # Boucle pour aller de point en point 
        pen.goto(x, y)
        pen.fillcolor("#FFFFF0")

    pen.fillcolor("#808080")# colorier en gris le contour des fentres
    pen.penup()
    pen.goto(coords[0])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[1])
    pen.goto(185,202)
    pen.goto(-82.5,175)
    pen.goto(coords[16])
    pen.goto(coords[15])
    pen.goto(-112,173)
    pen.goto(coords[8])
    pen.goto(coords[9])
    pen.goto(coords[10])
    pen.goto(coords[0])
    pen.end_fill()
    return

def fenetre_gauche():
    coords = [ (-370, 162),(185, 216),(209, 202),(209,53),(185,52),(185, 201.5),(185, 216),(185, 201.5),(-350, 148),(-350, 31),(-370, 30),(-370, 162),(-350, 148),(-370, 162),(-112,187),(-112,38),(-82.5,39),(-82.5,189),(-62.5,175),(-62.5,41),]
    pen.pensize(3)
    pen.fillcolor("#FF69B4")
    pen.penup()
    pen.goto(coords[8])         
    pen.pendown()
    pen.begin_fill()
    pen.goto(-112,173)
    pen.goto(coords[15])
    pen.goto(coords[9])
    pen.goto(coords[8])
    pen.end_fill()
    return

def fenetre_droite():
    coords = [ (-370, 162),(185, 216),(209, 202),(209,53),(185,52),(185, 201.5),(185, 216),(185, 201.5),(-350, 148),(-350, 31),(-370, 30),(-370, 162),(-350, 148),(-370, 162),(-112,187),(-112,38),(-82.5,39),(-82.5,189),(-62.5,175),(-62.5,41),]
    pen.pensize(3)
    pen.fillcolor("#FF69B4")# fenetre de droite
    pen.penup()
    pen.goto(coords[18])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(185,202)
    pen.goto(coords[4])
    pen.goto(coords[19])
    pen.goto(coords[18])
    pen.end_fill()
    return
    
batiment(), pare_soleil(), contour_fenetre(), fenetre_gauche(), fenetre_droite()

Fonction usuelle et mise en place du code :

def point(x,y): 
    l = (1920 * x) / 14,5
    h = (1080 * y) / 8
    return l,h

Pour commencer, nous avons créé une fonction appelée point. Cette fonction consiste à convertir l’emplacement des points de notre image de référence ( en cm) en point sur python tout en gardant les proportions et symétries. Cette fonction est très simple, il suffit de lui donner 2 valeurs soit x et y puis elle va procéder à un produit en croix qui nous rendra l et h l’emplacement du point sur python.

pen.turtle est utilisé pour créer un nouvel objet turtle nommé pen.

On utilisera la “tortue” (ici pen) , on pourra ensuite l’utiliser pour dessiner.

pen.penup() Cette commande indique à la tortue de lever son stylo, ce qui signifie qu’elle va se déplacer sans tracer de ligne.

pen.pensize(3) Cette ligne définit la taille du stylo de la tortue, c’est-à-dire l’épaisseur des lignes qu’elle va tracé.

pen.speed(15000) Cette commande fixe la vitesse de déplacement de la tortue. Nous avons choisi 15000 pour être sûr que la tortue va à sa vitesse maximale en fonction de l’ordinateur utilisé.

 pen.ht() cela signifie “hide turtle”, c’est-à-dire cacher la tortue.

pen = turtle.Turtle() 
pen.penup() 
pen.pensize(3)
pen.speed(15000)
pen.ht()

Définition des fonctions :

def batiment():
    coords = [ (315, 445),(-398, 324),(-398, -284),(315, -351), (398, -351), (973, -324), (973, 324),  (315, 445), ] 
    pen.fillcolor("light gray")
    pen.penup()
    pen.goto(coords[0])          
    pen.pendown()
    pen.begin_fill()
    for x, y in coords[1:]:
        pen.goto(x, y)
    pen.end_fill()
# Dessiner la ligne centrale 
    pen.penup() 
    pen.goto(315, 445) # Retour au sommet
    pen.pendown() 
    pen.goto(315, -351) # Descend jusqu'au bas du bâtiment
    return

def batiment()

Tout d’abord, nous avons créé une fonction nommée batiment. Elle regroupe le code utilisé pour faire la base du bâtiment.

Puis , nous avons créé une liste de tuples nommé coords, chacun représentant les coordonnées (x, y) de points autour desquels la « tortue » va se déplacer pour dessiner les contours du bâtiment.

pen.fillcolor(“light gray”)

Cette commande fixe la couleur de remplissage pour le dessin de la tortue en gris clair. La tortue remplira l’intérieur du bâtiment avec cette couleur.

pen.goto(coords[0]) La tortue va se déplacer au premier point de la liste de tuples.

pen.begin_fill() démarre le processus de remplissage. Une fois que la tortue trace tous les côtés du bâtiment, elle remplira la forme fermée avec la couleur light gray définie plus haut.

pen.end_fill() termine le processus de remplissage et remplit la forme fermée (le bâtiment) en gris clair.

for x, y in coords[1:]:

 pen.goto(x, y)

Cette boucle for parcourt les coordonnées dans coords à partir du deuxième point (indiqué par coords[1:]).

À chaque itération, la tortue se déplace vers le point (x, y), dessinant une ligne entre chaque point consécutif.

pen.penup() 

pen.goto(315, 445)

pen.pendown() 

pen.goto(315, -351)

Cette partie dessine une ligne centrale verticale à l’intérieur du bâtiment.

pen.penup() lève le stylo pour que la tortue se déplace sans tracer jusqu’au sommet du bâtiment.

pen.goto(315, 445) la place en haut du bâtiment.

pen.pendown() abaisse le stylo, permettant de dessiner une ligne jusqu’à pen.goto(315, -351), qui est le bas du bâtiment.

def pare_soleil():
    coords = [(-398, 27),  (315, 54), (315, -108),(-438, -81), (-398, 27), (-110, 37.5),(-157, -90.5),(108, -101.2),(151,47.25),(315,54),(274,-108),]
    pen.penup()
    pen.goto(coords[0]) # Aller au premier point de la liste
    pen.pendown() 
    pen.fillcolor("#FFD700")# colorier le pare soleil côté gauche
    pen.penup()
    pen.goto(coords[0])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[5])
    pen.goto(coords[6])
    pen.goto(coords[3])
    pen.end_fill()

    pen.fillcolor("#FAFAD2")# colorier le pare soleil côté droit
    pen.penup()
    pen.goto(coords[5])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[1])
    pen.goto(coords[2])
    pen.goto(coords[6])
    pen.end_fill()

    pen.fillcolor("#FFFFF0")# colorier le pare soleil sur l'extrémité de droite pour donné un effet de 3D
    pen.penup()
    pen.goto(coords[1])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[1])
    pen.goto(coords[2])
    pen.goto(coords[10])
    pen.goto(coords[1])
    pen.end_fill()

    pen.color("black")          # Couleur du contour
    pen.pensize(5)              # Épaisseur du contour
    pen.penup()# Début du remplissage de la figure
    pen.goto(coords[0])          # Aller au premier point de la liste des coordonnées
    pen.pendown()
# Dessiner la figure en reliant les points
    for x, y in coords[1:]:
        pen.goto(x, y)
    pen.penup()
    return

pare soleil

Ensuite, nous avons créé une fonction nommée pare_soleil, qui regroupe le code utilisé pour dessiner un pare-soleil avec des effets de remplissage et de contour.

Puis, nous avons créé une liste de tuples nommée coords. Chacun de ces tuples représente les coordonnées (x, y) de points autour desquels la tortue va se déplacer pour dessiner le contour du pare-soleil.

La commande pen.penup() lève le stylo pour éviter de tracer une ligne jusqu’à la position initiale. Ensuite, pen.goto(coords[0]) déplace la tortue au premier point de la liste coords, et pen.pendown() abaisse le stylo pour pouvoir commencer à dessiner.

Pare soleil extrémité de gauche

La commande pen.fillcolor(« #FFD700 ») fixe la couleur de remplissage en jaune doré pour dessiner le côté gauche du pare-soleil.

La séquence suivante utilise pen.penup() pour lever le stylo, puis pen.goto(coords[0]) pour déplacer la tortue au premier point de la liste des coordonnées, et pen.pendown() pour abaisser le stylo à cet endroit.

pen.begin_fill() démarre le processus de remplissage. 

Ensuite, la tortue se déplace successivement aux coordonnées coords[5], coords[6], et coords[3], ce qui crée un contour fermé. pen.end_fill() termine le processus et remplit cette zone en jaune doré.

Pare soleil extrémité de droite

Ici, pen.fillcolor(« #FAFAD2 ») change la couleur de remplissage pour un jaune clair (presque crème) qui sera utilisé pour le côté droit du pare-soleil.

pen.penup() lève le stylo pour déplacer la tortue sans tracer. pen.goto(coords[5]) place la tortue au point coords[5], et pen.pendown() abaisse le stylo pour commencer à tracer le contour.

pen.begin_fill() démarre le remplissage, puis pen.goto(coords[1]), pen.goto(coords[2]), et pen.goto(coords[6]) déplace la tortue aux différents points du côté droit du pare-soleil pour former le contour. Enfin, pen.end_fill() remplit cette zone avec la couleur jaune clair.

Troisième remplissage(effet 3d)

Ensuite, pen.fillcolor(« #FFFFF0 ») définit la couleur de remplissage en blanc cassé pour créer un effet 3D sur le bord droit du pare-soleil.

La séquence suivante lève le stylo avec pen.penup(), puis pen.goto(coords[1]) positionne la tortue au point de départ du bord droit. pen.pendown() abaisse le stylo pour commencer à dessiner.

pen.begin_fill() démarre le remplissage, et la tortue se déplace aux coordonnées coords[2], coords[10], et retourne à coords[1] pour former la bordure en blanc cassé, créant ainsi un effet de profondeur. pen.end_fill() termine et remplit cette zone.

Contour

Pour le contour du pare-soleil, pen.color(« black ») définit la couleur du contour en noir, et pen.pensize(5) fixe l’épaisseur de ce contour à 5 pixels.

pen.penup() lève le stylo pour éviter de tracer avant d’arriver au point de départ. pen.goto(coords[0]) place la tortue au premier point de la liste, et pen.pendown() abaisse le stylo pour commencer le tracé.

La boucle for parcourt les coordonnées à partir du deuxième point (indiqué par coords[1:]). À chaque itération, la tortue se déplace aux coordonnées (x, y), dessinant un segment de ligne entre chaque point et formant ainsi le contour complet du pare-soleil.

Enfin, pen.penup() lève le stylo pour éviter de tracer accidentellement en dehors de la figure.

def contour_fenetre():
    coords = [ (-370, 162),(185, 216),(209, 202),(209,53),(185,52),(185, 201.5),(185, 216),(185, 201.5),(-350, 148),(-350, 31),(-370, 30),(-370, 162),(-350, 148),(-370, 162),(-112,187),(-112,38),(-82.5,39),(-82.5,189),(-62.5,175),(-62.5,41),]
    pen.pensize(3)
    pen.penup()
    pen.goto(coords[0]) # Aller au premier point de la liste des coordonnées
    pen.pendown() 
    for x, y in coords[1:]: # Boucle pour aller de point en point 
        pen.goto(x, y)
        pen.fillcolor("#FFFFF0")

    pen.fillcolor("#808080")# colorier en gris le contour des fentres
    pen.penup()
    pen.goto(coords[0])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(coords[1])
    pen.goto(185,202)
    pen.goto(-82.5,175)
    pen.goto(coords[16])
    pen.goto(coords[15])
    pen.goto(-112,173)
    pen.goto(coords[8])
    pen.goto(coords[9])
    pen.goto(coords[10])
    pen.goto(coords[0])
    pen.end_fill()
    return

def contour fenetre

Nous avons créé une fonction appelée contour_fenetre, qui contient le code nécessaire pour dessiner le contour d’une fenêtre avec des effets de remplissage.

Nous définissons ensuite une liste de coordonnées coords pour représenter les points (x, y) que la tortue va relier afin de former le contour de la fenêtre.

La commande pen.pensize(3) définit l’épaisseur du trait à 3 pixels. Ensuite, pen.penup() lève le stylo pour éviter de tracer pendant le déplacement initial, et pen.goto(coords[0]) positionne la tortue au premier point de la liste de coordonnées. pen.pendown() abaisse le stylo pour commencer à dessiner.

La boucle for parcourt chaque point dans coords à partir du deuxième (indiqué par coords[1:]). À chaque itération, la tortue se déplace aux coordonnées (x, y) et trace une ligne entre chaque point, créant ainsi le contour extérieur de la fenêtre.

La commande pen.fillcolor(« #808080 ») change la couleur de remplissage en gris pour le contour de la fenêtre. Ensuite, pen.penup() lève le stylo pour éviter de tracer avant d’arriver au point de départ. pen.goto(coords[0]) positionne la tortue au premier point des coordonnées, et pen.pendown() abaisse le stylo pour commencer le remplissage de la fenêtre.

La commande pen.begin_fill() démarre le processus de remplissage. Tous les segments suivants seront remplis de gris jusqu’à ce qu’on appelle pen.end_fill()

Dans cette séquence, la tortue se déplace de point en point pour dessiner les différents segments de la fenêtre en utilisant des coordonnées spécifiques dans coords ainsi que des coordonnées explicites pour certaines parties de la fenêtre.

  • pen.goto(coords[1]), pen.goto(185, 202), et pen.goto(-82.5, 175) tracent le premier côté du contour intérieur de la fenêtre.
  • pen.goto(coords[16]), pen.goto(coords[15]), et pen.goto(-112, 173) dessinent le côté gauche de la fenêtre.
  • pen.goto(coords[8]), pen.goto(coords[9]), et pen.goto(coords[10]) tracent la partie inférieure.
  • pen.goto(coords[0]) ramène la tortue au point de départ, fermant ainsi la forme.
def fenetre_gauche():
    coords = [ (-370, 162),(185, 216),(209, 202),(209,53),(185,52),(185, 201.5),(185, 216),(185, 201.5),(-350, 148),(-350, 31),(-370, 30),(-370, 162),(-350, 148),(-370, 162),(-112,187),(-112,38),(-82.5,39),(-82.5,189),(-62.5,175),(-62.5,41),]
    pen.pensize(3)
    pen.fillcolor("#FF69B4")
    pen.penup()
    pen.goto(coords[8])         
    pen.pendown()
    pen.begin_fill()
    pen.goto(-112,173)
    pen.goto(coords[15])
    pen.goto(coords[9])
    pen.goto(coords[8])
    pen.end_fill()
    return

def fenetre Gauche

La fonction fenetre_gauche dessine une fenêtre colorée en rose (code hexadécimal #FF69B4) sur le côté gauche de la structure.

Nous commençons par définir la liste coords, qui contient les points (x, y) pour tracer les contours de la fenêtre. Ces coordonnées permettent de structurer la forme que la tortue va dessiner.

Ensuite, pen.pensize(3) définit l’épaisseur du trait à 3 pixels, et pen.fillcolor(« #FF69B4 ») fixe la couleur de remplissage en rose.

La tortue se rend au point de départ coords[8] sans tracer, puis abaisse le stylo pour commencer le remplissage.

La tortue se déplace vers plusieurs points pour créer une forme fermée

Cette séquence de déplacements trace les côtés de la fenêtre. pen.end_fill() termine le processus de remplissage en rose, créant ainsi une fenêtre complète.

def fenetre_droite():
    coords = [ (-370, 162),(185, 216),(209, 202),(209,53),(185,52),(185, 201.5),(185, 216),(185, 201.5),(-350, 148),(-350, 31),(-370, 30),(-370, 162),(-350, 148),(-370, 162),(-112,187),(-112,38),(-82.5,39),(-82.5,189),(-62.5,175),(-62.5,41),]
    pen.pensize(3)
    pen.fillcolor("#FF69B4")# fenetre de droite
    pen.penup()
    pen.goto(coords[18])          
    pen.pendown()
    pen.begin_fill()
    pen.goto(185,202)
    pen.goto(coords[4])
    pen.goto(coords[19])
    pen.goto(coords[18])
    pen.end_fill()
    return

def fenetre droite

Pour la fenêtre de droite, la fonction est exactement la même.Les seules différences sont les pen.goto(coords[.]) car les fenêtres ne sont pas superposées mais côte à côte.

Appel des fonctions :

batiment(), pare_soleil(), contour_fenetre(), fenetre_gauche(), fenetre_droite()

On appel là toutes les fonctions pour dessiner le bâtiment.

La voiture

Pour terminer le visuel de l’image on dessine la voiture.

turtle.pensize(6)

def ligne(xa,ya,xb,yb):
    turtle.penup()
    turtle.goto(xa,ya)
    turtle.pendown()
    turtle.goto(xb,yb)

# faire une ellipse
def roue(x,y,rad):
  	turtle.color("black")
    turtle.begin_fill()
    turtle.penup()
    turtle.goto(x,y)
    turtle.pendown()
    # inclinaison
    turtle.seth(45)
     
    # rad --> rayon de l'arc
    for i in range(2):
        # deux arcs de cercles

        turtle.circle(rad,90)		#côté grand
        turtle.circle(rad//2.5,90)	#côté petit, le diviseur est responsable de la perspective
    turtle.end_fill()

def phares(x,y):
    positions = [(x, y, "orange"), (x + 40, y + 5, "light cyan"), (x + 150, y + 20, "light cyan"), (x + 190, y + 25, "orange")]
    
    for pos_x, pos_y, color in positions:
        turtle.penup()
        turtle.goto(pos_x, pos_y)
        turtle.pendown()
        turtle.dot(30, color)

def pc(x,y):
    turtle.color("white smoke")
    for i in range (7):
        turtle.pensize(2)
        ligne(x+40,y+abs((y/25))-(4*i)+5,x+150,y+abs((y/20))-(4*i)+15)
    phares(x,y)


def voiture_v2():
    #CÔTÉ
    turtle.color("black","grey")
    coord = [((-330,-200),(-310,-210)),((-310,-210),(-310,-223)),((-310,-223),(-265,-220)),((-265,-220),(-270,-233)),((-270,-233),(-35,-240)),((-35,-240),(100,-280)),((100,-280), (100,-388)),((100, -388),(-293,-365)),((-293,-365),(-330,-350)),((-330,-350),(-330,-200))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    ligne(-35,-240,-35,-380)
    #AILERON
    turtle.color("black")
    coord = [((-330,-200),(-250,-195)),((-250,-195),(-255,-206)),((-255,-206),(-310,-210)),((-310,-210),(-330,-200))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    #HAUT
    turtle.color("black","royal blue")
    coord = [((-270,-233),(-220,-140)),((-220,-140),(-50,-150)),((-50,-150),(180,-130)),((180,-130),(250,-210)),((250,-210),(-35,-240)),((-35,-240),(-270,-233))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    turtle.color("black")
    coord = [((-220,-140),(-30,-127)),((-30,-127),(180,-130)),((180,-130),(-50,-150)),((-50,-150),(-220,-140))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    ligne(-50,-150,-35,-240)
    ligne(-210,-370,-190,-140)
    #AVANT
    roue(280,-380,57)		#roue avant droite
    turtle.color("black","dim gray")
    coord = [((-35,-240),(-50,-210)),((-50,-210),(-83,-207)),((-83,-207),(-83,-223)),((-83,-223),(-59,-227)),((-59,-227),(-35,-240)),((-35,-240),(100,-280)),((100,-280),(100,-388)),((100,-388), (350,-355)),((350,-355),(350,-250)),((350,-250),(250,-210)),((250,-210),(-35,-240))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    ligne(100,-280,350,-250)
        
voiture_v2()
roue(-250,-383,55)		#roue arrière
roue(50,-410,65)		#roue avant gauche
pc(130,-300)

Déclaration d’une fonction usuelle :

On définit d’abord de quelle épaisseur seront les traits pour construire la voiture avec turtle.pensize(), ici défini sur 6.
On déclare ensuite une fonction ligne() avec en argument les coordonnées de deux points. Cette fonction va permettre le traçage de lignes en évitant d’utiliser la fonction turtle.forward(), trop imprécise, ou les quatre lignes habituelles en utilisant des turtle.goto(). Faisant gagner quelques lignes.

turtle.pensize(6)

def ligne(xa,ya,xb,yb):
    turtle.penup()
    turtle.goto(xa,ya)
    turtle.pendown()
    turtle.goto(xb,yb)

Traçage d’une roue :

# faire une ellipse
def roue(x,y,rad):
  	turtle.color("black")
    turtle.begin_fill()
    turtle.penup()
    turtle.goto(x,y)
    turtle.pendown()
    # inclinaison
    turtle.seth(45)
     
    # rad --> rayon de l'arc
    for i in range(2):
        # deux arcs de cercles

        turtle.circle(rad,90)		#côté grand
        turtle.circle(rad//2.5,90)	#côté petit, le diviseur est responsable de la perspective
    turtle.end_fill()

Pour la génération de la voiture on va devoir générer 3 roues. Pour ce faire on déclare une fonction roue() avec en arguments les coordonnées et le rayon de la roue.

On souhaite la roue pleine on utilise donc les balises turtle.begin_fill()() et turtle.end_fill() qui vont déterminer le remplissage des roues. On lève le stylo pour ne plus dessiner et on va aux coordonnées demandés. La fonction turtle.seth() permet d’incliner le curseur.

La route est enfaite une ellipse, une ellipse, c’est simplement deux arc de cercles de mêmes rayons reliés par deux autres arc de cercles de rayon différents aux premiers mais communs entre eux. Pour réaliser cela on utilise la fonction turtle.circle() avec en arguments le rayon demander et 90 degrés qui correspond au traçage d’un quart de cercle. On va utiliser une nouvelle fois cette fonction mais cette fois-ci avec en arguments le rayon diviser par 2.5, et toujours 90 degrés. Ceci va permettre le traçage à la suite du premier arc de cercle un deuxième plus petit.

On met ceci dans une boucle à deux itération à l’aide de for i in range(2), et on obtient une ellipse

Pare-chocs avant :

def phares(x,y):
    positions = [(x, y, "orange"), (x + 40, y + 5, "light cyan"), (x + 150, y + 20, "light cyan"), (x + 190, y + 25, "orange")]
    
    for pos_x, pos_y, color in positions:
        turtle.penup()
        turtle.goto(pos_x, pos_y)
        turtle.pendown()
        turtle.dot(30, color)

def pc(x,y):
    turtle.color("white smoke")
    for i in range (7):
        turtle.pensize(2)
        ligne(x+40,y+abs((y/25))-(4*i)+5,x+150,y+abs((y/20))-(4*i)+15)
    phares(x,y)
def phares(x,y):
    positions = [(x, y, "orange"),
                 (x + 40, y + 5, "light cyan"),
                 (x + 150, y + 20, "light cyan"),
                 (x + 190, y + 25, "orange")]
    
    for pos_x, pos_y, color in positions:
        turtle.penup()
        turtle.goto(pos_x, pos_y)
        turtle.pendown()
        turtle.dot(30, color)

Pour tracer les phares je déclare la fonction phares() avec comme arguments les coordonnées des phares.

On déclare en premier lieu les positions et couleur qu’auront les phares à l’aide d’une boucle for avec trois indices correspondants aux coordonnées en x et en y, et la couleur on parcours la liste « positions ». On lève le stylo, se déplace aux positions x et y correspondants à là où se trouvent les variables pos_x et pos_y qui parcourent la liste des coordonnées, on abaisse le stylo, utilise la fonction turtle.dot() avec en arguments une épaisseur de 30 et la couleur correspondante à là où se trouve la variable color qui parcoure la liste. La boucle for permet de mettre fin aux itérations lorsqu’on atteint la fin de la boucle.

def pc(x,y):
    turtle.color("white smoke")
    for i in range (7):
        turtle.pensize(2)
        ligne(x+40,y+abs((y/25))-(4*i)+5,x+150,y+abs((y/20))-(4*i)+15)
    phares(x,y)

Le pare-chocs est composé des phares qu’on a défini juste avant et de 7 lignes qui relient les phares. On déclare donc une fonction pc() avec en argument les coordonnées où le dessin va se faire.

On défini premièrement la couleur sur « white smoke » et l’épaisseur du trait à 2.

On commence une boucle for avec comme indice i avec une range() de 7.

Pour tracer ces traits en gardant une perspective on appelle la fonction ligne(), déclarée en début de code, avec comme arguments les coordonnées des points inscrits en argument de pc(). Quant aux abscisses de points on se contente de garder les valeurs en argument de la fonction en ajouter un léger décalage. Par contre pour les coordonnées en ordonné on ajoute à l’ordonnée de départ une fraction de celle-ci, et de manière à ce que l’ordonné d’arrivé est plus haut pour créer une perspective. On ajoute ensuite 4*i, permettant d’espacer chaque traits à chaque itérations.

Suite à cette boucle on appelle la fonction phares() avec comme arguments les arguments de pc().

def voiture_v2():
    #CÔTÉ
    turtle.color("black","grey")
    coord = [((-330,-200),(-310,-210)),((-310,-210),(-310,-223)),((-310,-223),(-265,-220)),((-265,-220),(-270,-233)),((-270,-233),(-35,-240)),((-35,-240),(100,-280)),((100,-280), (100,-388)),((100, -388),(-293,-365)),((-293,-365),(-330,-350)),((-330,-350),(-330,-200))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    ligne(-35,-240,-35,-380)
    #AILERON
    turtle.color("black")
    coord = [((-330,-200),(-250,-195)),((-250,-195),(-255,-206)),((-255,-206),(-310,-210)),((-310,-210),(-330,-200))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    #HAUT
    turtle.color("black","royal blue")
    coord = [((-270,-233),(-220,-140)),((-220,-140),(-50,-150)),((-50,-150),(180,-130)),((180,-130),(250,-210)),((250,-210),(-35,-240)),((-35,-240),(-270,-233))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    turtle.color("black")
    coord = [((-220,-140),(-30,-127)),((-30,-127),(180,-130)),((180,-130),(-50,-150)),((-50,-150),(-220,-140))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    ligne(-50,-150,-35,-240)
    ligne(-210,-370,-190,-140)
    #AVANT
    roue(280,-380,57)		#roue avant droite
    turtle.color("black","dim gray")
    coord = [((-35,-240),(-50,-210)),((-50,-210),(-83,-207)),((-83,-207),(-83,-223)),((-83,-223),(-59,-227)),((-59,-227),(-35,-240)),((-35,-240),(100,-280)),((100,-280),(100,-388)),((100,-388), (350,-355)),((350,-355),(350,-250)),((350,-250),(250,-210)),((250,-210),(-35,-240))]
    turtle.begin_fill()
    for start, end in coord:
        turtle.penup()
        turtle.goto(start)
        turtle.pendown()
        turtle.goto(end)
    turtle.end_fill()
    ligne(100,-280,350,-250)

Cette voiture est construite de plusieurs polygones le code en est de ce fait assez répétitif suivant toujours la même méthodologie :

  • On défini la couleur de contour et de remplissage du polygone avec la fonction turtle.color()
  • On déclare une liste de coordonnées des sommets de nos polygones
  • On débute le remplissage de nos polygones avec la fonction turtle.begin_fill()
  • On créer une boucle for() qui avec comme indices deux variables, start et end, qui parcourent la liste de coordonnées pour déterminer les deux points à relier
  • On relit les deux points à l’aide de turtle.goto() auxquels on met en argument les valeurs de start et end
  • On conclue avec turtle.end_fill(), pour finir le remplissage.

On note quelques moments où on ajoute des lignes supplémentaires :

  • Lorsque dans #CÔTÉ on ajoute une ligne pour définir la partie droite de la portière
  • Dans #HAUT on fait deux lignes, une qui relie le toit à ligne faite juste avant pour définir le par brise. Et une autre ligne définissant la partie gauche de la portière en reliant le toit au bas de la voiture.
  • Dans #AVANT une ligne définissant la partie haute du pare-chocs
  • Dans #AVANT on appelle la fonction roue() pour faire la roue gauche qui sera en partie cachée par la voiture

Appel des fonctions :

On appel toutes les fonctions ensembles

voiture_v2()
roue(-250,-383,55)		#roue arrière
roue(50,-410,65)		#roue avant gauche
pc(130,-300)

L’enregistrement de l’image :

# Enregistrement de l'image
image = turtle.getcanvas()
nom_du_fichier_sans_extension=titre+"_"+hex(random.randint(2**30+2**25,2**30+2**25+2**24-1))[2:]
image.postscript(file=nom_du_fichier_sans_extension+".ps", colormode='color')
try:
    psimage = Image.open(nom_du_fichier_sans_extension+".ps")
    psimage.load(scale=2)
    psimage_resized = psimage.resize((1920, 1080))
    psimage.save(nom_du_fichier_sans_extension+".png")
    print(nom_du_fichier_sans_extension+".png", psimage.size, "sauvegardé dans le dossier")    
except:
    if not pillow_installed:
        print("Oops! - ModuleNotFoundError: No module named 'PIL' - RTFM :")
        print("https://nsi.xyz/py2png")
    else:
        print("Oops! - 'ghostscript' not installed- RTFM :")
        print("https://nsi.xyz/py2png")
turtle.exitonclick()

turtle.done()

On récupère l’image, on lui assigne un nom aléatoire afin de ne pas écraser les autres et on l’enregistre automatiquement dans le fichier où se trouve le programme, au format png.

Image finale

Lors de l’enregistrement en png, on voit des traits qui apparaissent suite aux déplacements en goto() même si le stylo est en position penup().

Télécharger le code

Art

De l’art sur la NumWorks

Les calculatrices graphiques programmables sont bien plus que de simples outils pour les calculs. Elles ouvrent un monde de possibilités aux élèves, en permettant d’explorer les mathématiques d’une manière interactive et visuelle. Mais leur potentiel va au-delà des formules et des graphiques : elles peuvent être utilisées pour créer de l’art numérique, des animations, et même des jeux !

En programmant ces machines, les étudiants développent leur créativité tout en renforçant leurs compétences en logique et en résolution de problèmes. Ces calculatrices deviennent ainsi des alliées indispensables pour apprendre de manière ludique et innovante, et pour repousser les limites de l’imagination.

Depuis 2019, j’invite mes élèves à explorer leur potentiel créatif en réalisant des dessins uniques, soit avec l’application Grapheur, soit avec l’application Python de la calculatrice. Chaque année, leurs œuvres sont soumises à l’éditeur de la calculatrice, NumWorks, qui sélectionne la plus belle réalisation. L’auteur de cette création se voit offrir une coque au design original pour sa calculatrice. Si vous croisez un élève au lycée Louis Pasteur arborant une telle coque, il est fort probable qu’il soit lauréat de l’un de ces concours.

Certaines de ces réalisations sont également partagées sur les réseaux sociaux, notamment sur Instagram et X (anciennement Twitter). Une quinzaine de ces images ont même été sélectionnées et publiées, avec l’accord de leurs auteurs, dans un livre intitulé Découvrir la calculatrice graphique NumWorks édité en Français et en Anglais.

Il est vrai que se lancer dans la programmation sur une calculatrice graphique peut sembler déroutant au premier abord. Le défi de traduire des idées créatives en code peut déstabiliser, surtout pour ceux qui n’ont jamais exploré cet univers. Cependant, une fois les premières hésitations surmontées, les élèves découvrent rapidement le plaisir de voir leurs concepts prendre vie à l’écran. Ce processus, bien qu’exigeant, devient une source de satisfaction personnelle, les encourageant à se dépasser. En fin de compte, ils s’amusent tout en repoussant leurs propres limites, transformant un défi intimidant en une expérience enrichissante.

Tout à commencé en 2019 …

En 2019, après réflexion et concertation de l’équipe des enseignants de mathématiques, nous avons pris la décision collective de passer aux calculatrices NumWorks dans toutes nos classes. Pour familiariser les élèves avec cet outil, leur premier travail a été de réaliser un projet simple, destiné à leur faire découvrir les différentes fonctionnalités de la calculatrice. Sans attente particulière, j’ai été agréablement surpris par l’enthousiasme et la créativité dont ils ont fait preuve. Certaines productions, bien au-delà de ce que j’avais imaginé, ont révélé un potentiel insoupçonné chez les élèves, ouvrant la voie à des projets encore plus ambitieux par la suite.

Le titre du sujet était : Les mathématiques sont belles. Voici les 30 réalisations des élèves.

Certaines de ces images sont issues de scripts bien connus, adaptés et optimisés pour la NumWorks. Elles ont été réalisées à distance, en ligne, pendant le confinement de mars 2020. J’ai alors demandé à mes élèves de Terminale de constituer un jury pour évaluer ces créations, en insistant sur l’importance d’une bienveillance maximale dans leurs délibérations. Bien que les codes Python aient parfois été maladroits, ces erreurs faisaient partie intégrante du processus d’apprentissage. Se tromper, tâtonner, recommencer et s’améliorer sont autant d’étapes essentielles dans le développement de leurs compétences.

Quelques thread colorés publiés sur X (Twitter)

J’ai rejoint X (anciennement Twitter) durant le confinement, à l’origine pour passer le temps. Depuis, j’ai régulièrement partagé des threads mettant en avant les créations de mes élèves. En voici quelques exemples. (Un tableau à la fin de cet article propose davantage de liens.)

https://twitter.com/nsi_xyz/status/1380526907477884930
https://twitter.com/nsi_xyz/status/1578772799136890880
https://twitter.com/nsi_xyz/status/1593645740219416577
https://twitter.com/nsi_xyz/status/1770189028366709193
https://twitter.com/nsi_xyz/status/1714607831733707112
https://twitter.com/nsi_xyz/status/1802272199455187027

De l’art sur la NumWorks depuis 2020

Le tableau ci-dessous récapitule tous les fils de discussion publiés sur X (anciennement Twitter). Malheureusement, depuis le rachat de Twitter par Elon Musk, il n’est plus possible de consulter ces fils sans posséder un compte sur la plateforme. Cette restriction illustre bien les défis liés à l’utilisation d’outils fermés, qui peuvent rapidement se transformer en véritables obstacles à l’accès à l’information. 🥴

A compter novembre 2024, compte tenu de la fermeture progressive de X, de l’abus des publicités et de la toxicité du réseau, les fils seront publiés sur Blue Sky.
Les fils sur X existent toujours, en cliquant sur le T vous pouvez le lire sans compte sur X (Twitter)

Création
(1er éd.)
Titre du travail à rendre
Classe cible
Thread X (Twitter)
Blue Sky
2019
(2020)
Les mathématiques sont belles.
Seconde, mathématiques
2020 2021 2022 2023 2024 2025
2021
(2021)
Le Python et la Tortue.
1ère, spécialité NSI
2021 2022 2023 20242025
2021
(2021)
Le python et les fractales
Tale, spécialité NSI
2021 2022 2023 20242025
2022
(2022)
Les mathématiques sont belles ! ed. spéciale cercle
Tale, option Mathématiques Expertes
2022 2023 2024
2022
(2023)
Les mathématiques sont belles ! ed. spéciale polynôme
Tale, option Mathématiques Expertes
2023, 2024 20252025
2022
(2024)
Un feu d’artifice en python
Tale, option Mathématiques Expertes
2024 20252025
2023
(2023)
Le Python et la Tortue.
Seconde, SNT
2023 20242025
2024
(2024)
Pixel Art en python
Tale, option Mathématiques Expertes
20242025

Projets

NsCinéma – La base de données

Trouver le film idéal à regarder ? C’est ce que vous proposera cette base de donnée de films utilisant tkinter.
Cette collection comportant des films de tout genre regroupe de nombreux films en les notant, très pratique pour dénicher les meilleurs films.

Le thème

Nous avons choisi le cinéma comme thème pour notre base de données car nous avons tous les deux cette passion en commun. En effet, en plus du fait que nous aimons le cinéma, l’idée de choisir des films pour constituer notre base de données semble correspondre aux consignes et nous a assez vite donné des idées de fonctionnalités, notamment avec les filtres de recherche que nous avons mis en place.

Le code

Tout d’abord nous avons importé toutes les bibliothèques nécessaires au bon fonctionnement de la base de donnée.

from tkinter import *
import tkinter as tk
import sqlite3
from tkinter import messagebox

Ici Tkinter va permettre d’avoir un rendu graphique de la base de données, le tutoriel présent sur le site nsi.xyz nous a fortement aidé (tutoriel).

Enfin Sqlite3 permet de nous connecter a la base de donné (DBbrowser)

Page d’accueil

Pour la page d’accueil, nous avons donc décidé de rester simple. En effet, sur cette page, on y trouve le nom de notre projet, « NsCinéma ». Ensuite, il y a deux boutons : tout d’abord, un bouton qui permet de consulter la base de données sans la modifier, avec un système de filtres et de défilement. Ensuite, un deuxième bouton permet de modifier la base de données, ce qui inclut la création d’une table Film, la suppression de la table, l’ajout de films, ainsi que la possibilité de modifier un film en cas d’erreur lors de l’ajout.

Ainsi, afin que les boutons puissent fonctionner correctement avec l’ouverture d’une nouvelle page, il était nécessaire d’implémenter une fonction permettant de créer la page « Modifier la base de donnée »

Fonction create permettant d’acceder a la page modifier la base de donnée :

def create():
    glodef create():
    global nouvelle_fenetre
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")
    
    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()
    
    def retour():
        nouvelle_fenetre.destroy()

    #titre
    titre = Label(nouvelle_fenetre, text="Mise a jour de la base de donnée", font=("Consolas",40) ,bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)
    
    #creer la table 
    bouton_creer_table = Button(nouvelle_fenetre, text="Créer Table Film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=create_table, width=60, height=3)
    bouton_creer_table.place(x=250, y=300)

    #Bouton supprimer 
    bouton_supprimer_table = Button(nouvelle_fenetre, text="Supprimer Table Film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=supprimer_table, width=60, height=3)
    bouton_supprimer_table.place(x=250, y=400)

    #bouton ajouter
    bouton_ajouter_film = Button(nouvelle_fenetre, text="Ajouter un film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=page_ajouter_film, width=60, height=3)
    bouton_ajouter_film.place(x=250, y=500)
    
    #bouton modifier
    bouton_ajouter_film = Button(nouvelle_fenetre, text="Modifier la table", font=("Consolas",13), bg="#1d1e2e", fg="white", command=update, width=60, height=3)
    bouton_ajouter_film.place(x=250, y=600)
    
    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)
    
    
    
    connection.commit()
    curseur.close()
    connection.close()

Dans cette fonction, nous pouvons observer la présence de la fonction ‘retour’ permettant de revenir en arrière en supprimant la page actuelle.

Bouton :

bouton1 = Button(screen, text="Consulter la base de donnée ", font=("Consolas",13), bg="#272936", fg="white", command= create, width=60, height=3)
bouton1.place(x=250, y=300)

bouton2 = Button(screen, text="Mettre a jour la base de donnée ", font=("Consolas",13), bg="#272936", fg="white", command= create, width=60, height=3)
bouton2.place(x=250, y =400)

Consultation de la base de donnée

Passons maintenons au code permettant de consulter cette base donnée. On créée donc une nouvelle fenêtre constituée de deux parties : une pour appliquer des filtres sur les films et une pour voir les films.

Comme on peut le constater dans le screen ci-dessous.

Vous constatez donc une boîte où l’on peut consulter les films de la base, et au-dessus de celle-ci, un bloc comportant un menu déroulant pour les filtres.

On pourra noter l’utilisation d’un bouton retour pour revenir à la page d’accueil et d’un bouton actualise qui nous servira plus tard pour ajouter/supprimer des films.

Passons plus en détail le code.

nouvelle_fenetre = tk.Tk()
    nouvelle_fenetre.title("NsCinema")
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")

main_frame = Frame(nouvelle_fenetre, bg="#0e101c")
main_frame.place(x=80, y=300, width=920, height=420)

canvas = Canvas(main_frame, bg="#0e101c")
canvas.pack(side=LEFT, fill=BOTH, expand=YES)

scrollbar = Scrollbar(main_frame, command=canvas.yview)
scrollbar.pack(side=RIGHT, fill=Y)

canvas.configure(yscrollcommand=scrollbar.set)

boitefilm = Frame(canvas, bg="#1d1e2e")
canvas.create_window((0, 0), window=boitefilm, anchor='nw')
    
bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
bouton_retour.place(x=80, y=120)
    
bouton_actualise = Button(nouvelle_fenetre, text="Actualise", font=("Consolas",13), bg="#1d1e2e", fg="white", command=actualise, width=20, height=2)
bouton_actualise.place(x=800, y=120)
    

boitefilm correspond à la fenêtre (c’est une frame) où l’on, comme on l’à déjà dit, consulte les films. Elle est dans un canevas (ici canvas) qui lui va regrouper tous les éléments graphiques dont boitefilm et notamment la scrollbar qui va permettre de défiler l’axe des ordonnées de canvas : command=canvas.yview.

def etoile(n):
        c = n
        note = ""
        while c != 0:
            note += "★"
            c -= 1
            
        while (5-n) != 0:
            note += "☆"
            n += 1
        return note

    def titre():
        t_nom = Label(boitefilm, text="Nom", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_genre = Label(boitefilm, text="Genre", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_realisateur = Label(boitefilm, text="Réalisateur", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_annee_pub = Label(boitefilm, text="Année", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_note = Label(boitefilm, text="Note", font=("Consolas", 13), bg="#1d1e2e", fg="black")

        t_nom.grid(row=0, column=0, sticky=W, pady=21, padx=20)
        t_genre.grid(row=0, column=1, sticky=W, pady=21, padx=5)
        t_realisateur.grid(row=0, column=2, sticky=W, pady=21, padx=5)
        t_annee_pub.grid(row=0, column=3, sticky=W, pady=21, padx=10)
        t_note.grid(row=0, column=4, sticky=W, pady=21, padx=12)
    
            
    def note(n):
        connection = sqlite3.connect('base_films.db')
        curseur = connection.cursor()

        curseur.execute("SELECT * FROM Film WHERE note=?", (n,))
        films = curseur.fetchall()

        connection.commit()
        curseur.close()
        connection.close()

        return films

Ici nous avons trois fonctions qui ont des tâches spécifiques :

  • etoile(n) : c’est une fonction qui va retourner, en fonction du paramètre n, une chaîne de caractère avec n étoiles « ★ » et (5-n) étoiles « ☆ ». Ce qui nous donnes au final un note sur 5 représentée avec des étoiles.
  • titre() : cette fonction va tout simplement permettre l’affichage des titres (avec l’objet Label de tkinter qui permet d’afficher du texte) à la première ligne de la grille avec le paramètre row=0.
  • note() : C’est une fonction qui se connecte à une notre de données contenant les informations sur les films. Elle récupère tous les films ayant une note égale à n et les renvoie. Ici, films = curseur.fetchall() permet de récupérer dans la variable films la requête SQL.

Passons maitenant à la fonction la plus importante de cette partie : ajoute_film()

    
    def ajoute_film(nom, genre, realisateur, annee_pub, note):
        global row_compteur
        connection = sqlite3.connect('base_films.db')
        curseur = connection.cursor()

        truc = (nom, genre, realisateur, annee_pub, note)
        curseur.execute('''CREATE TABLE IF NOT EXISTS Film (
                        id_film INTEGER PRIMARY KEY AUTOINCREMENT,
                        nom TEXT,
                        genre TEXT,
                        realisateur TEXT,
                        annee_pub INTEGER,
                        note REAL
                    )''')
        

        curseur.execute("INSERT INTO Film (nom, genre, realisateur, annee_pub, note) VALUES (?,?,?,?,?)", truc)
        
        

        connection.commit()
        curseur.close()
        connection.close()

        b_nom = Label(boitefilm, text="%s" %(nom), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_genre = Label(boitefilm, text="%s" %(genre), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_realisateur = Label(boitefilm, text="%s" %(realisateur), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_annee_pub = Label(boitefilm, text="%s" %(annee_pub), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_note = Label(boitefilm, text="%s" %(etoile(note)), font=("Consolas", 13), bg="#1d1e2e", fg="white")

        b_nom.grid(row=row_compteur, column=0, sticky=W, pady=21, padx=30)
        b_genre.grid(row=row_compteur, column=1, sticky=W, pady=21, padx=15)
        b_realisateur.grid(row=row_compteur, column=2, sticky=W, pady=21, padx=10)
        b_annee_pub.grid(row=row_compteur, column=3, sticky=W, pady=21, padx=10)
        b_note.grid(row=row_compteur, column=4, sticky=W, pady=21, padx=20)
		
        row_compteur += 1


Cette fonction, ajoute_film, ajoute les informations d’un film (nom, genre, réalisateur, année de publication, note) à une base de données SQLite. Elle crée également des objets Label pour afficher ces informations dans une grille, en utilisant la variable row_compteur (c’est une globale) pour déterminer la ligne à laquelle il faut placer ces dernières.

En somme, tel un plat de lasagnes, on ajoute les films ligne après ligne.

Nous utilisons donc cette fonction pour construire notre base de données à l’aide du bout de code suivant et d’une bonne veille boucle for i in range() :

data = [
        ("American Sniper", "Biopic", "Clint Eastwood", 2015, 4),
        ("The Fabel Man", "Biopic", "Steven Spielberg", 2022, 3),
        ("Truman Show", "Science-Fiction", "Peter Weir", 1998, 4),
        ("Prisoners", "Drame", "Denis Villeneuve", 2013, 4),
        ("Very Bad Trip 1", "Comédie", "Todd Phillips", 2009, 3),
        ("Very Bad Trip 2", "Comédie", "Todd Phillips", 2011, 3),
        ("Very Bad Trip 3", "Comédie", "Todd Phillips", 2013, 2),
        ("Ocean Eleven", "Action", "Steven Soderbergh", 2001, 3),
        ("Ocean Twelve", "Action", "Steven Soderbergh", 2004, 2),
        ("Ocean Thirteen", "Action", "Steven Soderbergh", 2007, 3),
        ("La La Land", "Drame", "Damien Chazelle", 2016, 4),
        ("Fury", "Action", "David Ayer", 2014, 3),
        ("Voyage de Chihiro", "Animation", "Hayao Miyazaki", 2001, 5),
        ("Le Château Ambulant", "Animation", "Hayao Miyazaki", 2004, 4),
        ("Goodfellas", "Drame", "Martin Scorsese", 1990, 3),
        ("A Man Called Otto", "Drame", "Marc Forster", 2022, 3),
        ("Elvis", "Biopic", "John Carpenter", 1979, 3),
        ("127 Hours", "Biopic", "Danny Boyle", 2010, 3),
        ("Ad Astra", "Science-Fiction", "James Gray", 2019, 3),
        ("Life: Origine Inconnue", "Science-Fiction", "Daniel Espinosa", 2017, 3),
        ("The Hobbit 1", "Action", "Peter Jackson", 2012, 4),
        ("The Hobbit 2", "Action", "Peter Jackson", 2013, 4),
        ("The Hobbit 3", "Action", "Peter Jackson", 2014, 4),
        ("The Lord of the Rings 1", "Action", "Peter Jackson", 2001, 4),
        ("The Lord of the Rings 2", "Action", "Peter Jackson", 2002, 4),
        ("The Lord of the Rings 3", "Action", "Peter Jackson", 2003, 5),
        ("The Imitation Game", "Biopic", "Morten Tyldum", 2014, 3),
        ("Seul au monde", "Drame", "Robert Zemeckis", 2000, 4),
        ("Nightcrawler", "Thriller", "Dan Gilroy", 2014, 4),
        ("Marriage Story", "Drame", "Noah Baumbach", 2019, 3),
        ("First Man", "Biopic", "Damien Chazelle", 2018, 3),
        ("L'Intervention", "Action", "Fred Grivois", 2019, 4),
        ("Stand by Me", "Drame", "Rob Reiner", 1986, 4),
        ("Middle Men", "Drame", "George Gallo", 2009, 3),
        ("Happiness Therapy", "Comédie", "David O. Russell", 2012, 4),
        ("The Interview", "Comédie", "Seth Rogen", 2014, 1),
        ("Joy", "Drame", "David O. Russell", 2015, 3),
        ("The Silence of the Lambs", "Thriller", "Jonathan Demme", 1991, 5),
        ("Dune : Partie 2", "Science-Fiction", "Denis Villeneuve", 2024, 4),
        ("Man on Fire", "Action", "Tony Scott", 2004, 3),
        ("Dead Poets Society", "Drame", "Peter Weir", 1989, 5),
        ("Batman & Robin", "Action", "Joel Schumacher", 1997, 2),
        ("Catwoman", "Action", "Pitof", 2004, 1),
        ("Battlefield Earth", "Science-Fiction", "Roger Christian", 2000, 1),
        ("Gigli", "Comédie", "Martin Brest", 2003, 1),
        ("The Love Guru", "Comédie", "Marco Schnabel", 2008, 2),
        ("Movie 43", "Comédie", "Various", 2013, 1),
        ("Birdemic: Shock and Terror", "Thriller", "James Nguyen", 2010, 1),
        ("The Happening", "Thriller", "Night Shyamalan", 2008, 2),
        ("The Room", "Drame", "Tommy Wiseau", 2003, 1),
        ("Troll 2", "Comédie", "Claudio Fragasso", 1990, 2),
        ("Manos: The Hands of Fate", "Thriller", "Harold P. Warren", 1966, 1)]
        
        for m in range(len(data)):
                ajoute_film(data[m][0], data[m][1], data[m][2], data[m][3], data[m][4])

Passons à l’explication du menu déroulant comportant les filtres.

On constate qu’il y a deux filtres : un par genre et un par note.

Cela est possible grâce au bout de code suivant :

boitefiltre = Frame(nouvelle_fenetre, bg="#1d1e2e")
boitefiltre.place(x=80, y=200, width=920, height=63)

m_genre= Menubutton(boitefiltre, text="Genre", relief="raised", borderwidth=2, width=75)
m_genre.grid(row=1, column=0, sticky="s")

m_note = Menubutton(boitefiltre, text="Note", relief="raised", borderwidth=2, width=75)
m_note.grid(row=1, column=1, sticky="s")

boitefiltre.grid_rowconfigure(0, weight=1)

m_filtre = Label(boitefiltre, text="Appliquer des filtres de recherche", font=("Consolas",20), bg="#1d1e2e", fg="white")
m_filtre.grid(row=0, column=0, sticky="s", columnspan=5)

menuDeroulant1 = Menu(m_genre)
menuDeroulant1.add_command(label='Action', command=lambda: filtrer_par_genre('Action'))
menuDeroulant1.add_command(label="Science-Fiction", command=lambda: filtrer_par_genre('Science fiction'))
menuDeroulant1.add_command(label="Biopic", command=lambda: filtrer_par_genre('Biopic'))
menuDeroulant1.add_command(label="Drame", command=lambda: filtrer_par_genre('Drame'))
menuDeroulant1.add_command(label="Thriller", command=lambda: filtrer_par_genre('Thriller'))
menuDeroulant1.add_command(label="Comédie", command=lambda: filtrer_par_genre('Comédie'))
m_genre.configure(menu=menuDeroulant1)

menuDeroulant2 = Menu(m_note)
menuDeroulant2.add_command(label='★☆☆☆☆', command=lambda: filtrer_par_note(1))
menuDeroulant2.add_command(label="★★☆☆☆", command=lambda: filtrer_par_note(2))
menuDeroulant2.add_command(label="★★★☆☆", command=lambda: filtrer_par_note(3))
menuDeroulant2.add_command(label="★★★★☆", command=lambda: filtrer_par_note(4))
menuDeroulant2.add_command(label="★★★★★", command=lambda: filtrer_par_note(5))

Ce bout de code créée une boîte de filtres comprenant deux menus déroulants (m_genre et m_note). Ces menus permettent de sélectionner des options de filtrage par genre de film et par note. La boîte de filtres contient également un label (m_filtre) indiquant à quoi elle sert, pour faciliter la compréhension de l’utilisateur.

Les menus déroulants sont associés à des commandes qui déclenchent des fonctions (filtrer_par_genre et filtrer_par_note) et en fonction des sélections de l’utilisateur, cela applique les filtres de recherche pour afficher les films voulues.

Modification de la base de donnée

Sur cette page nous pouvons retrouver les 5 boutons principaux :

  • Créer la table film
  • Supprimer la table film
  • Ajouter un film a la table
  • modifier la table film
  • supprimer un film

Fonction Création Table

def create_table():
    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()
    
    curseur.execute('''CREATE TABLE IF NOT EXISTS Film (
                        id_film INTEGER PRIMARY KEY AUTOINCREMENT,
                        nom TEXT,
                        genre TEXT,
                        realisateur TEXT,
                        annee_pub INTEGER,
                        note REAL
                    )''')
     messagebox.showinfo("Creation Table", "La table a été crée.")
    
    connection.commit()
    curseur.close()
    connection.close()

Cette fonction create_table permet de créer une table dans une base de données SQLite nommée 'base_films.db‘. Elle établit une connexion avec la base de donnée 'base_films.db‘ , permettant d’accéder à la base de donnée et d’y effectuer des operations, ajouter, supprimer, mettre à jour… Ainsi la requête SQL permet de creer la table Film avec les attributs suivants : id_film, nom, genre, realisateur, annee_pub, note)

De plus un message est affiché lorsque la table est crée.

Fonction Supprimer Table

def supprimer_table():
    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()
    
    curseur.execute("DROP TABLE IF EXISTS Film")
    messagebox.showinfo("Suppression effectuee", "La table a été supprimée.")
   
    connection.commit()
    curseur.close()
    connection.close()

Cette fonction supprimer_table permet de supprimer la table crée précédemment.

Fonction ajouter un film

Lorsque l’on souhaite ajouter un film, une nouvelle page s’ouvre.

La fonction permettant d’ouvrir cette page :

def page_ajouter_film():
    global variable, variable1, variable2, variable3, variable4  
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")
    def retour():
        nouvelle_fenetre.destroy()

    titre = Label(nouvelle_fenetre, text="Ajouter un film", font=("Consolas",40) ,bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)


    variable = tk.StringVar()
    variable.set("Nom du film")
    entree = Entry(nouvelle_fenetre, textvariable=variable, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree.pack(padx=10, pady=10)


    variable1 = tk.StringVar()
    variable1.set("Genre du Film")
    entree1 = Entry(nouvelle_fenetre, textvariable=variable1, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree1.pack(padx=10, pady=10)

    
    variable2 = tk.StringVar()
    variable2.set("Realisateur")
    entree2 = Entry(nouvelle_fenetre, textvariable=variable2, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree2.pack(padx=10, pady=10)

 
    variable3 = tk.StringVar()
    variable3.set("Annee de publication")
    entree3 = Entry(nouvelle_fenetre, textvariable=variable3, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree3.pack(padx=10, pady=10)

     
    variable4 = tk.StringVar()
    variable4.set("Note")
    entree4 = Entry(nouvelle_fenetre, textvariable=variable4, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree4.pack(padx=10, pady=10)

   
    bouton_ajouter_film = Button(nouvelle_fenetre, text="Ajouter le film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=ajouter_film, width=20, height=2)
    bouton_ajouter_film.pack(pady=20)
    
    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)

Ainsi cette fonction permet d’avoir un rendu graphique explicite pour l’utilisateur. En effet cette page est composé de 5 encadrés remplie d’un champ permettant de faire savoir a l’utilisateur ou rentrer le titre du film… Le bouton ajouter film permet d’ajouter le film a la base de donnée

Afin que cette page puisse fonctionner correctement il est imperatif d’avoir une fonction executant le code SQL qui va avec.

Code permettant l’ajout du film :

def ajouter_film():
    
    global variable, variable1, variable2, variable3, variable4, data  
    nom_film = variable.get()
    genre = variable1.get()
    realisateur = variable2.get()
    annee_pub = variable3.get()
    note = variable4.get()

    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()

    curseur.execute('''INSERT INTO Film (nom, genre, realisateur, annee_pub, note)
                        VALUES (?, ?, ?, ?, ?)''', (nom_film, genre, realisateur, annee_pub, note))

    connection.commit()
    curseur.close()
    connection.close() 

    messagebox.showinfo("Film ajoute", "Le film a ete ajoute avec succes.")

Cette fonction ajouter un film dans la base de données SQLite et informe l’utilisateur que le film a été ajouté . En effet la ligne 13 exécute une commande SQL qui insere le film dans la base de données. Les “?” sont des paramètres qui sont remplacés par les valeurs des variables spécifiées.

Fonction modifier la table

Lorsque l’on souhaite modifier la table, une nouvelle page s’ouvre.

Fonction permettant d’ouvrir la page de modification:

def update():
    global variable, variable1, variable2, variable3, variable4 
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")
    nouvelle_fenetre.title("Modifier un film")

    def retour():
        nouvelle_fenetre.destroy()

    titre = Label(nouvelle_fenetre, text="Modifier un film", font=("Consolas",40) ,bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)

    variable = tk.StringVar()
    variable.set("Nom du film")
    entree = Entry(nouvelle_fenetre, textvariable=variable, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree.pack(padx=10, pady=10)

    variable1 = tk.StringVar()
    variable1.set("Genre du Film")
    entree1 = Entry(nouvelle_fenetre, textvariable=variable1, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree1.pack(padx=10, pady=10)

    variable2 = tk.StringVar()
    variable2.set("Realisateur")
    entree2 = Entry(nouvelle_fenetre, textvariable=variable2, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree2.pack(padx=10, pady=10)

    variable3 = tk.StringVar()
    variable3.set("Annee de publication")
    entree3 = Entry(nouvelle_fenetre, textvariable=variable3, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree3.pack(padx=10, pady=10)

    variable4 = tk.StringVar()
    variable4.set("Note")
    entree4 = Entry(nouvelle_fenetre, textvariable=variable4, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree4.pack(padx=10, pady=10)

    bouton_update_film = Button(nouvelle_fenetre, text="Mettre a jour le film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=update_film, width=20, height=2)
    bouton_update_film.pack(pady=20)

    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)

Ce code est semblable a celui de la creation de la page ajouter_film puisque on y retrouve les champs ou l’on doit inserer les informations necessaire.

Comme la page precedente afin que celle ci puisse fonctionner correctement il est imperatif d’avoir une fonction executant le code SQL qui va avec.

def update_film():
    nom_film = variable.get()
    genre = variable1.get()
    realisateur = variable2.get()
    annee_pub = variable3.get()
    note = variable4.get()

    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()

    curseur.execute('''UPDATE Film SET genre=?, realisateur=?, annee_pub=?, note=? WHERE nom=?''', (genre, realisateur, annee_pub, note, nom_film))

    connection.commit()
    curseur.close()
    connection.close()

    messagebox.showinfo("Film modifié", "Le film a été modifié.")

Cette fonction met à jour les informations d’un film spécifique dans une base de données SQLite en utilisant le nom du film comme clé, et informe l’utilisateur que le film a été modifié.

Fonction Supprimer un film

Fonction permettant d’afficher cette page :

def page_supprimer_film():
    global variable_title
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")

    def retour():
        nouvelle_fenetre.destroy()

    titre = Label(nouvelle_fenetre, text="Supprimer un film", font=("Consolas", 40), bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)

    variable_title = tk.StringVar()
    variable_title.set("Titre du film à supprimer")
    entree_title = Entry(nouvelle_fenetre, textvariable=variable_title, font=("Helvetica", 15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree_title.pack(padx=10, pady=10)

    bouton_supprimer_film = Button(nouvelle_fenetre, text="Supprimer le film", font=("Consolas", 13), bg="#1d1e2e", fg="white", command=supprimer_titre, width=20, height=2)
    bouton_supprimer_film.pack(pady=20)

    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)

Fonction permettant de faire marcher la page :

def supprimer_titre():
    title = variable_title.get()

    connection = sqlite3.connect('base_films.db')
    cursor = connection.cursor()

    cursor.execute("SELECT id_film FROM Film WHERE nom=?", (title,))
    result = cursor.fetchone()

    if result:
        film_id = result[0]

        cursor.execute("DELETE FROM Film WHERE id_film=?", (film_id,))
        messagebox.showinfo("Film supprimé", "Le film a été supprimé avec succès.")
    else:
        messagebox.showwarning("Film introuvable", "Aucun film trouvé avec ce titre.")

    connection.commit()
    cursor.close()
    connection.close()

Cette fonction supprime un film spécifique de la base de données SQLite en utilisant le titre du film comme clé, et informe l’utilisateur que le film a été supprimé ou qu’aucun film n’a été trouvé avec le titre donné.

Bouton Retour

En effet, a la fin du projet nous avons constater que il manquait un bouton afin de revenir en arrière. Sans bouton l’utilisateur est obligé de fermer la fenêtre par la croix.

Ainsi la fonction suivante permet de fermer la page actuelle afin de revenir sur la page précédente.

def retour():
        nouvelle_fenetre.destroy()

Télécharger le projet

Vous trouverez ici le .zip du projet que vous puissiez le télécharger. PS : si vous relancer le script, il vaut mieux recréer la base de donnée à l’aide de DB Browser.

En espérant que vous trouverez un bon film.

Projets

Un Explorateur de fractales en Python

Plongez dans des mondes visuels éblouissants créés à partir de simples équations mathématiques, les fractales. Découvrez des fractales emblématiques telles que Julia et Mandelbrot, personnalisez votre expérience avec des palettes de couleurs. Embarquez pour un voyage captivant au cœur de l’art mathématique !

Les prémices du projet

Tout débute durant mon année de seconde. En math, j’ai utilisé le script Mandelbrot initialement présent dans la calculatrice Numworks. Je me suis amusé à analyser le script et grâce à ça et à mon père, je suis tombé dans les fractales. 

Ensuite, j’ai commencé la NSI en première et j’ai réalisé mon premier tutoriel sur ledit script. Et en fin d’année, j’ai modifié ce script afin de faire de plus belles couleurs.

Cette année, j’ai eu envie d’aller bien plus loin, l’idée vient du fait que pour  réaliser de belles images de fractale, j’utilise des explorateurs de fractales trouvables facilement sur internet pour trouver des coordonnées intéressantes. Mais l’expression le dit bien, on n’est jamais mieux servi que par soi-même, et j’ai donc choisi pour mon dernier projet de lycée de faire mon propre explorateur de fractale.

Le début du projet

Je suis parti du script Julia que j’avais fait durant mon projet de fin d’année de première, vous pourrez retrouver l’explication du script ici (lien vers le tuto Julia palette). Ainsi, nous partons de ce script : 

from kandinsky import*
def julia(N_iteration):
    palette = []
    xmax = 2
    xmin = -2
    ymax = 1.3875
    ymin = -1.387
    r = 255
    g = 255
    b = 255
    for j in range(0,128):
        b = 255 -2 * j
        palette.append([r,g,b])
    for j in range(128,256):
        r = 255 -2 * (j-128)
        g = 255 -2 * (j-128)
        palette.append([r,g,b])
    for x in range(320):
        for y in range(222):
            i = 0
            z = complex(xmin+(xmax-xmin)*x/320+(ymax-(ymax-ymin)*y/222)*1J)
            c = complex(0.36,0.36)
            while i < N_iteration and abs(z) < 2:
                i = i + 1
                z = z*z+c
            couleur = palette[int(255*i/N_iteration)]
            col = color(couleur[0],couleur[1],couleur[2])
            set_pixel(x,y,col)

La logique

I – optimisation

Pour faire mon explorateur, je veux générer une nouvelle fractale à chaque zoom, dézoome, déplacement, sur la droite, la gauche, le haut, le bas en modifiant les valeurs des coins de l’image. Mais si je suis cette logique, le script Julia pose déjà un problème. En effet, le script met des dizaines de secondes à fabriquer une seule image, vous conviendrez qu’un zoom en deux minutes, c’est un peu lent non ? Étrangement, un tutoriel a été écrit durant les vacances de Noël 2023 et bizarrement ce script présente exactement ce dont j’ai besoin ici.

Ainsi, j’ai un script qui me permet de générer des images en .png de fractale rapidement. Maintenant, j’ai remplacé la bibliothèque Kandinsky par Pillow et j’ai entièrement modifié les calculs de la fractale pour utiliser NumPy. Nous avons donc ce script :

from PIL import Image
import time
import tkinter as tk
from tkinter import PhotoImage
import keyboard
import numpy as np

global xmax, xmin, ymax, ymin, réso_x, réso_y
#valeurs de base
xmax = 2
xmin = -2
ymax = 1.3875
ymin = -1.387
réso_x = 30720 
réso_y = 21312

def julia_PIL(N_iteration = 100):
    palette = []
    r = 255
    g = 255
    b = 255
    for j in range(0, 128):
        b = 255 - 2 * j
        palette.append((r, g, b))
    for j in range(128, 256):
        r = 255 - 2 * (j - 128)
        g = 255 - 2 * (j - 128)
        palette.append((r, g, b))

    palette = np.array(palette, dtype=np.uint8)

    # Créer une grille de coordonnées complexes
    x = np.linspace(xmin, xmax, réso_x)
    y = np.linspace(ymin, ymax, réso_y)
    X, Y = np.meshgrid(x, y)
    Z = X + 1J * Y

    # Initialiser une matrice d'indices pour la palette
    indices_matrix = np.zeros((réso_y, réso_x), dtype=int)

    for i in range(N_iteration):
        print(i)
        # Mettre à jour les pixels
        mask = np.logical_and(i < N_iteration, np.abs(Z) < 2)
        indices_matrix[mask] = np.round(255 * i / N_iteration).astype(int)

        # Mettre à jour Z pour les pixels actifs
        Z[mask] = Z[mask] ** 2 + complex(0.36, 0.36)

    # Créer une image à partir de la matrice d'indices
    img_array = palette[indices_matrix]

    # Créer une image PIL à partir de l'array
    img = Image.fromarray(img_array, 'RGB')

    # Sauvegarder l'image
    img.save('fractales_images/test.png')

II – Affichage de l’image 

Pour afficher l’image, j’utilise la fonction Pillow mixé à Tkinter. Pillow génère une image en .png et l’enregistre dans un dossier et à chaque nouvelle génération la nouvelle image remplace l’ancienne. 

Tkinter lui génère une fenêtre de la même résolution que l’image générée par Pillow. Ensuite, il va chercher dans le dossier cette image faite par Pillow et l’affiche dans la fenêtre précédemment créée.

def fractal_builder(N_iteration = 100,nom_img='Explorer_image/Image.png'):
  """
  script de génération de la fractale
  """
    # Créer une image à partir de la matrice des indices
    img_array = palette[indices_matrix]

    # Créer une image à partir de l'array
    img = Image.fromarray(img_array, 'RGB')

    # Sauvegarder l'image
    img.save(nom_img)    

def afficher_image():
    img = PhotoImage(file=chemin_image)
    # stocke l'image dans la variable img
    canvas.create_image(0, 0, anchor=tk.NW, image=img)
    #affiche l'image

III – déplacement et zoom 

Afin de me déplacer et de zoomer, je vais utiliser la bibliothèque keyboard. Elle me permet de détecter quand j’appuie sur les touches de mon clavier. Je crée donc une fonction “explorer” qui s’occupe des déplacements et de leur logique. Je vais ainsi modifier les valeurs des coordonnées des coins de l’image que je prends de la fractale puis la générer avec ses nouvelles valeurs dans la fenêtre de Tkinter.

def explorer(action):
    global xmax, xmin, ymax, ymin,img,nbr_img    
    x = xmax - xmin
    y = ymax - ymin
    
    if keyboard.is_pressed('up'):# zoom avant
        xmax = xmax - (x/20)
        xmin = xmin - (x/20)*-1
        ymax = ymax - (y/20)
        ymin = ymin - (y/20)*-1
        fractal_builder()
        afficher_image()      
    elif keyboard.is_pressed('down'):# zoom arrière
        xmax = xmax + (x/20)
        xmin = xmin + (x/20)*-1
        ymax = ymax + (y/20)
        ymin = ymin + (y/20)*-1
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('z'): # déplacement haut
        ymax = ymax - (y/40)
        ymin = ymin - (y/40)
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('s'): # déplacement bas
        ymax = ymax + (y/40)
        ymin = ymin + (y/40)
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('q'): # déplacement gauche
        xmax = xmax - (x/40)
        xmin = xmin - (x/40)
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('d'): # déplacement droite
        xmax = xmax + (x/40)
        xmin = xmin + (x/40)
        fractal_builder()
        afficher_image()

IV – Menu, accueil

Au début du projet, ma volonté était qu’à la fin, j’ai réussi à faire un explorateur de fractaleS. Pouvoir explorer des fractales comme Julia et Mandelbrot que je maîtrise déjà et peut-être même d’autres.

Mais pour faire ça il faut que l’utilisateur puisse choisir dans un menu quelle fractale veut-il explorer et faire un menu, quand j’ai commencé le projet été pour moi le plus dur à réaliser.

Durant les vacances de février 2024 il nous a été demandé de faire un gestionnaire de base de données, d’ailleurs voici ce que j’ai réalisé (lien vers le projet). Mais quel rapport avec des fractales ? Eh bien le fait que ce projet utilise Tkinter pour réaliser un menu, exactement ce que cherche à faire et surtout, c’est simple ! (même si honnêtement, on ne dirait pas).

Parallèlement à ce projet de base de données, j’ai donc piqué des bouts de mon code pour le mettre dans mon explorateur et en seulement quelques minutes, j’ai un menu ! Il ne sert à rien et il est moche (et il le restera, je suis pas designer).

Faire une fenêtre avec Tkinter est plutôt simple à comprendre, on crée des variables qui vont être chaque élément de la page, texte, menu déroulant, champs pour écrire, etc. Pour les construire, Tkinter nous offre des fonctions comme Label() pour les textes, Entry() pour les champs d’écritures entre autres. Il ne nous reste qu’à trouver sur internet les paramètres à mettre à l’intérieur (comme text = “le texte que l’on veut afficher” pour un texte).

titre_acceuil = tk.Label(cadre_acceuil, text="Bienvenue sur cet explorateur de fractales", font=("Consolas", 20), bg="#C2C2C2", fg="black")
soustitre_acceuil = tk.Label(cadre_acceuil, text="Choisissez les paramètres de votre fractale", font=("Consolas", 15), bg="#C2C2C2", fg="black")
titre_acceuil.pack(pady=10)
soustitre_acceuil.place(x=130,y=50)

#création de la liste déroulante de choix des palettes
desc_liste_deroul_palette = tk.Label(cadre_acceuil, text="Choisissez la palette de couleur que vous voulez utiliser", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc_liste_deroul_palette.place(x=15,y=100)
liste_choix = ["Blanc - Jaune - Noir","Blanc - Noir"] # élément de la liste déroulante
variable_palette = tk.StringVar() # élément initial
variable_palette.set("Blanc - Noir")
liste_deroulante_palette = ttk.Combobox(cadre_acceuil, textvariable=variable_palette, values=liste_choix)
liste_deroulante_palette.place(x=15,y=125)
select = liste_deroulante_palette.get() # sélection de l'élément choisi dans la liste

#création de la liste déroulante de choix de la fractale
desc_liste_deroul_fractale = tk.Label(cadre_acceuil, text="Choisissez la fractale que vous voulez voir", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc_liste_deroul_fractale.place(x=15,y=145)
liste_choix = ["Julia","Mandelbrot"] # élément de la liste déroulante
variable_fractal = tk.StringVar() # élément intiaux
variable_fractal.set("Julia")
liste_deroulante_fractal = ttk.Combobox(cadre_acceuil, textvariable=variable_fractal, values=liste_choix)
liste_deroulante_fractal.place(x=15,y=170)

# création du champ de texte pour choisir la valeur de c
desc1_chmp_val_julia = tk.Label(cadre_acceuil, text="Si vous avez choisi de générer une fractale de Julia :", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc2_chmp_val_julia = tk.Label(cadre_acceuil, text="Choisissez la valeur de la constante c (où laisser par défaut), sachant c est un complexe", font=("Consolas", 10), bg="#C2C2C2", fg="black")
desc1_chmp_val_julia.place(x=15,y=195)
desc2_chmp_val_julia.place(x=25,y=220)
variable_r = tk.StringVar()
variable_r.set("0.36")
chmp_str_c_r_julia = tk.Entry(cadre_acceuil, textvariable=variable_r, font=("Helvetica",12), bg="#ffffff", fg="black", width=5)
chmp_str_c_r_julia.place(x=40,y=245)
variable_i = tk.StringVar()
variable_i.set("0.36")
chmp_str_c_i_julia = tk.Entry(cadre_acceuil, textvariable=variable_i, font=("Helvetica",12), bg="#ffffff", fg="black", width=5)
chmp_str_c_i_julia.place(x=100,y=245)

#création de la liste déroulante de choix de la résolution
desc_liste_deroul_resolution = tk.Label(cadre_acceuil, text="Choisissez la résolution de l'explorateurr", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc_liste_deroul_resolution.place(x=15,y=270)
liste_choix = ["320x222","480x333","720x555"]
variable_resolution = tk.StringVar()
variable_resolution.set("320x222")
liste_deroulante_resolution = ttk.Combobox(cadre_acceuil, textvariable=variable_resolution, values=liste_choix)
liste_deroulante_resolution.place(x=15,y=295)

V – Menu, faire devenir utile l’accueil

Maintenant qu’on a un superbe menu, rendons le utile. Pour cela, je crée des fonctions pour sélectionner la palette, la résolution, la fractale etc que j’affecte à une grande fonction start que j’affecte elle au bouton qui lance l’explorateur. Les fonctions de sélection récupèrent ce qui est écrit dans les champs de textes, menu déroulant pour pouvoir donner c’est paramètre à la fonction mère, start().

start (), elle initialise toutes les variables utiles à l’explorateur, supprime la fenêtre du menu, crée la fenêtre de l’explorateur, initialise l’explorateur avec la première image et lance les dernières fonctions nécessaires comme explorer().

def selection_reso(): # fonction de sélection de la résolution utilisée
    global réso_x,réso_y
    if liste_deroulante_resolution.get() == "320x222":
        réso_x = 320
        réso_y = 222
    if liste_deroulante_resolution.get() == "480x333":
        réso_x = 480
        réso_y = 333
    if liste_deroulante_resolution.get() == "720x555":
        réso_x = 720
        réso_y = 555

def selection_palette(): # fonction de sélection de la palette utilisée
    global palette
    palette = []
    select = liste_deroulante_palette.get()
    if select == "Blanc - Jaune - Noir":
        r, g, b = 255, 255, 255
        for j in range(0, 128):
            b = 255 - 2 * j
            palette.append((r, g, b))
        for j in range(128, 256):
            r = 255 - 2 * (j - 128)
            g = 255 - 2 * (j - 128)
            palette.append((r, g, b))
    else :
        palette = [[i,i,i] for i in range(255)]

def start(): # fonction de lancement de l'exploration
    global img,chemin_image,canvas,choix_fract,compl_r,compl_i, réso_x, réso_y
    selection_palette()
    # sélèction de la fractale
    if liste_deroulante_fractal.get() == "Julia":
        choix_fract = 0
    if liste_deroulante_fractal.get() == "Mandelbrot":
        choix_fract = 1
    selection_reso()
    compl_r,compl_i = chmp_str_c_r_julia.get(),chmp_str_c_i_julia.get() # sélection des valeurs de c
    
    fenetre.destroy() #supprime la fenêtre de l'accueil
    cadre_acceuil_explo = tk.Tk() # crée la fenètre de l'explorateur
    cadre_acceuil_explo.title("Affichage d'une Image")
    
    chemin_image = "Explorer_image/Image.png"
    # Charge l'image
    img = PhotoImage(file=chemin_image)
    # Crée un widget Canvas pour afficher l'image
    canvas = tk.Canvas(cadre_acceuil_explo, width=réso_x, height=réso_y)
    canvas.pack()
    # initialisation de la 1ère fractale
    fractal_builder()
    afficher_image()
    cadre_acceuil_explo.mainloop()

# création et placement du bouton de lancement de l'explorateur
bouton_start_explo = tk.Button(cadre_acceuil, text="Commencer l'exploration", font=("Consolas",15), bg="white", fg="black", command = start)
bouton_start_explo.place(x=230,y=495)

VI – Menu, prévisualisation

Actuellement, j’ai donc un menu qui me permet d’explorer 2 fractales différentes avec x palette différente et une infinité de fractales de Julia. Mais pendant que je joue avec mon script, je me rends compte que c’est lourd de devoir mettre les paramètres, lancer l’exploration, se rendre compte que ce n’est pas ce qu’on veut, donc on relance le script et ainsi de suite… Une idée me vient alors en tête : “pouvoir prévisualiser la fractale qu’on génère dans le menu, ce serait bien non ?” et voilà un nouvel objectif et pas des moindres, il me demande de mettre plusieurs variables en global pour y accéder, d’utiliser les fonctions de sélection, etc.

Pour réaliser cette fonction, on fait la même chose que la fonction start(). Mais cette fois, on ne supprime ni ne crée aucune fenêtre, on vient juste placer un canvas, l’image de la fractale que nous voulons prévisualiser au bon endroit avec les bons paramètres.

def previsu():
    global palette, choix_fract,compl_r,compl_i, réso_x, réso_y
    #sélèction des diffèrents paramètres
    réso_x, réso_y = 240,167
    selection_palette()
    if liste_deroulante_fractal.get() == "Julia":
        choix_fract = 0
    if liste_deroulante_fractal.get() == "Mandelbrot":
        choix_fract = 1
    compl_r,compl_i = chmp_str_c_r_julia.get(),chmp_str_c_i_julia.get()
    
    fractal_builder() # génération d'une fractale avec ces paramètres
    prévi = PhotoImage(file="Explorer_image/Image.png") # récupération de l'image
    # création du cardre et placement de l'image dans le cadre
    label_image = tk.Label(cadre_acceuil, image=prévi)
    label_image.place(x=245,y=310)
    label_image.mainloop()

VII – Menu, tutoriel

La dernière barrière entre mon explorateur et l’utilisateur est ainsi ma logique. En effet, les touches que j’ai choisies pour mon explorateur ne sont sûrement pas les meilleures, donc je rajoute une nouvelle page à mon menu pour y écrire un petit guide des touches. Je profite de ça pour ajouter deux nouvelles fonctions. La première fonction récupère les coordonnées de la fractale qu’on voit pour les nerds. La seconde, plus complexe, prend des “screenshot” de ce qu’on voit dans l’explorateur. En réalité, elle génère une fractale avec les mêmes paramètres que celle que l’on voit dans l’explorateur, mais change son nom au moment de l’enregistrer.

#création du cadre de tutoriel dans la fenêtre de l'accueil
cadre_acceuil = tk.Frame(fenetre, bg="#C2C2C2",heigh = 555,width=720)
cadre_acceuil.pack_propagate(False) 
cadre_tuto = tk.Frame(fenetre, bg="#C2C2C2",heigh = 555,width=720)
cadre_tuto.pack_propagate(False) 
# création et placement du texte du tuto
titre_tuto = tk.Label(cadre_tuto, text="Guide des Touches", font=("Consolas", 20), bg="#C2C2C2", fg="black")
soustitre_tuto = tk.Label(cadre_tuto, text="Vous vous sentez un peu  perdu ?", font=("Consolas", 15), bg="#C2C2C2", fg="black")
titre_tuto.pack()
soustitre_tuto.pack(pady=10)
texte_tuto = tk.Label(cadre_tuto, text="z - aller vers le haut \ns - aller vers le bas \nq - aller à droite \nd - aller à gauche \n\nflèche du haut - zoom avant \nflèche du bas - zoom arrière \n\nm - renvoie les coordonnées de \n    où vous êtes dans la fractale \n\nc - sauvegarde un PNG de ce que \n    vous voyez dans le dossier Explorer_image \n", font=("Consolas", 13), bg="#C2C2C2", fg="black")
texte_tuto.pack()
fin = tk.Label(cadre_tuto, text="Amusez-vous !", font=("Consolas", 15), bg="#C2C2C2", fg="black")
fin.pack(pady=20)

#nouvelles fonctions ajoutées à l'explorateur
def explorer(action):
    global xmax, xmin, ymax, ymin,img,nbr_img
    x = xmax - xmin
    y = ymax - ymin
    """
    différentes fonctions de déplacements
    """
    elif keyboard.is_pressed('m'): # récupération de nos coordonnées (pour les nerds)
        print("xmax = ",xmax)
        print("xmin = ",xmin)
        print("ymax = ",ymax)
        print("ymin = ",ymin)
    elif keyboard.is_pressed('c'): # prend un screenshot de la fractale
        print("Image enregistrée !")
        nbr_img +=1
        fractal_builder(100,'Explorer_image/Image_saved'+str(nbr_img)+'.png')
        afficher_image()
        print(nbr_img)

Conclusion

En conclusion, ce projet ma permis d’utiliser la plupart de mes compétences. Il m’a suivi durant la moitié de mon année de terminale.

Galerie

Quelques exemples de fractale que vous pourrez à votre tour explorer :

Script complet

Voici le script final en .7z, il vous suffit de l’extraire et vous pourrez directement commencer à explorer !

vous pouvez également voir les futures mise à jour sur GitHub ici.

Projets

Un site pour mettre à l’épreuve vos connaissances du…

web.snt.nsi.xyz est un projet répertoriant 10 énigmes sur-mesure accessibles pour un élève en classe de seconde. Plus globalement, ce site va vous permettre de découvrir différents aspects de l’informatique et du Web, en passant par l’histoire de l’informatique jusqu’au fonctionnement d’un site Web. Aujourd’hui vous est présenté la V2 de ce projet, une mise à jour majeure.

La V1 de ce projet

Ce projet, dopé au HTML, CSS, JavaScript, et PHP, a été lancé par Ilyas R. qui a conçu et développé l’entièreté du site, incluant les 10 énigmes, le système de chronométrage, etc. Il a également échafaudé la direction artistique en s’aidant d’une librairie CSS (également appelé «Framework CSS»). Il s’agit de PureCSS, une librairie gratuite et open-source que vous pouvez également utiliser dans vos projets. De manière générale, Ilyas a conçu la structure principale du site. Le site web.snt.nsi.xyz est né suite aux vacances de la Toussaint.

Une V2 ?

Pour les vacances d’hiver, une V2 du site a été envisagée incluant un bon nombre de fonctionnalités. Pour ce faire, Thomas S. a rejoint le projet et l’objectif principal est maintenant que le site fonctionne autour d’une base de données. Voici le cahier des charges que nous avons rédigés :

  • Système de gestion de session :
    • Créer une session (en tant que professeur notamment)
    • Rejoindre une session (en tant qu’élève notamment)
    • Observation d’une session (pour que le professeur voit quelles énigmes ont été faites)
    • Statistiques pour les sessions (pourcentages des énigmes résolues, etc.)
    • Et bien plus…
  • Système de comptes (Créer un compte / Se connecter)
  • Système pour parcourir des sessions précédemment créées
  • Système de traductions
  • Ainsi que d’autres petites fonctionnalités…

À partir de là, l’aventure et les galères vont commencer.

Structure de la base de données

Tout d’abord, avant de nous lancer directement à l’aveugle dans le développement, nous avons conçu et organisé notre base de données, en déterminant toutes les relations à créer ainsi que les attributs. Ainsi, voici la structure finale de notre base de données relationnelle.

Nous n’aurons, à notre plus grand regret, pas le temps nécessaire de développer un système de permissions élaboré et un système de gestion des traductions, ainsi dans la V2, les relations groups, permissions, et assets ne sont pas présentes.

Des briques fondamentales

Avant toute chose, il est important de concevoir des fonctions et des bouts de pages qui seront utilisés à maintes reprises. Par exemple le panneau de navigation à gauche.
Ou encore, ce qui nous intéresse le plus ici des fonctions pour manipuler la base de données. Il a fallu créer des fonctions pour créer, lire, modifier et supprimer (CRUD) des enregistrements dans des relations (tables) de notre base de données.

Créer un enregistrement

Voici la fonction que nous avons développé pour créer un enregistrement dans notre base de données :

function addRow($database, $relation, $values) {
    $attributs = getAttributs($database, $relation, 0); // Permet de récupérer la liste des attributs de la relation, hormis la clé primaire
    $attributs_query = "(".implode(", ", array_map(function($value) {
        return $value;
    }, $attributs)).")";
    $values_query = "(".implode(", ", array_map(function($value) {
        return "\"".$value."\"";
    }, $values)).")";
    $sql = "INSERT INTO $relation $attributs_query VALUES $values_query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
}

La fonction prend 3 paramètres, $database, $relation, et $values, le premier indique dans quelle base de données agir (en l’occurrence, ce sera toujours la même étant donné que nous possédons uniquement une base de données), le deuxième indique la relation (table) où agir, et le dernier les valeurs à ajouter dans l’enregistrement. La fonction utilise ainsi la requête SQL INSERT INTO $relation {attributs} VALUES $values, en déterminant automatiquement les attributs de la relation où agir, il n’est pas nécessaire de rentrer en paramètre les attributs.

Lire des enregistrements

Voici la fonction que nous avons développé pour lire un ou plusieurs enregistrements dans notre base de données :

function getRows($database, $relation, $attribut, $query, $force_multiples_rows = 0) {
    $sql = "SELECT $attribut FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return rowsCount($database, $relation, $query) > 1 || $force_multiples_rows == 1 ? $stmp->fetchAll() : $stmp->fetch(PDO::FETCH_ASSOC);
}

La fonction prend 4 paramètres, $database, $relation, $attribut, et $query. De la même manière que la fonction addRow(), nous retrouvons les mêmes paramètres $database et $relation, mais ici avec 2 autres paramètres, $attribut qui représente le ou les attributs dans le ou lesquels les données doivent être collectées, ainsi que $query qui représente la ou les conditions à appliquer pour filtrer les lignes de la table de la base de données. La fonction utilise ainsi la requête SQL SELECT $attribut FROM $relation WHERE $query. Sachant que si nous ne voulons pas mettre de condition, il suffit de renseigner « 1 » dans le paramètre $query.

Mettre à jour un enregistrement

Voici la fonction que nous avons développé pour mettre à jour un enregistrement dans notre base de données :

function updateRow($database, $relation, $attributs_values, $query){
    $setCommand = "";
    $attributs = array_keys($attributs_values);
    for ($i = 0; $i < count($attributs) - 1; $i++){
        $setCommand = $setCommand.$attributs[$i]." = \"".$attributs_values[$attributs[$i]]."\", ";
    }
    $setCommand = $setCommand.$attributs[count($attributs) - 1]." = \"".$attributs_values[$attributs[count($attributs) - 1]]."\"";
    $sql = "UPDATE $relation SET $setCommand WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
}

La fonction prend 4 paramètres, $database, $relation, $attributs_values, et $query. De la même manière que la fonction addRow() et getRows(), nous retrouvons les mêmes paramètres $database, $relation et $query, mais ici avec un autre paramètre, $attributs_values qui représente, pour simplifier au plus, un tableau de la forme array(key => value) avec l’attribut en tant que key et la nouvelle valeur en tant que value. La fonction utilise ainsi la requête SQL UPDATE $relation SET {modifications} WHERE $query.

Supprimer un enregistrement

Voici la fonction que nous avons développé pour supprimer un enregistrement dans notre base de données :

function delRow($database, $relation, $query) {
    $sql = "DELETE FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
}

La fonction prend 3 paramètres, $database, $relation, et $query. De la même manière que la fonction addRow() et getRows(), et updateRows() nous retrouvons les mêmes paramètres $database, $relation et $query. La fonction utilise ainsi la requête SQL DELETE FROM $relation WHERE $query.

D’autres fonctions agissant sur la base de données

On remarque finalement une structure souvent assez similaire, plus ou moins complexe. Au niveau des paramètres pour la fonction, on retrouve toujours $database, une variable qui indique donc sur quelle base de données on agit. Les autres paramètres demandés sont en rapport avec la requête SQL que l’on souhaite exécuter. Que ce soit la création, la lecture, la modification ou la suppression, on a toujours besoin d’indiquer la $relation qui est donc une variable indiquant dans quelle relation (table) on va faire cette requête. Suite à cela, les autres variables dépendent de la requête.

D’autres fonctions peuvent être pratiques, comme une fonction pour récupérer le nombre d’enregistrements pour une table ($relation) selon une condition de recherche ($query) :

function rowsCount($database, $relation, $query) {
    $sql = "SELECT * FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return $stmp->rowCount();
}

Par exemple, je veux le nombre de sessions actuellement actives :

$db = new PDO("mysql:host=localhost:3306;dbname=web_snt_nsi_xyz", "root", "root");
$nbr_sessions_actives = rowsCount($db, "sessions", "status = 1");

Il y a bien plus de fonctions que nous avons codées en avance pour pouvoir les réutiliser plus tard, comme createSession() qui va crée un session dans la base de donnée, getRowsInJson(), qui fait la même chose que getRows() mais qui renvoie le résultat sous un autre format (le format JSON). De même on retrouve une fonction qui crée un utilisateur, qui le modifie, qui le supprime, en bref on s’est fait notre boite à outils de fonctions.

/**
* Cette fonction crée un enregistrement dans la table sessions avec des informations sur la sessions :
* son code, le propriétaire de la session, la date de création, son statut, etc. 
*/

function createSession($database, $id_owner) {
    $codeSession = generateSessionCode($database);
    addRow($database, "sessions", array($codeSession, $id_owner, date('Y-m-d H:i:s', time()), 1));
    $id_session_created = getRows($database, "sessions", "*", "id_owner = $id_owner AND status = 1")["id"];
    updateLocalDB("[]", "../js/db-$id_session_created.json");
}

/**
* Il est intéressant de comparer cette fonction avec getRows() pour voir les différences qu'il y a entre les deux.
*/

function getRowsInJSON($database, $relation, $attribut, $query) {
    $sql = "SELECT $attribut FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return json_encode($stmp->fetchAll());
}

function getRows($database, $relation, $attribut, $query, $force_multiples_rows = 0) {
    $sql = "SELECT $attribut FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return rowsCount($database, $relation, $query) > 1 || $force_multiples_rows == 1 ? $stmp->fetchAll() : $stmp->fetch(PDO::FETCH_ASSOC);
}

Gestion des comptes

Ce système est pour les utilisateurs possédants un compte vérifié sur le site, c’est-à-dire ceux qui peuvent créer une session, ou ceux qui peuvent administrer le site.

Pour cela, comme dit plus haut, des fonctions ont été conçues pour créer, modifier et supprimer des utilisateurs. Ces fonctions manipulent directement la base de données, et passent donc par les fonctions plus basiques addRow(), updateRow(), getRows(), ou delRow(). Maintenant pour permettre ces actions, il est important de faire une interface graphique. Pour cela, une page est conçue pour pouvoir créer un utilisateur, pour avoir la liste des utilisateurs et pouvoir les gérer. Voici à quoi elle ressemble :

Suite à cela, il est important qu’il y ait un système de permissions pour pouvoir empêcher certains utilisateurs d’accéder à certaines pages. En effet, on veut que seul les administrateurs aient accès à cette page, et même à d’autres.

Le système de session

Lorsqu’un utilisateur n’a pas de session active, on lui propose de créer une session, à terme, il pourra configurer certains éléments de la session (notamment la durée).

Le système de session inclut un système de génération de codes, qui permettra à d’autres utilisateurs de rejoindre la session liée à ce code.

Une session va permettre à son propriétaire d’administrer les utilisateurs temporaires (les élèves) et d’observer leur avancement sur les énigmes.

De plus, pour les administrateurs, il sera possible d’avoir des statistiques combinées de toutes les sessions qui ont été créées. De même, il leur sera possible de parcourir les sessions en cours et les anciennes.

Une interface pour rejoindre une session ou se connecter

Voici l’interface de la page pour s’identifier sur le site :

On peut rejoindre une session, se connecter, créer un compte, ou accéder aux énigmes avec le menu à gauche.

Conclusion

Finalement, ce projet a été très enrichissant pour nous, il nous a vraiment permis de nous améliorer en PHP, nous avons énormément travaillé dessus, et nous sommes fiers du résultat, malgré le fait qu’on ait pas eu le temps d’intégrer tout ce qui était prévu à la base (rendez-vous à la V3 🤫).

La V2 est accessible à l’adresse : web.snt.nsi.xyz/dev/

Code source du projet

Vous pouvez retrouver le code source du projet dans notre dépôt GitHub.

Projets

PhpMonDésastre


Dans le cadre du projet SGBDR, j’ai eu l’occasion de créer un site Internet en PHP. Je vous propose donc de découvrir mon projet, nommé phpMonDésastre. Il s’agit d’une version simplifiée de phpMyAdmin qui vous permet d’interagir de manière simplifiée avec une base de données.

Idée

Mon idée était de créer une version simplifiée de phpMyAdmin où les utilisateurs pourraient venir interagir de manière minimaliste avec une base de données et observer les résultats. Cette version, que je nommerais phpMonDésastre, devrait suivre les règles du CRUD, c’est-à-dire Créer, Lire, Mettre à jour et Supprimer.

Cahier des charges

Pour accéder à mon site, les utilisateurs doivent d’abord se connecter à leur session. S’ils n’ont pas de compte, ils devront en créer un. Une fois connectés, ils pourront parcourir les différentes tables de la base de données ainsi que leur contenu. Ensuite, ils auront la possibilité d’exécuter uniquement quatre types de commandes respectant la règle CRUD : créer une table, ajouter du contenu à une table, modifier le contenu d’une table et supprimer une table. Cela permettra de limiter les possibilités des utilisateurs afin d’éviter les abus de la base de données mise à leur disposition.

Mise en place


Pour mettre en place ce projet, il faut commencer par mettre en place une base de données qui contiendra les informations des comptes utilisateurs et qui recevra ces expériences des utilisateurs.

<?php
/*On se connecte à la base de données en entrant les informations nécessaires,
telles que le nom de l'hôte, le nom de la base, les identifiants et le mot de passe.
Ensuite, on définit cette connexion sur la variable $mysqlClient que l'on utilisera
pour gérer toutes les requêtes SQL. 
On permet aussi l'affichage des erreurs pour pouvoir déboguer plus facilement*/
$sql_host="";
$sql_id="";
$sql_password="";
$sql_dbname="";

try {
    $mysqlClient = new PDO(sprintf('mysql:host=%s;dbname=%s;charset=utf8', $sql_host, $sql_dbname),
    $sql_id,
    $sql_password);

    $mysqlClient->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (Exception $exception) {
    die('Erreur : ' . $exception->getMessage());
}



Ensuite, on met en place la page de connexion. D’abord, on s’occupe de la partie frontend, puis de la partie backend.

<!DOCTYPE html>
<html lang="fr" >
<head>
    <meta charset="UTF-8">
    <title>Login Form</title>
    <link rel="stylesheet" href="style.css">
</head>
 <body>
    <?php if (!isset($_SESSION['LOGGED_USER'])) : ?> // On vérifie si l'utilisateur est connecté.
        <center><div class = "message">
            <?php if (isset($_SESSION['LOGIN_ERROR_MESSAGE'])) : ?> // Si une erreur survient, on l'affiche.
                <?php echo $_SESSION['LOGIN_ERROR_MESSAGE'];
                unset($_SESSION['LOGIN_ERROR_MESSAGE']); ?> // Une fois affichée, on l'enlève.
            <?php endif; ?>
            <?php if (isset($_SESSION['SIGNUP_ERROR_MESSAGE'])) : ?> // de même avec les erruer pour l'incription
                <?php echo $_SESSION['SIGNUP_ERROR_MESSAGE'];
                unset($_SESSION['SIGNUP_ERROR_MESSAGE']); ?>
            <?php endif; ?>
        </div></center>
        <section class = "fond-noir"> 
            <div class="signin"> 
                <div class="content"> 
                    <h2>Sign In</h2> 
                        <form class = 'form' action="submit_login.php" method="POST"> 
                            <div class="inputBox" action="submit_login.php" method="POST"> // Boîte pour demander l'adresse e-mail.
                                <input type="Email" name="email" required> <i>Username</i> 
                            </div> 
                            <div class="inputBox" action="submit_login.php" method="POST"> // Boîte pour demander le mot de passe.
                                <input type="password" name="password" required> <i>Password</i> 
                            </div> 
                            <div class="links"> 
                                <a href="forgot_psw.php">Forgot Password</a> <a href="sing_up.php">Signup</a> // Bouton renvoyant vers la page de réinitialisation de mot de passe et la page d'inscription.
                            </div> 
                            <div class="inputBox"> 
                                <input type="submit" value="Login"> // Envoyer la requête de connexion.
                            </div>
                        </form>
                    </div> 
                </div> 
            </div> 
        </section>
    <?php endif; ?>
 </body>
</body>
</html>
<?php
// On démarre la session.
session_start();
/* On importe les différentes pages PHP nécessaires pour l'utilisation complète du script,
telles que "databaseconnect.php" mentionnée ci-dessus pour se connecter à la base de données.*/
require_once(__DIR__ . '/config/mysql.php');
require_once(__DIR__ . '/databaseconnect.php');
require_once(__DIR__ . '/variables.php');
require_once(__DIR__ . '/functions.php');

$postData = $_POST;

// On vérifie que les informations demandées soient bien rentrées.
if (isset($postData['email']) && isset($postData['password'])) { 
  	// On vérifie la validité de l'email fourni.
    if (!filter_var($postData['email'], FILTER_VALIDATE_EMAIL)) {
        $_SESSION['LOGIN_ERROR_MESSAGE'] = 'Il faut un email valide pour soumettre le formulaire.';
    } else {
      	// On vérifie la présence des informations données dans la base de données.
        foreach ($users as $user) {
            if (
                $user['email'] === $postData['email'] &&
                $user['password'] === $postData['password']
            ) {
                $_SESSION['LOGGED_USER'] = [
                    'email' => $user['email'],
                    'user_id' => $user['user_id'],
                ];
            }
        }
		// Si elles ne sont pas présentes dans la base de données, on renvoie une erreur.
        if (!isset($_SESSION['LOGGED_USER'])) {
            $_SESSION['LOGIN_ERROR_MESSAGE'] = sprintf(
                'Les informations envoyées ne permettent pas de vous identifier : (%s/%s)',
                $postData['email'],
                strip_tags($postData['password'])
            );
        }
      // De même pour la page d'inscription.
        if (isset($_SESSION['SIGNUP_ERROR_MESSAGE'])) {
            $_SESSION['SIGNUP_ERROR_MESSAGE'] = sprintf(
                'Les informations envoyées ne permettent pas de vous inscrire'
            );
        }
    }

    redirectToUrl('index.php');
}


En ce qui concerne la page d’inscription, c’est à peu près le même principe, sauf qu’au lieu de vérifier la présence des informations entrées dans la base de données, on les inscrit dans celle-ci avant de rediriger l’utilisateur vers la page de connexion afin qu’il puisse se connecter avec son profil fraîchement créé.


Une fois cette étape franchie, vous arrivez sur une page de connexion semblable à celle-ci :


Une fois connecté, on arrive sur la page principale.

Pour concevoir la page principale, j’ai élaboré une maquette que je suivrai pour la création de la page :

Tout d’abord, nous procédons à l’affichage des tables :

<form class="form" action="index.php" method="post"> // Affichage des tables
    <?php foreach ($TABLES as $table) : ?>
         <?php if ($table[0] === 'users') : ?>
         <?php else : ?>
             <input type="checkbox" value="<?= $table[0] ?>" name="choix_cle" onchange="disableOtherCheckboxesTable(this)" <?= isset($_SESSION['choix_cle']) && $_SESSION['choix_cle'] === $table[0] ? 'checked' : '' ?>>
             <label for="<?= $table[0] ?>"> <?= $table[0] ?></label><br>
         <?php endif ?>
    <?php endforeach; ?>
    <div calss = "case-boutton">
         <input class="submit-bouton" type="submit" value="Valider">
    </div>
</form>

L’affichage des colonnes :

<form class="form" action="index.php" method="post">
     <?php foreach ($CLE as $clecle) : ?>
          <input type="checkbox" name="choix_contenu" value="<?= $clecle['Field'] ?>" <?= isset($_POST['choix_contenu']) && $_POST['choix_contenu'] === $clecle['Field'] ? 'checked' : '' ?>>
          <label for="<?= $clecle['Field'] ?>"> <?= $clecle['Field'] ?></label><br>
     <?php endforeach; ?>
     <div calss = "case-boutton">
          <input class="submit-bouton" type="submit" value="Valider">
     </div>
</form>

L’affichage du contenu :

<form class="form" action="index.php" method="post">
     <?php foreach ($CONT as $cont1) : ?>
          <?php foreach($cont1 as $cont2) : ?>
               <input type="checkbox" name="choix_contenu" value="<?= $cont2 ?>" <?= isset($_POST['choix_contenu']) && $_POST['choix_contenu'] === $cont2 ? 'checked' : '' ?>>
               <label for="<?= $cont2 ?>"> <?= $cont2 ?></label><br>
          <?php endforeach; ?>
     <?php endforeach; ?>
</form>

Ensuite, on procède à l’affichage des commandes :

<div class="div-submit-bouton">
     <form class="form" action="index.php" method="post">
          <center>
          CREATE TABLE IF NOT EXISTS <input class="inputBox2" type="text" name="cree_table" required> (<br><input class="inputBox1" type="text" name="cree_cont" required>);
          <input class="submit-bouton2" type="submit" value="Crée une table"/>
          </center>
     </form>
     <form class="form" action="index.php" method="post">
          <center>
          INSERT INTO <input class="inputBox2" type="text" name="ajout_table" required> (<input class="inputBox2" type="text" name="ajout_valeurs1" required>) VALUES (<input class="inputBox2" type="text" name="ajout_valeurs2" required>);
          <input class="submit-bouton2" type="submit" value="Ajouté"/>
          </center>
     </form>
     <form class="form" action="index.php" method="post">
          <center>
          UPDATE <input class="inputBox2" type="text" name="modif_table" required> SET <input class="inputBox2" type="text" name="modif_def" required> WHERE <input class="inputBox2" type="text" name="modif_condi" required>;
          <input class="submit-bouton2" type="submit" value="Modifier"/>
          </center>
     </form>
     <form class="form" action="index.php" method="post">
          <center>
          DROP TABLE <input class="inputBox2" type="text" name="drop_table" required>;
          <input class="submit-bouton2" type="submit" value="Supprimé"/>
          </center>
     </form>
</div>

Et voici le résultat :

Conclusion

En conclusion, phpMonDésastre représente une tentative de créer une version simplifiée de phpMyAdmin, offrant une interaction minimale avec une base de données. Bien que le projet suive les principes du CRUD et offre des fonctionnalités de base telles que la création, la lecture, la mise à jour et la suppression de données, il reste encore des améliorations à apporter. L’intégration de fonctionnalités supplémentaires et l’amélioration de l’interface utilisateur pourraient rendre cette plateforme plus conviviale et fonctionnelle. En fin de compte, phpMonDésastre constitue un premier pas vers un outil de gestion de base de données simplifié, mais il reste encore du travail à faire pour atteindre son plein potentiel.

Dans le lien de téléchargement ci-dessous, vous trouverez le projet dans son intégralité. Si vous utilisez un environnement local, vous devrez utiliser XAMPP pour exécuter PHP. Si vous hébergez le projet sur un serveur, vous devrez modifier la configuration de « databaseconnect.php » comme présenté précédemment.