This commit is contained in:
Tipragot 2023-10-19 15:29:33 +02:00
parent 41d3bd84a7
commit cabf456282
No known key found for this signature in database
3 changed files with 628 additions and 0 deletions

View file

@ -0,0 +1,117 @@
# coding: utf-8
#
# Chapitre 6 - TP 2 - Dessiner des fractales (page 129)
#
from math import cos, sin, radians
from matplotlib.colors import rgb2hex
from dessin import Dessin, Trait, Arc, main_loop
# constantes
TAILLE = 800 # taille de la surface de dessin
# la surface de dessin, avec un fond blanc
dessin_arbre = Dessin("white", TAILLE, TAILLE)
def ytree(
n,
x,
y,
orientation_arbre,
longueur,
ecart_angulaire_fils=radians(22.5),
ratio_longueur=0.8,
n_max=None,
):
""" Dessine un arbre binaire ytree de hauteur n+1.
Entrée:
- n : hauteur de l'arbre courant -1
- x,y : coordonnées de la racine de l'arbre
- orientation_arbre : orientation de l'arête partant de la racine du y-tree courant, en radians: radians(0) si horizontale, radians(90) = pi/2 pour orienter verticalement vers le haut
- longueur : longueur de l'arête partant de la racine du y-tree courant
- ratio_longueur : le ratio servant à diminuer la longueur d'un niveau à l'autre.
- n_max : le nombre de niveaux de l'arbre global. Utilisé uniquement pour attribuer la bonne couleur en fonction du niveau du y-tree courant dans l'arbre global.
-------
Un y-tree de niveau 1 (n=1) est un simple trait.
Un y-tree de niveau n>=2 est formé d'un trait suivi de 2 y-trees de niveau n-1.
Dans un y-tree:
=> c'est l'arête rejoignant un noeud à son père qui est l'axe de symétrie entre ses 2 sous-arbres.
=> l'angle entre les fils est constant: 2*ecart_angulaire_fils, donc chaque arête s'écarte de l'arête parente d'un angle +/-'ecart_angulaire_fils'.
On a choisi de diminuer la longueur d'un facteur constant à chaque niveau.
Les branches sont colorées proportionnellement à leur niveau dans l'arbre :
=> le lien partant de la racine globale est noir
=> pour n >= 2, les liens aboutissant aux feuilles sont verts
-------
Complexité: linéaire dans la taille de l'arbre.
"""
if n >= 0:
if n_max == None:
n_max = n
x1 = x + cos(orientation_arbre) * longueur
y1 = y + sin(orientation_arbre) * longueur
couleur = rgb2hex((0.0, 1.0, 0.0))
if n_max >= 1:
couleur = rgb2hex((0.0, float(n_max - n) / (n_max), 0.0))
Trait(dessin_arbre, x, y, x1, y1, couleur)
ytree(n-1, x1, y1, orientation_arbre + ecart_angulaire_fils, longueur*ratio_longueur, ecart_angulaire_fils, ratio_longueur, n_max)
ytree(n-1, x1, y1, orientation_arbre - ecart_angulaire_fils, longueur*ratio_longueur, ecart_angulaire_fils, ratio_longueur, n_max)
# A compléter
# Pour effectuer les appels récursifs :
# - ecart_angulaire_fils, ratio_longueur, n_max ne changent pas
# - la nouvelle origine est x1, y1
# - la longueur diminue et devient longueur*ratio_longueur
# - l'orientation des sous-arbre est obtenue en fonction de orientation_arbre et ecart_angulaire_fils
def dessiner_arbre_binaire_arc(n, x, y, w):
""" Dessine un arbre binaire de hauteur n+1, en représentant les arêtes par des arcs de cercle.
Entrée:
- x,y : coordonnées de la racine de l'arbre
- w : écart horizontal entre les 2 fils de la racine.
- h : écart vertical entre 2 niveaux
- n : hauteur de l'arbre courant -1
-------
L'arbre est représenté verticalement de haut en bas.
Les 2 noeuds sont reliés par des arcs de cercles à leurs parents (1/4 de cercle pour chaque fils, de manière à former un demi-cercle d'un fils à l'autre).
=> C'est l'axe vertical qui est l'axe de symétrie entre les 2 sous-arbres.
=> En divisant w par 2 à chaque niveau, on a la garantie que les branches ne se croisent pas (et comme les noeuds sont de simples points qui n'ont pas de largeur, il n'y a pas de risque que les feuilles se recouvrent).
-------
Complexité: linéaire dans la taille de l'arbre.
"""
if n > 0:
Arc(dessin_arbre, x - w / 2, y, x + w / 2, y + w, 0, 180)
dessiner_arbre_binaire_arc(n-1, x-w/2, y+w/2, w/2)
dessiner_arbre_binaire_arc(n-1, x+w/2, y+w/2, w/2)
def dessiner_arbre_binaire_trait(n, x, y, w, h):
""" Dessine un arbre binaire classique de hauteur n+1.
Entrée:
- x,y : coordonnées de la racine de l'arbre
- w : écart horizontal entre les 2 fils de la racine.
- h : écart vertical entre 2 niveaux
- n : hauteur de l'arbre courant -1
-------
L'arbre est représenté verticalement de haut en bas.
=> C'est l'axe vertical qui est la bisectrice entre les 2 arêtes rejoignant un noeud à ses fils.
=> En divisant w par 2 à chaque niveau, on a la garantie que les branches ne se croisent pas (et comme les noeuds sont de simples points qui n'ont pas de largeur, il n'y a pas de risque que les feuilles se recouvrent).
A vous de modifier pour que cet arbre soit fractal en modifiant l'écart vertical.
Tel qu'il est écrit en l'absence de modification, cet arbre n'est pas réellement fractal car l'angle entre les fils varie à chaque niveau (donc en "zoomant" on n'a pas exactement la même figure que la figure globale).
-------
Complexité: linéaire dans la taille de l'arbre.
"""
# A modifier :
if n > 0:
Trait(dessin_arbre, x, y, x - w / 2, y + h)
Trait(dessin_arbre, x, y, x + w / 2, y + h)
dessiner_arbre_binaire_trait(n - 1, x - w / 2, y + h, w / 2, h)
dessiner_arbre_binaire_trait(n - 1, x + w / 2, y + h, w / 2, h)
# Test :
ytree(10, 100, 200, 0, 50, radians(22.5), .8)
dessiner_arbre_binaire_arc(5, 600, 20, 160)
dessiner_arbre_binaire_trait(5, 600, 400, 160, 40)
main_loop()

