Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lines are blurry sometimes when using whole number coordinates #149

Open
brendanzab opened this issue Sep 3, 2023 · 5 comments
Open

Lines are blurry sometimes when using whole number coordinates #149

brendanzab opened this issue Sep 3, 2023 · 5 comments

Comments

@brendanzab
Copy link

I’m a bit confused about the behavior of line drawing… sometimes using whole numbers gives a crisp, non-blurry result, other times it requires using half-increments in either the x axis or y axis, or both. It’s a bit unpredictable! Here’s a test I came up with to exercise some of this:

On a highdpi display:

Screenshot 2023-09-03 at 1 16 22 pm

On a standard resolution display:

Screenshot 2023-09-03 at 1 18 05 pm

The code:

module Backend = OcamlCanvas.V1.Backend
module Canvas = OcamlCanvas.V1.Canvas
module Color = OcamlCanvas.V1.Color
module Event = OcamlCanvas.V1.Event

let () =

  Backend.init ();

  let c =
    Canvas.createOnscreen ~title:"line test"
      ~pos:(300, 200) ~size:(200, 200) ()
  in

  (* Vertical lines *)

  Canvas.clearPath c;
  Canvas.moveTo c (10.0, 10.0);
  Canvas.lineTo c (10.0, 40.0);
  Canvas.stroke c;

  Canvas.clearPath c; (* non-blurry *)
  Canvas.moveTo c (20.5, 10.0);
  Canvas.lineTo c (20.5, 40.0);
  Canvas.stroke c;

  Canvas.clearPath c;
  Canvas.moveTo c (30.5, 10.5);
  Canvas.lineTo c (30.5, 40.5);
  Canvas.stroke c;

  (* Horizontal lines *)

  Canvas.clearPath c;
  Canvas.moveTo c (40.0, 10.0);
  Canvas.lineTo c (80.0, 10.0);
  Canvas.stroke c;

  Canvas.clearPath c; (* non-blurry *)
  Canvas.moveTo c (40.0, 20.5);
  Canvas.lineTo c (80.0, 20.5);
  Canvas.stroke c;

  Canvas.clearPath c;
  Canvas.moveTo c (40.5, 30.5);
  Canvas.lineTo c (80.5, 30.5);
  Canvas.stroke c;

  (* Stroked rectangles *)

  Canvas.strokeRect c ~pos:(10.0, 50.0) ~size:(10.0, 30.0);
  Canvas.strokeRect c ~pos:(30.5, 50.0) ~size:(10.0, 30.0);
  Canvas.strokeRect c ~pos:(50.0, 50.5) ~size:(10.0, 30.0);
  Canvas.strokeRect c ~pos:(70.5, 50.5) ~size:(10.0, 30.0); (* non-blurry *)

  (* Filled rectangles *)

  Canvas.setFillColor c Color.black;
  Canvas.fillRect c ~pos:(10.0, 90.0) ~size:(10.0, 30.0); (* non-blurry *)
  Canvas.fillRect c ~pos:(30.5, 90.0) ~size:(10.0, 30.0);
  Canvas.fillRect c ~pos:(50.0, 90.5) ~size:(10.0, 30.0);
  Canvas.fillRect c ~pos:(70.5, 90.5) ~size:(10.0, 30.0);

  Canvas.show c;

  let events = [
    Event.close |> React.E.map (fun _ ->
      Backend.stop ()
    );

    Event.key_down |> React.E.map (fun Event.{ data = { key; _ }; _ } ->
      match key with
      | KeyEscape -> Backend.stop ()
      | _ -> ()
    );
  ] in

  Backend.run (fun () ->
    ignore events;
    Printf.printf "Goodbye!\n"
  )
@brendanzab
Copy link
Author

brendanzab commented Sep 3, 2023

The equivalent in Processing:

Screenshot 2023-09-03 at 2 46 39 pm
windowTitle("line test");
size(200, 200);

// Vertical lines

line(10.0, 10.0, 10.0, 40.0); // non-blurry
line(20.5, 10.0, 20.5, 40.0); // non-blurry
line(30.5, 10.5, 30.5, 40.5); // non-blurry

// Horizontal lines

line(40.0, 10.0, 80.0, 10.0); // non-blurry
line(40.0, 20.5, 80.0, 20.5); // non-blurry
line(40.5, 30.5, 80.5, 30.5); // non-blurry

// Stroked rectangles

