Eddie Dane
Eddie Dane

Reputation: 1569

HTML Canvas: ctx.stroke restroke behaviour with transparent colors

I am working on a sketch tool with html canvas.

I am using a common algorithm for this, that uses the mousedown, mousemove, mouseup events.

mousedown I beginPath(), and moveTo(// the mouse coordinates).

mousemove I draw lineTo(// the mouse coordinates), and then stoke(// the line to render it)

mouseup I do nothing, // closePath()

And I noticed that, calling the stroke method without first calling closePath or beginPath, will redraw or restroke all previous paths or lines, which makes them appear thicker than the define color.

without a transparent color its is barely noticeable, but the colors do appear thicker than they should be.

but with color with transparency|alpha e.g. rgba(). The most recent path or line respects the color's transparency, but all previous line due to the redraw, the transparent colored line overlap and that causes previous lines to get thicker in sequence or succession.

is there a way to avoid|prevent this behavior. thank in advance. sample is below, try drawing really fast!

var cvs = document.querySelector("canvas");
    cvs.width = cvs.parentElement.clientWidth;
    
var colorInput = document.querySelector("input");
    
var ctx = cvs.getContext("2d");

ctx.strokeStyle = "rgba(0, 0, 0, 0.4)"
ctx.lineWidth = 20;

onDraw(cvs, {

    penDown: function(e) {
        var x = e.pageX - this.offsetLeft;
        var y = e.pageY - this.offsetTop;
        
        ctx.strokeStyle = colorInput.value;
        
        ctx.beginPath();
        ctx.moveTo(x, y);
    },
    
    penMove: function(e) {
        var x = e.pageX - this.offsetLeft;
        var y = e.pageY - this.offsetTop;
        
        ctx.lineTo(x, y);
        ctx.stroke();
    },
    
    penUp: function() {
        // ctx.closePath;
    }

});


function onDraw(node, drawHandler, beginHandler, endHandler, outOfBoundHandler, sticky) {
		var mouseDown = false, mouseOut = false;

		if( typeof drawHandler === "object" ) {
			var drawEvents = drawHandler;

			drawHandler = get(drawEvents.penMove);
			beginHandler = get(drawEvents.penDown);
			endHandler = get(drawEvents.penUp);
			outOfBoundHandler = get(drawEvents.penOff);
			sticky = drawEvents.sticky;
		}

		function get(name) {
			return typeof name === "string" ? drawEvents[ name ] : name;
		}

		node.addEventListener('mousedown', function(e) {
			mouseDown = true;
			beginHandler&&beginHandler.call(this, e);
		});

		node.addEventListener('mousemove', function(e) {
			mouseDown&&drawHandler&&drawHandler.call(this, e);
		});

		node.addEventListener('mouseup', function(e) {
			mouseDown = false;
			endHandler&&endHandler.call(this, e);
		});

		node.addEventListener('mouseout', function(e) {
			mouseDown&&outOfBoundHandler&&outOfBoundHandler.call(this, e);

			if( !sticky ) {
				mouseDown = false;
			}
		});
	}
.wrapper { border: 1px solid #aaa }
<div class="wrapper">
    <canvas border="1" width="600" hieght="400">Canvas is not supported</canvas>
    <input type="text" value="rgba(0, 0, 0, 0.3)" placeholder="rgba(#, #, #, #)">
</div>

Upvotes: 1

Views: 728

Answers (1)

Kaiido
Kaiido

Reputation: 136678

If no Path argument is passed to stroke and fill methods they will use the path currently being declared with the context's drawing methods.

const ctx = c.getContext('2d');
// starts Path declaration
ctx.moveTo(20, 20);
ctx.lineTo(30, 80);

ctx.stroke(); // first rendering

setTimeout(() => {
  ctx.clearRect(0, 0, 300, 150); // even if we clear the canvas
  ctx.lineTo(70, 20); // this will continue path declaration

  setTimeout(() => {
    ctx.stroke(); // and this will draw everything
  }, 1000);
}, 1000);
<canvas id="c"></canvas>

The only ways to start a new path declaration (except for the first one) are to either reset the whole context (not good), or to use beginPath method.

const ctx = c.getContext('2d');
// starts Path declaration
ctx.moveTo(20, 20);
ctx.lineTo(30, 80);

ctx.stroke(); // first rendering

setTimeout(() => {
  ctx.clearRect(0, 0, 300, 150);
  ctx.beginPath(); // start a new Path declaration
  ctx.moveTo(30, 80); // we need to move to the previous coords
  ctx.lineTo(70, 20); // this will be alone
  ctx.stroke(); // and this will draw only the new path
}, 1000);
<canvas id="c"></canvas>

About closePath, it's just a lineTo(last_point_in_current_path_declaration), and doesn't ends a path declaration in no way.

So for your problem, there are two strategies you can adopt :

  • keep only the last coordinates, and at every mousemove,

    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(nextX, nextY);
    
  • keep all your coordinates in an array and redraw everything every time

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();
    coords.forEach(pt => ctx.lineTo(pt.x, pt.y));
    ctx.stroke();
    

Personally, I prefer the second one, which allows some undo - redo, and to e.g change your pen's style.

Upvotes: 3

Related Questions