Writing a client/server Eliom application

The code of this tutorial has been tested against Eliom 4.

In this chapter, we will write a collaborative drawing application. It is a client/server web application displaying an area where users can draw using the mouse, and see what other users are drawing at the same time and in real-time.

The final eliom code is available for download.

Basics

To get started, we recommend to use Eliom's distillery, a program which creates scaffolds for Eliom projects. The following command creates a very simplicistic project called "graffiti" in the directory "graffiti" (adapt it to your needs!):

$ eliom-distillery -name graffiti -template basic -target-directory graffiti

My first page

Our web application consists of a single page for now. Let's start by creating a very basic page. We define the service that will implement this page by the following declaration:

open Eliom_content.Html5.D (* provides functions to create HTML nodes *)

let main_service =
  Eliom_registration.Html5.register_service
    ~path:["graff"]
    ~get_params:Eliom_parameter.unit
    (fun () () ->
      Lwt.return
        (html
           (head (title (pcdata "Page title")) [])
           (body [h1 [pcdata "Graffiti"]])))

If you're using eliom-distillery just replace the content of the eliom-file by the above lines and run

$ make test.byte

This will compile your application and run ocsigenserver on it. (Refer to the manual on how to compile your project "by hand".)

Your page is now available at URL http://localhost:8080/graff.

Execute parts of the program on the client

To create our first service, we used the function Eliom_registration.​Html5.​register_service, as we wanted to return HTML5. But actually we want our service to send an Eliom application. To do so, we will create our own registration module by using the functor Eliom_registration.App:

module My_app =
  Eliom_registration.App (struct
      let application_name = "graffiti"
    end)

It is now possible to use My_app for registering our main service (now at URL /):

let main_service =
  My_app.register_service
    ~path:["graff"]
    ~get_params:Eliom_parameter.unit
    (fun () () ->
      Lwt.return
        (html
           (head (title (pcdata "Graffiti")) [])
           (body [h1 [pcdata "Graffiti"]]) ) )

We now want to add some OCaml code to be executed by the browser. For that purpose, Eliom provides a syntax extension to distinguish between server and client code in the same file. We start by a very basic program, that will display a message to the user by calling the JavaScript function alert. Add the following lines to the program,

{client{
  let _ = Eliom_lib.alert "Hello!"
}}

After running again make test.byte, and visiting http://localhost:8080/graff, the browser will now load the file graffiti.js, and open an alert-box accordingly.

Accessing server side variables on client side code

The client side process is not really separated from the server side, we can access some server variables from client code. For instance:

let count = ref 0

let main_service =
  My_app.register_service
     ~path:[""]
     ~get_params:Eliom_parameter.unit
    (fun () () ->
      let c = incr count; !count in
      ignore {unit{
        Dom_html.window##alert(Js.string
	  (Printf.sprintf "You came %i times to this page" %c))
      }};
      Lwt.return
        (html
           (head (title (pcdata "Graffiti")) [])
           (body [h1 [pcdata "Graffiti"]]) ) )

Here, we are increasing the reference count each time the page is accessed. When the page is loaded and the document is in-place, the client program initialize the value inside {unit{ ... }} and thus triggers an alert window. More specifically, the variable c , in the scope of the client value on the server is made available in the client value using the syntax extension %c. In doing so, the server side value c is displayed in a message box on the client.

Collaborative drawing application

Drawing on a canvas

We now want to draw something on the page using an HTML5 canvas. The drawing primitive is defined in the client-side function called draw that just draws a line between two given points in a canvas.

To start our collaborative drawing application, we define another client-side function init_client , which just draws a single, orange line for now.

Here is the (full) new version of the program:

{shared{
  (* Modules opened in the shared-section are available in client-
     and server-code *)
  open Eliom_content.Html5.D
  open Lwt
}}

module My_app =
  Eliom_registration.App (
    struct
      let application_name = "graffiti"
    end)

{shared{
  let width = 700
  let height = 400
}}

{client{
  let draw ctx (color, size, (x1, y1), (x2, y2)) =
    ctx##strokeStyle <- (Js.string color);
    ctx##lineWidth <- float size;
    ctx##beginPath();
    ctx##moveTo(float x1, float y1);
    ctx##lineTo(float x2, float y2);
    ctx##stroke()
}}

let canvas_elt =
  canvas ~a:[a_width width; a_height height]
    [pcdata "your browser doesn't support canvas"]

let page =
  (html
    (head (title (pcdata "Graffiti")) [])
    (body [h1 [pcdata "Graffiti"];
           canvas_elt] ) )
{client{
  let init_client () =
    let canvas = Eliom_content.Html5.To_dom.of_canvas %canvas_elt in
    let ctx = canvas##getContext (Dom_html._2d_) in
    ctx##lineCap <- Js.string "round";
    draw ctx ("#ffaa33", 12, (10, 10), (200, 100))
}}

