Author: Christian.Queinnec@lip6.fr

## Affichage graphique

Les éléments de base de l'affichage graphique sont : le repère et le contexte graphique, les couleurs, les tracés et les remplissages de formes fermées, les textes, et les bitmaps.

### Repère et contexte graphique

La bibliothèque `Graphics` gère une fenêtre principale et unique. Les coordonnées du repère de la fenêtre vont du point (0,0) en bas à gauche au point supérieur droit de la fenêtre. Les principales fonctions sur cette fenêtre sont :

- `open_graph`, de type string -> unit, qui ouvre une fenêtre;
- `close_graph`, de type unit -> unit, qui la ferme;
- `clear_graph`, de type unit -> unit, qui l'efface.

La taille de la fenêtre graphique est donnée par les fonctions `size_x` et `size_y`.
La chaîne de caractères passée en argument de la fonction `open_graph` dépend du système de fenêtrage de la machine sur laquelle est exécuté le programme et n'est donc pas indépendante de la plate-forme. Toutefois, la chaîne vide ouvre une fenêtre dont les caractéristiques sont données par défaut. Il est possible de spécifier la taille de la fenêtre : sous X-Windows, `" 200x300"` donnera une fenêtre de 200 pixels de large et de 300 pixels de haut. Attention, l'espace, premier caractère de la chaîne `" 200x300"` est indispensable.
Le contexte graphique contient un certain nombre de composants qui sont accessibles ou modifiables :

le point courant : | `current_point` : unit -> int * int
| `moveto` : int -> int -> unit
la couleur courante : | `set_color` : color -> unit
la largeur du tracé de lignes : | `set_line_width` : int -> unit
la police de caractères courante : | `set_font` : string -> unit
la taille des caractères : | `set_text_size` : int -> unit

### Couleurs

Les couleurs sont représentées par trois octets : chacun est la valeur de l'intensité d'une couleur principale dans le modèle RGB (rouge, vert, bleu) comprise entre un minimum 0 et un maximum 255. La fonction `rgb` (de type int -> int -> int -> color) permet de construire une nouvelle couleur à partir de ses trois composantes. Si les trois composantes sont identiques, la couleur obtenue est un gris plus ou moins clair selon la valeur. Le noir correspond au minimum d'intensité pour chacune des composantes (`0``` `0``` `0`) et le blanc correspond au maximum (`2` `5` `5``` `2` `5` `5``` `2` `5` `5`). Certaines couleurs sont prédéfinies : `black` , `white`, `red`, `green`, `blue`, `yellow`, `cyan`, `magenta`.
Les variables `foreground` et `background` correspondent respectivement à la couleur courante et à la couleur du fond. L'effacement de l'écran provoque son remplissage par la couleur du fond.
Une couleur (une valeur de type color) est en fait un entier qu'il est possible de manipuler pour, par exemple, la décomposer selon ses trois composantes (`from_rgb`) ou lui appliquer une fonction d'inversion vidéo (`inv_color`).

    (* couleur == R * 256 * 256  +  G * 256  +  B  *)
    # let from_rgb (c : Graphics.color)  =
       let r = c / 65536  and  g = c / 256 mod 256  and  b = c mod 256
       in (r,g,b) ;;
    val from_rgb : Graphics.color -> int * int * int = <fun>
    # let inv_color (c : Graphics.color) =
        let (r,g,b) = from_rgb c
        in Graphics.rgb (255-r) (255-g) (255-b) ;;
    val inv_color : Graphics.color -> Graphics.color = <fun>

La fonction `point_color`, de type int -> int -> color, retourne la couleur du point dont les coordonnées lui ont été passées en argument.

### Tracé et remplissage

Une fonction de tracé dessine un trait à l'écran. Le trait est de la largeur courante et de la couleur courante. Une fonction de remplissage colorie une forme fermée avec la couleur courante. Les différentes fonctions de tracé et de remplissage sont données dans la figure 5.1.

