tinyweb for OCaml
tinyweb’s OCaml implementation is a library of its own: a OCaml program can use it to draw on a web page, add buttons, labels and sliders to it, play sounds and answer the reader, with no Pyking anywhere. It is the file Pyking copies beside a program compiled for ocaml5.5, renamed tinyweb.ml, tinyweb.mli.
Download tinyweb-ocaml.zip: tinyweb.ml, tinyweb.mli, the example below, and the samples in tinyweb_assets/.
Running a program
The program serves its own page: web_init starts the server and prints its address, and once the program’s own code has finished it goes on serving the page and calling the functions it handed over until it is stopped (Ctrl-C). Open the address in a browser.
ocamlopt -I +unix unix.cmxa tinyweb.mli tinyweb.ml example.ml -o example./exampleThe page asks for a sample beside where the program runs first, then in tinyweb_assets/ there.
An example
(* A canvas, two circles, two samples and buttons that play them, written in OCaml against tinyweb.ml. Build it with ocamlopt -I +unix unix.cmxa tinyweb.mli tinyweb.ml example.ml -o example then run ./example and open the address it prints. *)
let () = Tinyweb.web_init 0; let kick = Tinyweb.load_sample "kick1.wav" in let snare = Tinyweb.load_sample "snare1.wav" in let canvas = Tinyweb.canvas 400 200 in Tinyweb.draw_circle canvas 100 50 30 "#ff0000"; Tinyweb.draw_circle canvas 100 50 20 "#000000"; let presses = ref 0 in ignore (Tinyweb.make_button "Play kick" (fun () -> Tinyweb.play_sample kick 0.0 1.0 1.0)); ignore (Tinyweb.make_button "Play snare" (fun () -> incr presses; Printf.printf "snare, press %d\n" !presses; Tinyweb.play_sample snare 0.0 0.5 1.0)); ignore (Tinyweb.make_button "Does nothing" (fun () -> ())); print_endline "built the page"Functions
Every function but web_init needs web_init to have been called first, or PYKING_TINYWEB_LOG to be set (below); otherwise it says so and the program ends with status 1.
web_init
val web_init : int -> unitServes the program’s page on port and prints its address, tinyweb: http://127.0.0.1:N/; port 0 picks any free port. Call it before anything else. With PYKING_TINYWEB_LOG set it does nothing.
canvas
val canvas : int -> int -> intA new canvas, width by height pixels, placed below what the page already shows. Answers the number that names it.
fill_canvas
val fill_canvas : int -> string -> unitFills the whole canvas c with color, a CSS color such as "#ff0000" or "red".
draw_circle
val draw_circle : int -> int -> int -> int -> string -> unitA filled circle on the canvas c, centred at (x, y).
draw_rect
val draw_rect : int -> int -> int -> int -> int -> string -> unitA filled rectangle on the canvas c, with its top left corner at (x, y).
draw_line
val draw_line : int -> int -> int -> int -> int -> string -> int -> unitA straight line on the canvas c, from (x1, y1) to (x2, y2), width pixels wide.
draw_text
val draw_text : int -> int -> int -> string -> string -> int -> unitwords written on the canvas c, with the top left corner of the text at (x, y), size pixels high.
make_button
val make_button : string -> (unit -> unit) -> intA button showing title, placed below what the page already shows, which calls on_click each time it is pressed. Answers the number that names it.
label
val label : string -> intA line of text, placed below what the page already shows. Answers the number that names it to set_label.
set_label
val set_label : int -> string -> unitReplaces the text of the label id with text.
make_slider
val make_slider : int -> int -> int -> (int -> unit) -> intA slider from low to high, starting at value, placed below what the page already shows, which calls on_change with each new value it is moved to. Answers the number that names it.
apply_style
val apply_style : int -> string -> string -> string -> unitChanges the page’s look. With kind “theme”, value names a theme: “light” (the default), “dark”, “plain”, “paper” or “retro”. With kind “page”, “button”, “label”, “slider” or “canvas”, property of every element of that kind becomes value; with kind “element”, only the element id’s. property is one of background, color, border, radius, font, size, padding, gap, accent and width, and value is CSS, such as “#336699” or “12px”. id is 0 except for an element.
load_sample
val load_sample : string -> intStarts loading the sound file name and answers a number naming it to play_sample. The name is looked up beside where the program runs first, and then among the samples tinyweb ships in tinyweb_assets/: kick1.wav, snare1.wav, hihat1.wav and piano_c1.wav.
play_sample
val play_sample : int -> float -> float -> float -> unitPlays the sample sample: at the time at on the page’s clock, or now if that has passed; volume times as loud, and rate times as fast, which raises its pitch as much.
time
val time : unit -> floatThe page’s clock, in seconds, as the page last reported it: inside an on_frame callback, the time the frame is for. It is the audio clock once sound has started, so a time read here is a time play_sample can be given.
on_frame
val on_frame : (float -> unit) -> unitCalls draw before each frame the page shows, with the time the frame is for.
on_mouse
val on_mouse : int -> (int -> int -> unit) -> unitCalls click with the position, in pixels from the top left corner, of every click on the canvas c.
on_key
val on_key : (string -> unit) -> unitCalls pressed with the name of every key pressed while the page has the focus: a letter or digit as itself, and otherwise the browser’s name for it, such as “ArrowLeft”, “Enter” or ” ”.
Without a page
Two environment variables, each a file path, run a program with no page at all, which is how it is tested: with PYKING_TINYWEB_LOG set, every command the program would send its page is appended to that file as one line, and once the program’s own code has finished the events PYKING_TINYWEB_EVENTS lists — click ID, slide ID VALUE, frame T, mouse ID X Y, key NAME, one per line — are delivered as if a reader had caused them.
PYKING_TINYWEB_LOG=page.log PYKING_TINYWEB_EVENTS=clicks.txt ./exampleWhat Pyking generates
Pyking’s own tinyweb tests, compiled for ocaml5.5. Each comes with the libraries it needs as Pyking writes them — tinyweb’s implementation under its own name, and the module Pyking compiles from tinyweb’s Pyking half — and is built and run with:
ocamlopt -I +unix unix.cmxa tinyweb_external.ml tinyweb.mli tinyweb.ml program.ml -o program./programlib_tinyweb_frames
# Animation kept in time with sound: each frame is drawn for the time# the page gives it, and each beat is scheduled half a second ahead on# the same clock, so it sounds where the picture says it is. The# .events file delivers four frames.
import tinyweb as tw
tw.web_init(0)
c = tw.canvas(200, 100)kick = tw.load_sample("kick1.wav")beat = 0.5var next_beat = 0.0
def draw(t: Float): tw.clear(c, "#111111") tw.circle(c, int(t * 100.0), 50, 10, "#ffcc00") if t >= next_beat: print("beat scheduled at " ++ string(next_beat + beat) ++ ", drawn at " ++ string(t)) tw.play(kick, at = next_beat + beat) next_beat := next_beat + beat
tw.on_frame(draw)print(tw.time())program.ml, as Pyking generates it
(* Generated from lib_tinyweb_frames.pyk by the Pyking compiler. Do not edit. *)(* Target: ocaml5.5. Compile with: ocamlopt <this file> or run it directly with: ocaml <this file> *)
let pyk_float_str x = if x <> x then "nan" else if x = infinity then "inf" else if x = neg_infinity then "-inf" else let negative = x < 0.0 || (x = 0.0 && 1.0 /. x < 0.0) in let sign = if negative then "-" else "" in let a = abs_float x in if a = 0.0 then sign ^ "0.0" else let rec shortest p = let s = Printf.sprintf "%.*e" p a in if p >= 16 || float_of_string s = a then s else shortest (p + 1) in let s = shortest 0 in let at = String.index s 'e' in let digits = String.concat "" (String.split_on_char '.' (String.sub s 0 at)) in let rec trim n = if n > 1 && digits.[n - 1] = '0' then trim (n - 1) else n in let digits = String.sub digits 0 (trim (String.length digits)) in let len = String.length digits in let decpt = int_of_string (String.sub s (at + 1) (String.length s - at - 1)) + 1 in let zeros n = String.make (max 0 n) '0' in if decpt <= -4 || decpt > 16 then let mantissa = if len = 1 then digits else String.sub digits 0 1 ^ "." ^ String.sub digits 1 (len - 1) in sign ^ mantissa ^ "e" ^ string_of_int (decpt - 1) else if decpt <= 0 then sign ^ "0." ^ zeros (-decpt) ^ digits else if decpt >= len then sign ^ digits ^ zeros (decpt - len) ^ ".0" else sign ^ String.sub digits 0 decpt ^ "." ^ String.sub digits decpt (len - decpt)
let pyk_truncate x = if Float.is_nan x then 0 else let t = Float.trunc x in if t < -2147483648.0 || t > 2147483647.0 then 0 else int_of_float t
let () = Tinyweb_external.web_init 0
let c = Tinyweb_external.canvas 200 100
let kick = Tinyweb_external.load_sample "kick1.wav"
let beat = 0.5
let next_beat = ref 0.0
let draw t = Tinyweb.clear c "#111111"; Tinyweb.circle c (pyk_truncate (t *. 100.0)) 50 10 "#ffcc00"; if t >= !next_beat then (print_endline ("beat scheduled at " ^ pyk_float_str (!next_beat +. beat) ^ ", drawn at " ^ pyk_float_str t); let tmp1 = Tinyweb.play__default__volume () in Tinyweb.play kick (!next_beat +. beat) tmp1 (Tinyweb.play__default__rate ()); next_beat := !next_beat +. beat)
let () = Tinyweb_external.on_frame draw; print_endline (pyk_float_str (Tinyweb_external.time ()))lib_tinyweb_input
# The mouse, the keyboard and a slider, each reported on a label, and# the other drawing functions. The .events file clicks the canvas,# presses two keys -- the second Enter -- and moves the slider.
import tinyweb as tw
tw.web_init(0)
c = tw.canvas(300, 200)tw.rect(c, 0, 0, 300, 200, "#eeeeee")tw.line(c, 0, 0, 300, 200)tw.line(c, 0, 200, 300, 0, "red", width = 3)tw.text(c, 10, 10, "click \"me\"", size = 20)status = tw.label("nothing yet")
def clicked(x: Int, y: Int): tw.set_label(status, "clicked at " ++ string(x) ++ ", " ++ string(y)) tw.circle(c, x, y, 5, "blue")
def pressed(key: String): print("key [" ++ key ++ "]") if key == "Enter": tw.play_file("snare1.wav")
def moved(value: Int): tw.set_label(status, "slider at " ++ string(value))
tw.on_mouse(c, clicked)tw.on_key(pressed)tw.slider(0, 10, 5, on_change = moved)tw.slider(1, 3, 2)program.ml, as Pyking generates it
(* Generated from lib_tinyweb_input.pyk by the Pyking compiler. Do not edit. *)(* Target: ocaml5.5. Compile with: ocamlopt <this file> or run it directly with: ocaml <this file> *)
let () = Tinyweb_external.web_init 0
let c = Tinyweb_external.canvas 300 200
let () = Tinyweb.rect c 0 0 300 200 "#eeeeee"; let tmp1 = Tinyweb.line__default__color () in Tinyweb.line c 0 0 300 200 tmp1 (Tinyweb.line__default__width ()); Tinyweb.line c 0 200 300 0 "red" 3; Tinyweb.text c 10 10 "click \"me\"" (Tinyweb.text__default__color ()) 20
let status = Tinyweb_external.label "nothing yet"
let clicked x y = Tinyweb_external.set_label status ("clicked at " ^ string_of_int x ^ ", " ^ string_of_int y); Tinyweb.circle c x y 5 "blue"
let pressed key = print_endline ("key [" ^ key ^ "]"); if key = "Enter" then Tinyweb.play_file "snare1.wav"
let moved value = Tinyweb_external.set_label status ("slider at " ^ string_of_int value)
let () = Tinyweb_external.on_mouse c clicked; Tinyweb_external.on_key pressed; ignore (Tinyweb.slider 0 10 5 moved); ignore (Tinyweb.slider 1 3 2 (Tinyweb.slider__default__on_change ()))lib_tinyweb_sound_graphics1
# A canvas, two circles, two samples and buttons that play them: the# first tinyweb program.
import tinyweb as tw
tw.web_init(0)
kick = tw.load_sample("kick1.wav")snare = tw.load_sample("snare1.wav")
canvas = tw.canvas(400, 200)
tw.circle(canvas, 100, 50, 30, "#ff0000") # canvas, x, y, radius, colortw.circle(canvas, 100, 50, 20) # the color defaults to "#000000"
var presses = 0
def play_kick(): tw.play(kick)
def play_sound(): presses := presses + 1 print("snare, press " ++ string(presses)) tw.play(snare, volume = 0.5)
tw.button("Play kick", on_click = play_kick)tw.button("Play snare", on_click = play_sound)tw.button("Does nothing") # on_click is optional
print("built the page")lib_tinyweb_sound_graphics1.zip
program.ml, as Pyking generates it
(* Generated from lib_tinyweb_sound_graphics1.pyk by the Pyking compiler. Do not edit. *)(* Target: ocaml5.5. Compile with: ocamlopt <this file> or run it directly with: ocaml <this file> *)
let () = Tinyweb_external.web_init 0
let kick = Tinyweb_external.load_sample "kick1.wav"
let snare = Tinyweb_external.load_sample "snare1.wav"
let canvas = Tinyweb_external.canvas 400 200
let () = Tinyweb.circle canvas 100 50 30 "#ff0000"; Tinyweb.circle canvas 100 50 20 (Tinyweb.circle__default__color ())
let presses = ref 0
let play_kick () = let tmp1 = Tinyweb.play__default__at () in let tmp2 = Tinyweb.play__default__volume () in Tinyweb.play kick tmp1 tmp2 (Tinyweb.play__default__rate ())
let play_sound () = presses := !presses + 1; print_endline ("snare, press " ^ string_of_int !presses); let tmp3 = Tinyweb.play__default__at () in Tinyweb.play snare tmp3 0.5 (Tinyweb.play__default__rate ())
let () = ignore (Tinyweb.button "Play kick" play_kick); ignore (Tinyweb.button "Play snare" play_sound); ignore (Tinyweb.button "Does nothing" (Tinyweb.button__default__on_click ())); print_endline "built the page"lib_tinyweb_style
# The page's look: a theme, the style of every element of a kind, and# the style of one element. The .events file presses the button, which# changes the theme and one label while the page is showing.
import tinyweb as tw
tw.web_init(0)
tw.theme("paper")tw.theme("no such theme")tw.page_style(background = "#fdf6e3", color = "#586e75", font = "serif", size = 18, padding = 12, gap = 16)tw.button_style(background = "#268bd2", color = "white", border = "none", radius = 14, font = "monospace", size = 16, padding = 8)tw.label_style(background = "lightyellow", color = "#333333", border = "1px solid gold", radius = 4, font = "sans-serif", size = 14, padding = 6)tw.slider_style(accent = "crimson", width = 240, color = "crimson", font = "monospace", size = 20)tw.canvas_style(border = "3px solid teal", radius = 10)
title = tw.label("Styled")tw.set_style(title, background = "navy", color = "white", border = "2px dashed orange", radius = 0, font = "Georgia", size = 28, padding = 10, width = 300)level = tw.slider(0, 10, 4)tw.set_colors(level, color = "seagreen")c = tw.canvas(120, 60)tw.set_style(c, border = "1px solid black")
def pressed(): tw.theme("retro") tw.set_colors(title, background = "black") print("now retro")
ok = tw.button("Retro", on_click = pressed)tw.set_colors(ok, color = "black", background = "gold")program.ml, as Pyking generates it
(* Generated from lib_tinyweb_style.pyk by the Pyking compiler. Do not edit. *)(* Target: ocaml5.5. Compile with: ocamlopt <this file> or run it directly with: ocaml <this file> *)
let () = Tinyweb_external.web_init 0; Tinyweb.theme "paper"; Tinyweb.theme "no such theme"; Tinyweb.page_style "#fdf6e3" "#586e75" "serif" 18 12 16; Tinyweb.button_style "#268bd2" "white" "none" 14 "monospace" 16 8; Tinyweb.label_style "lightyellow" "#333333" "1px solid gold" 4 "sans-serif" 14 6; Tinyweb.slider_style "crimson" 240 "crimson" "monospace" 20; Tinyweb.canvas_style "3px solid teal" 10
let title = Tinyweb_external.label "Styled"
let () = Tinyweb.set_style title "navy" "white" "2px dashed orange" 0 "Georgia" 28 10 (Tinyweb.set_style__default__accent ()) 300
let level = Tinyweb.slider 0 10 4 (Tinyweb.slider__default__on_change ())
let () = Tinyweb.set_colors level "seagreen" (Tinyweb.set_colors__default__background ())
let c = Tinyweb_external.canvas 120 60
let () = let tmp1 = Tinyweb.set_style__default__background () in let tmp2 = Tinyweb.set_style__default__color () in let tmp3 = Tinyweb.set_style__default__radius () in let tmp4 = Tinyweb.set_style__default__font () in let tmp5 = Tinyweb.set_style__default__size () in let tmp6 = Tinyweb.set_style__default__padding () in let tmp7 = Tinyweb.set_style__default__accent () in Tinyweb.set_style c tmp1 tmp2 "1px solid black" tmp3 tmp4 tmp5 tmp6 tmp7 (Tinyweb.set_style__default__width ())
let pressed () = Tinyweb.theme "retro"; Tinyweb.set_colors title (Tinyweb.set_colors__default__color ()) "black"; print_endline "now retro"
let ok = Tinyweb.button "Retro" pressed
let () = Tinyweb.set_colors ok "black" "gold"