let main_service =
  My_app.register_service ~path:[""] ~get_params:Eliom_parameter.unit
    (fun () () ->
      (* Cf. the box "Client side side-effects on the server" *)
      let _ = {unit{ init_client () }} in
      Lwt.return page)

Single user drawing application

We now want to catch mouse events to draw lines with the mouse like with the brush tools of any classical drawing application. One solution would be to mimic classical JavaScript code in OCaml ; for example by using the function Dom_events.​listen that is the Js_of_ocaml's equivalent of addEventListener. However, this solution is at least as much verbose than the JavaScript equivalent, hence not satisfactory. Js_of_ocaml's library provides a much easier way to do that, together with Lwt.

Replace the init_client of the previous example by the following piece of code, then compile and draw!

{client{
let init_client () =

  let canvas = Eliom_content.Html5.To_dom.of_canvas %canvas_elt in
  let ctx = canvas##getContext (Dom_html._2d_) in
  ctx##lineCap <- Js.string "round";

  let x = ref 0 and y = ref 0 in

  let set_coord ev =
    let x0, y0 = Dom_html.elementClientPosition canvas in
    x := ev##clientX - x0; y := ev##clientY - y0
  in

  let compute_line ev =
    let oldx = !x and oldy = !y in
    set_coord ev;
    ("#ff9933", 5, (oldx, oldy), (!x, !y))
  in

  let line ev = draw ctx (compute_line ev); Lwt.return () in

  Lwt.async
    (fun () ->
      let open Lwt_js_events in
      mousedowns canvas
        (fun ev _ ->
          set_coord ev; line ev >>= fun () ->
          Lwt.pick [mousemoves Dom_html.document (fun x _ -> line x);
		    mouseup Dom_html.document >>= line]))
}}

We use two references x and y to record the last mouse position. The function set_coord updates those references from mouse event data. The function compute_line computes the coordinates of a line from the initial (old) coordinates to the new coordinates–the event data sent as a parameter.

The last four lines of code, that implement the event handling loop, could be read as: for each mousedown event on the canvas, do set_coord then line (this will draw a dot), then behave as the first of the two following lines to terminate:

  • For each mousemove event on the document, call line (never terminates)
  • If there is a mouseup event on the document, call line.

Collaborative drawing application

In order to see what other users are drawing, we now want to do the following:

  • Send the coordinates to the server when the user draw a line, then
  • Dispatch the coordinates to all connected users.

We first declare a type, shared by the server and the client, describing the color and coordinates of drawn lines.

{shared{
  type messages = (string * int * (int * int) * (int * int))
    deriving (Json)
}}

We annotate the type declaration with deriving (Json) to allow type-safe deserialization of this type. Eliom forces you to use this in order to avoid server crashes if a client sends corrupted data. This is defined using as custom version of the Deriving syntax extension. You need to do that for each type of data sent by the client to the server. This annotation can only be added on types containing exclusively basics type or other types annotated with deriving. See the Js_of_ocaml API, for more information on the Deriving_Json module.

Then we create an Eliom bus to broadcast drawing events to all client with the function Eliom_bus.​create. This function take as parameter the type of values carried by the bus.

let bus = Eliom_bus.create Json.t<messages>

To write draw orders into the bus, we just replace the function line in init_client by:

let line ev =
  let v = compute_line ev in
  let _ = Eliom_bus.write %bus v in
  draw ctx v;
  Lwt.return () in

Finally, to interpret the draw orders read on the bus, we add the following line at the end of function init_client:

Lwt.async (fun () -> Lwt_stream.iter (draw ctx) (Eliom_bus.stream %bus))

Now you can try the program using two browser windows to see that the lines are drawn on both windows.

Color and size of the brush

In this section, we add a color picker and slider to choose the size of the brush. For that we used add two widgets provided by the OClosure widget library.

In the Makefile.options, created by Eliom's distillery, just add oclosure to the (currently empty) CLIENT_PACKAGES:

CLIENT_PACKAGES := oclosure

To create the widgets, we add the following code in the init_client immediatly after canvas configuration:

(* Size of the brush *)
let slider = jsnew Goog.Ui.slider(Js.null) in
slider##setMinimum(1.);
slider##setMaximum(80.);
slider##setValue(10.);
slider##setMoveToPointEnabled(Js._true);
slider##render(Js.some Dom_html.document##body);
(* The color palette: *)
let pSmall =
  jsnew Goog.Ui.hsvPalette(Js.null, Js.null,
                           Js.some (Js.string "goog-hsv-palette-sm"))