> tracé | remplissage | type
> `plot` |  | int -> int -> unit
> `lineto` |  | int -> int -> unit
> | `fill_rect` | int -> int -> int -> int -> unit
> | `fill_poly` | ( int * int) array -> unit
> `draw_arc` | `fill_arc` | int -> int -> int -> int -> int -> unit
> `draw_ellipse` | `fill_ellipse` | int -> int -> int -> int -> unit
> `draw_circle` | `fill_circle` | int -> int -> int -> unit

> Figure 5.1 : Fonctions de tracé et de remplissage

Attention, la fonction `lineto` a pour effet de bord de modifier la position du point courant.

##### Tracé de polygones

En guise d'exemple, nous ajoutons les primitives de tracé qui ne sont pas prédéfinies. Un polygone est décrit par un tableau contenant ses sommets.

    # let draw_rect x0 y0 w h =
       let (a,b) = Graphics.current_point()
       and x1 = x0+w and y1 = y0+h
       in
         Graphics.moveto x0 y0;
         Graphics.lineto x0 y1; Graphics.lineto x1 y1;
         Graphics.lineto x1 y0; Graphics.lineto x0 y0;
         Graphics.moveto a b  ;;
    val draw_rect : int -> int -> int -> int -> unit = <fun>

    # let draw_poly r =
       let (a,b) = Graphics.current_point () in
       let (x0,y0) = r.(0) in Graphics.moveto x0 y0 ;
         for i = 1 to (Array.length r)-1 do
           let (x,y) = r.(i) in Graphics.lineto x y
         done ;
         Graphics.lineto x0 y0 ;
         Graphics.moveto a b ;;
    val draw_poly : (int * int) array -> unit = <fun>

Notez que ces fonctions prennent les mêmes arguments que les fonctions prédéfinies de remplissage de ces formes. Et que comme les autres fonctions de tracé de formes, elles ne modifient pas le point courant.

##### Illustration du modèle du peintre

Cet exemple construit l'illustration d'un réseau à jeton en anneau (figure 5.2). Chaque machine est représentée par un petit cercle. On répartit l'ensemble des machines sur un grand cercle et l'on trace un trait entre les machines connectées. La position courante du jeton dans le réseau est matérialisée par un petit disque noir.
La fonction `ens_points` construit les coordonnées des différentes machines du réseau. Les données résultantes sont stockées dans un tableau.

    # let pi = 3.1415927 ;;
    val pi : float = 3.1415927
    # let ens_points (x,y) l n =
       let a = 2. *. pi /. (float n) in
       let rec aux (xa,ya) i =
         if i > n then []
         else
           let na = (float i) *. a in
           let x1 = xa + (int_of_float ( cos(na) *. l))
           and y1 = ya + (int_of_float ( sin(na) *. l))  in
           let np = (x1,y1) in
             np::(aux np (i+1))
       in Array.of_list (aux (x,y) 1) ;;
    val ens_points : int * int -> float -> int -> (int * int) array = <fun>

La fonction `dessine` affiche les connexions, les machines et le jeton.

    # let dessine (x,y) l n sc st =
      let r = ens_points (x,y) l n in
        draw_poly r ;
        let dessine_cercle (x,y) =
          Graphics.set_color Graphics.background ;
          Graphics.fill_circle x y sc ;
          Graphics.set_color Graphics.foreground ;
          Graphics.draw_circle x y sc
        in
          Array.iter dessine_cercle r ;
          Graphics.fill_circle x y st ;;
    val dessine : int * int -> float -> int -> int -> int -> unit = <fun>

L'appel suivant correspond au dessin gauche de la figure 5.2.

    # dessine (140,20) 60.0 10 10 3;;
    - : unit = ()

    # save_screen "IMAGES/tokenring.caa";;
    - : unit = ()

> Figure 5.2 : Réseau en anneau

Notons que l'ordre de l'affichage a son importance. Nous traçons d'abord les connexions puis les noeuds. Le dessin des noeuds du réseau efface une partie des traits de connexion. Il n'y a donc pas besoin de calculer le point d'intersection entre les segments de connexion et les cercles des sommets. L'illustration de droite de la figure 5.2 inverse cet ordre d'affichage. On voit donc le tracé des segments apparaître à l'intérieur des cercles représentant les noeuds.

