Author: Sylvain B.

Projets

Un météorologue personnel en briquette !

Qui n’a jamais rêvé d’avoir un robot en LEGO qui lui annonce la météo ? Et bien même si ce n’est pas le cas, nous l’avons fait pour vous ! Venez découvrir un robot capable de comprendre son environnement pour vous donner la météo.

Fonctionnalités du robot

Notre robot au doux nom de ROBKEY est capable de :

  • déplacer ses bras et son bassin ;
  • reconnaitre l’endroit où il se trouve pour donner précisément la météo ;
  • afficher l’heure, la météo et la température ambiante ;
  • d’avoir quelques réactions faciales ;
  • et bien plus.

La conception du robot

Pour concevoir notre robot, nous avons dû prendre en compte plusieurs choses. Tout d’abord, les moteurs n’ont pas été particulièrement dur à installer car il ne font que la moitié de la taille de l’esp 32. Il nous a donc paru évident de mettre les moteurs des bras dans le torse du robot et le moteur du bassin dans le châssis de ses chenilles.

Ensuite il a fallu installer planche de test qui a eu plusieurs positions possibles. Premièrement dans son dos car cela aurait permis de pouvoir faire tourner le bassin du robot à 360°, mais les câbles auraient difficilement bien tenu à l’esp 32 et au robot lui-même. Nous nous sommes finalement entendu à l’idée de mettre la planche sur la batterie des chenilles bien que cela empêche le robot de faire des tours sur lui même.

Maintenant, le plus important : le placement de l’ESP32. Nous avions deux idées, l’une le plaçait dans le torse du robot, l’autre faisait en sorte qu’il soit la tête légèrement rentrée dans les épaules. Nous avons opté pour la seconde option car celle-ci nous permet d’afficher un petit visage sur l’écran du microcontrôleur.

Pour conceptualiser l’apparence de notre robot, nous avons donc réalisé un schéma en 3D très simple sur Paint 3D en regroupant nos idées pour imaginer ce à quoi il ressemblera.

Construction de la structure du robot

Pour construire le robot, j’ai (Sylvain) commencé par regarder les constructions Lego que j’avais précédemment réalisées et qui pouvaient être détruite pour commencer à récupérer leurs pièces. Avec toutes ses matières premières je me suis laissé porter par mon imagination pour construire le LEGO le plus ressemblant à un robot en essayant de faire en sorte qu’il soit plutôt résistant (bon d’accord j’ai peut-être failli à cette tâche).

C’est grâce aux joies de la maladie que j’ai pu entamer la construction. Pour commencer, j’ai récupéré le châssis d’une autochenille radiocommandée en la modifiant pour la rendre plus compacte et en laissant un espace pour le moteur du bassin.

Ensuite j’ai commencé à faire ledit bassin, que j’élargis à la fin de la construction car je l’avais fait trop fin. Suivi du torse, le plus simple car il devait être creux pour laisser la places aux câbles et aux moteurs. Il fallait aussi préparer les trous pour les axes des bras.

Après j’ai fait les bras qui sont vraiment très simplistes mais à ce moment je commençais sérieusement à manquer de pièces plus techniques et même des plus simples pour faire quelque chose de plus sympathique. Mais quoi qu’il en est l’un des deux bras représente un soleil pour quand il fait beau, tandis que l’autre représente un ciel nuageux.

Pour finir j’ai réalisé la cage de la planche de test et solidifié les éléments les moins solides et résolu les quelques problèmes mécaniques rencontrés lors des premiers tests des moteurs.

L’électronique du robot

En parallèle de la construction du LEGO, je (Thomas) me suis lancé dans la conception électronique de celui-ci.

Pour cela, j’ai d’abord listé tout ce que je voulais intégrer à l’électronique :

  • 2 boutons,
  • 1 capteur de température,
  • 3 moteurs.

Pour m’aider dans la réalisation électronique, je me suis aidé de tous ces sites :

Allez c’est parti !

On commence par le branchement des boutons :

Les câbles blancs (entourés en rouge) récupèrent l’information de la pression du bouton et les câbles noirs (entourés en vert) s’occupe de fermer le circuit (de l’électricité passent dans le bouton, et il faut que l’électricité puisse faire une boucle). Si vous voulez, les boutons reçoivent de l’énergie par les câbles blancs et l’expulsent par les câbles noirs.

Concernant le branchement du capteur :

A droite à quoi ressemble le capteur de température de face et à gauche son branchement. La câble de gauche, en blanc récupère les mesures du capteur, celui du milieu en marron, alimente positivement le capteur (le capteur reçoit de l’énergie par ce câble) et le câble noir à droite du câble marron lui ferme la boucle (le capteur expulse l’énergie par ce câble.)

Et pour finir, les moteurs ont 3 câbles :

Le câble orange sert à contrôler le moteur, le rouge à l’alimenter et le marron à fermer la boucle. On refait ça 3 fois puisqu’on a 3 moteurs.

On branche tout ceci à la planche de test puis à l’ESP32, et voilà montage terminé :

Programmation du robot

Maintenant que le robot est construit et le montage électronique assemblé, il faut passer à la programmation de l’ESP32. Veuillez noter que depuis l’écriture de ce qui suit, le code a bénéficié de quelques mises à jours. Vous retrouverez l’archive en fin d’article avec les fichiers à jours.

Tout d’abord, il faut coder les briques de bases, qui vont nous permettre de manipuler différents outils, capteurs, actionneurs, api, etc.

  • C’est pour cela que nous créons un fichier buttons.py qui contient une classe Button dans laquelle nous initialisons 4 boutons : les 2 du microcontrôleur et les deux boutons ajoutés. Dans cette classe, vous verrez que chaque boutons a des caractéristiques supplémentaires. En effet il est possible de rendre un bouton poussoir équivalent à un levier. Nous créons différentes fonctions pour récupérer ou modifier des informations en rapport avec les boutons.
from machine import Pin
from time import sleep

# Importer les modules est inutile car ils sont déjà importés dans boot.py

