Projet de programmation fonctionnelle

Faire bonne figure avec OCaml

Etienne Lozes - novembre 2019


1. Aperçu du projet

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.

Qu'est-ce qu'un dessin compositionnel?

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

A propos de la librairie Graphics

Au cours du projet, vous rencontrerez deux sortes de fonctions à écrire:

  • des fonctions "de calcul", qui vont manipuler des structures de données mais qui n'afficheront rien
  • des fonctions "de dessin", qui se chargeront d'afficher une image sur l'écran.

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

Travail à rendre

Vous pourrez faire ce projet seul ou en binôme. Vous devrez rendre un seul fichier xxx.mlxxx 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.

Exemples de dessins

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!


2. Démarrage du projet

2.1 Lecture du code fourni

Récupérez le fichier projet.ml et renommez-le en xxx.mlxxx sera remplacé par votre nom. Ouvrez ce fichier avec votre éditeur.

  1. Repérez la définition du type énuméré 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.
  2. Repérez la définition du type enregistrement box. C'est un enregistrement qui contient deux champs height et width. Une boite décrit les dimensions d'une image.
  3. Repérez la fonction
    box_of_img : img -> box
    
    c'est la fonction qui calcule les dimensions d'une image. À chaque nouvelle extension que vous apporterez au type img, il vous faudra ajouter un cas à cette fonction.
  4. 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.

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

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

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

  8. Repérez la 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.

2.2. Rectangle

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.

2.3 Carré

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.

2.4 Ellipse

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

2.5 Retour au cercle

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

3 De nouvelles formes élémentaires

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.

3.2 Ligne

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

3.3 Polygone

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.

3.4 Polygones particuliers

  1. Supprimez le constructeur 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.
  2. Ajoutez des racourcis du constructeur 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

$$ z_k = \frac{c}{2\mathsf{sin}(\frac{\theta}{2})}e^{i(k\theta-\varphi)} \qquad \mbox{où} \quad \theta=\frac{2\pi}{n} \quad\mbox{et}\quad \varphi = \frac{\pi+\theta}{2} $$

Testez avec plusieurs polygones, par exemple regular_polygon 7 100 Graphics.magenta FILLED.


4 Opérateurs de composition (opérateurs binaires)

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

  • dans 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.
  • avant d'appeler récursivement 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

  • le point d'ancrage de la sous-boite gauche est lui aussi $(x,y)$
  • le point d'ancrage de la sous-boite droite est $(x+w,y)$, où $w$ est la largeur de la sous-boite gauche.

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

Simplifiez, factorisez! (BONUS)

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!


5 Opérateurs de transformation (opérateurs unaires)

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)

6 Dessin libre

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