### Texte

Les fonctions d'affichage de texte sont des plus simples. Les deux fonctions `draw_char` (de type char -> unit) et `draw_string` (de type string -> unit) affichent respectivement un caractère et une chaîne de caractères au point courant. Après l'affichage, ce dernier est modifié. Ces affichages tiennent compte du choix de la police courante et de sa taille courante.

### Remarque

Le rendu d'affichage sur les chaînes peut différer selon les systèmes graphiques.

La fonction `text_size` prend une chaîne en entrée et retourne un couple d'entiers correspondant aux dimensions de cette chaîne affichée avec la police et la taille courante.

##### Affichage vertical de chaînes

Cet exemple décrit la fonction `draw_string_v` qui affiche une chaîne de caractères verticalement à partir du point courant. Elle est utilisée à la figure 5.3. Chaque lettre est affichée séparément en changeant la coordonnée verticale.

    # let draw_string_v  s =
       let (xi,yi) = Graphics.current_point()
       and l = String.length s
       and (_,h) = Graphics.text_size s
       in
          Graphics.draw_char s.[0] ;
          for i=1 to l-1 do
            let (_,b) = Graphics.current_point()
            in Graphics.moveto xi (b-h) ;
               Graphics.draw_char s.[i]
           done ;
         let (a,_) = Graphics.current_point() in Graphics.moveto a yi ;;
    val draw_string_v : string -> unit = <fun>

Cette fonction modifie le point courant. Après l'affichage celui-ci se retrouve à la position initiale décalée de la largeur d'un caractère.
Le programme suivant permet d'afficher une légende autour des axes (figure 5.3)

    #
     Graphics.moveto 0 150   ; Graphics.lineto 300 150 ;
     Graphics.moveto 2 130   ; Graphics.draw_string "abscisses" ;
     Graphics.moveto 150 0   ; Graphics.lineto 150 300 ;
     Graphics.moveto 135 280 ; draw_string_v "ordonnées" ;;
    - : unit = ()

> Figure 5.3 : Légende autour des axes

Si on souhaite réaliser l'affichage vertical de texte, il faut tenir compte du fait que le point courant est modifié par la fonction `draw_string_v`. Pour cela, on définit la fonction `draw_text_v` qui prend pour paramètres l'espacement entre les colonnes et la liste de mots.

    # let draw_text_v n l =
       let f s = let (a,b) = Graphics.current_point()
                 in draw_string_v s ;
                    Graphics.moveto (a+n) b
       in List.iter f l ;;
    val draw_text_v : int -> string list -> unit = <fun>

Si l'on désire effectuer d'autres transformations, comme une rotation, sur un texte, il devient nécessaire de prendre le bitmap de chaque lettre et d'effectuer la rotation sur ces ensembles de pixels.

### Bitmaps

Un bitmap est représenté soit par une matrice de couleur (color array array), soit par une valeur du type abstrait1 image de la bibliothèque `Graphics`. Les noms et les types des fonctions de manipulation de bitmaps sont donnés à la figure 5.4.

> fonction | type
> `make_image` | color array array -> image
> `dump_image` | image -> color array array
> `draw_image` | image -> int -> int -> unit
> `get_image` | int -> int -> int -> int -> image
> `blit_image` | image -> int -> int -> unit
> `create_image` | int -> int -> image

> Figure 5.4 : Fonctions de manipulation de bitmaps

Les fonctions `make_image` et `dump_image` sont des fonctions de conversion entre le type image et le type color array array. La fonction `draw_image` affiche un bitmap à partir des coordonnées de son coin inférieur gauche.
De manière inverse, on peut capturer une partie rectangulaire de l'écran pour fabriquer une image par la fonction `get_image` en indiquant les coins inférieur gauche et supérieur droit de la zone à capturer. La fonction `blit_image` modifie son premier paramètre (de type image) en capturant la région dont le coin inférieur gauche est le point passé en paramètre. La taille de la région capturée est celle de l'image passée en argument. La fonction `create_image` permet d'initialiser des images en spécifiant leur taille pour éventuellement les utiliser avec `blit_image`.
La couleur prédéfinie `transp` est utilisée pour créer des points transparents dans une image. Cela permet d'afficher une image seulement sur une partie d'un rectangle ; les points transparents ne modifient pas l'écran initial.

