The code of this tutorial has been tested against Eliom 6.7.1
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.The final eliom code is available for download.
To get started, we recommend using Eliom 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.Html.D (* provides functions to create HTML nodes *) let%server main_service = Eliom_registration.Html.create ~path:(Eliom_service.Path ["graff"]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> Lwt.return (html (head (title (txt "Page title")) ) (body [h1 [txt "Graffiti"]])))
If you're using eliom-distillery just replace the content of file graffiti.eliom by the above lines and run:
$ make test.byte
This will compile your application and run ocsigenserver.
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.Html.create, as all we wanted to do was return HTML. But we actually want a service that corresponds to a full 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" let global_data_path = None end)
It is now possible to use module Graffiti_app for registering our main service (now at URL /):
let%server main_service = Graffiti_app.create ~path:(Eliom_service.Path [""]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> Lwt.return (html (head (title (txt "Graffiti")) ) (body [h1 [txt "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:
open%shared Js_of_ocaml let%server count = ref 0 let%server main_service = Graffiti_app.create ~path:(Eliom_service.Path [""]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> let c = incr count; !count in let text = Printf.sprintf "You came %i times to this page" 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 (txt "Graffiti")) ) (body [h1 [txt @@ text c]])))
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 HTML 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:
(* Modules opened with open%shared are available in client and server-code *) open%shared Eliom_content.Html.D open%shared Eliom_content open%shared Js_of_ocaml
module%server Graffiti_app = Eliom_registration.App ( struct let application_name = "graffiti" let global_data_path = None 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%server canvas_elt = canvas ~a:[a_width width; a_height height] [txt "your browser doesn't support canvas"] let%server page () = html (head (title (txt "Graffiti")) ) (body [h1 [txt "Graffiti"]; canvas_elt])
let%client init_client () = let canvas = Eliom_content.Html.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%server main_service = Graffiti_app.create ~path:(Eliom_service.Path [""]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> (* Cf. section "Client side side-effects on the server" *) let _ = [%client (init_client () : unit) ] in Lwt.return (page ()))
Single user drawing application
To use this, add js_of_ocaml-lwt to the CLIENT_PACKAGES in Makefile.options, created by Eliom's distillery:
CLIENT_PACKAGES := ... js_of_ocaml-lwt
Then, 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.Html.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; let%lwt () = line ev in Lwt.pick [mousemoves Dom_html.document (fun x _ -> line x); let%lwt ev = mouseup Dom_html.document in line ev]))
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.
type%shared 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 a JSON plugin for ppx_deriving, which you need to install. 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%server bus = Eliom_bus.create [%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 Toolkit.
To install Ocsigen Toolkit, do:
opam install ocsigen-toolkit
In Makefile.options, created by Eliom's distillery, add ocsigen-toolkit.client and ocsigen-toolkit.server to the CLIENT_PACKAGES:
SERVER_PACKAGES := ... ocsigen-toolkit.server CLIENT_PACKAGES := ... ocsigen-toolkit.client
To create the widget, we replace page by :
let%server page () = let colorpicker, cp_sig = Ot_color_picker.make ~a:[Html.D.a_class ["colorpicker"]] in ( Html.D.html (Html.D.head (Html.D.title (Html.D.txt "Graffiti")) ) (Html.D.body [h1 [txt "Graffiti"] ; canvas_elt ; colorpicker]) , cp_sig )
Replace main_service by:
let%server main_service = Graffiti_app.create ~path:(Eliom_service.Path [""]) ~meth:(Eliom_service.Get Eliom_parameter.unit) (fun () () -> (* Cf. section "Client side side-effects on the server" *) let page, cp_sig = page () in let _ = [%client (init_client ~cp_sig:~%cp_sig () : unit) ] in Lwt.return page)
We subsequently add a simple HTML slider to change the size of the brush. Near the canvas_elt definition, simply add the following code:
let%server slider = Eliom_content.Html.D.Form.input ~a: [ Html.D.a_id "slider" ; Html.D.a_class ["slider"] ; Html.D.a_input_min (`Number 1) ; Html.D.a_input_max (`Number 80) ; Html.D.a_value "22" ] ~input_type:`Range Html.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, between the canvas and the colorpicker.
To change the size and the color of the brush, we add parameter ~cp_sig to init_client and modify function compute_line:
let%client init_client ~cp_sig () = ... let compute_line ev = let oldx = !x and oldy = !y in set_coord ev; let size_slider = Eliom_content.Html.To_dom.of_input ~%slider in let size = int_of_string (Js.to_string size_slider##.value) in let h, s, v = Eliom_shared.React.S.value cp_sig in let r, g, b = Ot_color_picker.hsv_to_rgb h s v in let rgb = int_of_float r, int_of_float g, int_of_float b in (rgb, size, (oldx, oldy), (!x, !y)) in ...
Finally, we need to add a stylesheet in the headers of our page with function Eliom_tools.D.css_link:
let%server page () = let colorpicker, cp_sig = Ot_color_picker.make ~a:[Html.D.a_class ["colorpicker"]] in ( Html.D.html (Html.D.head (Html.D.title (Html.D.txt "Graffiti")) [ Html.D.css_link ~uri: (Html.D.make_uri (Eliom_service.static_dir ()) ["css"; "graffiti.css"]) () ; Html.D.css_link ~uri: (Html.D.make_uri (Eliom_service.static_dir ()) ["css"; "ot_color_picker.css"]) () ]) (Html.D.body [canvas_elt; slider; colorpicker]) , cp_sig )
You need to install the corresponding stylesheets and images into your project. The stylesheet files should go to the directory static/css. Download file graffiti.css from here. Copy file ot_color_picker.css from directory ~/.opam/<version>/share/ocsigen-toolkit/css into static/css.
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%server draw_server, image_string = let rgb_ints_to_floats (r, g, b) = float r /. 255., float g /. 255., float b /. 255. in (* needed by cairo *) let surface = Cairo.Image.create Cairo.Image.ARGB32 ~w:width ~h:height in let ctx = Cairo.create surface in ( (fun (rgb, size, (x1, y1), (x2, y2)) -> (* Set thickness of brush *) let r, g, b = rgb_ints_to_floats rgb in Cairo.set_line_width ctx (float size); Cairo.set_line_join ctx Cairo.JOIN_ROUND; Cairo.set_line_cap ctx Cairo.ROUND; 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%server _ = Lwt_stream.iter draw_server (Eliom_bus.stream bus)
We also define a service that sends the picture:
let%server imageservice = Eliom_registration.String.create ~path:(Eliom_service.Path ["image"]) ~meth:(Eliom_service.Get 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 fater the creation of the canvas context in init_client:
(* The initial image: *) let img = Eliom_content.Html.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);
Finally, we can add a new canvas where we would draw a visualisation of the current size of the brush. The complete code of this application can be found here.
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).
If you want to continue learning client-server programming with Eliom and build your first application, we suggest to read the tutorial about Ocsigen Start.