class Buttons():
    def __init__(self):
        #self.name = "t-display-s3"
        
        # NomDuBouton : [                    PinUtilisé                     ,    mode    , value ]
        #      1      : [ Pin 3 en mode INPUT avec une résistance Pull-Down ,   levier   ,   0   ]
        # 1 : [ Pin(3, mode=Pin.IN, pull=Pin.PULL_DOWN), 1, 0 ]
        
        # Note : le chiffre de value dans la liste n'est pas pris en compte si le bouton est en mode "poussoir" -> On ne peut pas mettre None car si on décide de changer le mode du bouton, la valeur devient importante.
        
        
        # Pour une raison inconnue, "left" et "right" ne fonctionne plus.
        self.list_button = {"left" : [Pin(0, Pin.IN, Pin.PULL_UP),0,1],
                            "right" : [Pin(14, Pin.IN, Pin.PULL_UP),0,1],
                            1 : [Pin(2, mode=Pin.IN, pull=Pin.PULL_UP),0,1],
                            2 : [Pin(3, Pin.IN, Pin.PULL_UP),0,1]
                            }


    def getButton(self, button):
        return self.list_button[button]
    
    def setButton(self, button, pin, mode, value=None): # Le set fait également add.
        self.list_button[button] = [pin,mode,value]

    def setMode(self, button, val:bool): # Ce mode change le mode du bouton -> soit "levier" soit "poussoir"
        if isinstance(val, bool) or val in (0,1):
            self.list_button[button][1] = val
            if not(val):
                self.list_button[button][2] = 1
            return print("Mode définie sur : " + str(self.list_button[button][1]))
        else:
            raise ValueError("val must be a boolean or a integer included between 0 and 1.")
    
    def setValue(self, button, value):
        if self.list_button[button][1]:
            self.list_button[button][2] = value
            return print("Valeur définie sur : " + str(self.list_button[button][2])) 
        else:
            print("Button must be a toogle button, not a push button.")
            return False
    
    def getValue(self, button): # Renvoie la valeur du bouton
        if not(self.list_button[button][1]): # Si le statut de mode_toogle est égal à 0 <=> Si le bouton est en mode "poussoir" et non "levier"
            return self.list_button[button][0].value()
        else:
            if not(self.list_button[button][0].value()):
                self.list_button[button][2] = 0 if self.list_button[button][2] else 1
                sleep(0.25)
            return self.list_button[button][2]
    
    def isButtonPressed(self,button): # Renvoie True si le bouton est pressé (donc si value = 0) et False sinon
        return not(self.getValue(button))
  • Continuons avec le fichier temperature.py dans lequel nous avons une simple fonction qui nous renvoie les données récupérées par le capteur de température et d’humidité.
from time import sleep  #Inutile car importé dans boot.py

def getTemperatureAndHumidity(capteur): # Capteur doit etre sous cette forme : capteur = dht.DHT11(Pin(17)) -> Ne pas oublier d'importer le module dht
    try:
        capteur.measure() # Met à jour les données de températures et d'humidités
        sleep(1)
        return (capteur.temperature(),capteur.humidity())
    except:
        print("Valeurs non récupérées")
        return False
  • Egalement, à l’aide des fichiers motor.py et servo.py (trouvé sur internet), nous créons des fonctions qui réaliseront des déplacements particuliers :
from servo import Servo
import time

motor=Servo(pin=12) # A changer selon la broche utilisée
motorbd=Servo(pin=10)
motorbg=Servo(pin=11)
 
def droit(t=1):
    motor.move(285)
    time.sleep(t)

def bassin(t=2):
    motor.move(265)
    time.sleep(t)
    motor.move(305)
    time.sleep(t)

def baissebg(t=0.5):
    motorbg.move(335)
    time.sleep(t)
    
def levebg(t=0.5):
    motorbg.move(270)
    time.sleep(t)

def baissebd(t=0.5):
    motorbd.move(335)
    time.sleep(t)
    
def levebd(t=0.5):
    motorbd.move(270)
    time.sleep(t)
    
droit()
  • Pour finir, à l’aide du fichier connection.py, nous créons plusieurs fonctions en rapport avec la partie sans-fil de l’ESP32.
import network
import st7789 #En théorie inutile car déjà importé dans boot.py
from time import sleep

def connect_to_wifi(display, font, ssid, mdp):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        display.init()
        display.draw(font,"Connexion a un point",0,111)
        display.draw(font,"d'acces en cours",15,126)
        wlan.connect(ssid,mdp)
        while not wlan.isconnected():
            pass
    display.fill_rect(0,107,170,25,st7789.BLACK)
    display.draw(font,'Connecte !',40,111)
    sleep(0.8)
    display.fill_rect(0,107,170,10,st7789.BLACK)
    display.deinit()

def search_wlan():
    station = network.WLAN(network.STA_IF)
    station.active(True)
    return station.scan()

Passons aux modules qui vont utiliser des apis :

  • En premier lieu, nous avons le fichier geoloc.py dans lequel nous utilisons l’api de google qui à l’aide des réseaux wifi aux alentours est capable de situer l’ESP32. Dans ce fichier est présent une fonction qui récupère les coordonnées géographiques de l’ESP32.
from modules.connection import search_wlan
import ustruct as struct
import urequests as requests
import ujson as json
import modules.api_txt as api_txt


def getLocation(api_key): # Avec Google maps API + sécurité sur le nombre de requête.    
    
    if int(api_txt.get_api_counter()[0]) > 24000:
        print("Quota de demande dépassé !!! Vous ne pouvez pas faire de requête...")
        return False
    
    list_wlan = search_wlan()
    data = {
        "considerIp": False,
        "wifiAccessPoints": []
    }

    for wifi in list_wlan:
        entry = {
            "macAddress": "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", wifi[1]),
            "signalStrength": wifi[3],
            "channel": wifi[2]
        }
        data["wifiAccessPoints"].append(entry)
    
    headers = {"Content-Type": "application/json"}
    url = "https://www.googleapis.com/geolocation/v1/geolocate?key=" + api_key
    response = requests.post(url, headers=headers, data=json.dumps(data))

    api_txt.add_api_counter(0)

    return json.loads(response.content)["location"]
  • En second et dernier lieu, il reste le fichier meteo.py qui va être capable d’utiliser une api de météorologie pour récupérer, analyser et mettre en forme des données en rapport avec la météo.
import urequests
import modules.api_txt as api_txt

