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
/
6
5
5
3
6
and
g
=
c
/
2
5
6
mod
2
5
6
and
b
=
c
mod
2
5
6
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
(2
5
5
-
r)
(2
5
5
-
g)
(2
5
5
-
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
.
1
4
1
5
9
2
7
;;
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
(1
4
0
,
2
0
)
6
0
.
0
1
0
1
0
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
1
5
0
;
Graphics.lineto
3
0
0
1
5
0
;
Graphics.moveto
2
1
3
0
;
Graphics.draw_string
"abscisses"
;
Graphics.moveto
1
5
0
0
;
Graphics.lineto
1
5
0
3
0
0
;
Graphics.moveto
1
3
5
2
8
0
;
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
1
0
0
and
grey2=
set_grey
1
7
0
and
grey3=
set_grey
2
4
0
;;
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=
2
0
;h=
2
0
;
bw=
2
;
b1_col=
grey1;
b2_col=
grey3;
b_col=
grey2;
r=
Top}
in
Array.of_list
(create_grid
5
2
4
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.
(1
2
)
Graphics.yellow
;
draw_string_in_box
Center
"O"
vb.
(1
1
)
Graphics.yellow
;;
- : unit = ()
Figure 5.6 : Affichage de boîtes avec texte