diff --git a/examples/axes.ml b/examples/axes.ml index 1ff41ee..21ad375 100644 --- a/examples/axes.ml +++ b/examples/axes.ml @@ -1,11 +1,12 @@ (* simple example demonstrating drawing x and y axes for debugging or plots *) -open Joy + +open Joy.Svg let _ = (* intialize rendering context with the axes flag set to true *) init ~axes:true (); (* set background to opaque white *) let c = circle 50 in - render c; + show [ c ]; (* Write to PNG! *) write ~filename:"axes.png" () diff --git a/examples/canvas.ml b/examples/canvas.ml new file mode 100644 index 0000000..b1c6771 --- /dev/null +++ b/examples/canvas.ml @@ -0,0 +1,7 @@ +open Joy.Canvas + +let () = + init ~size:(400, 400) (); + let c = circle 100 in + let r = rectangle 200 150 in + show [ c; r ] diff --git a/examples/circle.ml b/examples/circle.ml index e3e911b..23222a5 100644 --- a/examples/circle.ml +++ b/examples/circle.ml @@ -1,7 +1,7 @@ -open Joy +open Joy.Svg let () = init (); let c = circle 50 in - render c; + show [ c ]; write ~filename:"circle.png" () diff --git a/examples/circle_packing.ml b/examples/circle_packing.ml index e94d733..4ef1e90 100644 --- a/examples/circle_packing.ml +++ b/examples/circle_packing.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg (* global constants // RNG initialization *) let w, h = (900., 900.) @@ -94,9 +94,7 @@ let () = let circles = List.map (fun ((x, y), radius) -> - circle - ~c:{x; y} - (int_of_float radius) + circle ~c:{ x; y } (int_of_float radius) |> with_stroke (rand_nth palette)) concentric in diff --git a/examples/circle_row_joy.ml b/examples/circle_row_joy.ml index e4c419b..6e25a3c 100644 --- a/examples/circle_row_joy.ml +++ b/examples/circle_row_joy.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); diff --git a/examples/color.ml b/examples/color.ml index bad55cb..13db095 100644 --- a/examples/color.ml +++ b/examples/color.ml @@ -1,7 +1,7 @@ -open Joy +open Joy.Svg let _ = init (); let c = circle 50 |> with_stroke red in - render c; + show [ c ]; write ~filename:"color.png" () diff --git a/examples/complex.ml b/examples/complex.ml index 615d15a..c80746e 100644 --- a/examples/complex.ml +++ b/examples/complex.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg (* Complex shapes can also be created from lists of shapes. diff --git a/examples/concentric_circles.ml b/examples/concentric_circles.ml index 4f9a21d..7a7c880 100644 --- a/examples/concentric_circles.ml +++ b/examples/concentric_circles.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); @@ -12,5 +12,5 @@ let () = | _, _ -> arr in let circles = complex (make_concentric [] 21) in - render circles; + show [ circles ]; write ~filename:"concentric_circles.png" () diff --git a/examples/donut_with_scale.ml b/examples/donut_with_scale.ml index 3cc0f03..5ce4ae4 100644 --- a/examples/donut_with_scale.ml +++ b/examples/donut_with_scale.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); diff --git a/examples/dune b/examples/dune index c5d0153..d7ca53a 100644 --- a/examples/dune +++ b/examples/dune @@ -103,6 +103,12 @@ (modules donut_with_scale) (libraries joy)) +(executable + (name canvas) + (modes js) + (modules canvas) + (libraries joy)) + (executable (name color) (modules color) diff --git a/examples/ellipse.ml b/examples/ellipse.ml index 0898890..deae301 100644 --- a/examples/ellipse.ml +++ b/examples/ellipse.ml @@ -1,9 +1,9 @@ -open Joy +open Joy.Svg let () = init (); (* create an ellipse *) let e = ellipse 100 75 in (* render it *) - render e; + show [ e ]; write ~filename:"ellipse.png" () diff --git a/examples/higher_transforms.ml b/examples/higher_transforms.ml index d4921b0..a320d0d 100644 --- a/examples/higher_transforms.ml +++ b/examples/higher_transforms.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg (* Higher order transformations can be composed with `comp`, which applies its function args right-to-left. @@ -10,5 +10,5 @@ let () = init (); let initial = rectangle ~c:(point (-250) (-250)) 100 100 in let shapes = repeat 32 transform initial in - render shapes; + show [ shapes ]; write ~filename:"higher_transforms.png" () diff --git a/examples/line.ml b/examples/line.ml index 2c250c1..0004198 100644 --- a/examples/line.ml +++ b/examples/line.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let size = 800 let interval = 16 diff --git a/examples/polygon.ml b/examples/polygon.ml index 91c691c..603713c 100644 --- a/examples/polygon.ml +++ b/examples/polygon.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let size = 100. @@ -8,5 +8,5 @@ let () = polygon [ { x = -.size; y = 0. }; { x = 0.; y = size }; { x = size; y = 0. } ] in - render poly; + show [ poly ]; write ~filename:"polygon.png" () diff --git a/examples/rectangle.ml b/examples/rectangle.ml index d95d3b1..d53b819 100644 --- a/examples/rectangle.ml +++ b/examples/rectangle.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); diff --git a/examples/rectangle_canvas.ml b/examples/rectangle_canvas.ml index d172962..d067e05 100644 --- a/examples/rectangle_canvas.ml +++ b/examples/rectangle_canvas.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init ~size:(500, 300) (); diff --git a/examples/repeat.ml b/examples/repeat.ml index 860fd72..7f34a22 100644 --- a/examples/repeat.ml +++ b/examples/repeat.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg (* demonstration of the repeat function @@ -12,5 +12,5 @@ let () = init (); let circle = circle ~c:(point (-100) 0) 50 in let shapes = repeat 10 (translate 10 0) circle in - render shapes; + show [ shapes ]; write ~filename:"repeat.png" () diff --git a/examples/rotate.ml b/examples/rotate.ml index 624756a..fb0321c 100644 --- a/examples/rotate.ml +++ b/examples/rotate.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let max = 32. let rec range a b = if a > b then [] else a :: range (a +. 1.) b diff --git a/examples/square.ml b/examples/square.ml index 542788c..257c4a2 100644 --- a/examples/square.ml +++ b/examples/square.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); diff --git a/examples/star.ml b/examples/star.ml index 8164804..a778295 100644 --- a/examples/star.ml +++ b/examples/star.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let outer_radius = 200. let inner_radius = 80. @@ -18,5 +18,5 @@ let () = init (); set_line_width 3; let star = List.init points star_section |> List.flatten |> polygon in - render star; + show [ star ]; write ~filename:"star.png" () diff --git a/examples/translate_circle.ml b/examples/translate_circle.ml index c7d17c1..c07cc28 100644 --- a/examples/translate_circle.ml +++ b/examples/translate_circle.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); diff --git a/examples/translate_ellipse.ml b/examples/translate_ellipse.ml index 20af612..2561a95 100644 --- a/examples/translate_ellipse.ml +++ b/examples/translate_ellipse.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); diff --git a/examples/translate_rectangle.ml b/examples/translate_rectangle.ml index b3c6bb1..09ed148 100644 --- a/examples/translate_rectangle.ml +++ b/examples/translate_rectangle.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let () = init (); diff --git a/examples/triangle.ml b/examples/triangle.ml index ef0b8a7..e7b44ca 100644 --- a/examples/triangle.ml +++ b/examples/triangle.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let size = 100. @@ -8,5 +8,5 @@ let () = polygon [ { x = -.size; y = 0. }; { x = 0.; y = size }; { x = size; y = 0. } ] in - render triangle; + show [ triangle ]; write ~filename:"triangle.png" () diff --git a/index.html b/index.html new file mode 100644 index 0000000..3561cc1 --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + + + Joy + + + + + diff --git a/lib/canvas.ml b/lib/canvas.ml new file mode 100644 index 0000000..a4c1bbd --- /dev/null +++ b/lib/canvas.ml @@ -0,0 +1,218 @@ +module Html = Js_of_ocaml.Dom_html +module Dom = Js_of_ocaml.Dom +module Js = Js_of_ocaml.Js + +(* JS type conversion helpers *) +let str = Js.string +let bl = Js.bool + +(* aliases for globals *) +let doc = Html.document + +(* Needed for 'write'/image save*) +let _window = Html.window + +module C : Modules.Impl = struct + type context = { + context : Html.canvasRenderingContext2D Js.t; + size : int * int; + axes : bool; + } + + let context = ref None + + exception Context of string + + (* Not working, could use help fixing *) + let () = + Printexc.register_printer (fun e -> + match e with Context err -> Some ("Context: " ^ err) | _ -> None) + + let fail () = raise (Context "not initialized") + + let create_canvas size = + let w, h = size in + let canvas = Html.createCanvas doc in + canvas##.width := w; + canvas##.height := h; + canvas + + let begin_path ctx = ctx##beginPath + let close_path ctx = ctx##closePath + + (* sets background color *) + let background { context; size; _ } color = + let rgba (r, g, b, a) = + str (Printf.sprintf "rgba(%d, %d, %d, %d)" r g b a) + in + let col = rgba color in + let w, h = size in + begin_path context; + context##.fillStyle := col; + context##fillRect 0. 0. (float_of_int w) (float_of_int h); + close_path context + + (** Returns 'axes' field of context *) + let axes ctx = ctx.axes + + (** Returns 'size' field of context *) + let resolution () = + match !context with Some ctx -> ctx.size | None -> fail () + + let set_line_width lw = + match !context with + | Some { context; _ } -> context##.lineWidth := float_of_int lw + | None -> fail () + + (* TODO: writ this fn, blob API + coercion in JSOO is tough, not sure where + all the methods etc I need are *) + let write _ctx _filename = + let _save_image _filename _blob = + (* Create anchor element, bind download fn to anchor, + and then programmatically click anchor to download png *) + () + in + (* Coerce canvas to blob, bind callback *) + () + + let save () = + match !context with Some { context; _ } -> context##save | None -> fail () + + let restore () = + match !context with + | Some { context; _ } -> context##restore + | None -> fail () + + let init_context background_color line_width ((w, h) as size) axes = + if Option.is_some !context then + raise (Context "context already initialized") + else + let canvas = create_canvas size in + Dom.appendChild doc##.body canvas; + let ctx = canvas##getContext Html._2d_ in + ctx##translate (w / 2 |> float_of_int) (h / 2 |> float_of_int); + ctx##.lineWidth := float_of_int line_width; + let temp = { context = ctx; size; axes } in + background temp background_color; + context := Some temp + + open Shape + + let stroke' ctx (r, g, b) = + let clr_str = Printf.sprintf "rgb(%d, %d, %d)" r g b |> str in + ctx##.strokeStyle := clr_str; + ctx##stroke + + let fill' ctx (r, g, b) = + let clr_str = Printf.sprintf "rgb(%d, %d, %d)" r g b |> str in + ctx##.fillStyle := clr_str; + ctx##fill + + let draw_circle ctx { c; radius; stroke; fill } = + let { x; y } = c in + begin_path ctx; + ctx##arc x y radius 0. (2. *. Float.pi) (bl false); + Option.iter (stroke' ctx) stroke; + Option.iter (fill' ctx) fill; + close_path ctx + + let draw_line ctx { a = { x = x1; y = y1 }; b = { x = x2; y = y2 }; stroke } = + begin_path ctx; + ctx##moveTo x1 y1; + ctx##lineTo x2 y2; + stroke' ctx stroke; + ctx##moveTo 0. 0.; + close_path ctx + + type curve = float * float * float * float * float * float + + let create_control_points (c, rx, ry) : float point * curve * curve = + let { x; y } = c in + let half_height = ry /. 2. in + let width_two_thirds = rx *. (2. /. 3.) in + ( { x; y = y -. half_height }, + ( x +. width_two_thirds, + y -. half_height, + x +. width_two_thirds, + y +. half_height, + x, + y +. half_height ), + ( x -. width_two_thirds, + y +. half_height, + x -. width_two_thirds, + y -. half_height, + x, + y -. half_height ) ) + + let draw_ellipse ctx { c; rx; ry; stroke; fill } = + begin_path ctx; + let start, curve_one, curve_two = create_control_points (c, rx, ry) in + ctx##moveTo start.x start.y; + let x1, y1, x2, y2, x3, y3 = curve_one in + ctx##bezierCurveTo x1 y1 x2 y2 x3 y3; + let x1, y1, x2, y2, x3, y3 = curve_two in + ctx##bezierCurveTo x1 y1 x2 y2 x3 y3; + Option.iter (stroke' ctx) stroke; + Option.iter (fill' ctx) fill; + close_path ctx + + let rec split_at n lst = + match (n, lst) with + | 0, _ -> ([], lst) + | _, [] -> ([], []) + | n, x :: xs -> + let taken, rest = split_at (n - 1) xs in + (x :: taken, rest) + + let rec partition n ?(step = 0) lst = + match lst with + | [] -> [] + | lst -> + let taken, _ = split_at n lst in + if List.length taken = n then taken :: partition n ~step (List.tl lst) + else [] + + let draw_polygon ctx { vertices; stroke; fill } = + let points = partition 2 ~step:1 (vertices @ [ List.hd vertices ]) in + begin_path ctx; + List.iter + (fun pair -> + let { x = x1; y = y1 }, { x = x2; y = y2 } = + (List.nth pair 0, List.nth pair 1) + in + ctx##moveTo x1 y1; + ctx##lineTo x2 y2) + points; + Option.iter (stroke' ctx) stroke; + Option.iter (fill' ctx) fill; + close_path ctx + + let render_axes () = + let tmap f (a, b) = (f a, f b) in + let render_axes' { context = ctx; size; _ } = + let fsize = tmap float_of_int size in + let hw, hh = tmap (fun n -> n /. 2.) fsize in + let w, h = fsize in + ctx##beginPath; + ctx##moveTo 0. hh; + ctx##lineTo w hh; + ctx##moveTo hw 0.; + ctx##lineTo hw h; + ctx##closePath + in + match !context with Some ctx -> render_axes' ctx | None -> fail () + + let show shapes = + let rec render' context = function + | Circle circle' -> draw_circle context circle' + | Ellipse ellipse' -> draw_ellipse context ellipse' + | Line line' -> draw_line context line' + | Polygon polygon' -> draw_polygon context polygon' + | Complex complex' -> List.iter (render' context) complex' + in + match !context with + | Some ctx -> List.iter (render' ctx.context) shapes + | None -> fail () +end + +module Backend = Modules.Make (C) diff --git a/lib/canvas.mli b/lib/canvas.mli new file mode 100644 index 0000000..a402467 --- /dev/null +++ b/lib/canvas.mli @@ -0,0 +1,11 @@ +module Html = Js_of_ocaml.Dom_html +module Dom = Js_of_ocaml.Dom +module Js = Js_of_ocaml.Js + +val str : string -> Js.js_string Js.t +val bl : bool -> bool Js.t +val doc : Html.document Js_of_ocaml.Js.t +val _window : Html.window Js_of_ocaml.Js.t + +module C : Modules.Impl +module Backend : Modules.Backend diff --git a/lib/context.ml b/lib/context.ml deleted file mode 100644 index 33ede8f..0000000 --- a/lib/context.ml +++ /dev/null @@ -1,72 +0,0 @@ -(* Global rendering context singleton definition and instantiation *) -type context = { - ctx : Cairo.context; - surface : Cairo.Surface.t; - size : int * int; - axes : bool; -} - -(* Renders context to PNG *) -let write ctx filename = - Cairo.PNG.write ctx.surface filename; - Cairo.Surface.finish ctx.surface - -let context = ref None - -exception Context of string - -(* Not working, could use help fixing *) -let () = - Printexc.register_printer (fun e -> - match e with Context err -> Some ("Context: " ^ err) | _ -> None) - -let fail () = raise (Context "not initialized") -let resolution () = match !context with Some ctx -> ctx.size | None -> fail () -let tmap3 f (a, b, c) = (f a, f b, f c) -let tmap4 f (a, b, c, d) = (f a, f b, f c, f d) -let ( >> ) f g x = g (f x) -let scale_channel n = n /. 255. -let scale_color_channel = float_of_int >> scale_channel - -let set_color color = - match !context with - | Some ctx -> - let r, g, b = tmap3 scale_color_channel color in - Cairo.set_source_rgb ctx.ctx r g b - | None -> fail () - -(* sets background color *) -let background color = - match !context with - | Some { ctx; _ } -> - let r, g, b, alpha = tmap4 scale_color_channel color in - Cairo.set_source_rgb ctx r g b; - Cairo.paint ctx ~alpha; - Cairo.fill ctx - | None -> fail () - -(** Sets the width of lines for both stroke of shapes and line primitives. - Can be any positive integer, with larger numbers producing thicker lines. - default is 2 *) -let set_line_width line_width = - match !context with - | Some ctx -> Cairo.set_line_width ctx.ctx (float_of_int line_width) - | None -> fail () - -let save () = - match !context with Some ctx -> Cairo.save ctx.ctx | None -> fail () - -let restore () = - match !context with Some ctx -> Cairo.restore ctx.ctx | None -> fail () - -let init_context background_color line_width (w, h) axes = - (* Fail if context has already been instantiated *) - if Option.is_some !context then - raise (Context "Cannot initialize context twice"); - - let surface = Cairo.Image.create Cairo.Image.ARGB32 ~w ~h in - let ctx = Cairo.create surface in - Cairo.set_line_width ctx line_width; - Cairo.translate ctx (w / 2 |> float_of_int) (h / 2 |> float_of_int); - context := Some { ctx; surface; size = (w, h); axes }; - background background_color diff --git a/lib/context.mli b/lib/context.mli deleted file mode 100644 index d9f36a9..0000000 --- a/lib/context.mli +++ /dev/null @@ -1,20 +0,0 @@ -type context = { - ctx : Cairo.context; - surface : Cairo.Surface.t; - size : int * int; - axes : bool; -} - -val context : context option ref -val fail : unit -> unit - -exception Context of string - -val init_context : int * int * int * int -> float -> int * int -> bool -> unit -val resolution : unit -> int * int -val set_color : int * int * int -> unit -val background : int * int * int * int -> unit -val set_line_width : int -> unit -val write : context -> string -> unit -val save : unit -> unit -val restore : unit -> unit diff --git a/lib/dune b/lib/dune index 5feb774..5cf1b27 100644 --- a/lib/dune +++ b/lib/dune @@ -1,5 +1,7 @@ (library (name joy) (public_name joy) - (libraries cairo2) + (libraries cairo2 js_of_ocaml) + (preprocess + (pps js_of_ocaml-ppx)) (wrapped false)) diff --git a/lib/joy.ml b/lib/joy.ml index b6eb36a..fdd5a8b 100644 --- a/lib/joy.ml +++ b/lib/joy.ml @@ -1,51 +1,2 @@ -let context = Context.context - -type 'a point = 'a Shape.point -type shape = Shape.shape -type shapes = Shape.shapes -type transformation = Transform.transformation - -type color = Color.color -(** Three-tuple representing a 24-bit RGB color *) - -let black = Color.black -let white = Color.white -let red = Color.red -let green = Color.green -let blue = Color.blue -let yellow = Color.yellow -let transparent = Color.transparent -let opaque = Color.opaque -let point = Shape.point -let circle = Shape.circle -let rectangle = Shape.rectangle -let polygon = Shape.polygon -let ellipse = Shape.ellipse -let line = Shape.line -let complex = Shape.complex -let with_stroke = Shape.with_stroke -let with_fill = Shape.with_fill -let rotate = Transform.rotate -let scale = Transform.scale -let translate = Transform.translate -let compose = Transform.compose -let repeat = Transform.repeat -let map_fill = Transform.map_fill -let map_stroke = Transform.map_stroke -let set_line_width = Context.set_line_width - -let init ?(background = Color.white) ?(line_width = 2) ?(size = (500, 500)) - ?(axes = false) () = - Context.init_context (Color.opaque background) - (float_of_int line_width) - size axes - -let write ?(filename = "joy.png") () = - match !Context.context with - | Some ctx -> - if ctx.axes then Render.render_axes (); - Context.write ctx filename - | None -> Context.fail () - -let render shape = Render.render shape -let show shapes = Render.show shapes +module Svg = Svg.Backend +module Canvas = Canvas.Backend diff --git a/lib/joy.mli b/lib/joy.mli index bfd4982..fdd5a8b 100644 --- a/lib/joy.mli +++ b/lib/joy.mli @@ -1,44 +1,2 @@ -type 'a point = 'a Shape.point -type shape = Shape.shape -type shapes = Shape.shapes -type transformation = Transform.transformation -type color = Color.color - -val point : int -> int -> float point -val circle : ?c:float point -> int -> shape -val rectangle : ?c:float point -> int -> int -> shape -val ellipse : ?c:float point -> int -> int -> shape -val line : ?a:float point -> float point -> shape -val polygon : float point list -> shape -val complex : shapes -> shape -val with_stroke : color -> shape -> shape -val with_fill : color -> shape -> shape -val rotate : int -> transformation -val translate : int -> int -> transformation -val scale : float -> transformation -val compose : transformation -> transformation -> transformation -val repeat : int -> transformation -> transformation -val map_stroke : (color -> color) -> shape -> shape -val map_fill : (color -> color) -> shape -> shape -val context : Context.context option ref -val set_line_width : int -> unit -val black : color -val white : color -val red : color -val green : color -val blue : color -val yellow : color -val transparent : int * int * int * int -val opaque : color -> int * int * int * int - -val init : - ?background:color -> - ?line_width:int -> - ?size:int * int -> - ?axes:bool -> - unit -> - unit - -val render : shape -> unit -val show : shapes -> unit -val write : ?filename:string -> unit -> unit +module Svg = Svg.Backend +module Canvas = Canvas.Backend diff --git a/lib/modules.ml b/lib/modules.ml new file mode 100644 index 0000000..442f22c --- /dev/null +++ b/lib/modules.ml @@ -0,0 +1,101 @@ +module type Impl = sig + type context + + val context : context option ref + + exception Context of string + + val fail : unit -> unit + val init_context : int * int * int * int -> int -> int * int -> bool -> unit + + (* Getters *) + val axes : context -> bool + val resolution : unit -> int * int + + (* Setters *) + val set_line_width : int -> unit + val render_axes : unit -> unit + val save : unit -> unit + val restore : unit -> unit + val write : context -> string -> unit + val show : Shape.shapes -> unit +end + +module type Backend = sig + type context + + val context : context option ref + + type 'a point = 'a Shape.point + type shape = Shape.shape + type shapes = Shape.shapes + type transformation = Transform.transformation + type color = Color.color + + val point : int -> int -> float Shape.point + val circle : ?c:float Shape.point -> int -> Shape.shape + val rectangle : ?c:float Shape.point -> int -> int -> Shape.shape + val polygon : float Shape.point list -> Shape.shape + val ellipse : ?c:float Shape.point -> int -> int -> Shape.shape + val line : ?a:float Shape.point -> float Shape.point -> Shape.shape + val complex : Shape.shape list -> Shape.shape + val rotate : int -> Transform.transformation + val scale : float -> Transform.transformation + val translate : int -> int -> Transform.transformation + val with_stroke : color -> shape -> shape + val with_fill : color -> shape -> shape + val map_stroke : (color -> color) -> shape -> shape + val map_fill : (color -> color) -> shape -> shape + val white : color + val black : color + val red : color + val blue : color + val green : color + val yellow : color + + val compose : + Transform.transformation -> + Transform.transformation -> + Transform.transformation + + val repeat : int -> Transform.transformation -> Transform.transformation + val set_line_width : int -> unit + val resolution : unit -> int * int + + val init : + ?background:int * int * int * int -> + ?line_width:int -> + ?size:int * int -> + ?axes:bool -> + unit -> + unit + + val write : ?filename:string -> unit -> unit + val show : Shape.shapes -> unit +end + +module Make (B : Impl) : Backend with type context = B.context = struct + type context = B.context + + let context = B.context + + include Shape + include Transform + include Color + + let set_line_width = B.set_line_width + let resolution = B.resolution + + let init ?(background = (255, 255, 255, 255)) ?(line_width = 2) + ?(size = (800, 800)) ?(axes = false) () = + B.init_context background line_width size axes + + let write ?(filename = "joy.png") () = + match !B.context with + | Some ctx -> + if B.axes ctx then B.render_axes (); + B.write ctx filename + | None -> B.fail () + + let show shapes = B.show shapes +end diff --git a/lib/modules.mli b/lib/modules.mli new file mode 100644 index 0000000..a060285 --- /dev/null +++ b/lib/modules.mli @@ -0,0 +1,73 @@ +module type Impl = sig + type context + + val context : context option ref + + exception Context of string + + val fail : unit -> unit + val init_context : int * int * int * int -> int -> int * int -> bool -> unit + val axes : context -> bool + val resolution : unit -> int * int + val set_line_width : int -> unit + val render_axes : unit -> unit + val save : unit -> unit + val restore : unit -> unit + val write : context -> string -> unit + val show : Shape.shapes -> unit +end + +module type Backend = sig + type context + + val context : context option ref + + type 'a point = 'a Shape.point + type shape = Shape.shape + type shapes = Shape.shapes + type transformation = Transform.transformation + type color = Color.color + + val point : int -> int -> float Shape.point + val circle : ?c:float Shape.point -> int -> Shape.shape + val rectangle : ?c:float Shape.point -> int -> int -> Shape.shape + val polygon : float Shape.point list -> Shape.shape + val ellipse : ?c:float Shape.point -> int -> int -> Shape.shape + val line : ?a:float Shape.point -> float Shape.point -> Shape.shape + val complex : Shape.shape list -> Shape.shape + val rotate : int -> Transform.transformation + val scale : float -> Transform.transformation + val translate : int -> int -> Transform.transformation + val with_stroke : color -> shape -> shape + val with_fill : color -> shape -> shape + val map_stroke : (color -> color) -> shape -> shape + val map_fill : (color -> color) -> shape -> shape + val white : color + val black : color + val red : color + val blue : color + val green : color + val yellow : color + + val compose : + Transform.transformation -> + Transform.transformation -> + Transform.transformation + + val repeat : int -> Transform.transformation -> Transform.transformation + val set_line_width : int -> unit + val resolution : unit -> int * int + + val init : + ?background:int * int * int * int -> + ?line_width:int -> + ?size:int * int -> + ?axes:bool -> + unit -> + unit + + val write : ?filename:string -> unit -> unit + val show : Shape.shapes -> unit +end + +module Make : functor (B : Impl) -> Backend with type context = B.context diff --git a/lib/render.ml b/lib/render.ml deleted file mode 100644 index eca4a77..0000000 --- a/lib/render.ml +++ /dev/null @@ -1,126 +0,0 @@ -open Shape -open Context - -let tmap f (x, y) = (f x, f y) - -let draw_circle ctx ({ c; radius; stroke; fill } : circle) = - let stroke_circle stroke = - set_color stroke; - Cairo.stroke_preserve ctx.ctx - in - let fill_circle fill = - set_color fill; - Cairo.fill_preserve ctx.ctx - in - Cairo.arc ctx.ctx c.x c.y ~r:radius ~a1:0. ~a2:(Float.pi *. 2.); - Option.iter stroke_circle stroke; - Option.iter fill_circle fill; - Cairo.Path.clear ctx.ctx - -let create_control_points ({x; y}, rx, ry) = - let half_height = ry /. 2. in - let width_two_thirds = rx *. (2. /. 3.) *. 2. in - ( { x; y = y -. half_height }, - ( x +. width_two_thirds, - y -. half_height, - x +. width_two_thirds, - y +. half_height, - x, - y +. half_height ), - ( x -. width_two_thirds, - y +. half_height, - x -. width_two_thirds, - y -. half_height, - x, - y -. half_height ) ) - -let draw_ellipse ctx { c; rx; ry; stroke; fill } = - let stroke_ellipse stroke = - set_color stroke; - Cairo.stroke_preserve ctx.ctx - in - let fill_ellipse fill = - set_color fill; - Cairo.fill_preserve ctx.ctx - in - let start, curve_one, curve_two = create_control_points (c, rx, ry) in - Cairo.move_to ctx.ctx start.x start.y; - let x1, y1, x2, y2, x3, y3 = curve_one in - Cairo.curve_to ctx.ctx x1 y1 x2 y2 x3 y3; - let x1, y1, x2, y2, x3, y3 = curve_two in - Cairo.curve_to ctx.ctx x1 y1 x2 y2 x3 y3; - Option.iter stroke_ellipse stroke; - Option.iter fill_ellipse fill; - Cairo.Path.clear ctx.ctx - -let draw_line ctx { a; b; stroke } = - set_color stroke; - let { x; y } = a in - Cairo.move_to ctx.ctx x y; - let { x; y } = b in - Cairo.line_to ctx.ctx x y; - Cairo.stroke ctx.ctx - -let rec take n lst = - match (n, lst) with - | 0, _ -> ([], lst) - | _, [] -> ([], []) - | n, x :: xs -> - let taken, rest = take (n - 1) xs in - (x :: taken, rest) - -let rec partition n ?(step = 0) lst = - match lst with - | [] -> [] - | _ -> - let taken, _ = take n lst in - if List.length taken = n then - taken - :: - partition n ~step (List.tl lst) - else [] - -let draw_polygon ctx { vertices = points; stroke; fill } = - let stroke_rect stroke = - set_color stroke; - Cairo.stroke_preserve ctx.ctx - in - let fill_rect fill = - set_color fill; - Cairo.fill_preserve ctx.ctx - in - let points = partition 2 ~step:1 (points @ [ List.hd points ]) in - List.iter - (fun pair -> - let { x = x1; y = y1 }, { x = x2; y = y2 } = - (List.nth pair 0, List.nth pair 1) - in - Cairo.move_to ctx.ctx x1 y1; - Cairo.line_to ctx.ctx x2 y2) - points; - Option.iter stroke_rect stroke; - Option.iter fill_rect fill; - Cairo.Path.clear ctx.ctx - -let rec render_shape ctx = function - | Circle circle -> draw_circle ctx circle - | Ellipse ellipse -> draw_ellipse ctx ellipse - | Line line -> draw_line ctx line - | Polygon polygon -> draw_polygon ctx polygon - | Complex complex -> List.iter (render_shape ctx) complex - -(* Validates context before rendering *) -let render shape = - match !context with Some ctx -> render_shape ctx shape | None -> fail () - -let show shapes = - match !context with - | Some ctx -> List.iter (render_shape ctx) shapes - | None -> fail () - -let render_axes () = - let x, y = resolution () |> tmap float_of_int in - let half_x, half_y = (x /. 2., y /. 2.) in - let x_axis = line ~a:{ x = 0.; y = -.half_y } { x = 0.; y = half_y } in - let y_axis = line ~a:{ x = -.half_x; y = 0. } { x = half_x; y = 0. } in - show [ x_axis; y_axis ] diff --git a/lib/render.mli b/lib/render.mli deleted file mode 100644 index c4c7e03..0000000 --- a/lib/render.mli +++ /dev/null @@ -1,8 +0,0 @@ -val draw_circle : Context.context -> Shape.circle -> unit -val draw_ellipse : Context.context -> Shape.ellipse -> unit -val draw_line : Context.context -> Shape.line -> unit -val draw_polygon : Context.context -> Shape.polygon -> unit -val render_axes : unit -> unit -val render_shape : Context.context -> Shape.shape -> unit -val render : Shape.shape -> unit -val show : Shape.shape list -> unit diff --git a/lib/svg.ml b/lib/svg.ml new file mode 100644 index 0000000..42a2590 --- /dev/null +++ b/lib/svg.ml @@ -0,0 +1,195 @@ +module S : Modules.Impl = struct + (* Global rendering context singleton definition and instantiation *) + type context = { + ctx : Cairo.context; + surface : Cairo.Surface.t; + size : int * int; + axes : bool; + } + + (* Renders context to PNG *) + let write ctx filename = + Cairo.PNG.write ctx.surface filename; + Cairo.Surface.finish ctx.surface + + let context = ref None + + exception Context of string + + (* Not working, could use help fixing *) + let () = + Printexc.register_printer (fun e -> + match e with Context err -> Some ("Context: " ^ err) | _ -> None) + + let fail () = raise (Context "not initialized") + let axes ctx = ctx.axes + + let resolution () = + match !context with Some ctx -> ctx.size | None -> fail () + + let tmap3 f (a, b, c) = (f a, f b, f c) + let tmap4 f (a, b, c, d) = (f a, f b, f c, f d) + let ( >> ) f g x = g (f x) + let scale_color_channel x = x /. 256. + + let set_color color = + match !context with + | Some ctx -> + let r, g, b = tmap3 (float_of_int >> scale_color_channel) color in + Cairo.set_source_rgb ctx.ctx r g b + | None -> fail () + + (* sets background color *) + let background { ctx; _ } color = + let r, g, b, alpha = tmap4 (float_of_int >> scale_color_channel) color in + Cairo.set_source_rgb ctx r g b; + Cairo.paint ctx ~alpha; + Cairo.fill ctx + + (** Sets the width of lines for both stroke of shapes and line primitives. + Can be any positive integer, with larger numbers producing thicker lines. + default is 2 *) + let set_line_width line_width = + match !context with + | Some ctx -> Cairo.set_line_width ctx.ctx (float_of_int line_width) + | None -> fail () + + let save () = + match !context with Some ctx -> Cairo.save ctx.ctx | None -> fail () + + let restore () = + match !context with Some ctx -> Cairo.restore ctx.ctx | None -> fail () + + let init_context background_color line_width (w, h) axes = + (* Fail if context has already been instantiated *) + if Option.is_some !context then + raise (Context "Cannot initialize context twice"); + + let surface = Cairo.Image.create Cairo.Image.ARGB32 ~w ~h in + let ctx = Cairo.create surface in + Cairo.set_line_width ctx (float_of_int line_width); + Cairo.translate ctx (w / 2 |> float_of_int) (h / 2 |> float_of_int); + let temp = { ctx; surface; size = (w, h); axes } in + context := Some temp; + background temp background_color + + open Shape + + let tmap f (x, y) = (f x, f y) + + let draw_circle ctx ({ c; radius; stroke; fill } : circle) = + let stroke_circle stroke = + set_color stroke; + Cairo.stroke_preserve ctx.ctx + in + let fill_circle fill = + set_color fill; + Cairo.fill_preserve ctx.ctx + in + Cairo.arc ctx.ctx c.x c.y ~r:radius ~a1:0. ~a2:(Float.pi *. 2.); + Option.iter stroke_circle stroke; + Option.iter fill_circle fill; + Cairo.Path.clear ctx.ctx + + let create_control_points ({ x; y }, rx, ry) = + let half_height = ry /. 2. in + let width_two_thirds = rx *. (2. /. 3.) *. 2. in + ( { x; y = y -. half_height }, + ( x +. width_two_thirds, + y -. half_height, + x +. width_two_thirds, + y +. half_height, + x, + y +. half_height ), + ( x -. width_two_thirds, + y +. half_height, + x -. width_two_thirds, + y -. half_height, + x, + y -. half_height ) ) + + let draw_ellipse ctx { c; rx; ry; stroke; fill } = + let stroke_ellipse stroke = + set_color stroke; + Cairo.stroke_preserve ctx.ctx + in + let fill_ellipse fill = + set_color fill; + Cairo.fill_preserve ctx.ctx + in + let start, curve_one, curve_two = create_control_points (c, rx, ry) in + Cairo.move_to ctx.ctx start.x start.y; + let x1, y1, x2, y2, x3, y3 = curve_one in + Cairo.curve_to ctx.ctx x1 y1 x2 y2 x3 y3; + let x1, y1, x2, y2, x3, y3 = curve_two in + Cairo.curve_to ctx.ctx x1 y1 x2 y2 x3 y3; + Option.iter stroke_ellipse stroke; + Option.iter fill_ellipse fill; + Cairo.Path.clear ctx.ctx + + let draw_line ctx { a; b; stroke } = + set_color stroke; + let { x; y } = a in + Cairo.move_to ctx.ctx x y; + let { x; y } = b in + Cairo.line_to ctx.ctx x y; + Cairo.stroke ctx.ctx + + let rec take n lst = + match (n, lst) with + | 0, _ -> ([], lst) + | _, [] -> ([], []) + | n, x :: xs -> + let taken, rest = take (n - 1) xs in + (x :: taken, rest) + + let rec partition n ?(step = 0) = function + | [] -> [] + | lst -> + let taken, _ = take n lst in + if List.length taken = n then taken :: partition n ~step (List.tl lst) + else [] + + let draw_polygon ctx { vertices = points; stroke; fill } = + let stroke_rect stroke = + set_color stroke; + Cairo.stroke_preserve ctx.ctx + in + let fill_rect fill = + set_color fill; + Cairo.fill_preserve ctx.ctx + in + let points = partition 2 ~step:1 (points @ [ List.hd points ]) in + List.iter + (fun pair -> + let { x = x1; y = y1 }, { x = x2; y = y2 } = + (List.nth pair 0, List.nth pair 1) + in + Cairo.move_to ctx.ctx x1 y1; + Cairo.line_to ctx.ctx x2 y2) + points; + Option.iter stroke_rect stroke; + Option.iter fill_rect fill; + Cairo.Path.clear ctx.ctx + + let show shapes = + let rec render ctx = function + | Circle circle -> draw_circle ctx circle + | Ellipse ellipse -> draw_ellipse ctx ellipse + | Line line -> draw_line ctx line + | Polygon polygon -> draw_polygon ctx polygon + | Complex complex -> List.iter (render ctx) complex + in + match !context with + | Some ctx -> List.iter (render ctx) shapes + | None -> fail () + + let render_axes () = + let x, y = resolution () |> tmap float_of_int in + let half_x, half_y = (x /. 2., y /. 2.) in + let x_axis = line ~a:{ x = 0.; y = -.half_y } { x = 0.; y = half_y } in + let y_axis = line ~a:{ x = -.half_x; y = 0. } { x = half_x; y = 0. } in + show [ x_axis; y_axis ] +end + +module Backend = Modules.Make (S) diff --git a/lib/svg.mli b/lib/svg.mli new file mode 100644 index 0000000..b13a2fd --- /dev/null +++ b/lib/svg.mli @@ -0,0 +1,2 @@ +module S : Modules.Impl +module Backend : Modules.Backend diff --git a/test/test_circle.ml b/test/test_circle.ml index 6153bfd..a699cd0 100644 --- a/test/test_circle.ml +++ b/test/test_circle.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let run () = init (); diff --git a/test/test_ellipse.ml b/test/test_ellipse.ml index df8600c..78d626e 100644 --- a/test/test_ellipse.ml +++ b/test/test_ellipse.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let run () = init (); diff --git a/test/test_rectangle.ml b/test/test_rectangle.ml index 73734e5..7e8d12d 100644 --- a/test/test_rectangle.ml +++ b/test/test_rectangle.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let run () = init (); diff --git a/test/test_scale_shape.ml b/test/test_scale_shape.ml index 3b3f0df..a8b4451 100644 --- a/test/test_scale_shape.ml +++ b/test/test_scale_shape.ml @@ -1,4 +1,4 @@ -open Joy +open Joy.Svg let run () = init ();