noFill();
rect(10.0, 50.0, 10.0, 30.0); // non-blurry
rect(30.5, 50.0, 10.0, 30.0); // non-blurry
rect(50.0, 50.5, 10.0, 30.0); // non-blurry
rect(70.5, 50.5, 10.0, 30.0); // non-blurry

// Filled rectangles

fill(0);
noStroke();
rect(10.0, 90.0, 10.0, 30.0); // non-blurry
rect(30.5, 90.0, 10.0, 30.0);
rect(50.0, 90.5, 10.0, 30.0);
rect(70.5, 90.5, 10.0, 30.0);

Quite interesting – seems like strokes are always “snapped” to whole number pixels, and fills are drawn crisply on whole number pixels, but anti-aliased otherwise. Which definitely feels more “artist friendly” I guess? 🤔

Setting pixelDensity(2); after the call to size results in this on a hidpi display:

Screenshot 2023-09-03 at 4 03 24 pm

And this mess when dragging that window into my standard resolution display:

Screenshot 2023-09-03 at 4 04 33 pm

@brendanzab
Copy link
Author

brendanzab commented Sep 3, 2023

Hmm Firefox’s implementation of HTML Canvas is different again:

Screenshot 2023-09-03 at 2 59 39 pm
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.moveTo(10.0, 10.0);
ctx.lineTo(10.0, 40.0);
ctx.stroke();

ctx.beginPath(); // non-blurry(?)
ctx.moveTo(20.5, 10.0);
ctx.lineTo(20.5, 40.0);
ctx.stroke();

ctx.beginPath();
ctx.moveTo(30.5, 10.5);
ctx.lineTo(30.5, 40.5);
ctx.stroke();

// Horizontal lines

ctx.beginPath();
ctx.moveTo(40.0, 10.0);
ctx.lineTo(80.0, 10.0);
ctx.stroke();

ctx.beginPath(); // non-blurry(?)
ctx.moveTo(40.0, 20.5);
ctx.lineTo(80.0, 20.5);
ctx.stroke();

ctx.beginPath();
ctx.moveTo(40.5, 30.5);
ctx.lineTo(80.5, 30.5);
ctx.stroke();

// Stroked rectangles

ctx.strokeRect(10.0, 50.0, 10.0, 30.0); // non-blurry(?)
ctx.strokeRect(30.5, 50.0, 10.0, 30.0);
ctx.strokeRect(50.0, 50.5, 10.0, 30.0);
ctx.strokeRect(70.5, 50.5, 10.0, 30.0);

// Filled rectangles

ctx.fillStyle = "black";
ctx.fillRect(10.0, 90.0, 10.0, 30.0); // non-blurry(?)
ctx.fillRect(30.5, 90.0, 10.0, 30.0);
ctx.fillRect(50.0, 90.5, 10.0, 30.0);
ctx.fillRect(70.5, 90.5, 10.0, 30.0);

@ddeclerck
Copy link
Collaborator

Hi,

OCaml-Canvas' behavior should match the HTML5 Canvas behavior.
Pixel edges lie at integer coordinates, so if you try to draw a straight horizontal or vertical line of width 1 between two points with integer coordinates, this will in fact cover half of each line of pixels around the "mathematical" line between the two points , hence the blurry effect. Note that this only affects stroking primitives. When you use filling primitives, the area drawn is precisely bound by the given coordinates, so there's no "bleeding" on the adjacent pixels if you use integer coordinates.

@brendanzab
Copy link
Author

brendanzab commented Sep 28, 2023

Ahhh ok, I think I’m understanding it better now (my mental model was wrong). So the lines lie on the middle of the pixel, and that’s why they are coming out blurry? I think the end-caps are also lying in the middle of the pixel as well – eg. if I used butt endcaps it wouldn’t be blurry.

Seems like for rectangles Firefox is putting the stroke “around” the fill rather than overlaying it on the center of the pixel? This seems potentially confusing, but I’m not sure if this is standard behavior for canvas implementations.

@ddeclerck
Copy link
Collaborator

ddeclerck commented Sep 28, 2023

Another interesting page that helps understand how coordinates map to pixels: http://html5tutorial.com/how-to-draw-a-point-with-the-canvas-api/.

Anyways, lines and rectangle should behave the same way. However, testing a bit with https://www.w3schools.com/html/tryit.asp?filename=tryhtml5_canvas_tut_path showed that Firefox's rect primitive behaves differently (its is likely a bug in Firefox...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants