Retour au sommaire.
Comme exemple, écrivons un petit module pour décrire des images à la façon de la librairie Vg. L'idée est de décrire des formes, une forme est une fonction qui à chaque point du plan décide si le point appartient à la forme. On peut ensuite couper des formes dans du papier coloré, puis coller les formes les unes sur les autres pour former des images. C'est un exemple typique d'interface fonctionnelle, où les objets sont des fonctions construites par assemblage de fonctions simples, à l'aide de combinateurs.
Cet exemple illustre comment on peut utiliser les types algébriques pour encoder une représentation d'un calcul, dans notre cas des fonctions de dessins d'image. Cet encodage se fait simplement en enregistrant les constructions utilisées, par des constructeurs d'un type union. Un autre aspect est l'utilisation de fonctions pour représenter certaines formes ou images.
Pour utiliser les exemples suivants dans un toplevel, il faut interpréter les lignes suivantes :
#use "topfind";;
#require "gg";;
#require "graphics";;
let () =
Graphics.open_graph "";
Graphics.rezise_window 500 500
Commençons par décrire les formes. De base, ce sont des fonctions. On pourrait directement écrire (Puisque Gg.v2 est le type des vecteurs à 2 dimensions) :
type shape = Gg.v2 -> bool
Puis on pourrait donner des fonctions pour combiner des formes :
let union shape1 shape2 =
fun point -> shape1 point || shape2 point
let intersection shape1 shape2 =
fun point -> shape1 point && shape2 point
En pratique, on préfère garder explicitement les constructions en utilisant un type somme, plus une fonction d'évaluation. Cela permet par exemple à Vg de retraduire les constructions nativement dans les différents langages de dessins vectoriels (svg, pdf, etc.).
module Shape =
struct
type t =
| Arbitrary of (Gg.p2 -> bool)
| Intersection of t * t
| Union of t * t
| SymDiff of t * t
| Transform of (Gg.p2 -> Gg.p2) * t
let empty = Arbitrary (fun _ -> false)
let circle center radius =
Arbitrary (fun point -> Gg.V2.(norm2 (point - center)) <= radius *. radius)
let rectangle sw_point ne_point =
Arbitrary (fun point ->
Gg.P2.(
x sw_point <= x point && x point <= x ne_point
&& y sw_point <= y point && y point <= y ne_point
)
)
let rec has_point ~shape p =
match shape with
| Arbitrary condition -> condition p
| Intersection (shape1, shape2) ->
has_point ~shape:shape1 p && has_point ~shape:shape2 p
| Union (shape1, shape2) ->
has_point ~shape:shape1 p || has_point ~shape:shape2 p
| SymDiff (shape1, shape2) ->
has_point ~shape:shape1 p <> has_point ~shape:shape2 p
| Transform (fct,shape) ->
has_point ~shape (fct p)
Dans Vg, le type pour décrire les formes est le type Vg.path, et son module Vg.P. Vg ne contiendrait pas les constructeurs Arbitrary et Transform, qui sont trop généraux pour être traduit vers les langages de dessins vectoriels. Il pourrait par contre avoir directement des constructeurs Rectangle ou Circle, et un constructeur pour les transformations linéaires. Nous nous permettons cette représentation très générale, car nous ne cherchons pas à obtenir des images dans ces formats, et ne nous préoccupons pas de l'inefficacité de l'algorithme de dessin.
has_point est la fonction donnant son sens aux formes : elle traduit une forme en une fonction de type Gg.p2 ->
bool. Sans surprise il s'agit d'une fonction inductive.
Transform permet d'appliquer une transformation quelconque à la forme. En pratique, Vg permet d'appliquer des transformations linéaires : symétrie, translations, rotations, homothétie,... On termine le module en ajoutant des fonctions pour le support des rotations et des translations, qui utilisent simplement le constructeur Transform avec des fonctions linéaires exprimées à l'aide de Gg.
let translate ~dir shape =
let move point = Gg.V2.(point - dir) in
Transform (move,shape)
let rot angle point = Gg.(V2.ltr (M2.rot2 angle) point)
let rotate ~theta ~center shape =
let move point =
Gg.V2.( point - center
|> rot (-. theta)
|> (+) center
)
in
Transform (move, shape)
end
On peut maintenant créer quelques formes :
let rec range a b =
if a > b then []
else a :: range (a+1) b
let (>>=) list fct = list |> List.map fct |> List.concat
let double_disc =
let open Shape in
SymDiff
(circle (Gg.V2.v (-100.) 0.) 200.,
circle (Gg.V2.v 100. 0.) 200.
)
let grid =
let open Shape in
let lines =
range (-6) 6 >>= fun i ->
let x = 40. *. float i in
[ rectangle (Gg.V2.v (x -. 3.) (-300.)) (Gg.V2.v (x +. 3.) 300.);
rectangle (Gg.V2.v (-300.) (x -. 3.)) (Gg.V2.v 300. (x +. 3.));
]
in
List.fold_left
(fun shape rect -> Union (rect, shape))
empty
lines
Pour tester, on écrit une petite fonction d'affichage.
module DrawShape = struct
let draw_pixel ~shape x y =
let point = Gg.V2.v (x - 250 |> float) (y - 250 |> float) in
if Shape.has_point ~shape point then Graphics.(set_color black)
else Graphics.(set_color white);
Graphics.plot x y
let draw_window ~shape =
ignore begin
range 0 499 |> List.map @@ fun x ->
range 0 499 |> List.map @@ fun y ->
draw_pixel ~shape x y
end
end
On peut alors tester :
let () = DrawShape.draw_window ~shape:double_disc
Notre représentation est suffisamment générique pour construire des formes variées et des transformations quelconques, par exemple :
let winding = 1.5 *. Gg.Float.pi
let maelstrom_move ?(radius=100.) ?(winding=winding) ~at point =
let dist = Gg.V2.(norm (point - at)) in
let theta = winding *. exp ( -. ((dist /. radius ) ** 2.)) in
Gg.V2.( at + Shape.rot (-. theta) (point - at))
let maelstrom ~at shape =
Shape.Transform (maelstrom_move ~at, shape)
let () = DrawShape.draw_window ~shape:(maelstrom ~at:Gg.V2.zero grid)
Nous pouvons maintenant créer des images colorées. Le modèle est de découper des formes dans du papier coloré, puis de coller ces formes les unes au-dessus des autres. On représente ceci par le type Image.t : une image est soit basique, découpée dans une seule feuille, soit composée d'un collage de deux images. La définition inductive permet ensuite de coller une quantité arbitraire d'images élémentaires :
module Image =
struct
type t =
| Basic of (Shape.t * Graphics.color)
| Glue of (t * t)
| Transform of (Gg.v2 -> Gg.v2) * t
let create ~shape ~color = Basic (shape, color)
let blend foreground background = Glue (foreground, background)
let translate ~dir image =
let move point = Gg.V2.(point - dir) in
Transform (move, image)
let rotate ~theta ~center shape =
let move point =
Gg.V2.( point - center
|> Shape.rot (-. theta)
|> (+) center
)
in
Transform (move, shape)
let rec color_of_point ~img point =
match img with
| Basic (shape, color) when Shape.has_point ~shape point -> Some color
| Basic (shape, color) -> None
| Glue (img1, img2) ->
let color1 = color_of_point ~img:img1 point in
if color1 = None then color_of_point ~img:img2 point
else color1
| Transform (f, img) -> color_of_point ~img (f point)
end
Hormis les deux fonctions de constructions, il est intéressant de noter que la fonction principale, qui attribue les couleurs aux points, est de nouveau une induction assez simple sur la représentation de nos données, les images. Dans le cas d'un collage, puisque img1 est au premier plan, on n'utilise img2 que si le point cherché n'est pas dans la forme de img1.
On modifie les fonctions de dessins des formes pour dessiner les images, puis on peut essayer :
module DrawImage = struct
let draw_pixel ~img x y =
let point = Gg.V2.v (x - 250 |> float) (y - 250 |> float) in
let color =
match Image.color_of_point ~img point with
| None -> Graphics.white
| Some color -> color
in
Graphics.set_color color;
Graphics.plot x y
let draw_window ~img =
ignore begin
range 0 499 |> List.map @@ fun x ->
range 0 499 |> List.map @@ fun y ->
draw_pixel ~img x y
end
end
let random_color () =
let r = Random.int 256 in
let g = Random.int 256 in
let b = Random.int 256 in
Graphics.rgb r g b
let discs =
range (-7) 7 >>= fun i ->
range (-7) 7 >>= fun j ->
let x = float i *. 40. +. 20. in
let y = float j *. 40. +. 20. in
[ Image.create
~shape:(Shape.circle (Gg.V2.v x y) 15.)
~color:(random_color ())
]
let psychegrid =
Image.create ~shape:grid ~color:Graphics.black
|> List.fold_right Image.blend discs
|> Image.transform ~by:(maelstrom_move ~at:Gg.V2.zero)
let () = DrawImage.draw_window ~img:psychegrid
La librairie Vg fonctionne d'une façon similaire. La principale différence, c'est que Vg utilise des constructions de bases différentes, celles supportées par les formats usuels de dessins vectoriels, et ne permet pas des transformations arbitraires (seulement des transformations linéaires). La version présentée ici est donc plus expressive, mais assez inefficace, même un dessin très simple prend du temps à être affiché.
Retour au sommaire.