def get_meteo(latitude, longitude): # Sur la prochaine heure
    
    if int(api_txt.get_api_counter()[1]) > 10000:
        print("Quota de demande dépassé !!! Vous ne pouvez pas faire de requête...")
        return False
    
    url = "https://api.open-meteo.com/v1/forecast?latitude={0}&longitude={1}&current=temperature_2m,precipitation&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=auto&forecast_days=3".format(latitude,longitude)
    json_meteo = urequests.get(url).json()
    
    api_txt.add_api_counter(1)
    
    return {"current":
            {"temperature":
             [
                 json_meteo["current"]["temperature_2m"],
                 json_meteo["current_units"]["temperature_2m"]
                 ],
             "precipitation":
             [
                 json_meteo["current"]["precipitation"],
                 json_meteo["current_units"]["precipitation"]
                 ]
             },
            "demain":
            {"temperature": [round((json_meteo["daily"]["temperature_2m_min"][1] + json_meteo["daily"]["temperature_2m_max"][1]) / 2, 1), json_meteo["daily_units"]["temperature_2m_min"]],
             "precipitation":
             [
                 json_meteo["daily"]["precipitation_sum"][1],
                 json_meteo["daily_units"]["precipitation_sum"]
                 ]
             },
            "apres-demain":
            {"temperature": [round((json_meteo["daily"]["temperature_2m_min"][2] + json_meteo["daily"]["temperature_2m_max"][2]) / 2, 1), json_meteo["daily_units"]["temperature_2m_min"]],
             "precipitation":[
                 json_meteo["daily"]["precipitation_sum"][2],
                 json_meteo["daily_units"]["precipitation_sum"]
                 ]
             }
            }

Passons à la gestion graphique, nous avons récupérer le module tft_config.py fournit par les développeurs du pilotes graphiques pour l’esp32. Celui-ci permet de créer facilement une gestion de l’écran avec les bonnes caractéristiques. Nous n’afficherons pas le code ici, car ce n’est pas nous qui l’avons codé. Vous pouvez le retrouver dans l’archive à la fin de cet article ou ici.

Nous avons créé le fichier affichage.py qui contient un certain nombre de fonctions pour réaliser le rendu graphique pour l’ESP32. Il contient également le code qui récupère des images pour les animations du visage et des icones. Ces dessins et animations ont été réalisé par Ilana, ancienne première NSI.

import st7789
from time import sleep, localtime
import modules.motor as motor

last_temps = -1

def display_time(display,font,x,y):
    global last_temps
    temps = localtime()
    if last_temps < temps[5]:
        display.png("/images/icones/horloge.png",x,y,True)
        display.fill_rect(x+30,y+5,60+6,10,st7789.BLACK)
        display.draw(font, str(temps[3]) + ":" + str(temps[4]) + ":" + str(temps[5]),x+30,y+10)
    last_temps = temps[5]

def display_meteo(display,font,infos_meteo, x, y):
    display.draw(font,str(infos_meteo["temperature"][0]),x,y)
    display.draw(font, font.LAST,x + display.draw_len(font,str(infos_meteo["temperature"][0])), y)
    display.draw(font, " C",x + display.draw_len(font,str(infos_meteo["temperature"][0])), y)
    
    display.draw(font,str(infos_meteo["precipitation"][0]),x+80,y)
    display.draw(font,str(infos_meteo["precipitation"][1]),x + 80 + display.draw_len(font,str(infos_meteo["precipitation"][0])),y)

def display_temperature(display,font,infos_temperature,x,y):
    display.fill_rect(x,y,70,20,st7789.BLACK)
    display.png("/images/icones/temperature.png",x,y,True)
    display.draw(font,str(infos_temperature) + " C",x+30,y+9)
    display.draw(font,font.LAST,x+30+display.draw_len(font,str(infos_temperature)), + y+9)    
    
def meteo_icon(display,icone,x,y):
    display.fill_rect(x,y,50,50,st7789.BLACK)
    if icone == "pluie":
        display.png("/images/icones/mauvais_temps.png",x,y,True) #Nuage pluie gris pas content
    else:
        display.png("/images/icones/beau_temps.png",x,y,True) # Soleil content
        

def meteo_widget(display,font,infos_meteo,x,y):
    display.fill_rect(x,y,170,200,st7789.BLACK)
    
    
    display.draw(font, "Auj.",x,y)
    display_meteo(display,font,infos_meteo["current"],x, y+20) #x,y+20)
    
    display.draw(font, "Dem.", x, y+40) #x+50, y)
    display_meteo(display,font,infos_meteo["demain"], x, y+60) #x+50,y+20)
    
    display.draw(font, "Apr-Dem.", x, y+80) #x+90, y)
    display_meteo(display,font,infos_meteo["apres-demain"], x, y+100) #x+90,y+20)

def main_menu(display,font,infos_temp,infos_meteo):
    display_time(display,font, 10, 111) # x = 52 pour un semblant de centrage
    display_temperature(display,font,infos_temp,10,151)
    
    if infos_meteo["current"]["precipitation"][0] > 0:
        icone = "pluie"
        motor.baissebg()
        motor.levebd()
    else:
        icone = "soleil"
        motor.baissebd()
        motor.levebg()
    
    meteo_widget(display,font,infos_meteo,10,191)
    meteo_icon(display,icone,110,111)

def animation(display,name:str,iteration:int,i_fixe:int): # list_anim : {"content":(2,1),"endormi":(4,1),"somnole":(3,1),"observe":(0,0),"monocle":(2,1)}
    display.fill_rect(0,0,170,105,st7789.BLACK)
    for i in range(2,iteration+1):
        display.png("/images/" + name + "/" + name + str(i) + ".png",0,0)
        if name == "endormi":
            sleep(0.3)
    for i in range(iteration,0,-1):
        display.png("/images/" + name + "/" + name + str(i) + ".png",0,0)
        if name == "endormi":
            sleep(0.3)
    display.png("/images/" + name + "/" + name + str(i_fixe) + ".png",0,0)
    

Pour finir nous avons le fichier api_txt.py qui s’occupe de gérer des fichiers de textes importants pour le fonctionnement du programme, présents dans le dossier data.

"""
api[0] => Google Maps API
api[1] => Météo API
"""

def get_api_counter():
    fichier_r = open("data/counter-api.txt",'r')
    liste = fichier_r.read().split(",")
    fichier_r.close()
    return liste

def add_api_counter(indice_api): # ne fonctionne qu'avec deux apis
    fichier_w = open("data/counter-api.txt",'w')
    val = get_api_counter()
    val[indice_api] = int(val[indice_api]) + 1
    
    fichier_w.write(str(val[0]) + "," + str(val[1]))
    fichier_w.close()

