jonobr1 / two.js

A renderer agnostic two-dimensional drawing api for the web.
https://two.js.org
MIT License
8.27k stars 454 forks source link

Smooth out lines and Round Corners #702

Closed robertcorponoi closed 1 year ago

robertcorponoi commented 1 year ago

Describe your question

I'm making a simple canvas that the user can draw on. This works well for the most part but I notice that when I draw slowly, the line isn't smooth (which I assume is because many smaller lines are being created). Is there a way to make the line smoother, like it is when drawing faster?

Also, I notice that when creating a corner, it sometimes does not make a nice corner but instead it either breaks the line and starts another or it creates a very sharp corner. I'm not sure if this would be solved by the same solution above or not.

These are definitely not bugs but I would appreciate any insight into how I might prevent/improve these issues with my use case.

Your code (either pasted here, or a link to a hosted example)

This is how the lines are drawn in my function that runs when the user clicks down and drags their mouse on the canvas.

const onDrag = (dragX: number, dragY: number) => {
    x = dragX;
    y = dragY;

    if (!line) {
        const v1 = new Two.Anchor(mouse.x, mouse.y);
        const v2 = new Two.Anchor(x, y);

        line = makeCurve([v1, v2]);
        line.noFill().stroke = "#333";
        line.linewidth = 10;
        line.cap = "round";
        backgroundGroup.add(line);

        line.vertices.forEach((vertex: Vector) =>
            vertex.addSelf(line!.translation),
        );
        line.translation.clear();
    } else {
        const v1 = new Two.Anchor(x, y);

        line.vertices.push(v1);
    }

    mouse.set(x, y);
};

Screenshots

Drawing slowly vs. fast

Screenshot 2023-04-28 at 12 51 41 PM

Corners

Screenshot 2023-04-28 at 12 52 56 PM

Environment (please select one):


If applicable:

Desktop (please complete the following information):

jonobr1 commented 1 year ago

Thanks for your questions. This is a big one to unpack. Hopefully this can get our conversation pointed in the right direction:

Drawing Slow vs Fast You have the right idea. When you draw slowly, there are more vertices (or Two.Anchors) added to the line. The closer together these points are, the less "curvy" or "flow" the line will be. There are a couple of ways to address.

  1. You could keep track of the distance between the the latest point in the vertices array and the current dragX dragY. Then only append Two.Anchors when the distance crosses a threshold, like 25px.
  2. The way that Photoshop and Illustrator do this is by having a line drawn at runtime like you do, but making it 1px wide so that the user can see where they're drawing. Then when you release your line (or at some delayed time from the drag event) you update the another line that is the smoothed line. This one would calculate the slope between a cluster of points and use that to determine what Two.Anchors are added.

Example 1. snippet would look like this:

const onDrag = (dragX: number, dragY: number) => {
    x = dragX;
    y = dragY;

    if (!line) {
        const v1 = new Two.Anchor(mouse.x, mouse.y);
        const v2 = new Two.Anchor(x, y);

        line = makeCurve([v1, v2]);
        line.noFill().stroke = "#333";
        line.linewidth = 10;
        line.cap = "round";
        backgroundGroup.add(line);

        line.vertices.forEach((vertex: Vector) =>
            vertex.addSelf(line!.translation),
        );
        line.translation.clear();
    } else {
        const v1 = new Two.Anchor(x, y);
        if (line.vertices[line.vertices.length - 1].distanceBetween(v1) > 25) {
            line.vertices.push(v1);
        }
    }

    mouse.set(x, y);
};

Corners This occurs because, again when drawing slowly vs fast, there are more points close to each other. The closer the points are to each other, the more information the browser has to infer the miter of the line. More information about how that works in this blog post: https://www.kirupa.com/canvas/modifying_how_corners_look.htm

Changing your code from above should make it more consistent, but also adjusting the line.miter and line.join property can give you different effects. Much of the projects I make in Two.js I set line.join = 'round'; which softens corners like that.

Hope this helps!

robertcorponoi commented 1 year ago

Ah yeah that makes a lot of sense, thank you for the explanation!

Your second point Photoshop and Illustrator is really interesting, I'm going to start with the modifications in code snippet you provided but I like the idea of the temporary line with the smoothing applied at the end so I'll pursue that longer term.

As for the corners, I'm not sure how I missed line.join but setting it to round solves most of my problems.

Thanks again for the guidance and the code snippet and whenever I get the Photoshop/Illustrator line smoothing effect I'll submit it as a community example.