in
pSmall##render(Js.some Dom_html.document##body);

And to change the size and the color of the brush, we replace the last line of the function compute_line in init_client by:

let color = Js.to_string (pSmall##getColor()) in
let size = int_of_float (Js.to_float (slider##getValue())) in
(color, size, (oldx, oldy), (!x, !y))

As last step, we need to add some stylesheets and one JavaScript file in the headers of our page. To ease the creation of the head-element in HTML we use the function Eliom_tools.​F.​head:

let page =
  html
    (Eliom_tools.F.head ~title:"Graffiti"
       ~css:[
         ["css";"common.css"];
         ["css";"hsvpalette.css"];
         ["css";"slider.css"];
         ["css";"graffiti.css"];
         ["graffiti_oclosure.js"];
      ]
      ~js:[ ["graffiti_oclosure.js"] ] ())
    (body [h1 [pcdata "Graffiti"]; canvas_elt])

You need to install the corresponding stylesheets and images into your project. The stylesheet files should go in the directory static/css

the following image should go into static/images:

Finally, the graffiti_oclosure.js script must be generated. To do this, first, generate the original graffiti.js script by

$ make byte

Then use oclosure_req to generate the Oclosure-specific local/var/www/graffiti/eliom/graffiti_oclosure.js script from the original

$ oclosure_req local/var/www/graffiti/eliom/graffiti.js

Then you can test your application (make test.byte).

Sending the initial image

To finish the first part of the tutorial, we want to save the current drawing on server side and send the current image when a new user arrives. To do that, we will use the Cairo binding for OCaml.

For using Cairo, first, make sure that it is installed. If you are using the bundle, it should have been configured with the option --enable-cairo. Second, add it to the SERVER_PACKAGES in your Makefile.options:

SERVER_PACKAGES := cairo2

The draw_server function below is the equivalent of the draw function on the server side and the image_string function outputs the PNG image in a string.

let rgb_from_string color = (* color is in format "#rrggbb" *)
  let get_color i =
    (float_of_string ("0x"^(String.sub color (1+2*i) 2))) /. 255.
  in
  try get_color 0, get_color 1, get_color 2 with | _ -> 0.,0.,0.

let surface = Cairo.Image.create Cairo.Image.ARGB32 ~width ~height

let draw_server =
  let ctx = Cairo.create surface in
  (fun ((color : string), size, (x1, y1), (x2, y2)) ->

    (* Set thickness of brush *)
    Cairo.set_line_width ctx (float size) ;
    Cairo.set_line_join ctx Cairo.JOIN_ROUND ;
    Cairo.set_line_cap ctx Cairo.ROUND ;
    let r, g, b =  rgb_from_string color in
    Cairo.set_source_rgb ctx ~r ~g ~b ;

    Cairo.move_to ctx (float x1) (float y1) ;
    Cairo.line_to ctx (float x2) (float y2) ;
    Cairo.Path.close ctx ;

    (* Apply the ink *)
    Cairo.stroke ctx ;
   )

let image_string =
   (fun () ->
     let b = Buffer.create 10000 in
     (* Output a PNG in a string *)
     Cairo.PNG.write_to_stream surface (Buffer.add_string b);
     Buffer.contents b
   )

let _ = Lwt_stream.iter draw_server (Eliom_bus.stream bus)

We also define a service that send the picture:

let imageservice =
  Eliom_registration.String.register_service
    ~path:["image"]
    ~get_params:Eliom_parameter.unit
    (fun () () -> Lwt.return (image_string (), "image/png"))

We now want to load the initial image once the canvas is created. Add the following lines just between the creation of the canvas context and the creation of the slider:

(* The initial image: *)
let img =
  Eliom_content.Html5.To_dom.of_img
    (img ~alt:"canvas"
       ~src:(make_uri ~service:%imageservice ())
       ())
in
img##onload <- Dom_html.handler
                (fun ev -> ctx##drawImage(img, 0., 0.); Js._false);

After compiling the project (make byte and the call to oclosure_req), you are ready to try your graffiti-application by make test.byte.

Note, that the Makefile from the distillery automatically adds the packages defined in SERVER_PACKAGES as an extension in your configuration file local/etc/graffiti/graffiti-test.conf:

<extension findlib-package="cairo2" />

The first version of the program is now complete. You may want now to install the application by running

$ make all
$ sudo make install

Download the code.

Exercises

  • Add a button to make possible to download the current image and save it to the hard disk (reuse the service imageservice).
  • Add a button with a color picker to select a color from the drawing. Pressing the button changes the mouse cursor, and disables current mouse events until the next mouse click event on the document. Then the color palette changes to the color of the pixel clicked. (Use the function Dom_html.pixel_get).

prev next