Cours 4

Htmligure
Version & licenses
Creative Commons License

Exemple complet

Guyslain
Thursday, 21 July 2016

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 :

  1. #use "topfind";;
  2. #require "gg";;
  3. #require "graphics";;

  4. let () =
  5. Graphics.open_graph "";
  6. 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) :

  1. type shape = Gg.v2 -> bool

Puis on pourrait donner des fonctions pour combiner des formes :

  1. let union shape1 shape2 =
  2. fun point -> shape1 point || shape2 point

  3. let intersection shape1 shape2 =
  4. 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.).

  1. module Shape =
  2. struct

  3. type t =
  4. | Arbitrary of (Gg.p2 -> bool)
  5. | Intersection of t * t
  6. | Union of t * t
  7. | SymDiff of t * t
  8. | Transform of (Gg.p2 -> Gg.p2) * t

  9. let empty = Arbitrary (fun _ -> false)

  10. let circle center radius =
  11. Arbitrary (fun point -> Gg.V2.(norm2 (point - center)) <= radius *. radius)

  12. let rectangle sw_point ne_point =
  13. Arbitrary (fun point ->
  14. Gg.P2.(
  15. x sw_point <= x point && x point <= x ne_point
  16. && y sw_point <= y point && y point <= y ne_point
  17. )
  18. )


  19. let rec has_point ~shape p =
  20. match shape with
  21. | Arbitrary condition -> condition p
  22. | Intersection (shape1, shape2) ->
  23. has_point ~shape:shape1 p && has_point ~shape:shape2 p
  24. | Union (shape1, shape2) ->
  25. has_point ~shape:shape1 p || has_point ~shape:shape2 p
  26. | SymDiff (shape1, shape2) ->
  27. has_point ~shape:shape1 p <> has_point ~shape:shape2 p
  28. | Transform (fct,shape) ->
  29. 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.

  1. let translate ~dir shape =
  2. let move point = Gg.V2.(point - dir) in
  3. Transform (move,shape)

  4. let rot angle point = Gg.(V2.ltr (M2.rot2 angle) point)

  5. let rotate ~theta ~center shape =
  6. let move point =
  7. Gg.V2.( point - center
  8. |> rot (-. theta)
  9. |> (+) center
  10. )
  11. in
  12. Transform (move, shape)

  13. end

On peut maintenant créer quelques formes :

  1. (* Some well-known List functions, always useful *)
  2. let rec range a b =
  3. if a > b then []
  4. else a :: range (a+1) b

  5. let (>>=) list fct = list |> List.map fct |> List.concat

  6. (* Some shapes *)
  7. let double_disc =
  8. let open Shape in
  9. SymDiff
  10. (circle (Gg.V2.v (-100.) 0.) 200.,
  11. circle (Gg.V2.v 100. 0.) 200.
  12. )

  13. let grid =
  14. let open Shape in
  15. let lines =
  16. range (-6) 6 >>= fun i ->
  17. let x = 40. *. float i in
  18. [ rectangle (Gg.V2.v (x -. 3.) (-300.)) (Gg.V2.v (x +. 3.) 300.);
  19. rectangle (Gg.V2.v (-300.) (x -. 3.)) (Gg.V2.v 300. (x +. 3.));
  20. ]
  21. in
  22. List.fold_left
  23. (fun shape rect -> Union (rect, shape))
  24. empty
  25. lines

Pour tester, on écrit une petite fonction d'affichage.

  1. module DrawShape = struct
  2. let draw_pixel ~shape x y =
  3. let point = Gg.V2.v (x - 250 |> float) (y - 250 |> float) in
  4. if Shape.has_point ~shape point then Graphics.(set_color black)
  5. else Graphics.(set_color white);
  6. Graphics.plot x y

  7. let draw_window ~shape =
  8. ignore begin
  9. range 0 499 |> List.map @@ fun x ->
  10. range 0 499 |> List.map @@ fun y ->
  11. draw_pixel ~shape x y
  12. end
  13. end

On peut alors tester :

  1. let () = DrawShape.draw_window ~shape:double_disc
La différence symétrique de deux disques.
La grille.

Notre représentation est suffisamment générique pour construire des formes variées et des transformations quelconques, par exemple :

  1. let winding = 1.5 *. Gg.Float.pi

  2. let maelstrom_move ?(radius=100.) ?(winding=winding) ~at point =
  3. let dist = Gg.V2.(norm (point - at)) in
  4. let theta = winding *. exp ( -. ((dist /. radius ) ** 2.)) in
  5. Gg.V2.( at + Shape.rot (-. theta) (point - at))

  6. let maelstrom ~at shape =
  7. Shape.Transform (maelstrom_move ~at, shape)

  8. let () = DrawShape.draw_window ~shape:(maelstrom ~at:Gg.V2.zero grid)
La grille en tourbillon.

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 :

  1. module Image =
  2. struct
  3. type t =
  4. | Basic of (Shape.t * Graphics.color)
  5. | Glue of (t * t)
  6. | Transform of (Gg.v2 -> Gg.v2) * t

  7. let create ~shape ~color = Basic (shape, color)
  8. let blend foreground background = Glue (foreground, background)

  9. let translate ~dir image =
  10. let move point = Gg.V2.(point - dir) in
  11. Transform (move, image)

  12. let rotate ~theta ~center shape =
  13. let move point =
  14. Gg.V2.( point - center
  15. |> Shape.rot (-. theta)
  16. |> (+) center
  17. )
  18. in
  19. Transform (move, shape)

  20. let rec color_of_point ~img point =
  21. match img with
  22. | Basic (shape, color) when Shape.has_point ~shape point -> Some color
  23. | Basic (shape, color) -> None
  24. | Glue (img1, img2) ->
  25. let color1 = color_of_point ~img:img1 point in
  26. if color1 = None then color_of_point ~img:img2 point
  27. else color1
  28. | Transform (f, img) -> color_of_point ~img (f point)

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


  1. module DrawImage = struct
  2. let draw_pixel ~img x y =
  3. let point = Gg.V2.v (x - 250 |> float) (y - 250 |> float) in
  4. let color =
  5. match Image.color_of_point ~img point with
  6. | None -> Graphics.white
  7. | Some color -> color
  8. in
  9. Graphics.set_color color;
  10. Graphics.plot x y

  11. let draw_window ~img =
  12. ignore begin
  13. range 0 499 |> List.map @@ fun x ->
  14. range 0 499 |> List.map @@ fun y ->
  15. draw_pixel ~img x y
  16. end
  17. end

  18. let random_color () =
  19. let r = Random.int 256 in
  20. let g = Random.int 256 in
  21. let b = Random.int 256 in
  22. Graphics.rgb r g b

  23. let discs =
  24. range (-7) 7 >>= fun i ->
  25. range (-7) 7 >>= fun j ->
  26. let x = float i *. 40. +. 20. in
  27. let y = float j *. 40. +. 20. in
  28. [ Image.create
  29. ~shape:(Shape.circle (Gg.V2.v x y) 15.)
  30. ~color:(random_color ())
  31. ]


  32. let psychegrid =
  33. Image.create ~shape:grid ~color:Graphics.black
  34. |> List.fold_right Image.blend discs
  35. |> Image.transform ~by:(maelstrom_move ~at:Gg.V2.zero)

  36. let () = DrawImage.draw_window ~img:psychegrid
La grille avec des disques colorés et un tourbillon.

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.