def set_api_counter(indice_api,value): # ne fonctionne qu'avec deux apis
    fichier_w = open("data/counter-api.txt",'w')
    api_value = get_api_counter()
    api_value[indice_api] = str(value)
    fichier_w.write(api_value[0] + "," + api_value[1])
    fichier_w.close()

Maintenant que toutes nos briques sont construites, il ne reste qu’à écrire le fichier python principal qui va organiser tout le programme tel un chef d’orchestre.

Assemblage final

Maintenant, il faut simplement assembler notre montage électronique à notre construction LEGO. Puis à brancher notre ESP32 à un câble, lui injecter le code, et tout en le gardant brancher, le lancer, et la magie opère !

Fichiers

Veuillez noter que vous devrez ajouter votre propre clé api Google Maps API pour que la position soit mise à jour (dans boot.py à la ligne 21). Sinon vous pouvez changer manuellement la position dans le fichier old-location.txt se trouvant dans le dossier data. Veuillez respecter l’ordre suivant et ne surtout rien ajouter d’autre dans le fichier, pas même un espace :

latitude,longitude

Vous devrez également ajouter vos propres informations pour un point d’accès dans le fichier boot.py au niveau de la ligne 19 : wlan_info = ("SSID","MDP")
Le projet ne fonctionne pas sans une connexion à internet !!

Projets

Un ensemble de Julia sur la calculatrice et avec…

Pour ce dernier projet de cette année, j’ai décidé de faire, le tout sur la calculatrice Numworks, un ensemble de Julia en utilisant des palettes de couleurs, cela donnera une image bien plus belle que le script Mandelbrot de la calculatrice par exemple.

D’où vient l’idée ?

J’ai déjà réalisé des projets sur les fractales, mais surtout sur les ensemble de Mandelbrot. Cette fois, j’ai voulu faire un ensemble de Julia, la « mère » des ensemble de Mandelbrot. Mais je voulais pousser la technique un peu plus loin.

Pour rendre mes fractales encore plus belles, j’ai voulu créer une palette de couleur ce qui rendra les dégradés de couleurs bien plus beaux.

Qu’est-ce qu’un ensemble de Julia ?

Avant d’expliquer comment faire cette palette, qu’est-ce qu’un ensemble de Julia ?

Un ensemble de Julia, c’est une fractale qui peut avoir plusieurs formes, ici, j’utilise l’ensemble ayant pour valeur du nombre complexe c : 0,36 + 0,36i ce qui nous donne cela quand on met 100 itérations maximum :

Comment ajouter et rendre les couleurs de ma fractale plus belle ainsi que les problèmes rencontrés ?

Avant de commencer à regarder les modifications, il faut comprendre la différence entre les couleurs de mon premier script et les couleurs de la palette. Dans le premier script, ma couleur prenait le nombre d’itérations pour se définir (voir si elle est rouge, bleu, vert…). La palette, elle, regarde à quel point le pixel est loin ou près de la fractale pour choisir la teinte et la couleur du pixel.

Pour créer une fractale de l’ensemble de Julia en utilisant des palettes de couleurs, j’ai procédé comme cela :

  • Tout d’abord à partir du script Mandelbrot de la Numworks et grâce à des recherches, je crée un script Julia qui fabrique un ensemble de Julia avec c = 0,36 + 0,36J. Cela donne une image comme ci-dessus. les lignes :
z = complex(0,0)
c = complex(xmin+*x/319-2.5, -2.5*y/221+1.25)

Deviennent :

z = complex(xmin+(xmax-xmin)*x/320+(ymax-(ymax-ymin)*y/222)*1J)
c = complex(0.36,0.36)
  • Ensuite, je crée dans mon script deux boucles for j in range qui vont me fabriquer une liste « palette » de 255 listes ayant 3 valeurs chacune qui représente les valeurs RGB :
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])
  • Explication : Pour modifier la couleur, je crée au début de ma fonction JULIA 3 variables, R pour le rouge, G pour le vert et B pour le bleu qui ont chacune comme valeur initiale 255. Pour réaliser la fractale, je rentre dans le range() les valeurs 0 et 128 pour faire une coupure entre quand je transforme le blanc en bleu et quand je transforme le bleu en noir. Je modifie les variables R et G en faisant 255 – 2 * j ce qui va diminuer les deux valeurs, puis je les rajoute dans une liste de ma palette qui contient mes valeurs R, G et B à chaque nouvelles valeurs de j dans une autre boucle for qui va de 128 à 255 je modifie la valeur du bleu avec l’équation 255 – 2 * (j – 128) et je rajoute à nouveau une liste avec les valeurs r g et b.
  • Je modifie la définition de ma variable « col » pour qu’elle récupère les trois valeurs contenues dans l’une des listes de ma palette et je rajoute une variable couleur pour rendre plus simple le code :
couleur = palette[int(255*i/N_iteration)]
col = color(couleur[0],couleur[1],couleur[2])
  • Et voilà, quand je lance mon programme avec 100 itérations, cela me donne cette fractale :

Code complet :

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)

Télécharger le .py

Autres modifications possibles :

  • Tout d’abord la couleur, pour l’instant, je n’ai seulement réussi à faire les 6 couleurs des synthèses additive et soustractive (rouge, vert, bleu, magenta, cyan et jaune). Je m’y suis pris trop tard pour faire les autres couleurs, car il faut modifier les équations des lignes qui modifie les variables R, G et B. Pour faire cela il faut modifier le code comme ceci :
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])
  • On peut modifier le point de vue en modifiant les valeurs de ces lignes :
xmax = 2
xmin = -2
ymax = 1.3875
ymin = -1.387

Il faut faire attention aux proportions sinon l’image sera distordue, par exemple si on met ces valeurs, on obtient :

xmax = 0.5
xmin = -0.5
ymax = 0.346875
ymin = -0.346875

Ici, j’ai seulement zoomé au milieu de l’image

  • Et on peut aussi faire comme sur l’image ci-dessous en passant par plusieurs couleurs en faisant plusieurs boucles for j in range :

Ici, on passe du gris, au jaune, au rouge, puis au noir.

En conclusion :

Même si ce travail ne m’a pas permis d’apprendre de nouvelles choses en python, il m’a fait travailler mon raisonnement pour programmer de futurs codes. Je me suis bien amusé à le faire, car je l’ai fait avec mon père qui avait déjà fait la même chose, mais dans d’autres langages de programmation comme RUST, et vu qu’il n’est pas très fort en python, même si il a la réflexion pour le code, nous avons pu croiser nos compétences pour finir le programme. Le seul bémol, la résolution de la calculatrice est tellement faible que cela nous donne une image très pixelisée.

Sources :

Tutoriels

Comprendre et s’amuser avec le script « Mandelbrot » de la…

Les fractales sont des figures mathématiques qui présentent des formes fragmentées et répétitives lorsqu’on les observe de manière de plus en plus précise. Elles peuvent prendre des formes similaires à des flocons de neige ou à des éponges.

C’est quoi une fractale / un ensemble de Mandelbrot ?

Pour analyser le code, il est important de comprendre ce qu’est une fractale de Mandelbrot. Cette figure mathématique a été nommée ainsi en l’honneur de Benoît Mandelbrot, qui en a popularisé les représentations dans les années 1980.


Les fractales de Mandelbrot sont basées sur l’ensemble des points c du plan complexe pour lesquels la suite de nombres complexes suivante est définie par récurrence :

Cet ensemble de points a été découvert par Gaston Julia et Pierre Fatou avant la première guerre mondiale, et permet de déterminer les ensembles de Julia associés à la suite. Chaque point du plan complexe correspond à un ensemble de Julia distinct. Les points de l’ensemble de Mandelbrot correspondent précisément aux ensembles de Julia connexes (c’est-à-dire formant une seule partie), tandis que ceux en dehors correspondent aux ensembles de Julia non connexes. L’ensemble de Mandelbrot est étroitement lié aux ensembles de Julia, et ils produisent des formes similairement complexes. (Wikipedia)

Pour en apprendre plus sur l’histoire et le fonctionnement de la géométrie fractale et des fractales de Mandelbrot le documentaire les couleurs de l’infini écrit par Arthur C. Clarke, sorti en 1995, disponible sur Netflix explique très bien, bien que la qualité d’image soit un peu dépassée.

Pour dessiner la fractale de Mandelbrot, on va associer à chaque point du plan un nombre complexe. On insère ce nombre dans la formule ci-dessus, puis le résultat de la formule est à nouveau mis dans la formule et ainsi de suite. Au bout d’un moment le résultat soit grandit de plus en plus vers l’infini, soit le résultat ne grandit plus. Selon si le résultat va vers l’infini ou ne grandit plus, on va colorier le point d’une couleur ou d’une autre. Pour ne pas calculer jusqu’à l’infini, on fixe un nombre maximum de fois où on fait le calcul, le nombre d’iterations. Quand on atteind ce nombre d’itérations, si le résultat de la formule est inférieur à deux, on considère que le point fait partie de l’ensemble de Mandelbrot.

Explication du script de la Numworks :

Dans notre calculatrice Numworks, il y a un script nommé « Mandelbrot », il sert, quand on lui donne un nombre d’itérations (répétition d’une opération sur une valeur de départ), à « dessiner » avec Kandinsky une fractale Mandelbrot. Mais on peut le modifier pour faire en sorte qu’il affiche une fractale différente :

Par exemple faire un flocon de neige beaucoup trop complexe bien que plus beau qu’un flocon de koch selon moi.

Analyse du script ligne par ligne :

# This script draws a Mandelbrot fractal set
# N_iteration: degree of precision
import kandinsky
def mandelbrot(N_iteration):
  for x in range(320):
    for y in range(222):
# Compute the mandelbrot sequence for the point c = (c_r, c_i) with start value z = (z_r, z_i)
      z = complex(0,0)
# Rescale to fit the drawing screen 320x222
      c = complex(3.5*x/319-2.5, -2.5*y/221+1.25)
      i = 0
      while (i < N_iteration) and abs(z) < 2:
        i = i + 1
        z = z*z+c
# Choose the color of the dot from the Mandelbrot sequence
      rgb = int(255*i/N_iteration)
      col = kandinsky.color(int(rgb*0.81),int(rgb*0.13),int(rgb*0.18))
# Draw a pixel colored in 'col' at position (x,y)
      kandinsky.set_pixel(x,y,col)

On va expliquer le script sans prêter attention au commentaire pour les anglophobes :

  • Ligne 3 : on importe le module Kandinsky qui sera utile pour dessiner pixel par pixel l’ensemble de Mandelbrot
  • Ligne 4 : on définit la fonction avec l’argument N_iteration qui représente ici le degré de précision du dessin, plus il est grand plus la fractale sera précise.
  • Ligne 5 et 6 : on crée deux boucles « for » qui vont faire en sorte de « peindre » sur tout le format de l’écran
  • Ligne 8 : on définit la variable z par un nombre complexe qui est ici (0,0)
  • Ligne 10 : on crée la variable c qui prend comme valeur un nombre complexe qui correspond au format de l’écran
  • Ligne 11 : on crée la variable i, on lui donne la valeur 0
  • Ligne 12 : on commence à peindre le Mandelbrot à partir d’une boucle while qui se répète tant que i est plus petit que l’argument N_iteration et que la valeur absolue de z est plus petite que 2
  • Ligne 13 : on incrémente 1 a i pour que la boucle ne soit pas infinie
  • Ligne 14 : on calcule la suite qui définit le Mandelbrot : z*z+c
  • Ligne 16 : on crée la variable rgb qui est l’integer de 255 fois i divisé par N_iteration
  • Ligne 17 : on crée la variable col qui choisit la couleur générale de la fractale
  • Ligne 19 : on pose le pixel à l’abscisse x l’ordonnée y la couleur col

Explication des modifications possibles et simples du script :

La couleur :

Maintenant que l’on a compris le script, on peut commencer par modifier très simplement la couleur du Mandelbrot. On ne peut pas vraiment changer le fond, mais la couleur de la fractale. Par défaut, elle est orange-jaune, mais peut être changée si on modifie les valeurs de la ligne 17 au niveau des endroits où il y a marqué rgb*une_valeur.

Si on modifie cette valeur, on va modifier la couleur du Mandelbrot. Il y a trois endroits avec rgb*une_valeur qui définissent donc le code RGB de la fractale, la première fois que l’on a l’opération, elle représente le rouge puis le vert puis le bleu. Ainsi avec ceci en tête, on peut faire une fractale de la couleur que l’on veut. On peut sinon essayer de saturer les couleurs par exemple en mettant des valeurs supérieures à 1.

