GUI in OCaml?
This is the second most ridiculous thing I’ve heard since the dawn of civilization—the first is inventing Node.js.
Well, obviously they had it compile to a desktop program for a few iterations of this class before, so we had all kinds of crappy serif-typeface font with awful interface design that doesn’t even match the aesthetic of Windows® 98.
Luckily we have JavaScript now, which is capable of interpreting ANYTHING. The typeface was changed to sans-serif, and now we have a <canvas> and ocaml_to_js, everything is beautiful now.
NO! The code itself is disgusting! Asking for a strict coding style while the provided code is unformatted and like a bunch of crap mixing together (a part of this attributes to the Graphics module).
It resulted in me manually reading the code and fixing a dozen of bugs, including the canvas won’t redraw unless user making an detectable movement, and the repaint function is invoked before making the change, making the canvas is always one frame behind.
Anyway, I have resolved most of the problems and updated related libraries, now we’re good to go.
Rajivelization
Original Flavor
Generate Bitmap
Unfortunately Graphics module is too incapable that we can’t put a PNG file into it. Therefore, after cutout Rajiv in Photoshop (one of the purest software in the world, $20 a month w/ student discount, instant own), I wrote a little Go program to transform the PNG file with alpha channel into a (int * int * int) option list list, where None means transparent (of course I could use some (-1, -1, -1) to represent it, tHaT’S nOt elEgaNt).
The psuedo-code is as follows:
file := open_png(png_file)
bmp := bitmap(file)
print("let rajiv : (int * int * int) option list list = [")
for j := 0...height(bmp)
print("[")
for i := 0...width(bmp)
r, g, b, a := rgba(bmp[i][j])
if a = 0 then print("None;")
else print("Some(r,g,b);")
print("];")
print("]")
Now create a new rajiv.ml file and put the printed code above into it, we’ve done generating the bitmap needed for the program.
First Attempt
The first attempt is done by a quick try of Graphics.plot method. The plot function simply draws a pixel with a given color at a certain location. By the way, that handsome photo of Mr. Rajiv was resized to 100x100px, so every time the program has to draw ten thousand pixels individually. Every time here means every movement including MouseMove. The following code is a simple implementation of the idea.
List.iteri (fun i l ->
List.iteri (fun j c ->
match c with
| None -> ()
| Some (r', g', b') ->
Graphics.set_color (Graphics.rgb r' g' b');
Graphics.plot (x - j) (y - i)
) l
) Rajiv.rajiv
This is clearly not a solution: the third Rajiv drawn made the whole interface stuck, and the user is impossible to make any move.
Alternative Method
Looking for alternatives in the documentation, I found an interesting method called Graphics.draw_image, which takes an Graphics.image and draws it at a position. With the hope that this would function well, I look for the function that would generate one, which is make_image. It takes a Graphics.color array array and turn it into an image. Notice that we also have Graphics.transp as a transparent color. This is exactly what we’re looking for!
Although in class we only learn list, it is easy to transfer them to array since we have Array.of_list. A list map call is enough:
Graphics.make_image (Array.of_list (
List.map (fun l ->
Array.of_list (
List.map (fun c ->
match c with
| None -> Graphics.transp
| Some (r', g', b') -> Graphics.rgb r' g' b'
) l
)
) Rajiv.rajiv
))
The draw_image is way faster than plot. However, every time* the program has to turn a two-dimensional list into a 2D array, which is SUPER laggy. The image will flash every time* we make a move. This is somehow still not acceptable.
Optimization
The optimization is easy through caching. We just need to store the converted image somewhere so that we don’t have to make_image every time*. It is easier said than done though, since make_image can’t be invoked at the beginning of the runtime when Graphics module are not initialized yet.
Luckily values in OCaml are not fully immutable. We then can create a image option ref and initialize it to be ref None first. For the first time we just generate the image from bitmap during runtime, and every time* after just take from the cache, and we’re good to go. The problem then is solved, the Rajivs are drawn with extreme smooth.
Chromajiv
Rajiv needs to be colorful, just as our lives. My idea was to use color sliders as an offset of the photo’s original color, specifically \(RGB_{new} = \left(RGB_{old} + RGB_{offset}\right) \mod 256\).
We can’t perform the exact optimization this time since color changes, and we only stored one image (w/ original color) for Rajiv. However, we can use a database-like structure to store a key-value pair, where key is the offset color, and value is the image with the offset color. The best thing for database is for sure a map.
OCaml provided a Map.Make function for us to make a map with custom comparable key type and generic value type. We could, of course, take key as an int tuple, but that would be less efficient. With a second look, Graphics.color is an alias of int, where the color is stored in 0xRRGGBB way. This is a classical example of bit compression. After knowing that, we can get ourselves a beautiful IntMap.
Every time* we just check whether the map have the color we want. If no, then make_image with the new offset and store it into the map; otherwise just use the cached image. Now, we have the flawless, aesthetic
chromajivelization.
Implementation
module IntMap = Map.Make(struct type t = int let compare = compare end)
let rajiv_image = ref IntMap.empty
let draw_rajiv (g: gctx) (cd: color) (p: position) : unit =
let (x, y) = ocaml_coords g p in
let c' = Graphics.rgb cd.r cd.g cd.b in
if not (IntMap.mem c' !rajiv_image) then
rajiv_image := IntMap.add c' (Graphics.make_image (Array.of_list (
List.map (fun l ->
Array.of_list (
List.map (fun c ->
match c with
| None -> Graphics.transp
| Some (r', g', b') ->
Graphics.rgb ((r' + cd.r) mod 256)
((g' + cd.g) mod 256)
((b' + cd.b) mod 256)
) l
)
) Rajiv.rajiv
))) !rajiv_image;
Graphics.draw_image (IntMap.find c' !rajiv_image) x y
Demo
Paint!
I prefer not to publish my source code on this one. However, I managed to port the Paint JS file locally and did some alteration. Now it runs on a single page without annoying pop-up. You are free to try it out and comment down below, or ask any questions via email.
