Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw stroke on HTML canvas with different levels of opacity

The problem

I'm trying to create a brush tool with opacity jitter (like in Photoshop). The specific problem is:

Draw a stroke on an HTML canvas with different levels of opacity. Pixels with higher opacity should replace pixels with lower opacity; otherwise, pixels are left unchanged.

Transparency should not be lost in the process. The stroke is drawn on a separate canvas and merged with a background canvas afterwards.

The result should look like this. All code and the corresponding output can be found here (JSFiddle).

Because you can't stroke a single path with different levels of opacity (please correct me if I'm wrong) my code creates a path for each segment with different opacity.

Non-solution 1, Using the 'darken' blend mode

The darken blend mode yields the desired result when using opaque pixels but doesn't seem to work with transparency. Loosing transparency is a dealbreaker.

With opaque pixels:

enter image description here

With transparent pixels:

enter image description here

Non-solution 2, Using the 'destination-out' compositing operator

Before drawing a new stroke segment, subtract its opacity from subjacent pixels by using the 'destination-out' compositing operator. Then add the new stroke segment with 'source-over'. This works almost but it's a little bit off.

enter image description here

Looking for a solution

I want to avoid manipulating each pixel by hand (which I have done in the past). Am I missing something obvious? Is there a simple solution to this problem?

"Links to jsfiddle.net must be accompanied by code."
like image 523
Another Guy Avatar asked Sep 17 '25 20:09

Another Guy


2 Answers

Because you can't stroke a single path with different levels of opacity (please correct me if I'm wrong)

You're wrong =)

When you use globalCompositeOperation = 'destination-out' (which you are in lineDestinationOut) you need to set the strokeStyle opacity to 1 to remove everything.

However, simply changing that in your fiddle doesn't have the required effect due to the order of your path build. Build the 10% transparent one first, the whole length, then delete and draw the two 40% transparent bits.

Here's a jsfiddle of the code below

var canvas = document.getElementById('canvas');
var cx = canvas.getContext('2d');
cx.lineCap = 'round';
cx.lineJoin = 'round';
cx.lineWidth = 40;

// Create the first line, 10% transparency, the whole length of the shape.
cx.strokeStyle = 'rgba(0,0,255,0.1)';
cx.beginPath();
cx.moveTo(20,20);
cx.lineTo(260,20);
cx.lineTo(220,60);
cx.stroke();
cx.closePath();

// Create the first part of the second line, first by clearing the first
// line, then 40% transparency.
cx.strokeStyle = 'black';
cx.globalCompositeOperation = 'destination-out';
cx.beginPath();
cx.moveTo(20,20);
cx.lineTo(100,20);
cx.stroke();
cx.strokeStyle = 'rgba(0,0,255,0.4)';
cx.globalCompositeOperation = 'source-over';
cx.stroke();
cx.closePath();

// Create the second part of the second line, same as above.
cx.strokeStyle = 'black';
cx.globalCompositeOperation = 'destination-out';
cx.beginPath();
cx.moveTo(180,20);
cx.lineTo(260,20);
cx.stroke();
cx.strokeStyle = 'rgba(0,0,255,0.4)';
cx.globalCompositeOperation = 'source-over';
cx.stroke();
cx.closePath();
like image 194
Henry Blyth Avatar answered Sep 20 '25 10:09

Henry Blyth


Use two layers to draw to:

  • First calculate the top layer opacity 40% - 10% and set this as alpha on top layer
  • Set bottom layer to 10%
  • Set top layer with dashed lines (lineDash) (calculate the dash-pattern size based on size requirements)
  • Draw lines to both layers and the bottom layer will be a single long line, the top layer will draw a dashed line on top when stroked.
  • Copy both layers to main canvas when done.