Draw stroke on HTML canvas with different levels of opacity - html

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:
With transparent pixels:
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.
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."

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

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.

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

Related

html canvas transform ? is it only: first do the transform, and then you draw into the canvas? Not, stick an image or etc on canvas, and transform it?

My goal is to bring in an image on to part of the canvas, then scale it, move/translate it, and optionally skew it, also rotate and make alpha changes, kind of the primary "2d image manipulations", in an animated form, which is: do little changes over time from the starting state to the target end state.
Well, I figured to be efficient, I should use the canvas/2d context transform, https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform -- as it does the first 3: scale, move/translate, and skew, "all in one." I did half that code, and now I'm looking at examples and seeking to debug it. All the examples I see, are do 1) some transform, away from the "unity transform":
{ a:1, b:0, c: 0, d:1, e:0, f:0 }; // this basic transform does nothing
and then 2) draw into that. But that's the opposite order from what I want: which is draw on the canvas (the image), and then do an animation over time using the above primary changes (scale, translate, skew, rotate, and alpha). My question is: does it only "work this way", meaning I must setup the (single) transformation on the page first, and then "draw into that?"
I hope not ... that won't give me what I want, and I have to "ditch it", and go to 5 individual "transformations." Comments?
Yes that only works this way, canvas transforms and compositing mode and filters and lineWidth and fillStyle etc. properties are only applied to the next drawing operations.
The canvas itself only holds pixels information, it has no concept of drawn object. Your js code has to do this part.
So for what you wish, you can simply redraw everything every time:
reset the transform so we can clear ctx.setTransform(1,0,0,1,0,0);
Clear the canvas ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height)
Set the transform to your new matrix ctx.translate(x,y); ctx.scale(s)...
Draw your transformed graphics ctx.fill(); ctx.drawImage(...
Wait next frame to do it again requestAnimationFrame(update)

Reverse Clipping in Canvas

I want to clip html5 canvas so that I can achieve drawing result as per following image.
I want to achieve clip path such that all drawing will be performed only in black area.
Method 1
Assuming the white areas are transparent and the black is non-transparent then you can use composite mode:
ctx.globalCompositeOperation = 'source-in';
... draw graphics on top - only solid color will be affected ...
ctx.globalCompositeOperation = 'source-over'; // reset to default mode
A demo fiddle for this here
Method 2
Another simpler approach is to simply fill the canvas with the graphics you need then use clearRect() for those areas you want transparent.
This operation is fairly fast so there shouldn't be any flashing (and in case you can trigger this operation with a single requestAnimationFrame call).
A demo fiddle with clearRect
A demo fiddle with clearRect + requestAnimationFrame
Just note that calling rAF makes the code asynchronous but the purpose of using it is that your draw operations are synchronized within a frame update so flicker will be removed (if you should for some reason get problem with that).
Method 3
Create rectangle regions surrounding the area you want to preserve by a series of call to rect(). The set that as a clipping mask by using clip().
This techniques works best if the non-clipped areas are in a certain order or else you would have to define a lot of regions.
Remember to translate canvas 0.5 pixel first and only use integer values for the rectangles.
Method 4
Parse the pixel buffer manually to fill in pixels in the areas fulfilling the requirements, for example non-transparent pixels only.
Just be aware of that this is probably the slowest approach, it's affected by CORS restrictions (in case you draw an external image onto the canvas first) and it's more tedious if you want to fill in shapes, images, gradients and so forth which in case you would probably prefer an off-screen canvas to copy from.
There are other ways using different composite modes and drawing order to achieve the same result but I leave it with this as it should cover most scenarios.
You can use layering to fill your need:
make a copy of your image with all black made transparent
draw the original image on the canvas
draw your desired shapes
draw the transparent image on top
A Demo: http://jsfiddle.net/m1erickson/dFRUf/
This function creates a temporary canvas with the color-range you specify made transparent:
function makeImageTransparentByColor(image,r1,r2,g1,g2,b1,b2){
// create a temporary canvas and draw the image on the canvas
var bk=document.createElement("canvas");
var bkCtx=bk.getContext("2d");
bk.width=image.width;
bk.height=image.height
bkCtx.drawImage(image,0,0);
// get the pixel array from the canvas
var imgData=bkCtx.getImageData(0,0,bk.width,bk.height);
var data=imgData.data;
// loop through each pixel and make every pixel transparent
// that is between r1-r2, g1-g2 and b1-b2
for(var i=0;i<data.length;i+=4){
var r=data[i];
var g=data[i+1];
var b=data[i+2]
if(r>=r1 && r<=r2 && g>=g1 && g<=g2 && b>=b1 && b<=b2){
data[i]=0;
data[i+1]=0;
data[i+2]=0;
data[i+3]=0;
}
}
// put the modified pixels back on the canvas
bkCtx.putImageData(imgData,0,0);
// return the canvas with transparent image
return(bk);
}

The intersection part of CSS Canvas paths seem to have incorrect color.

I'm trying to draw a path using HTML Canvas. It consists of several Bezier curves linked together. For some reason, I cannot draw the whole path and then stroke. Instead, I need to stroke for each Bezier curve. I'm using a light purple color as the stroke color, but at the intersection of the curves, I seem to get something like white instead of the light purple I expect. Like this (sorry I can't post an image since I'm new on Stack Overflow):
I'm using stroke style with opacity 1, so I believe it's not a transparency issue. So what might be causing this problem?
FYI, I'm drawing each Bezier curve with code like this, where a is the drawing context of the canvas, and this.bloom.c is something like "rgba(xxx,xxx,xxx,1)":
a.strokeStyle = this.bloom.c;
a.beginPath();
a.moveTo(e.x, e.y);
a.bezierCurveTo(c.x, c.y, b.x, b.y, d.x, d.y);
a.stroke();
Thanks very much!
Use the appropriate "blend modes" natively supported by HTML5 Canvas context for Composite Operations. In your case you may use 'source-over'
For example:
var context = document.getElementById('myCanvas').getContext('2d');
context.globalCompositeOperation = 'source-over';
See Compositing and Blending 1.0
source-atop
source-in
source-out
source-over
destination-atop
destination-in
destination-out
destination-over
lighter
xor
copy

HTML5 Canvas and Line Width

I'm drawing line graphs on a canvas. The lines draw fine. The graph is scaled, every segment is drawn, color are ok, etc. My only problem is visually the line width varies. It's almost like the nib of a caligraphy pen. If the stroke is upward the line is thin, if the stroke is horizontal, the line is thicker.
My line thickness is constant, and my strokeStyle is set to black. I don't see any other properties of the canvas that affect such a varying line width but there must be.
Javascript:
var badCanvas = document.getElementById("badCanvas"),
goodCanvas = document.getElementById("goodCanvas"),
bCtx = badCanvas.getContext("2d"),
gCtx = goodCanvas.getContext("2d");
badCanvas.width = goodCanvas.width = badCanvas.height = goodCanvas.height = 300;
// Line example where the lines are blurry weird ect.
// Horizontal
bCtx.beginPath();
bCtx.moveTo(10,10);
bCtx.lineTo(200,10);
bCtx.stroke();
//Verticle
bCtx.beginPath();
bCtx.moveTo(30,30);
bCtx.lineTo(30,200);
bCtx.stroke();
// Proper way to draw them so they are "clear"
//Horizontal
gCtx.beginPath();
gCtx.moveTo(10.5,10.5);
gCtx.lineTo(200.5,10.5);
gCtx.stroke();
//Verticle
gCtx.beginPath();
gCtx.moveTo(30.5,30);
gCtx.lineTo(30.5,200);
gCtx.stroke();
// Change the line width
bCtx.lineWidth = 4;
gCtx.lineWidth = 4;
// Line example where the lines are blurry weird ect.
// Horizontal
bCtx.beginPath();
bCtx.moveTo(10,20.5);
bCtx.lineTo(200,20.5);
bCtx.stroke();
//Verticle
bCtx.beginPath()
bCtx.moveTo(50.5,30);
bCtx.lineTo(50.5,200);
bCtx.stroke();
// Proper way to draw them so they are "clear"
//Horizontal
gCtx.beginPath();
gCtx.moveTo(10,20);
gCtx.lineTo(200,20);
gCtx.stroke();
//Verticle
gCtx.beginPath();
gCtx.moveTo(50,30);
gCtx.lineTo(50,200);
gCtx.stroke();
HTML:
<h2>BadCanvas</h2>
<canvas id="badCanvas"></canvas>
<h2>Good Canvas</h2>
<canvas id="goodCanvas"></canvas>
CSS:
canvas{border:1px solid blue;}
Live Demo
My live demo basically just recreates what the MDN says. For even stroke widths you can use integers for coordinates, for odd stroke widths you want to use .5 to get crisp lines that fill the pixels correctly.
From MDN Article
If you consider a path from (3,1) to (3,5) with a line thickness of
1.0, you end up with the situation in the second image. The actual
area to be filled (dark blue) only extends halfway into the pixels on
either side of the path. An approximation of this has to be rendered,
which means that those pixels being only partially shaded, and results
in the entire area (the light blue and dark blue) being filled in with
a color only half as dark as the actual stroke color. This is what
happens with the 1.0 width line in the previous example code.
To fix this, you have to be very precise in your path creation.
Knowing that a 1.0 width line will extend half a unit to either side
of the path, creating the path from (3.5,1) to (3.5,5) results in the
situation in the third image — the 1.0 line width ends up completely
and precisely filling a single pixel vertical line.
If linewidth is an odd number, just add 0.5 to x or y.
I just solved a problem of a similar nature. It involved a bug in a For loop.
PROBLEM: I had created a for loop to create a series of connected line segments and noticed that the line was thick to start but thinned out significantly by the final segment (no gradients were explicitly used).
FIRST, DEAD END THOUGHT: At first I assumed it was the above pixel issue, but the problem persisted even after forcing all the segments to remain at a constant level.
OBSERVATION: I noticed that I made a newbie's mistake -- I only used a single "ctx.beginPath()" and "ctx.moveTo(posX,posY)" PRIOR to the For loop and a single "ctx.stroke()" AFTER the For loop and the loop itself wrapped a single ctx.lineTo().
SOLUTION: Once I moved all methods (.beginPath(), .moveTo(), .lineTo() and .stroke()) together into the For loop so they would all be hit on each iteration, the problem went away. My connected line had the desired uniform thickness.
Try lineCap = "round" and lineJoin = "round". See "Line Styles" in this PDF to see what these parameters do.
Edit 17-July-2015: Great cheat sheet, but the link is dead. As far as I can tell, there's a copy of it at http://www.cheat-sheets.org/saved-copy/HTML5_Canvas_Cheat_Sheet.pdf.

Custom Line stroke in HTML5 canvas

When drawing lines with the HTML5 canvas element, is it possible to define the stroke style of the lines? Basically in Photoshop and other similar programs, you can define a stroke style for lines that looks like it is "hand drawn". Is is possible to do anything like that in HTML5 canvas or am I shooting for the moon here?
Thanks
-Jesse
It is possible but not by default. See ShadowCloud's post for what you can do by default (very little).
Depending on what you want, it shouldn't be too hard.
If by "hand drawn" you mean you want jitter, you'd have to break up every drawn line/curve into smaller parts and add some noise to each of the points.
If you want a brush you'd have to break up every drawn line/curve into smaller parts and call drawImage every few pixels to emulate a photoshop brush.
Almost all of them rely on breaking up your lines and curves into smaller bits, so you should figure that out foremost.
If you decide to implement these and are having trouble breaking up bezier curves and want help, let me know and I'll give you my code for that.
There is no standard API in HTML5 Canvas to manage such thing.
You can just set the color or the width of the stroke, for example:
context.strokeStyle = '#f00'; // red color
context.lineWidth = 4; // 4px wide
// Draw some rectangles.
context.fillRect (0, 0, 100, 100);
context.strokeRect(0, 0, 100, 100);
You can try to get more control using a library (Processing.js or Fabric.js)