Reputation: 41
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.
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:
With transparent pixels:
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.
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
Reputation:
Use two layers to draw to:
lineDash
) (calculate the dash-pattern size based on size requirements)Upvotes: 1
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
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