Drawing overlapping semi-transparent lines without visible overlap
Asked Answered
E

2

8

I'm developing a painter program using HTML5 canvas. I have created a drawing tool where the user drags and moves the mouse.

I have a listener on mousemove event that draws short lines:

Painter.mainCanvas.beginPath();
Painter.mainCanvas.moveTo(Painter.lastX, Painter.lastY);
Painter.lastX = e.offsetX;
Painter.lastY = e.offsetY;
Painter.mainCanvas.lineTo(Painter.lastX, Painter.lastY);
Painter.mainCanvas.stroke();

Everything works well until I set the global Alpha to < 1. When using this method to draw, the end dot is also start dot. So the dot is drawn twice. And because we have transparent color, the dot now has different color with other dots in line.

I tried another method that when mousemove fires, it only uses lineTo() and stroke() when mouseup fires.

This solves the double drawing problem, but also introduces a new problem: when user intend to draw same dot twice, ie, cross line without mouseup, the dot won't be drawn twice. Because lineTo() function won't draw a dot twice without stroke between.

Eastman answered 7/12, 2011 at 9:18 Comment(4)
For future readers: Below is a link to a related question & answer that deals with painting with transparency and how subpaths may overlay with undesired behavior. https://mcmap.net/q/1323294/-html5-canvas-opacity-problem-with-a-paint-appDiscommend
@Eastman is your paint web-app available anywhere? I'm doing the exact same thing right now, and I had the same dilemma (to make intersecting lines add up or not). So, it seems to me your paint app might be a bit similar. Mine's currently ~900 lines already, and it's working quite well, but I would find it interesting to see your app too. If you want, I'll also post a link to mine when it goes live.Catastrophe
@Catastrophe So bad the project has been shutdown for years. To deal with that problem, you need to NOT use the default lineTo or stroke. Instead, to make lines intersected, you need to draw each line POINT BY POINT. See this: losingfight.com/blog/2007/08/18/…Eastman
@Eastman the way I do it: I getImageData on mousedown, then simply putImageData(oldData) and then stroke. That way, you reset the whole canvas to before a stroke was made, and then re-draw the whole canvas. I'm thinking of switching to a more efficient method though: Put a <svg> on top of the canvas, and direct all drawing to that: Create the line by using <path> and save the coordinates passed by the brush in an array. Then, on mouseup simply remove the <path>, follow the same coordinates on the canvas using lineTos.Catastrophe
R
20

(Restating your problem) Your original problem was that by calling beginPath() and stroke() for each segment you had many overlapping semi-transparent paths. Your new "problem" is that by creating all your lineTo() commands as part of the same path and then calling stroke() once at the end any self-intersecting paths intended by the user do not show a visible overlap.

Here is an example showing the difference between making

  • many semi-transparent lines in a single path and stroking once (upper left), versus
  • many semi-transparent lines in distinct paths and stroking each (bottom right)

                        http://jsfiddle.net/jhyG5/2/

A semi-transparent set of crossing lines (all the same opacity) in the upper left and a set of crossing lines with increasing opacity where they cross in the bottom right.

I would say that your current solution (a single path) is the correct way to do it, even though a single self-crossing path does not double-up in opacity. This is what you see in Adobe Photoshop and Illustrator when drawing semi-transparent paths: all drawing with the mouse down is part of the same, single, non-overlapping transparent object. Only when the user releases and re-presses the mouse button do you accumulate more transparency:

  • Two Photoshop paintbrush strokes at 50% opacity:
    Photoshop Overlapping Style

  • Two Illustrator paths at 50% opacity: Illustrator Overlapping Strokes

Notice in particular that the self-intersecting stroke and path do not show double the opacity during crossing, but that a separate new path does.

I recommend that you stick with your current solution given that this is how these traditional, well-thought-out applications behave. I say this both because you want your package to mimic user expectations, and also because if these packages do it like this, there's probably a very good reason for it: the exact problem you originally had! :)

Riggins answered 7/12, 2011 at 17:24 Comment(1)
I think you're right. Then the problem is gone forever, thanks^^Eastman
T
0

A good workaround is to draw the canvas background colour before drawing each line.

https://jsfiddle.net/gfsrt6L5/

Here is the code I added. See the JS Fiddle

        if (distinctPaths){
            const oldStyle = ctx.strokeStyle;
            ctx.strokeStyle = 'white';
            ctx.moveTo(pts[0][0]+x,pts[0][1]+y);
            ctx.lineTo(pts[1][0]+x,pts[1][1]+y);
            ctx.stroke();
            ctx.beginPath();
            ctx.strokeStyle = oldStyle;
        }

Separate strokes workaround

Tumer answered 12/10, 2023 at 12:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.