Writing a client/server Eliom application
The code of this tutorial has been tested with the 2.2 release of
-the Ocsigen bundle.
-
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.
The final code is available for download.Basics ¶
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 Lwt
-open Eliom_content.Html5.D
-open Eliom_service
-open Eliom_parameter
-open Eliom_registration.Html5
-
-let main_service =
- register_service ~path:["graff"] ~get_params:unit
- (fun () () -> return (html (head (title (pcdata "Page title")) [])
- (body [h1 [pcdata "Graffiti"]])))
The same, written with fully qualified names (without open): -
let main_service =
- Eliom_registration.Html5.register_service
- ~path:["graff"]
- ~get_params:Eliom_parameter.unit
- (fun () () ->
- Lwt.return
- (Eliom_content.Html5.D.html
- (Eliom_content.Html5.D.head (Eliom_content.Html5.D.title
- (Eliom_content.Html5.D.pcdata "")) [])
- (Eliom_content.Html5.D.body
- [Eliom_content.Html5.D.h1
- [Eliom_content.Html5.D.pcdata "Graffiti"]])))
Copy one of the two pieces of code above in a file graffiti.ml, -then compile it with the following command ; it produce a file called -graffiti.cmo. -
ocamlfind ocamlc -thread -package eliom.server -c graffiti.ml -
Download the configuration file graffiti.conf, then launch -Ocsigen server, with the following command: -
ocsigenserver -c graffiti.conf -
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 that, 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 MyAppl instead of -Eliom_registration.Html5 for registering our main service -(now at URL /): -
let main_service =
- My_app.register_service ~path:[""]
- ~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 _ = Dom_html.window##alert(Js.string "Hello")
-}}
Download the Makefile -and graffiti.conf -files. You may want to adapt the configuration file to set some paths, -especially the directory for static files (see also STATICDIR in -the Makefile). It is not necessary (and even not a good idea, for -security reasons) to put .ml and .cma/.cmo in the -static files directory! -
The default Makefile recognizes files using Eliom's syntax extension by -their extension .eliom (instead of .ml). -Put the new version of our program in a file named graffiti.eliom -and compile it by typing: -
make -
This will generate a file called graffiti.cma and another one -called graffiti.js. The latter must be placed in the static -file directory. To copy it to its rightful place, type: -
make install -
And finaly, run Ocsigen server: -
ocsigenserver -c graffiti.conf -
Your page is now available at URL http://localhost:8080/. It -should open an alert box. If not, check that the static file directory -in the configuration file is correct. -
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 count = incr count; !count in
- Eliom_service.onload {{
- Dom_html.window##alert(Js.string
- (Printf.sprintf "You came %i times to this page" %count))
- }};
- 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 the client execute the event -handler registred with Eliom_service.onload : this client side code access to the -counter using the syntax extension %count, and displays it in a -message box. -
>> -
Collaborative drawing application ¶
Drawing on a canvas
We now want to draw something on the page using an HTML5 canvas. As a -first-step, we define a client-side function called draw that -draw a line between two given points in a canvas and we call this -function once in the onload event handler to draw an orange line. Here is the (full) new version -of the program: -
module My_app =
- Eliom_registration.App (
- struct
- let application_name = "graffiti"
- end)
-
-{shared{
- open Eliom_content
- open Eliom_content.Html5.D
- let width = 700
- let height = 400
-}}
-
-{client{
- open Eliom_content
- 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] ) )
-
-let onload_handler = {{
- let canvas = 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 () () ->
- Eliom_service.onload onload_handler;
- 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 Js_of_ocaml.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. Another idea is to use the expressivity allowed by the -functional part of OCaml to hide the complexity behind a nice -combinator library and dramaticaly reduce the code size. In this -tutorial, we will use the experimental Event_arrows module from the -Js_of_ocaml's library. -
Replace the onload_handler of the previous example by the -following piece of code, then compile and draw ! -
let onload_handler = {{
-
- let canvas = 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) in
-
- let open Event_arrows in
- ignore (run (mousedowns canvas
- (arr (fun ev -> set_coord ev; line ev) >>>
- first [mousemoves Dom_html.document (arr line);
- mouseup Dom_html.document >>> (arr line)])) ())
-}}
We use two references x and y to record the last mouse -position. The function set_coord updates these 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 implements 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's bus to broadcast draw orders to each 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 of the onload_handler by: -
let line ev =
- let v = compute_line ev in
- let _ = Eliom_bus.write %bus v in
- draw ctx v in
Finally, to interpret the draw orders read on the bus, we add the -following line in the onload_handler just before the "arrow -event handler". -
let _ = Lwt_stream.iter (draw ctx) (Eliom_bus.stream %bus) in
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. -
For using OClosure, you have to make sure that it is installed. If you are -using the bundle, it should have been configured with the option –enable-oclosure. -Cf. the corresponding bundle documentation. -
To create the widgets, we add the following code in the -onload_handler 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 of the onload_handler -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 JS file in the -headers of our page: -
let page =
- html
- (head
- (title (pcdata "Graffiti"))
- [ css_link
- ~uri:(make_uri (Eliom_service.static_dir ())
- ["css";"common.css"]) ();
- css_link
- ~uri:(make_uri (Eliom_service.static_dir ())
- ["css";"hsvpalette.css"]) ();
- css_link
- ~uri:(make_uri (Eliom_service.static_dir ())
- ["css";"slider.css"]) ();
- css_link
- ~uri:(make_uri (Eliom_service.static_dir ())
- ["css";"graffiti.css"]) ();
- js_script
- ~uri:(make_uri (Eliom_service.static_dir ())
- ["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 {{{common.css}}} -and {{{hsvpalette.css}}} -are taken from the Google Closure library; -
- the {{{slider.css}}} -and {{{graffiti.css}}} are -home-made css; and -
the following image should go into static/images: -
- the {{{hsv-sprite-sm.png}}} -
Finally, the graffiti_oclosure.js script is generated according to the -graffiti.js with the following command: -
oclosure_req graffiti.js -
Using the example {{{Makefile}}} all -those static files are generated and installed by the install -rule in the configured static directory (see the STATICDIR -variable). -
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, make sure that it is installed. If you are using the bundle, -it should have been configured with the option –enable-cairo. -
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 draw_server, image_string =
- let surface =
- Cairo.image_surface_create Cairo.FORMAT_ARGB32 ~width ~height
- in
- 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.LINE_JOIN_ROUND ;
- Cairo.set_line_cap ctx Cairo.LINE_CAP_ROUND ;
- let red, green, blue = rgb_from_string color in
- Cairo.set_source_rgb ctx ~red ~green ~blue ;
-
- Cairo.move_to ctx (float x1) (float y1) ;
- Cairo.line_to ctx (float x2) (float y2) ;
- Cairo.close_path ctx ;
-
- (* Apply the ink *)
- Cairo.stroke ctx ;
- ),
- (fun () ->
- let b = Buffer.create 10000 in
- (* Output a PNG in a string *)
- Cairo_png.surface_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 =
- 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);
This new version of the graffiti.cma module now depends on -cairo. We must ask ocsigenserver to load cairo -before loading graffiti.cma. This is done by adding the -following line in the configuration file. -
<extension findlib-package="cairo" /> -
The first version of the program is now complete.
Download the code.Exercises
- Add an OClosure button to make possible to download the current -image and save it to the hard disk (reuse the service -imageservice). -
- Add an OClosure 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).