Le but de ce projet est d'écrire une libraire de fonctions permettant de faire du dessin compositionnel en OCaml, puis de tester cette librairie en faisant un dessin.
Un dessin compositionnel est un dessin qui résulte de formes élémentaires et d'opérations d'assemblage ou de transformation de dessins.
Par exemple, le dessin
pourra être réalisé avec le code ci-dessous
let maison = above (square 100 brown FILLED) (regular_polygon 3 100 red FILLED) in
let porte = underlay_center_right (rectangle 20 40 black OUTLINED) (circle 3 yellow FILLED)
in underlay_center_bottom maison porte
Au cours du projet, vous rencontrerez deux sortes de fonctions à écrire:
Pour ce deuxième type de fonctions, vous travaillerez avec la libraire Graphics. Il s'agit d'une librairie de dessin qui nécessite un serveur X (le système de fenêtrage classique sous Unix, et émulé sous Mac par Xquartz et sous Windows par Cygwin, entre autres).
Jusqu'à très récemment, la librairie Graphics était installée automatiquement avec OCaml, et vous ne devriez pas avoir à l'installer chez vous. Vous pouvez même travailler sur tryocaml, qui émule la librairie dans votre navigateur, même si je vous recommande plutôt d'installer OCaml sur votre machine. Pour tester votre installation, récupérez le fichier projet.ml, compilez-le et exécutez-le.
ocamlc graphics.cma projet.ml
./a.out
Si vous n'arrivez pas à compiler ou à exécuter projet.ml
, il se peut simplement que la librairie Graphics ne soit pas installée chez vous: tentez dans ce cas de l'installer avec opam install graphics
et réessayez.
Si ça ne marche toujours pas, c'est probablement que le compilateur ne parvient pas à trouver le fichier graphics.cma
dans votre installation. Demandez-nous de l'aide!
Au cours du projet, vous aurez à lire la documentation en ligne de la librairie Graphics et vous aurez peut-être envie de chercher une fonction prédéfinie dont vous auriez besoin dans le manuel de référence d'OCaml. Vous pouvez aussi nous demander de l'aide si vous ne trouvez pas quelque chose. Et si vous n'aimez pas le mail, posez vos questions en cours ou en TD/TP.
Dernière chose à propos de Graphics: les coordonnées en Graphics sont des entiers positifs. Le point (0,0) correspond à l'angle en bas à gauche de la fenêtre. Il s'agit du système de coordonnées standard des mathématiciens (mais moins courant chez les informaticiens).
Vous pourrez faire ce projet seul ou en binôme. Vous devrez rendre un seul fichier xxx.ml
où xxx
sera remplacé par votre nom de famille.
CRITERE IMPORTANT
Le fichier fourni doit pouvoir être compilé avec la commande
ocamlc graphics.cma xxx.ml
, oùxxx
sera remplacé par votre nom de famille.
Si vous avez une partie du code qui ne marche pas, vous pouvez la mettre en commentaire et expliquer ce que vous avez voulu faire, mais rendez un code qui compile. L'exécution du fichier doit ouvrir une fenêtre qui affiche un dessin. Chacun d'entre vous doit faire son propre dessin. Les fichiers sont à déposer sur Moodle. Si vous êtes deux sur le projet, il faudra que chacun fasse un dessin différent à la fin et rende un fichier différent. Vous indiquerez en commentaire au début du fichier qui est l'autre personne qui était en binome avec vous.
DATE BUTOIR
Vous devez rendre votre projet avant le 1 décembre minuit.
Travaillez un peu chaque semaine, et commencez votre projet le plus tôt possible! Le temps de travail total est estimé à 12 heures.
QUELQUES CONSEILS
Ne restez pas bloqués sur un problème, il y a beaucoup de questions, elles ne sont pas toujours de difficultés croissantes, donc n'hésitez pas à avancer dans le sujet. Par exemple, le début de la partie 5 est plus facile que la fin de la partie 4.
Il y a quelques années, les étudiants de L1 réalisaient des dessins en Racket, vous pourrez encore trouver leurs oeuvres de la dernière année ici. Ils utilisaient la libraire images.rkt de Racket, dont nous nous inspirons pour ce projet. Vous pourrez donc trouver d'autres exemples de dessins compositionnels en Racket dans la documentation de Racket, en particulier ici et ici. Pour rester sur OCaml, et sur une touche un peu plus scientifique, vous pourrez aussi regarder la galerie de dessins en MLPost, une autre librairie de dessin en OCaml, plus orientée vers le dessin dans des documents latex. Le dessin réalisé ne comptera pas autant que l'état d'avancement de la librairie de dessin, mais je vous encourage à rendre un beau dessin!
Récupérez le fichier projet.ml
et renommez-le en xxx.ml
où xxx
sera remplacé par votre nom. Ouvrez ce fichier avec votre éditeur.
img
. Pour le moment, il n'y a que deux types d'images possibles: des cercles, ou des rectangles. Il vous faudra bientôt étendre et modifier ce type énuméré pour rajouter d'autres formes. Repérez aussi les fonctions racourcis de constructeurs circle
et rectangle
.box
. C'est un enregistrement qui contient deux champs height
et width
. Une boite décrit les dimensions d'une image.box_of_img : img -> box
img
, il vous faudra ajouter un cas à cette fonction.Repérez la fonction
draw_at : int * int -> img -> unit
C'est la fonction de dessin proprement dite. À chaque nouvelle extension que vous apporterez au type img
, il vous faudra ajouter un cas à cette fonction. Le paramètre (x,y)
de la fonction est le point d'ancrage de la boite dans laquelle on dessine, autrement dit le coin en bas à gauche de la boîte. Jusqu'à la fin de la partie 3, vous n'aurez que des figures élémentaires dont la boite correspond à toute la fenêtre d'affichage: (x,y)
restera donc égal à (0,0)
, le coin en bas à gauche de la fenêtre d'affichage. Dans la partie 4, vous manipulerez des sous-boites à des positions quelconques de la fenêtre d'affichage, et le point d'ancrage de la boite sera un point (x,y)
quelconque de la fenêtre d'affichage.
Regardez maintenant plus précisément comment est dessiné un cercle : on commence par sélectioner la couleur, puis on appelle la fonction de dessin (Graphics.draw_circle
ou Graphics.fill_circle
selon le cas) avec pour premiers paramètres le centre du cercle. Ce centre du cercle se trouve au centre de la boite qui contient le cercle, d'où le calcul qui est fait.
Regardez aussi comment est dessiné le rectangle: après avoir sélectionné la couleur, on appelle la fonction de dessin avec les coordonnées de l'angle en bas à gauche du rectangle, suivies de la largeur et la hauteur du rectangle.
Repérez la fonction
display : img -> unit
C'est la fonction qui s'occupe de mettre en place la fenêtre d'affichage. Vous n'avez pas besoin de comprendre en détail comment elle fonctionne. Retenez simplement qu'on redimensionne la fenêtre graphique aux dimensions de l'image à afficher, on dessine l'image en appelant la fonction auxiliaire draw_at
, puis on installe une boucle d'attente pour que la fenêtre ne se ferme pas immédiatement. Pour fermer la fenêtre, il vous suffit d'appuyer sur n'importe quel bouton. Vous n'aurez pas à modifier cette fonction.
main ()
: c'est la fonction principale: c'est là que vous aurez à définir votre image et à demander de l'afficher. Pour le moment, l'image est simplement un cercle plein jaune de rayon 50.Modifiez la définition de my_cool_image
, dans la fonction main, pour afficher un rectangle vide de taille $300\times 200$ avec un contour en rouge. Vous devrez utiliser le constructeur OUTLINED
du type énuméré mode
et la couleur prédéfinie Graphics.red
(il s'agit de l'entier 0xff0000
correspondant à la couleur rgb rouge). Attention, vous ne devez rien changer d'autre que la définition de my_cool_image.
Ajoutez un racourci de constructeur square : int -> color -> mode
tel que square n color mode
construit une image qui est un carré de côté $n$, autrement dit un rectangle $n\times n$. Faites afficher un carré plein bleu de côté 100. A nouveau, vous ne devez rien changer d'autre que la définition de my_cool_image
dans la fonction main
.
Ajoutez un constructeur de type Ellipse of int * int * color * mode
au type img
afin de pouvoir dessiner des ellipses. Les deux entiers correspondent aux rayons horizontaux et verticaux de l'ellipse. Il va vous falloir mettre à jour les fonctions box_of_img
et draw_at
pour prendre en compte ce nouveau cas. Cherchez dans la documentation de Graphics comment dessiner une ellipse avec Graphics.draw_ellipse
et Graphics.fill_ellipse
.
N'oubliez pas de rajouter la fonction racourci ellipse
pour le constructeur Ellipse
.
Testez en dessinant une ellipse pleine verte dans une boite de taille $300\times 100$.
let my_cool_image = ellipse 150 50 Graphics.green FILLED
Supprimez le constructeur Circle of int * color * mode
du type img
et retirez les cas correspondants dans les fonctions box_of_img
et draw_at
. Votre code va se simplifier! Pour terminer, il vous faut changer la fonction racourci circle
: un cercle est un cas particulier d'ellipse! Testez en dessinant le cercle du début.
let my_cool_image = circle 50 Graphics.yellow FILLED
Vous disposez de quelques formes élémentaires.
Vous allez maintenant devoir ajouter d'autres formes élémentaires à votre librairie de dessin.
Si vous n'arrivez pas à les faire toutes fonctionner, vous pouvez sauter directement à la partie 4 et revenir sur la partie 3 plus tard: les seules formes élémentaires nécessaires pour les tests de la partie 4 sont celles que vous avez programmées à la partie 2.
Ajoutez un constructeur Line
au type img
et définissez un racourci
line : int * int -> int * int -> color -> img
tel que line p1 p2 color mode
représente une ligne entre les points p1
et p2
. Les coordonnées de p1
et p2
peuvent être des entiers négatifs. Au moment de l'affichage, il vous faudra translater les coordonnées de ces deux points pour que la ligne tienne entièrement dans la fenêtre et fasse une diagonale dans la fenêtre. Ce n'est pas si simple que cela a l'air!
Testez avec line (10, 10) (200, 500) Graphics.red
puis line (0,100) (-300,400) Graphics.magenta
Ajoutez le constructeur Polygon of (int * int) array * color * mode
. Le premier argument est un tableau de points qui peuvent être négatifs. Au moment de l'affichage, il vous faudra translater les coordonnées de ces points pour que le polygone tienne entièrement dans la fenêtre en touchant sur les côtés ou les angles.
Testez avec polygon [|(100,100); (-100,200); (0,300)|] Graphics.blue OUTLINED
.
Rectangle of int * int * color * mode
et passez par le racourci polygon
pour définir les racourcis rectangle
et square
. Testez votre code avec les dessins de la partie 2.Polygon
pour faire des losanges et des polygones réguliers rhombus : int -> int -> color -> mode -> img
regular_polygon : int -> int -> color -> mode -> color -> img
rhombus width height color mode
représente un losange dont les diagonales sont horizontales et verticales et qui tient dans un rectangle de largeur width
et de hauteur height
. Testez rhombus 400 100 Graphics.cyan FILLED
.
regular_polygon n c color mode
représente un polygone régulier à n
côtés, chacun de longueur c
, dont un côté est adjacent au bord du bas de la boite qui le contient. Vous pourrez utiliser les nombres complexes $(z_k)_{k=0,\dots,n-1}$ suivants comme sommets du polygone
Testez avec plusieurs polygones, par exemple regular_polygon 7 100 Graphics.magenta FILLED
.
Vous allez maintenant définir des fonctions permettant de composer des images (regardez à nouveau l'exemple en début de sujet). Si vous bloquez à un moment, vous pouvez sauter à la partie 5 et écrire l'opérateur frame
.
beside
¶Ajoutez le constructeur de type Beside of img * img
au type img
et une fonction racourci beside: img -> img -> img
. Votre type img
est maintenant un type récursif: les fonctions box_of_img
et draw_at
vont devenir elles aussi des fonctions récursives!
Indications
box_of_img
, il va falloir calculer la taille de la boite qui englobe les deux images côte à côte; la largeur de cette boite est égale à la somme des largeurs des deux boites des sous-images, et la hauteur est la hauteur maximale.draw_at
pour dessiner chacune des sous-images, il va falloir calculer leur point d'ancrage. Le point d'ancrage d'une sous-image est l'angle inférieur gauche de sa sous-boite dans la fenêtre d'affichage. Les coordonnées de ce point sont les deux premiers paramètres passés à la fonction draw_at
.Supposons que le point d'ancrage de la boite qui contiendra l'image composée par beside est $(x,y)$. Alors
Testez votre code en faisant afficher l'image
let my_cool_image =
let cb = circle 125 Graphics.blue FILLED in
let sb = square 300 Graphics.black FILLED in
let rr = rhombus 100 250 Graphics.red FILLED in
let (||) = beside in cb || ((rr || sb) || cb)
beside_list
¶On souhaite généraliser beside
en une fonction
beside_list : img list -> img
prennant en argument une liste d'images et produisant l'image de leur juxtaposition. Un cas particulier est la juxtaposition d'une liste vide, qui renvoie l'image vide empty_img
. Vous pourrez définir empty_img
comme le cercle de rayon 0. Ceci fait, vous devriez pouvoir définir beside_list
en une seule ligne avec
List.fold_left
!
beside_center
et beside_top
¶Définissez les fonctions beside_center
et beside_top
, de type img -> img -> img
, qui placent elles aussi deux images côte à côte, mais qui alignent les boites différemment, cf exemples ci-dessous. Vous devez étendre le type img
, ainsi que box_of_img
et draw_at
.
Par exemple, beside_top (square 300 Graphics.black FILLED) (circle 125 Graphics.blue FILLED)
produit
et beside_center (square 300 Graphics.black FILLED) (circle 125 Graphics.blue FILLED)
produit
above
, above_center
, et above_right
¶Définissez les fonctions de composition d'images above
, above_center
et above_right
qui placent deux images l'une au dessus de l'autre avec différentes façon de les aligner. Vous devez étendre le type img
, ainsi que box_of_img
et draw_at
. Testez votre trois fonction, par exemple above_center (square 300 Graphics.black FILLED) (circle 125 Graphics.blue FILLED)
produit
underlay_at
¶Définissez la fonctions de composition d'images
underlay_at : img -> int * int -> img -> img
qui permet de superposer deux images: underlay_at img1 (dx,dy) img2
produit l'image obtenue en dessinant img1
en dessous de img2
. Le vecteur $(dx,dy)$ représente le vecteur $\overrightarrow{p_1p_2}$, où $p_1$ et $p_2$ sont les points d'ancrage des sous-boites respectives de img1
et img2
.
Par exemple, underlay_at (square 300 Graphics.black FILLED) (200, -100) (circle 125 Graphics.blue FILLED)
produit
si vous n'arrivez pas à faire cette question, vous pouvez commencer par faire la suivante, qui est un cas particulier.
underlay
centrés¶Définissez les fonctions
underlay : img -> img -> img
underlay_center_top : img -> img -> img
underlay_center_bot : img -> img -> img
underlay_center_left : img -> img -> img
underlay_center_right : img -> img -> img
Par exemple, underlay (square 300 Graphics.black FILLED) (circle 125 Graphics.blue FILLED)
produit
et underlay_center_right (square 300 Graphics.black FILLED) (circle 125 Graphics.blue FILLED)
produit
Retirer les constructeurs de type Beside
et Above
de la déclaration du type img
. Vous ne devriez avoir besoin que d'un seul constructeur pour traiter underlay_at
, qui permet de traiter tous les autres...
Bref: simplifiez votre code!
Pour cette partie, vous devez coder des opérateurs de transformation d'images. D'un point de vue algébrique, les opérateur précédents étaient des opérateurs binaires, ceux de cette section sont des opérateurs unaires.
framed
¶Définissez la fonction framed : img -> img
telle que framed img
est l'image obtenue en rajoutant un cadre noir sur le pourtour de la boite de img
.
Exemple:
let (||) = beside in
let cb = circle 250 Graphics.blue FILLED in
let rr = rhombus 100 150 Graphics.red FILLED in
cb || (framed rr) || cb
`
with_pen_size
¶Tous les traits que vous avez tracés jusqu'à présent utilisaient une largeur de trait par défaut. Plutôt que de redéfinir toutes les fonctions précédentes pour rajouter la largeur de trait en paramètre, vous allez introduire un opérateur qui permet de spécifier la largeur de trait à utiliser pour dessiner une sous-image.
Définissez la fonction with_pen_size : int -> img -> img
telle que with_pen_size n img
"installe" la largeur de trait n
avant de commencer à dessiner img
. Vous prendrez garde à restaurer l'ancienne largeur de trait une fois le desssin de img
terminé.
let (||) = beside in
let cb = circle 100 Graphics.black OUTLINED in
with_pen_size 5 (cb || (with_pen_size 20 cb) || cb)
A vous de jouer pour faire un beau dessin! La seule contrainte est de n'utiliser que des fonctions racourci de constructeur pour fabriquer des images. Il est interdit d'appeler des fonctions de dessin de la librairie Graphics
directement. La structure de votre fonction main
doit rester de la forme
let main () =
let my_cool_image = ...
in display my_cool_image
Si vous voulez par ailleurs explorer la gestion du clavier et de la souris avec Graphics, vous pouvez lire la documentation de Graphics et rendre votre image animée... il faudra aller modifier la fonction display
pour changer la boucle infinie loop_at_exit
... Mais vous devriez être déjà pas mal occupé par le reste, donc ce n'est réservé qu'aux plus aventureux.
let rec sierpinsky n c =
if n = 0
then regular_polygon 3 c Graphics.red FILLED
else let img = sierpinsky (n-1) (c/2) in
above_center (beside img img) img
in sierpinsky 4 300