The code of this tutorial has been tested against Eliom 5.
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.
This tutorial is a good starting point if you want a step-by-step introduction to Eliom programming.
To get started, we recommend using Eliom's distillery, a program which creates scaffolds for Eliom projects. The following command creates a very simple project called graffiti in the directory graffiti:
$ eliom-distillery -name graffiti -template basic.ppx -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 all we wanted to do was return HTML5. But we actually want a service that corresponds to a full Eliom application with client and server parts. To do so, we need to create our own registration module by using the functor Eliom_registration.App:
module Graffiti_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 = Graffiti_app.register_service ~path:[""] ~get_params:Eliom_parameter.unit (fun () () -> Lwt.return (html (head (title (pcdata "Graffiti")) ) (body [h1 [pcdata "Graffiti"]]) ) )
let%client _ = Eliom_lib.alert "Hello!"
After running again make test.byte, and visiting http://localhost:8080/, the browser will load the file graffiti.js, and open an alert-box.
Accessing server side variables on client side code
The client side process is not strictly separated from the server side. We can access some server variables from the client code. For instance:
let count = ref 0 let main_service = Graffiti_app.register_service ~path:[""] ~get_params:Eliom_parameter.unit (fun () () -> let c = incr count; !count in ignore [%client (Dom_html.window##alert (Js.string (Printf.sprintf "You came %i times to this page" ~%c)) : unit) ]; 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 initializes the value inside [%client ... ] , and thus triggers an alert window. More specifically, the variable c, in the scope of the client value on the server is made available to 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, 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)
let%shared width = 700 let%shared height = 400
let%client draw ctx ((r, g, b), size, (x1, y1), (x2, y2)) = let color = CSS.Color.string_of_t (CSS.Color.rgb r g b) in 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]))
let%client 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 ((0, 0, 0), 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 _ = [%client (init_client () : unit) ] in Lwt.return (page ()))
Single user drawing application
Replace the init_client of the previous example by the following piece of code, then compile and draw!
let%client 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; ((0, 0, 0), 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 implement the event-handling loop. They can be read as follows: 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 that terminates:
- 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 (as RGB values) and coordinates of drawn lines.
[%%shared type messages = ((int * int * int) * 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 the ppx_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 basic types, or other types annotated with [@@deriving json].
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 [%derive.json: messages]
To write draw commands 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 the colorpicker we used a widget available in Ocsigen-widgets.
To install Ocsigen widgets, do:
opam pin add ocsigen-widgets https://github.com/ocsigen/ocsigen-widgets.git opam install ocsigen-widgets
In Makefile.options, created by Eliom's distillery, add ocsigen-widgets.client to the CLIENT_PACKAGES:
CLIENT_PACKAGES := ... ocsigen-widgets.client
To create the widget, we add the following code in the init_client immediately after canvas configuration:
(* Color of the brush *) let colorpicker = Ow_color_picker.create ~width:150 () in Ow_color_picker.append_at (Dom_html.document##.body) colorpicker; Ow_color_picker.init_handler colorpicker;
We subsequently add a simple HTML5 slider to change the size of the brush. Near the canvas_elt definition, simply add the following code:
let slider = Eliom_content.Html5.D.Form.input ~a:[ Html5.D.a_id "slider"; Html5.D.a_input_min 1.; Html5.D.a_input_max 80. ] ~input_type:`Range Html5.D.Form.int
Form.int is a typing information telling that this input takes an integer value. This kind of input can only be associated to services taking an integer as parameter.
We then add the slider to the page body, as follows:
let page = (html (head (title (pcdata "Graffiti")) ) (body [h1 [pcdata "Graffiti"]; canvas_elt; div [slider]] ))
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 rgb = Ow_color_picker.get_rgb colorpicker in let size_slider = Eliom_content.Html5.To_dom.of_input ~%slider in let size = int_of_string (Js.to_string size_slider##.value) in (rgb, size, (oldx, oldy), (!x, !y))
Finally, we need to add a stylesheet in the headers of our page. To easily create the head HTML element, we use the function Eliom_tools.F.head:
let page = html (Eliom_tools.F.head ~title:"Graffiti" ~css:[ ["css";"graffiti.css"];] ~js: ()) (body [h1 [pcdata "Graffiti"]; canvas_elt; div [slider]])
You need to install the corresponding stylesheets and images into your project. The stylesheet files should go to the directory static/css:
- the graffiti.css is a custom-made CSS file.
You can then 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 (it is available as cairo2 via OPAM). 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 draw_server, image_string = let surface = Cairo.Image.create Cairo.Image.ARGB32 ~width ~height in let ctx = Cairo.create surface in let rgb_floats_from_ints (r, g, b) = float r /. 255., float g /. 255., float b /. 255. in ((fun (rgb, 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_floats_from_ints rgb 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 ; ), (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 sends 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);
You are then 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.
- Add a button that allows download the current image, and saving 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).