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);
}
Related
How can I make a canvas for the video to fit perfectly in the area
See picture
...It can't be done with native Html5 Canvas transformations.
You are trying to transform the display Canvas content (== your video) into a non-parallelogram -- which is not possible with native Canvas transformations. The Canvas can only be reshaped into parallelograms.
But...
You can skew the canvas to approximate your desired display.
The gold parallelogram in this image can be done with native Canvas transformations
Example code an a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var angle=-Math.PI*.06;
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/billboard1.jpg";
function start(){
cw=canvas.width=img.width;
ch=canvas.height=img.height;
ctx.drawImage(img,0,0);
ctx.transform(1,Math.tan(angle),0,1,0,0);
ctx.strokeStyle='gold';
ctx.lineWidth=4;
ctx.strokeRect(333,135,275,150);
}
<canvas id="canvas" width=300 height=300></canvas>
Drawing process
Prepare the background image offline by removing the red area making an alpha channel out of it instead, then use it like this:
Draw the video to canvas with transformation applied matching closely the shape plus a little overlap
Draw the background (or parts of it after first draw) on top of the video. This will mask the edges of the video that doesn't fit perfectly.
Transformation
Offline process:
Plot the points of the (now) alpha area so you have the coordinates, either a triangle or a quadrilateral. Note: canvas only support affine transformations (2D/parallelograms) so triangle has to be used. For CSS you can use 3D transforms with basis in a quadrilateral shape.
Normalize the values
In action:
Scale the canvas and the normalized values to the desired size
Apply transform, draw video, draw background as overlay on top of video
And since most video is max 30 frames per second you can also throttle the drawing loop to half to leave more resources for other things (use requestAnimationFrame() with a toggle flag).
Alternative approach:
Prepare a video with everything setup as you want. It should compress well since the surrounding areas don't change (use a long key-frame interval), and in this case the background doesn't contain much details due to low depth-of-field which also helps.
I split my canvas into two. I need to draw on each side, defining a clipping region for both. Each side has to do similar things, like drawing text in colours, drawing circles etc.
I have read that you should keep your fillStyle and strokeStyle changes to a minimum. However I have also read you must keep your save and restores to a minimum as well.
So what is faster?
Save the canvas, clip the left hand side, do ALL the drawing for that side for the multiple colours, then restore and repeat for the right hand side?
Or ... Set the first text colour. Clip the left hand side. Draw all text for this colour. Then clip the right hand side (without restting fillStyle) and draw all the same colour text for the right hand side. Then set the next fillStyle and then clip each side and draw text for this colour? Etc
Anyone know?
Also if I set one clip region, then set another clip region without saving and restoring, what actually happens?
As always, you must performance test with your own project's code.
The context maintains internal variables relating to its current state (colors, transforms, current path, compositing applied, etc).
You can save a copy of the current context state using context.save. Then after altering the context state, you can restore the original context state with context.restore. Note: you can nest multiple save/restore if needed.
fillStyle is a context state so changing fillStyle='green' followed later by fillStyle='blue' is faster than save/restore because just 1 state variable is being reset rather than every state variable that's done during save/restore.
The point is that you often gain performance by saving+resetting individual state values rather than doing a full context.save/context.restore.
Minimizing state changes will maximize performance, so for example, batching all green drawings will help performance.
Clipping is a complex operation and is more expensive than simple state changes like changing fillStyle. With GPU acceleration the clipping is accomplished much more efficiently and is therefore considerably less costly than clipping without a GPU.
About changing clipping regions: Clipping is always done only on the last path defined. So setting another clipping region will undo the previous clipping region (unless the previous and current path are identical).
About left and right clipping regions: If your design permits, you might want to define both the left and right clipping regions at once into 1 combined clipping region. Since paths can be disconnected, defining 1 clipping path with 2 non-intersecting parts is allowed.
As the car companies warn: "Your mileage may vary". But you might perf test this method:
Define your left and right clipping regions in 1 clipping path
Set fillStyle="red" and do all your red drawings at once (both right and left)
Reset fillStyle="blue" and do all your blue drawings at once, etc, etc.
One final thought: All projects have their own distinct requirements so you must perf test your actual code rather than relying exclusively on general rules. Don't skip the perf tests--especially if you will be deploying on mobile where canvas is slow by nature.
Good luck with your project!
[ Additional note about clipping ]
A clipping region is semi-permanent. Once it is set it remains even if the context.clip command is issued again. The new clip command will further restrict all previous clipping regions.
To clear a clipping path you must wrap your clips inside context.save & context.restore(). Alternatively, resizing the canvas will force the context state to be restored to defaults--but resizing will also clear all canvas content: canvas.width=canvas.width
Here's and example of a single path consisting of a left and right square:
// left square
ctx.beginPath();
ctx.moveTo(50,50);
ctx.lineTo(150,50);
ctx.lineTo(150,150);
ctx.lineTo(50,150);
ctx.closePath();
// right square
ctx.moveTo(200,50);
ctx.lineTo(300,50);
ctx.lineTo(300,150);
ctx.lineTo(200,150);
ctx.closePath();
ctx.strokeStyle="red";
ctx.stroke();
Create a clipping region from this left+right path.
// create a clipping region from the left+right path
ctx.clip();
Fill the entire canvas with green. The green will only be drawn inside the clipping region (inside the left and right squares).
// draw a green rect over the entire canvas
// the green will only be drawn inside the clipping region
ctx.fillStyle="green";
ctx.fillRect(0,0,canvas.width,canvas.height);
If you add a second clipping region, the result is that drawing will only be visible in the union of all clipping paths.
So if the second clipping region slightly overlaps the first, all drawings will only be visible inside the union of the first and second clipping regions. The blue section in the following illustration is the union of the 2 clipping paths.
ctx.beginPath();
ctx.moveTo(0,115);
ctx.lineTo(canvas.width,115);
ctx.lineTo(canvas.width,135);
ctx.lineTo(0,135);
ctx.clip();
ctx.closePath();
ctx.fillStyle="blue";
ctx.fillRect(0,0,canvas.width,canvas.height);
A few ideas :
To handle your two screens, you might use two canvases side by side.
Even if you keep only one canvas, you can reduce the use of clipping by doing :
- erase left half of screen
- draw left part
- erase right part
- clip on right half
- draw right part
Thus you clip only once.
For your color vs clip concern, clipping is much costly than changing color.
And if one keeps on clipping with no save/restore, clipping zones will add-up.
For your strokeStyle/fillStyle, yes there's a cost, especially if using color names ('blue'), or rgb 'rgb()' strings or worse 'hsl()' strings.
So here's a trick : pre-compute and store the colors whenever you can. Simply use the context to convert !
var blue = 'hsl(23, 75%, 75%)';
context.fillStyle = blue ;
blue = context.fillStyle ;
I am currently programming a little game using canvas. For the game I need some kind of fog which hides the most part of the map and only a small area around the player should be visible. Therfor I use a second canvas to overlay the one where the game takes place and fill it with a gradient (from transparent to black):
function drawFog(){
fogc.clearRect(0,0,700,600);
// Create gradient
var grd=fogc.createRadialGradient(player.getPosX(),player.getPosY(),0,player.getPosX(),player.getPosY(),100);
grd.addColorStop(0,"rgba(50,50,50,0)");
grd.addColorStop(1,"black");
// Fill with gradient
fogc.fillStyle=grd;
fogc.fillRect(0,0,700,600);
}
Unfortunatly this is causing huge perfomance problems since it will be redrawn for every frame.
I wanted to ask if there might be a better solution to achieve the same effect with a better performance.
Cache the gradient to an off-screen canvas then draw in the canvas with drawImage() instead:
Create an off-screen canvas the size of the fog
Draw in the gradient
Use off-screen canvas as an image when you need the fog.
This way the processing creating and calculating the gradient is eliminated. Drawing an image is basically a copy operation (there is a little bit more, but performance is very good).
function createFog(player) {
// Create off-screen canvas and gradient
var fogCanvas = document.createElement('canvas'),
ctx = fogCanvas.getContext('2d'),
grd = fogc.createRadialGradient(player.getPosX(),
player.getPosY(),
0,player.getPosX(),
player.getPosY(),100);
fogCanvas.width = 700;
fogCanvas.height = 700;
grd.addColorStop(0,"rgba(50,50,50,0)");
grd.addColorStop(1,"black");
// Fill with gradient
ctx.fillStyle = grd;
ctx.fillRect(0,0,700,600);
return fogCanvas;
}
Now you can simply draw in the canvas returned from the above function instead of creating the gradient every time:
var fog = createFog(player);
ctx.drawImage(fog, x, y);
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.
I'm working on a simple image crop where the user draws a line with the mouse around an area that they want to keep. When they confirm, the rest of the image will be cropped out. Here's how I'm currently handling said cropping:
var data = c.getImageData(0,0,canvas.width,canvas.height);
for (var x = 0; x < data.width; x++) {
for (var y = 0; y < data.height; y++) {
if (!c.isPointInPath(x,y)) {
var n = x + (data.width * y);
var index = n*4;
data.data[index+3] = 0;
}
}
}
However, this can bog down really quickly. The less of the image you try to retain, the faster it goes, but even saving 30% of the image (canvas is 800x800) causes it to hang for several seconds. Is there a faster way to go about this?
I don't really understand why you are diving into pixel details to manipulate your cropping image functionality. It's understandable as bigger the image is get as more time is needed for cropping out the rest of the image, because practically with iterating over a two dimensional array of pixels the processing time needed for the operation is exponentially increasing with the increasing in size of the pixels map.
So my suggestion would be to try to remake the function without to even touch the getImageData and putImageData function. It's useless. I would make in the following way:
Obtain the pixel coordinates at the mouse down.
Create an event listener for the mouse move.
Create a semi-transparent image over the original image and use the fillRect function to draw into the created canvas.
Create an event listener for mouse up.
Obtain the pixel coordinates at the mouse up.
Calculate the coordinates of the resulting square.
Draw the resulting image into the canvas using as parameters the square coordinates.
As a final step draw the content of the canvas to an image.
This way you will save a lot of overhead on image cropping processing.
Here is a script for your reference: https://github.com/codepo8/canvascropper/blob/master/canvascrop.js
There is no real way to speed it up when you have to use a user defined shape, but the bogging down can be handled with a worker.
Some ideas:
Restrict getImageData to the bounding box of the polygon the user draws.
Put data.height, data.width etc. used inside the loop in a variable.
Maybe you can split up inside/outside tests and setting the imagedata alpha value.
Maybe even draw the polygon to a black and white imagedata object, then get the difference between the two?
Can you share the isPointInPath(x,y) function?