View file

@ -0,0 +1,511 @@
# coding: utf-8
#
# Bibliothèque de dessin pour le TP du C06 et l'activité du C07.
#
# Cette bibliothèque définit un classe Dessin pour créer une surface de dessin :
# s = Dessin()
# et les classes Disque, Trait, Rectangle pour créer des formes que l'on peut ensuite modifier
# d = Disque(s, 40, 50, 10, 'red') # disque de centre (40, 50), de rayon 10 et de couleur rouge
# t = Trait(s, 10, 20, 100, 100, 'blue') # trait de (10, 20) à (100, 100) de couleur bleue
# r = Rectangle(s, 100, 20, 200, 120, 'green') # rectangle de coins (100, 20) et (200, 120), de couleur verte
# a = Arc(s, 10, 20, 100, 120, 45, 90, 'red') # arc de cercle ou d'ellipse contenu dans le rectangle (10, 20), (100, 120),
# commençant à 45º et de longueur 90º, de couleur rouge
# t = Texte(s, 100, 200, "Hello", 'black') # texte "Hello" centré à la position 100, 200, de couleur noire
#
# Note : chaque constructeur peut prendre des paramètres optionnels supplémentaires :
# Dessin: couleur de fond, largeur, hauteur
# Disque, Rectangle : épaisseur du bord, couleur du bord (pas de bord par défaut)
# Trait, Arc : épaisseur du trait (1 par défaut)
# Texte : police (triplet nom de police, taille, style), ancre (point cardinal par rapport auquel le texte est positionné)
#
# Les méthodes disponibles sur une forme f :
# f.couleur('yellow') changer la couleur de remplissage (sauf Trait et Arc)
# f.epaisseur(4) changer l'épaisseur du bord ou du trait
# f.bord('black') changer la couleur du bord (pour le trait et l'arc utiliser f.couleur)
# f.rayon(20) changer le rayon (Disque seulement)
# f.angle_debut(90) changer l'angle de début (Arc seulement)
# f.largeur_angulaire(180) changer la longueur de l'arc (Arc seulement)
# f.police(("Times", 12, "bold")) changer la police de caractères (Texte seulement)
# f.ancre('nord-est') changer la position relative du texte (Texte seulement, voir code ci-dessous)
#
# f.bouge(10, 20) déplacer la forme de 10 pixels en x et 20 pixels en y
# f.bouge_1(10, 20) déplacer le premier point de la forme (Rectangle, Trait et Arc seulement)
# f.bouge_2(10, 20) déplacer le second point de la forme (Rectangle, Trait et Arc seulement)
#
# f.position(50, 70) déplacer la forme pour que son centre soit à la position (50, 70)
# f.position_1(50, 70) déplacer le premier point de la forme à cette position (Rectangle, Trait et Arc seulement)
# f.position_2(50, 70) déplacer le second point de la forme à cette position (Rectangle, Trait et Arc seulement)
#
# f.dessous() mettre la forme f en-dessous de toutes les autres
# f.dessus() mettre la forme f au-dessus de toutes les autres
# f.efface() effacer la forme, qui devient alors inutilisable (les autres méthodes de font rien)
#
# Enfin on peut animer le dessin grâce à la méthode `anime` de la classe Surface :
# s.anime(50, pas) # appelle la fonction `pas` toutes les 50 millisecondes
# s.arrete() # arrête l'animation
#
# IMPORTANT :
# La fonction main_loop() doit être appelée à la fin du programme principal
# pour lancer la boucle d'interaction.
# Eviter le bug qui plante (certains) MacOS avec TkInter et Python3 !
import platform
# if platform.mac_ver()[0] != '' and platform.python_version_tuple()[0] != '2':
# exit('TkInter crashes on this Mac with Python 3. Use Python 2.x')
# Importer la bonne version de TkInter
import tkinter as tk
_root = tk.Tk()
def main_loop():
""" Lancer la boucle d'interaction """
tk.mainloop()
class Dessin:
""" Cette classe représente une surface de dessin.
Toutes les autres classes prennent une instance de cette classe dans leur constructeur.
"""
def __init__(self, fond="white", largeur=800, hauteur=800):
# créer une surface de dessin qui remplit la fenêtre
self.canvas = tk.Canvas(_root, bg=fond, width=largeur, height=hauteur)
self.canvas.pack(expand=True, fill="both")
self.stop_animation = False
def anime(self, dt, pas):
""" Appeler la fonction `pas` toutes les dt ms """
pas()
if not self.stop_animation:
self.canvas.after(dt, lambda: self.anime(dt, pas))
self.stop_animation = False
def arrete(self):
""" Arrêter l'animation """
self.stop_animation = True
class Disque:
""" Cette classe représente un disque de centre, rayon et couleur donnés.
Le disque peut avoir un bord d'épaisseur et de couleur donnés.
Si la couleur de remplissage est '', le disque est "vide",
et si son épaisseur de bord n'est pas nulle, il s'affiche comme un cercle.
"""
def __init__(self, dessin, x, y, r, couleur='black', epaisseur=0, bord="black"):
self.dessin = dessin
self.item = dessin.canvas.create_oval(
x - r, y - r, x + r, y + r, fill=couleur, width=epaisseur, outline=bord
)
def couleur(self, c):
""" Changer la couleur du disque """
if self.item:
self.dessin.canvas.itemconfigure(self.item, fill=c)
def epaisseur(self, e):
""" Changer l'épaisseur du bord du disque """
if self.item:
self.dessin.canvas.itemconfigure(self.item, width=e)
def bord(self, c):
""" Changer la couleur du bord du disque """
if self.item:
self.dessin.canvas.itemconfigure(self.item, outline=c)
def rayon(self, r):
""" Changer le rayon du disque """
if self.item:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
x, y = (x1 + x2)/2, (y1 + y2)/2
self.dessin.canvas.coords(self.item, x - r, y - r, x + r, y + r)
def position(self, x, y):
""" Changer la position du disque """
if self.item:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
r = (x2 - x1)/2
self.dessin.canvas.coords(self.item, x - r, y - r, x + r, y + r)
def bouge(self, dx, dy):
""" Déplacer le disque de dx, dy """
if self.item:
self.dessin.canvas.move(self.item, dx, dy)
def dessus(self):
""" Placer le disque au-dessus des autres objets """
if self.item:
self.dessin.canvas.tag_raise(self.item)
def dessous(self):
""" Placer le disque en dessous des autres objets """
if self.item:
self.dessin.canvas.tag_lower(self.item)
def efface(self):
""" Effacer le disque, qui ne peut plus alors être utilisé """
if self.item != None:
self.dessin.canvas.delete(self.item)
self.item = None
class Trait:
""" Cette classe représente un trait d'extrémités et de couleur donnés """
def __init__(self, dessin, x1, y1, x2, y2, couleur="black", epaisseur=1):
self.dessin = dessin
self.item = dessin.canvas.create_line(
x1, y1, x2, y2, fill=couleur, width=epaisseur
)
def couleur(self, c):
""" Changer la couleur du trait """
if self.item != None:
self.dessin.canvas.itemconfigure(self.item, fill=c)
def epaisseur(self, e):
""" Changer l'épaisseur du trait """
if self.item:
self.dessin.canvas.itemconfigure(self.item, width=e)
def position(self, x, y):
""" Changer la position du (milieu du) trait """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
self.dessin.canvas.move(self.item, x - cx, y - cy)
def position_1(self, x, y):
""" Changer la position de la première extrémité du trait """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x, y, x2, y2)
def position_2(self, x, y):
""" Changer la position de la seconde extrémité du trait """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1, y1, x, y)
def bouge(self, dx, dy):
""" Déplacer le trait de dx, dy """
if self.item != None:
self.dessin.canvas.move(self.item, dx, dy)
def bouge_1(self, dx, dy):
""" Déplacer la première extrémité du trait de dx, dy """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1 + dx, y1 + dy, x2, y2)
def bouge_2(self, dx, dy):
""" Déplacer la seconde extrémité du trait de dx, dy """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1, y1, x2 + dx, y2 + dy)
def dessus(self):
""" Placer le trait au-dessus des autres objets """
if self.item:
self.dessin.canvas.tag_raise(self.item)
def dessous(self):
""" Placer le trait en dessous des autres objets """
if self.item:
self.dessin.canvas.tag_lower(self.item)
def efface(self):
""" Effacer le trait, qui ne peut plus alors être utilisé """
if self.item != None:
self.dessin.canvas.delete(self.item)
self.item = None
class Arc:
""" Cette classe représente un arc de cercle donné par le carré qui l'encadre,
l'angle de départ, la largeur angulaire, et sa couleur
"""
def __init__(
self,
dessin,
x_min,
y_min,
x_max,
y_max,
s_angle,
largeur_angulaire,
couleur="black",
epaisseur=1,
):
"""
x_min, y_min, x_max, y_max : coordonnées du carré contenant le cercle complet
s_angle : angle en degré du départ de l'arc par rapport à l'axe des x : 0 est situé à 3h, 90 à 12h...
largeur_angulaire : largeur angulaire de l'arc, en degré (180 pour un demi-cercle)
"""
self.dessin = dessin
# dans canvas un arc est créé par
self.item = dessin.canvas.create_arc(
x_min,
y_min,
x_max,
y_max,
start=s_angle,
extent=largeur_angulaire,
outline=couleur,
width=epaisseur,
style="arc",
)
def couleur(self, c):
""" Changer la couleur du trait """
if self.item != None:
self.dessin.canvas.itemconfigure(self.item, outline=c)
def epaisseur(self, e):
""" Changer l'épaisseur du trait """
if self.item:
self.dessin.canvas.itemconfigure(self.item, width=e)
def dessus(self):
""" Placer le rectangle au-dessus des autres objets """
if self.item:
self.dessin.canvas.tag_raise(self.item)
def angle_debut(self, a):
""" Changer l'épaisseur du trait """
if self.item:
self.dessin.canvas.itemconfigure(self.item, start=a)
def largeur_angulaire(self, a):
""" Changer l'épaisseur du trait """
if self.item:
self.dessin.canvas.itemconfigure(self.item, extent=a)
def position(self, x, y):
""" Changer la position du centre de l'arc """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
self.dessin.canvas.move(self.item, x - cx, y - cy)
def position_1(self, x, y):
""" Changer la position du premier coin du rectangle englobant de l'arc """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x, y, x2, y2)
def position_2(self, x, y):
""" Changer la position du second coin du rectangle englobant de l'arc """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1, y1, x, y)
def bouge(self, dx, dy):
""" Déplacer l'arc de dx, dy """
if self.item != None:
self.dessin.canvas.move(self.item, dx, dy)
def bouge_1(self, dx, dy):
""" Déplacer le premier coin du rectangle englobant de l'arc """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1 + dx, y1 + dy, x2, y2)
def bouge_2(self, dx, dy):
""" Déplacer le second coin du rectangle englobant de l'arc """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1, y1, x2 + dx, y2 + dy)
def dessous(self):
""" Placer le rectangle en dessous des autres objets """
if self.item:
self.dessin.canvas.tag_lower(self.item)
def efface(self):
""" Effacer le trait, qui ne peut plus alors être utilisé """
if self.item != None:
self.dessin.canvas.delete(self.item)
self.item = None
class Rectangle:
""" Cette classe représente un rectangle de coordonnées et de couleur donnés.
Le rectangle peut avoir un bord d'épaisseur et de couleur donnés.
Si la couleur de remplissage est '', le rectangle est "vide",
et si son épaisseur de bord n'est pas nulle, il s'affiche comme un cadre.
"""
def __init__(
self, dessin, x1, y1, x2, y2, couleur="black", epaisseur=0, bord="black"
):
self.dessin = dessin
self.item = dessin.canvas.create_rectangle(
x1, y1, x2, y2, fill=couleur, width=epaisseur, outline=bord
)
def couleur(self, c):
""" Changer la couleur du rectangle """
if self.item:
self.dessin.canvas.itemconfigure(self.item, fill=c)
def epaisseur(self, e):
""" Changer l'épaisseur du bord du rectangle """
if self.item:
self.dessin.canvas.itemconfigure(self.item, width=e)
def bord(self, c):
""" Changer la couleur du bord du rectangle """
if self.item:
self.dessin.canvas.itemconfigure(self.item, outline=c)
def position(self, x, y):
""" Changer la position du (centre du) rectangle """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
self.dessin.canvas.move(self.item, x - cx, y - cy)
def position_1(self, x, y):
""" Changer la position du premier coin du rectangle """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x, y, x2, y2)
def position_2(self, x, y):
""" Changer la position du second coin du rectangle """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1, y1, x, y)
def bouge(self, dx, dy):
""" Déplacer le rectangle de dx, dy """
if self.item:
self.dessin.canvas.move(self.item, dx, dy)
def bouge_1(self, dx, dy):
""" Déplacer le premier coin du rectangle """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1 + dx, y1 + dy, x2, y2)
def bouge_2(self, dx, dy):
""" Déplacer le second coin du rectangle """
if self.item != None:
x1, y1, x2, y2 = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x1, y1, x2 + dx, y2 + dy)
def dessus(self):
""" Placer le rectangle au-dessus des autres objets """
if self.item:
self.dessin.canvas.tag_raise(self.item)
def dessous(self):
""" Placer le rectangle en dessous des autres objets """
if self.item:
self.dessin.canvas.tag_lower(self.item)
def efface(self):
""" Effacer le rectangle, qui ne peut plus alors être utilisé """
if self.item != None:
self.dessin.canvas.delete(self.item)
self.item = None
# Traduire les directions cardinales en constantes Tk
tk_anchors = {
"n": tk.N,
"nord": tk.N,
"s": tk.S,
"sud": tk.S,
"e": tk.E,
"est": tk.E,
"w": tk.W,
"o": tk.W,
"ouest": tk.W,
"ne": tk.NE,
"nord-est": tk.NE,
"se": tk.SE,
"sud-est": tk.SE,
"nw": tk.NW,
"no": tk.NW,
"nord-ouest": tk.NW,
"sw": tk.SW,
"so": tk.SW,
"sud-ouest": tk.SW,
"c": tk.CENTER,
"centre": tk.CENTER,
}
class Texte:
""" Cette classe représente un texte de position, contenu et couleur donnés.
Le texte peut aussi avoir une police et une ancre.
La police est un tuple (famille, taille, style) famille est le nom de la police
(par exemple Times ou Helvetica), taille la taille en points, et style (optionnel)
une chaîne de caractères contenant les mots "bold", "italic", "underline", et/ou "overstrike".
L'ancre indique la position du texte par rapport à sa position.
Par défaut ('c') l'ancre est au milieu du texte.
On peut aussi indiquer un des points cardinaux ('n', 's', 'e', 'o', 'ne', 'no', 'se', 'so').
Par exemple 'so' indique que l'ancre est au coin sud-ouest du texte,
donc que le texte est au-dessus et à droite de la position x, y.
"""
def __init__(self, dessin, x, y, texte, couleur="black", police=None, ancre="c"):
self.dessin = dessin
self.item = dessin.canvas.create_text(x, y, text=texte, fill=couleur)
self.police(police)
self.ancre(ancre)
def texte(self, t):
""" Changer la contenu du texte """
if self.item:
self.dessin.canvas.itemconfigure(self.item, text=t)
def couleur(self, c):
""" Changer la couleur du texte """
if self.item:
self.dessin.canvas.itemconfigure(self.item, fill=c)
def police(self, p):
""" Changer la police du texte """
if self.item:
self.dessin.canvas.itemconfigure(self.item, font=p)
def ancre(self, a):
""" Changer l'ancre du texte """
if self.item:
self.dessin.canvas.itemconfigure(self.item, anchor=tk_anchors[a.lower()])
def position(self, x, y):
""" Changer la position du texte """
if self.item:
x, y = self.dessin.canvas.coords(self.item)
self.dessin.canvas.coords(self.item, x, y)
def bouge(self, dx, dy):
""" Déplacer le texte de dx, dy """
if self.item:
self.dessin.canvas.move(self.item, dx, dy)
def dessus(self):
""" Placer le texte au-dessus des autres objets """
if self.item:
self.dessin.canvas.tag_raise(self.item)
def dessous(self):
""" Placer le texte en dessous des autres objets """
if self.item:
self.dessin.canvas.tag_lower(self.item)
def efface(self):
""" Effacer le texte, qui ne peut plus alors être utilisé """
if self.item != None:
self.dessin.canvas.delete(self.item)
self.item = None