Ici par exemple en mettant 98 à chaque valeur on obtient cela :

La forme :

Si on veut modifier la forme, la tâche n’est pas beaucoup plus dure. Il faut modifier le calcul de la suite à la ligne 14, par exemple de base, on a z*z+c, mais on peut y mettre ce que l’on veut, par exemple z**42+c le plus important est de garder au moins un c et un z, car sinon la calculatrice n’affichera que le font.

Et voila quelques exemple de forme et de couleur jolies :

La fractale parfaite :

  • Ligne 14 :
z = z**42+c
  • Ligne 17 :
 col = kandinsky.color(int(rgb*0.95),int(rgb*0.2),int(rgb*0.2))

Et voila le résultat :

Le flocon de neige :

  • Ligne 14 :
:z = z**7+c
  • Ligne 17 :
 col = kandinsky.color(int(rgb*0.75),int(rgb*0.75),int(rgb*0.98))

Et voilà le résultat :

Et voilà, je trouve ça mieux qu’un flocon de Koch personnellement. Maintenant libre à vous de créer votre fractale !

Explication possible mais complexe de modification du script :

En fin d’année scolaire 2022, M. Robert, professeur de mathématiques de seconde nous a proposé de faire un devoir maison nommé « les mathématiques sont belles« , le but : créer de la beauté via des maths sous forme de fonction ou de script python en turtle. Personnellement, j’avais découvert le script Mandelbrot de la calculatrice et je voulais le réutiliser, j’ai donc modifié sa couleur (et pas sa forme, ça m’aurait valu un 20, mais bon) et surtout, j’ai modifié la prise de vue de la fractale :

Pour réaliser cette fractale, j’ai dû être aidé, car de base en tant qu’élève de seconde, je ne connaissais pas grand choses au nombre complexe, mon père m’a donc aidé et nous avons réussi à modifier la prise de vue du Mandelbrot.

Tout d’abord, on rajoute avant les deux boucles 4 variables qui vont delimiter le cadre de la vue que l’on veut. Mais il faut faire attention à incrémenter des valeurs qui feront un cadre proportionnel au format de l’écran de la calculatrice. Ci-dessous les valeurs que j’ai personnellement utilisées :

  • Xmax = -0.635
  • Xmin = -1.905
  • Ymax =  0.491
  • Ymin = 0.488

Ensuite On modifie la ligne 10 on l’écrit comme cela :

c = complex(Xmin+(Xmax-Xmin)*x/319,Ymin+(Ymax-Ymin)*y/221)

Et voilà, pour l’instant avec les valeurs inscrites dans les quatre nouvelles variables, on a juste zoomé légèrement sur l’avant du Mandelbrot, mais vous pouvez modifier à souhait les valeurs pour voir le Mandelbrot sous d’autres angles. Pour rechercher les coordonnées que vous voulez, vous pouvez rechercher des « Mandelbrot explorer » comme sur le site Science démos.

Conclusion :

En conclusion, avec tout ce qui vous est proposé dans ce tutoriel, vous pouvez modifier : la couleur, la forme et la prise de vue de votre fractale. À vous de créer votre œuvre d’art !

Art

Astronomie : L’amas de trous noirs

« L’amas de trous noirs » est le nom de notre premier projet développé en Python. Notre petit programme vous permettra de profiter d’un magnifique ciel étoilé… avec quelques trous noirs. En effet, ce programme générera pour vous une image d’une beauté à couper le souffle… Enfin, nous espérons !

Introduction

Représenter ce que l’être humain ne peut atteindre est une de ses passions favorites. En effet, nous allons vous présenter notre projet Python se prénommant : L’amas de trous noirs. Effrayant, n’est-ce pas ?

Le résultat final doit ressembler à ceci, mais vous le savez déjà :

Cette image est un des multiples résultats de notre code. Nous y reviendrons plus tard.

Comme vous pouvez le voir, il y a plusieurs éléments sur cette image : des trous noirs, des étoiles et un arrière-plan noir. Commençons par le programme pour former les trous noirs.

Nous avons utilisé le module turtle de python qui permet de dessiner à l’écran. Dans la suite de l’article, nous ferons référence à ce module sous le nom de tortue.

Les trous noirs

Un code non optimisé

C’est la première chose sur laquelle nous avons travaillé. Afin que vous conserviez votre santé mental, nous éviterons de vous montrer le code qui forme cette image :

Si vous le voulez vraiment :

def cercle(rayon, x, y, r=1, v=1, b=1):
    pensize(5)
    while rayon > 372 and r < 254:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        r += 2
    while rayon > 244 and v < 254:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        v += 2
    while rayon > 119 and b < 254:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        b += 2
    while r > 0 and v > 0 and b > 0:
        penup()
        goto(x, y - rayon)
        pendown()
        pensize(2)
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        b -= 16
        v -= 16
        r -= 16


cercle(500, 0, 0)

Ce code ne nous satisfait pas car il a des limites d’utilisations. C’est-à-dire qu’il est impossible de dessiner des trous noirs de n’importe quel diamètre. On a par exemple ce cas où l’on choisi un rayon de 200 :

Ici, on constate que le dégradé n’est pas celui que l’on souhaitait.

Maintenant que nous avons vu un code qui vous pique encore les yeux, nous allons voir une autre version plus optimisée et bien plus flexible.

Un code optimisé

Avant toute chose, nous définissons au début de notre programme colormode(255) qui nous permettra de définir des couleurs au format (r, v, b), r pour rouge, v pour vert et b pour bleu qui sont les trois teintes permettant de composer n’importe quelle couleur.

Ensuite nous définissons notre fonction trou_noir(). Nous allons la découper en plusieurs portions de code afin de vous l’expliquer étape par étape.

  • Première étape : Initialisation de diverses informations.
colormode(255)

def trou_noir(x, y, rayon):
    color(0, 0, 0)
    pensize(5)
    r = -1
    v = -1
    b = -1
    penup()
    goto(x, y - rayon - 20)
    pendown()

Notre fonction aura donc besoin de trois paramètres : les coordonnées x, y et le rayon du trou noir. Nous indiquons que la couleur du stylo sera noir, en raison de l’arrière-plan qui sera noir également. Nous définissons le taille du trait à 5, qui est la valeur la plus basse tout en évitant des artéfacts au niveau du trou noir comme ceci :

