Another Guy
Another Guy

Reputation: 41

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."

Upvotes: 3

Views: 2446

Answers (3)

user1693593
user1693593

Reputation:

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.

Upvotes: 1

Hylianpuffball
Hylianpuffball

Reputation: 1561

@HenryBlyth's answer is probably the best you're going to get; there's no native API to do what you're being asked to do (which, in my opinion, is kinda weird anyways... opacity isn't really supposed to replace pixels).

To spell out the solution in one paragraph: Split up your "stroke" into individual paths with different opacities. Draw the lowest opacity paths as normal. Then, draw the higher opacities with "desitination-out" to remove the low-opacity paths that overlap. Then, draw the high opacity paths as usual, with "source-over", to create the effect desired.

As suggested in the comments to that answer, @markE's comment about making each path an object that is pre-sorted before drawing is a great suggestion. Since you want to perform manual drawing logic that the native API can't do, turning each path into an object and dealing with them that way will be far easier than manually manipulating each pixel (though that solution would work, it could also drive you mad.)

You mention that each stroke is being done on another canvas, which is great, because you can record the mouseevents that fire as that line is being drawn, create an object to represent that path, and then use that object and others in your "merged" canvas without having to worry about pixel manipulation or anything else. I highly recommend switching to an object-oriented approach like @markE suggested, if possible.

Upvotes: 0

Henry Blyth
Henry Blyth

Reputation: 1750

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();

Upvotes: 4

Related Questions