##### Polarisation de Jussieu

Cet exemple inverse la couleur des points d'un bitmap. Pour ce faire on utilise la fonction d'inversion des couleurs présentée page ?? en l'appliquant à chaque pixel d'un bitmap.

    # let inv_image i =
       let inv_vec = Array.map (fun c -> inv_color c) in
       let inv_mat = Array.map inv_vec  in
       let matrice_inversee = inv_mat (Graphics.dump_image i) in
       Graphics.make_image matrice_inversee ;;
    val inv_image : Graphics.image -> Graphics.image = <fun>

Soit le bitmap `jussieu` affiché sur la partie gauche de la figure 5.5, en utilisant la fonction `inv_image` on obtient un nouveau bitmap <<solarisé>>, comme affiché dans la partie droite de la même figure.

    # let f_jussieu2 () = inv_image jussieu1;;
    val f_jussieu2 : unit -> Graphics.image = <fun>

> Figure 5.5 : Négation de Jussieu

### Exemple : tracé de boîtes en relief

On se propose dans cet exemple de définir quelques outils d'affichage pour le rendu du relief de boîtes. Une boîte est un objet générique qui servira dans de nombreux cas d'affichage. Une boîte est inscrite dans un rectangle caractérisé par un point d'origine, une hauteur et une largeur.

Pour donner une impression de relief à une boîte, il suffit de dessiner autour deux trapèzes de couleurs plus claires et deux autres de couleurs plus sombres.

En inversant les couleurs on peut donner l'impression que les boîtes sont pleines ou creuses.

##### Implantation

On ajoute aux caractéristiques d'une boîte, la largeur de sa bordure, son mode d'affichage (plein, creux ou plat), ainsi que les couleurs de ses bords et de son fond. Ces informations sont rassemblées dans un enregistrement.

    # type relief = Top | Bot | Flat ;;
    # type box_config =
      { x:int; y:int; w:int; h:int; bw:int; mutable r:relief ;
        b1_col : Graphics.color ;
        b2_col : Graphics.color ;
        b_col : Graphics.color} ;;

Seul le champ `r` peut être modifié. On utilise la fonction `draw_rect`, définie page ??, qui dessine un rectangle.
Par commodité, on se donne une fonction d'affichage du contour d'une boîte.

    # let draw_box_outline bcf col =
      Graphics.set_color col ;
      draw_rect bcf.x bcf.y bcf.w  bcf.h ;;
    val draw_box_outline : box_config -> Graphics.color -> unit = <fun>

La fonction d'affichage d'une boîte se décompose en trois parties : le dessin du premier bord, le dessin du deuxième bord et le dessin de l'intérieur de la boîte.

    # let draw_box bcf =
       let x1 = bcf.x and y1 = bcf.y in
       let x2 = x1+bcf.w and y2 = y1+bcf.h in
       let ix1 = x1+bcf.bw and ix2 = x2-bcf.bw
       and iy1 = y1+bcf.bw and iy2 = y2-bcf.bw in
       let border1 g =
         Graphics.set_color g;
         Graphics.fill_poly
           [| (x1,y1);(ix1,iy1);(ix2,iy1);(ix2,iy2);(x2,y2);(x2,y1) |]
       in
       let border2 g =
         Graphics.set_color g;
         Graphics.fill_poly
           [| (x1,y1);(ix1,iy1);(ix1,iy2);(ix2,iy2);(x2,y2);(x1,y2) |]
       in
       Graphics.set_color bcf.b_col;
       ( match bcf.r with
             Top  ->
               Graphics.fill_rect ix1 iy1 (ix2-ix1) (iy2-iy1) ;
               border1 bcf.b1_col ;
               border2 bcf.b2_col
           | Bot  ->
               Graphics.fill_rect ix1 iy1 (ix2-ix1) (iy2-iy1) ;
               border1 bcf.b2_col ;
               border2 bcf.b1_col
           | Flat ->
               Graphics.fill_rect x1 y1 bcf.w bcf.h ) ;
       draw_box_outline bcf Graphics.black ;;
    val draw_box : box_config -> unit = <fun>