Nous définissons trois variables qui, comme leur nom l’indique, seront les variables liées au changement de couleur. Nous les définissons avec une valeur négative, ce qui peut paraitre assez étrange, car les valeurs minimales pour le vert, le rouge ou le bleu sont zéro. Nous vous expliquerons un peu plus bas pourquoi nous faisons cela.

La fonction penup() permet de lever le stylo, la fonction goto() permet de le déplacer. On met en paramètre les coordonnées x et y en paramètre. La fonction pendown() permet elle, de poser le stylo.

Concernant la fonction goto(), elle prend en paramètre les coordonnées x et y, qui représente un point de l’écran. La tortue n’est capable de dessiner un cercle qu’à partir de sa base (le point le plus bas du cercle). Nous souhaitons utiliser le centre géométrique d’un cercle pour les positionner. Nous allons donc décaler la coordonnée y de la distance du rayon. Cela donne le code suivant : goto(x, y - rayon).

Nous avons choisi d’ajouter autour de notre trou noir une zone noire (🥴) de 20 pixels d’épaisseur (cette valeur a été défini de façon arbitraire). Nous verrons par la suite que c’est un choix esthétique, cette marge de 20 pixels se retrouve donc dans l’appel de notre fonction goto() : goto(x, y - rayon - 20).

  • Deuxième étape : Création d’un disque noir.
	begin_fill()
    fillcolor("black")
    circle(rayon + 20)
    end_fill()

Ce disque a été rajouté afin d’améliorer la fonction trou_noir() : en effet, après une première version de la fonction trou noir, nous avons amélioré le rendu graphique en dessinant un premier disque noir sur lequel la tortue dessine le trou noir. Cela apporte deux avantages : produire la marge de 20 pixels autour du trou noir (cf. paragraphe au-dessus) et remplir le centre de notre trou noir. Voyez ainsi :

Ce disque noir sert de transition entre le ciel étoilé (que vous verrons par la suite) et le trou noir en lui-même.

  • Troisième étape : Construction du trou noir (dégradé du rouge vers le blanc).
    while r < 255:
        penup()
        goto(x, y - rayon)
        pendown()
        r += 2
        pencolor((r, 0, 0))
        circle(rayon)
        rayon -= rayon/500

    while v < 255:
        penup()
        goto(x, y - rayon)
        pendown()
        v += 2
        pencolor((r, v, 0))
        circle(rayon)
        rayon -= rayon/500

    while b < 255:
        penup()
        goto(x, y - rayon)
        pendown()
        b += 2
        pencolor((r, v, b))
        circle(rayon)
        rayon -= rayon/500

Ces trois boucles while sont très similaires. Elles servent à créer le dégradé du trou noir. Ce que notre tortue fait est de dessiner un cercle d’une couleur différente à chaque fois que la boucle se répète. La boucle se termine une fois que la variable de la couleur donnée dans la condition while a atteint son maximum, soit 255. A chaque itération, nous incrémentons de 2 chaque variable de couleur, et nous le faisons juste avant de dessiner le cercle. Avec cette incrémentation, il nous faudrait 128 itérations pour arriver à la valeur 256. Cette valeur est donc supérieur à 255, nous avons donc décidé d’initialiser chaque variable de couleur à -1 afin de compenser ce problème. Pour la variable r par exemple, la première fois que notre boucle va s’exécuter, elle va s’incrémenter de 2 ( -1 + 2 = 1 ), puis choisir une couleur pour le stylo : pencolor((r, 0, 0)), puis dessiner le cercle. Lors de la dernière itération, r est égal à 253, donc 253 + 2 = 255, la tortue dessine le cercle avec cette valeur. Nous sortirons ensuite de la boucle. Nous faisons ceci trois fois, pour les trois variables de couleurs.

La dernière ligne qui est intéressant dans l’extrait cité est rayon -= rayon/500. La valeur 500 provient de notre « programme d’essai », celui non optimisé. Nous avons utilisé la proportionnalité par rapport à la valeur trouvée grâce à ce programme. Dans le programme d’essai nous retirions 1 au rayon pour un cercle de rayon 500. Ici nous retirons au rayon le résultat de la division du rayon par 500. Par exemple si le rayon est 500, on retirera 1 ( 500/500 = 1 ), si c’est 250, on retirera 0,5 ( 250/500 = 0,5 ), si c’est 750, on retirera 1,5 ( 750/500 = 1,5 ), etc. Ceci permet de faire des trous noir de la taille que l’on souhaite.

  • Quatrième étape : Dégradé du blanc au noir de finition.
    while r > 0 and v > 0 and b > 0:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= rayon / 500
        b -= 16
        v -= 16
        r -= 16
    pencolor("black")
    circle(rayon)

Cette dernière boucle permet de faire un fort dégradé entre le blanc et le centre du trou noir, qui est… noir (🥴). Elle n’a rien de particulier par rapport à ce que l’on a expliqué plus haut mis à part la valeur forte de la réduction des valeurs des trois variables de couleurs (que l’on décrémente de 16) et également la condition de la boucle qui permet qu’elle s’arrête une fois que la couleur atteinte est le noir.

Les deux dernières lignes de l’extrait permettent d’éviter ce problème :

Les étoiles

Voici le code pour former les étoiles :

def etoile(x, y, branche, longueur, couleur=(255, 230, 0)):
    penup()
    goto(x, y)
    degre_angle = 180 - (360 / (branche * 2))
    begin_fill()
    fillcolor(couleur)
    for i in range(branche):
        forward(longueur)
        right(degre_angle)
    end_fill()

Cette fonction prend en paramètre les coordonnées x et y, le nombre de branche, la longueur des branches (plus précisément la longueur des segments constituants l’étoile) et la couleur (avec une couleur par défaut qui est un jaune-orange qui tend plus vers le jaune). On lève notre stylo, on se positionne de manière à ce que le centre de l’étoile soit les coordonnées données en paramètres et… pas de pendown() ? En effet celui-ci serait inutile pour former les étoiles car nous n’allons pas utiliser la fonction de tracé de ligne mais plutôt utiliser la fonction de remplissage (de couleur) de zone. Ensuite, nous créons une variable degre_angle. Elle est très importante car elle va définir le degré des angles qui vont permettre de former l’étoile. Regardez :

L’étoile est formée de la même manière que l’extrait cité sauf qu’ici le stylo est baissé, et il n’y a pas de couleur de remplissage. Les angles qui nous intéressent sont au bout des branches. Ce sont ces angles là que le programme contrôle.

En A), notre tortue avance. Elle est à une position avec un angle de 0°. L’angle en rouge est égal à 180°. C’est notre 180 dans la formule ! Et (360 / (branche * 2)) est en fait l’angle intérieur. Pour savoir la valeur de cet angle, il faut imaginer l’étoile dans un cercle. La somme de tous les sommets de cette étoile dans un cercle donne 360°. Et dans une étoile, le nombre de sommet est le double du nombre de branche.

Ensuite en B), elle s’oriente de l’angle calculé dans la variable degre_angle. Le trait violet est un trait de construction rajouté pour que le schéma soit plus clair.

Pour finir en C), la tortue avance. Ici on voit bien l’angle formé qui est celui de degre_angle. Le trait en cyan est un trait de construction.

D’une pierre deux coup, nous vous avons expliqué aussi la boucle qui est dans le programme. Avec begin_fill(), fillcolor(couleur) et end_fill() nous remplissons l’intérieur de l’étoile avec la couleur donnée en paramètre.

Le ciel (arrière-plan et étoiles)

Cool ! On a bien avancé. On continue avec cette fois-ci un bon bgcolor("black") qui est censé faire un arrière-plan en noir et… non. Cela ne va pas marcher, nous verrons cela un peu après, une fois le code entièrement présenté. Nous allons donc concevoir une fonction arriere_plan() :

def arriere_plan():
    # .ps est incapable de récupérer le bgcolor() selon internet...
    # Et des barres blanches apparaissent avec ce code dans le .png généré...
    penup()
    goto(-640, 0)
    pendown()
    pensize(900)
    forward(1280)

Il y a un bavard dans le code de ce que je vois, mais il a raison (ou on a tous les deux torts, c’est une éventualité). Encore une fois, nous verrons ceci un peu plus tard. Cette fonction est très simple, elle conçoit simplement un fond noir, en se positionnant le plus à gauche du canvas (zone de dessin) et en traçant un trait de la taille du canvas (le résultat doit avoir une taille de 1280 pixels de largeur par 720 pixels d’hauteur) avec une taille exagérée.

def ciel():
    arriere_plan()
    liste_couleur = [(255, 255, 255), (255, 230, 0)]
    for i in range(0, randint(71, 121)):
        liste_couleur[1] = (255, randint(150, 230), 0)
        etoile(randint(-630, 630), randint(-350, 350), 5, 10, choice(liste_couleur))
    for i in range(0, randint(16, 31)):
        liste_couleur[1] = (255, randint(150, 230), 0)
        etoile(randint(-620, 620), randint(-340, 340), 7, 20, choice(liste_couleur))
    for i in range(0, randint(6, 16)):
        liste_couleur[1] = (255, randint(150, 230), 0)
        etoile(randint(-600, 600), randint(-320, 320), 15, 40, choice(liste_couleur))

Occupons nous maintenant de la fonction ciel(). Nous appelons la fonction arriere_plan() puis le programme dessine des étoiles dans notre ciel. Nous utilisons simplement trois boucles qui dessineront 3 types d’étoiles différentes. En effet grâce aux paramètres branche et longueur, nous pouvons former des étoiles complètements différentes, diversifiant le ciel. Concernant le paramètre couleur, nous avons une liste qui varie à chaque fois que la boucle se répète. C’est plus précisément le second élément qui varie. On modifie aléatoirement la quantité de vert dans la couleur, rendant l’étoile plus ou moins orangée. La fonction choice() permet de choisir un élément aléatoirement parmi une liste donnée en paramètre. Pour finir sur ce bout de code, chaque boucle for génère un nombre aléatoire d’étoile entre deux valeurs précisées (ex : randint(71, 121)). On remarque que les étoiles plus petites apparaissent plus nombreuses.

Voici un rendu de la fonction ciel() :

Taille du canvas : 2560×1080 pixels
Taille du canvas : 1280×720 pixels

On remarque bien le fond généré « artificiellement » dans la première image, et comment cela rend dans les bonnes dimensions.

Assemblement et problèmes

Bien, nous avons notre ciel, plus qu’à rajouter les trous noirs. Nous en ferons trois :

ciel()
trou_noir(randint(-490, -250), randint(-260, 260), randint(50, 150))
trou_noir(randint(100, 250), randint(-260, 260), randint(50, 150))
trou_noir(randint(400, 490), randint(-260, 260), randint(50, 150))

On appelle donc notre fonction ciel() et nous générons trois trous noirs. Ils sont positionnés aléatoirement mais pour éviter une fusion de trou noir, nous les plaçons dans des coordonnées où ils ne peuvent s’entrechoquer. On remarque qu’il y a un décalage de 150 entre chaque plage de coordonnées x. C’est dû à la taille qui est aléatoire entre 50 et 150.

Une fusion de trou noir… C’est très beau, vous ne trouvez pas ?

Nous vous avions proposé un résultat en début d’article, en voici un autre :

Le rendu turtle
Le fichier PNG généré

On remarque diverse imperfection. La plupart provienne d’un problème lors de la conception du fichier postscript et nous n’avons pas trouver comment régler ce problème.

Un autre problème que nous avons eu a été celui-là :

Effectivement, je n’ai plus l’impression de voir un ciel…

Bon, qu’est-ce qui s’est passé ? Lorsque nous avons généré cette image, nous utilisions la fonction bgcolor("black") pour générer le fond noir sauf qu’après une petite recherche sur internet, le fichier postscript ne prend pas en compte cette action, il ne la « voit » pas. Nous avons donc dû changer par la fonction arriere_plan() que nous vous avons expliqué plus haut.

Conclusion

Vous pouvez former des images complètement différentes de ce que l’on a généré simplement en modifiant quelques valeurs ou en rajoutant une boucle for… Vous pouvez faire un ciel beaucoup plus dense, des dégradés plus profonds, des trous noirs plus grands, etc. Nous avons voulu faire un programme qui puisse avec très peu de modification générer des résultats d’une grande diversité.

Télécharger le .py

Si l’envie vous prend de rendre ces personnalisations plus simples d’accès et/ou optimiser notre code, voici un petit cadeau :

L’image finale