Le contour des boîtes est surligné en noir. L'effacement d'une boîte remplit sa zone d'affichage de la couleur du fond.

    # let erase_box bcf =
        Graphics.set_color bcf.b_col ;
        Graphics.fill_rect (bcf.x+bcf.bw) (bcf.y+bcf.bw)
                           (bcf.w-(2*bcf.bw)) (bcf.h-(2*bcf.bw)) ;;
    val erase_box : box_config -> unit = <fun>

On définit enfin une fonction d'affichage d'une chaîne de caractères positionnée à gauche, à droite ou au centre de la boîte. On utilise le type position pour décrire le type de placement de la chaîne dans une boîte.

    # type position = Left | Center | Right ;;
    type position = | Left | Center | Right
    # let draw_string_in_box pos str bcf col =
         let (w, h) = Graphics.text_size str in
         let ty = bcf.y + (bcf.h-h)/2 in
         ( match pos with
               Center -> Graphics.moveto (bcf.x + (bcf.w-w)/2) ty
             | Right  -> let tx = bcf.x + bcf.w - w - bcf.bw - 1 in
                         Graphics.moveto tx ty
             | Left   -> let tx = bcf.x + bcf.bw + 1 in Graphics.moveto tx ty  ) ;
         Graphics.set_color col ;
         Graphics.draw_string str ;;
    val draw_string_in_box :
      position -> string -> box_config -> Graphics.color -> unit = <fun>

##### Exemple : tracé d'un jeu

On illustre l'utilisation des boîtes en affichant la position d'un jeu de type << morpion >> comme à la figure 5.6. Pour simplifier les constructions des boîtes, on se donne des couleurs prédéfinies.

    # let set_grey x =  (Graphics.rgb x x x) ;;
    val set_grey : int -> Graphics.color = <fun>
    # let grey1= set_grey 100 and grey2= set_grey 170 and grey3= set_grey 240 ;;
    val grey1 : Graphics.color = 6579300
    val grey2 : Graphics.color = 11184810
    val grey3 : Graphics.color = 15790320

On définit alors une fonction de construction d'une grille de boîtes de même taille.

    # let rec create_grid nb_col n sep b  =
       if n < 0 then []
       else
         let px = n mod nb_col and py = n / nb_col in
         let nx = b.x +sep + px*(b.w+sep)
         and ny = b.y +sep + py*(b.h+sep) in
         let b1 = {b with x=nx; y=ny} in
         b1::(create_grid nb_col (n-1) sep b) ;;
    val create_grid : int -> int -> int -> box_config -> box_config list = <fun>

Et on construit le vecteur des boîtes :

    # let vb =
       let b =  {x=0; y=0; w=20;h=20; bw=2;
                 b1_col=grey1; b2_col=grey3; b_col=grey2; r=Top} in
       Array.of_list (create_grid 5 24 2  b) ;;
    val vb : box_config array =
      [|{x=90; y=90; w=20; h=20; bw=2; r=Top; b1_col=6579300; b2_col=15790320;
         b_col=11184810};
        {x=68; y=90; w=20; h=20; bw=2; r=Top; b1_col=6579300; b2_col=15790320;
         b_col=...};
        ...|]

La figure 5.6 correspond aux appels suivants :

    # Array.iter draw_box vb ;
     draw_string_in_box Center "X" vb.(5) Graphics.black ;
     draw_string_in_box Center "X" vb.(8) Graphics.black ;
     draw_string_in_box Center "O" vb.(12) Graphics.yellow ;
     draw_string_in_box Center "O" vb.(11) Graphics.yellow ;;
    - : unit = ()

> Figure 5.6 : Affichage de boîtes avec texte