In attempting to get a drawing of a canvas and redrawing it onto the same canvas later is giving unexpected behavior. See Jsfiddle for example.
The logic is fairly straight forward:
Given an image on the canvas, and translation to the center (I'm using a rectangle to demonstrate).
Clip out a section of the image. I'm simply copying the entire "image" in the example.
Possibly do manipulation.
Paste back to the same canvas after clearing it. The point being to clip a section of the original image at a given point with a width / height, and paste it later with modifications.
var c=document.getElementById("cvs1"),
ctx = c.getContext("2d"),
bufferCvs = document.createElement('canvas'),
bufferCtx = bufferCvs.getContext('2d');
bufferCvs.width = c.width;
bufferCvs.height = c.height;
ctx.translate( c.width/2, c.height/2 );
ctx.fillStyle="#FF0000";
ctx.fillRect( -75, -50 ,150,100);
//Image data experiment
//Set the buffer width / height same size as rectangle
bufferCvs.width = 150;
bufferCvs.height = 100;
//Draw the "image" from the first context to the buffer by clipping the first, and pasting to 0,0 width the same "image" dimensions.
bufferCtx.drawImage( c, -75, -50, 150, 100, 0, 0, 150, 100 );
//clear out old canvas drawing
ctx.save()
ctx.setTransform(1,0,0,1,0,0,1,0,0);
ctx.clearRect(0, 0, c.width, c.height);
ctx.restore();
ctx.drawImage( bufferCvs, -75, -50, 150, 100 );
Since I'm keeping the exact same coordinates / dimensions, the expected output would be what was originally on canvas to begin with. However, only part of the upper left corner is drawn (see fiddle). I'm using drawImage for efficiency reasons, but I've used get/putImageData with the same results. Both width and height are defined as mentioned to fix other strange behaviors.
How would you go about making sure everything stored in the buffer canvas is drawn, instead of just the top corner?
Edit:
To help with my question and what behavior I believe is going on I'll post some screens.
Step 1:
Translate context 1 to the center, and draw a rectangle to represent an image.
ctx.translate( c.width/2, c.height/2 );
ctx.fillStyle="#FF0000";
ctx.fillRect( -75, -50 ,150,100);
Step 2:
Use drawImage to "clip" from the -75, -50 point and cut out just the rectangle using the width 150, 100. This should be drawn onto the canvas buffer, but it is not
bufferCvs.width = 150;
bufferCvs.height = 100;
//Draw the "image" from the first context to the buffer by clipping the first at -75, -50 (the start of the image), and pasting to 0,0 width the same "image" dimensions.
bufferCtx.drawImage( c, -75, -50, 150, 100, 0, 0, 150, 100 );
I would expect the buffer canvas to look like this (It is not):
However, if I change the drawImage to
bufferCtx.drawImage( c, 0, 0, 150, 100, 0, 0, 150, 100 );
I get an expected amount of white space on the buffer (and the last drawImage back to the context without issue)
Step 3: Clear out the old "image" from the first context. This shouldn't change the translation since I restore the context state after performing the clear. (This works as expected)
ctx.save()
ctx.setTransform(1,0,0,1,0,0,1,0,0);
ctx.clearRect(0, 0, c.width, c.height);
ctx.restore();
Step 4: Simply take what is in the buffer canvas and draw it onto the original context where it started to return to this:
The idea is to more "clip" a region of the original, clear the old image, and paste the new clipped region centered back into the original context. I have looked at MDN's example, but the clipping portion of drawImage isn't performing as I expect.
The main issue is that the first canvas drawn over the bufferCvs is not drawn on the place where you expect. The easiest way to prove this is by adding bufferCvs to the DOM tree: http://jsfiddle.net/CdWn6/4/
I guess this is what you're looking for: http://jsfiddle.net/CdWn6/6/
var c=document.getElementById("cvs1"),
c2 = document.getElementById("cvs2"),
bufferCvs = document.createElement('canvas'),
bufferCtx = bufferCvs.getContext('2d'),
ctx = c.getContext("2d"),
ctx2 = c2.getContext("2d");
bufferCvs.width = c.width;
bufferCvs.height = c.height;
ctx.translate( c.width/2, c.height/2 );
ctx.fillStyle="#FF0000";
ctx2.translate( c.width/2, c.height/2 );
ctx2.fillStyle="#FF0000";
bufferCtx.translate( c.width/2, c.height/2 );
ctx.fillRect( -75, -50 ,150,100);
ctx2.fillRect( -75, -50 ,150,100);
//Image data experiment
bufferCtx.drawImage( c, -135, -110, 270, 220);
ctx.save()
ctx.setTransform(1,0,0,1,0,0,1,0,0);
ctx.clearRect(0, 0, c.width, c.height);
ctx.restore();
//Draw background to demonstrate coordinates still work
ctx.fillStyle="#00FF00";
//ctx.fillRect( -75, -50 ,150,100);
ctx.drawImage( bufferCvs, -75, -50, 150, 100 );
Related
I've been experimenting with the <canvas> recently, and I noticed a strange behaviour when stroking rectangles near the origin (0, 0) of the canvas.
// canvas context
var ctx = document.querySelector('#screen').getContext('2d');
// draw a rectangle
ctx.fillStyle = 'orange';
ctx.fillRect(0, 0, 100, 100);
// stroke a border for the rectangle
ctx.lineWidth = 20;
ctx.strokeRect(0, 0, 100, 100);
<canvas id="screen"></canvas>
What went wrong?
In the example above, the rectangle itself was drawn at (0, 0) as intended, but its border (the stroked rectangle) seems to be drawn at an offset.
Generally, when stroking a rectangle at a position away from the origin, this effect is omitted —
Meaning that the stroked rectangles aren't being drawn starting at the position specified, but at an offset, I suppose.
Why is that?
The stroke is centered around the coordinates that your primitve is defined at. In the case of your rectangle with stroke width of 20, drawing this at the top left of the canvas will cause half of the strokes width to be drawn outside of the canvas boundary.
Adjusting the coordinates of strokeRect() to 10,10,.. causes the rectangle to be offset from the canvas origin, meaning that the full stroke of 20 pixels will be visible from the top-left of the canvas:
ctx.lineWidth = 20;
ctx.strokeRect(10, 10, 100, 100);
Consider the following adjustments, made to ensure the stroke is fully visible around the drawn rectangle:
var canvas = document.querySelector('#screen');
// Set the width and height to specify dimensions of canvas (in pixels)
// Choosing a 100x100 square matches the strokeRect() drawn below and
// therefore achieves the appearance of a symmetric stroke
canvas.width = 100;
canvas.height = 100;
// canvas context
var ctx = canvas.getContext('2d');
// draw a rectangle
ctx.fillStyle = 'orange';
ctx.fillRect(10, 10, 90, 90);
// stroke a border for the rectangle
ctx.lineWidth = 20;
var halfStroke = ctx.lineWidth * 0.5;
ctx.strokeRect(halfStroke, halfStroke, 100 - (halfStroke * 2), 100 - (halfStroke * 2));
<canvas id="screen"></canvas>
Update
Here is a visualisation of the stroke in relation to the line/rectangle edge provided by Ibrahim Mahrir:
So at the moment i'm working on a small project that renders an isometric tile space using diamonds that are at a height : width ratio of 1:2.
I plan on making a "shield" effect above them by basically putting more and more blue closer to the edges with the inner circle being transparent to give the illusion of a semi-transparent sphere being above it.
To do this i am using radial gradient.
Currently my canvas when rendered looks like this:
And my code looks like this:
var gradient = ctx.createRadialGradient(450, tileheight * 5 / 2, 100, 450, tileheight * 5 / 2, 200);
gradient.addColorStop(0, "rgba(0, 0, 55, 0.5)");
gradient.addColorStop(1, "rgba(10, 10, 55, 0.8)");
drawdiamond(gradient, 450, 0, tilewidth * 5, tileheight * 5);
Where drawdiamond is a function that draws the tiles, this function will draw a diamond the size of the map with the gradient specified by the variable "gradient".
And tilewidth and tileheight are the width and height at the longest parts of each tile, they are 100 and 50 respectively.
If you look at the image, there is a problem, since i'm drawing my tiles to be wider than they are taller, i need to draw an oval where the height is half the radius of the width in order for the effect to be convincing.
There is ctx.setPattern where i can make an image that has the gradient i want, but if i want to make the map bigger during run time, i would need to create multiple images for each size, which is not ideal.
Is there a way to transform the image gradient so it draws an oval instead of a circle?
(Sorry if this has already been posted).
Matt posted an article that suggested making a shape that is larger than it should be height wise, they use ctx.transform to set the height scaling to 0.5, which will scale the gradient with it as it's part of what's drawn.
I played around with it and came up with this, thanks for the help!
var gradient = ctx.createRadialGradient(450, tilewidth * mapsize / 2, 100, 450, tilewidth * mapsize / 2, 200);
gradient.addColorStop(0, "rgba(0, 0, 55, 0.5)");
gradient.addColorStop(1, "rgba(10, 10, 55, 0.8)");
ctx.setTransform(1, 0, 0, 0.5, 0, 0);
drawdiamond(gradient, 450, 0, tilewidth * mapsize, tilewidth * mapsize);
ctx.setTransform(1, 0, 0, 1, 0, 0);
This function draws the diamond with the shield effect at double it's intended height, then scales it down when it is drawn, thus causing a scaled gradient.
Map size is the maps length in tiles on both length and height sides.
This will work for any size map.
I'm trying to draw a circle with cut off sides looking somewhat like this:
My first approach was to just draw a stroke-circle and do clearRect on the sides - but I want to render many of these adjacent to each other and I can't afford to clear what's already been drawn on the canvas.
var size = 100;
c.save();
c.strokeStyle = '#ff0000';
c.lineWidth = 50;
c.beginPath();
c.arc(0, 0, size - c.lineWidth / 2, 0, Math.PI * 2);
c.closePath();
c.stroke();
// clear rects on each side to get this effect
c.restore();
Is there a way to limit the arc to not draw further or is there a way to clear on just my little shape somehow and later add it to the main canvas?
I'm not keen on the idea of having multiple canvas elements on top of each other.
Just add a clip mask to it:
DEMO
c.save();
/// define clip
c.beginPath();
c.rect(120, 120, 160, 160);
c.clip();
/// next drawn will be clipped
c.beginPath();
c.arc(200, 200, size - c.lineWidth / 2, 0, Math.PI * 2);
c.closePath();
c.stroke();
// clear rects on each side to get this effect
/// and remove clipping mask
c.restore();
The clip() method uses the current defined path to clip the next drawn graphics.
I'm currently drawing an image to an HTML5 Canvas and masking it with an arc, calling clip() before I draw the image so that only the portion that's in the arc is shown. How can I feather the edges of this arc? I know from googling around that there is no simple way to simply apply a "feather" to a shape drawn with canvas. What abut going in on the pixel data for the image where its edges touch the arc? Thanks for any help.
Here is the relevant portion of my code:
ctx.arc(canvas.width/2, canvas.height/2, 250, 0, 6.28, false);//draw the circle
ctx.restore();
ctx.save();
ctx.drawImage(background, 0, 0,
background.width * scale, background.height * scale);
ctx.clip();//call the clip method so the next render is clipped in last path
ctx.drawImage(img, 0, 0,
img.width * scale, img.height * scale);
ctx.closePath();
ctx.restore();
UPDATE
Thanks for the thorough answer and very helpful code/comments Ken!! I spent a few hours last night trying to work this solution in my particular use case and I'm having trouble. It seems that if I clip an image with the second-canvas technique you describe I can't redraw it on transforms the same way that I can with an arc() and clip() routine. Here's a JS Fiddle of what I'm trying to accomplis, minus the feathering on the arc, notice the click and drag events on the two layered images.
http://jsfiddle.net/g3WkN/
I tried replacing the arc() with your method, but I'm having a hard time getting that to be responsive to the transforms that happen on mouse events.
Update 2017/7
Since this answer was given there are now a new option available in newer browsers, the filter property on the context. Just note that not all browsers currently supports it.
For browsers which do we can cut down the code as well as remove temporary canvas like this:
var ctx = demo.getContext('2d');
ctx.fillStyle = '#f90';
ctx.fillRect(0, 0, demo.width, demo.height);
clipArc(ctx, 200, 200, 150, 40);
function clipArc(ctx, x, y, r, f) {
ctx.globalCompositeOperation = 'destination-out';
ctx.filter = "blur(25px)"; // "feather"
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
// reset comp. mode and filter
ctx.globalCompositeOperation = 'destination-out';
ctx.filter = "none";
}
body {background:#07c}
<canvas id="demo" width=400 height=400></canvas>
Old answer
Technique
You can achieve this by combining the following steps:
Use off-screen canvas
Use the shadow feature (the secret ingredient)
Use composite modes
The concept is based on having the browser make the feather internally by utilizing the blurred shadow. This is much faster than blurring in JavaScript. As we can make shadow for any object you can make complex feathered masks.
The off-screen canvas is used to draw the shadow only. We achieve this by moving the actual shape outside the canvas and then offset the shadow accordingly. The result is that shadow is drawn on the off-screen canvas while the actual shape is "invisible".
Now that we have a feathered version of our shape we can use that as a mask for composite mode. We choose destination-out to cleat where the shadow is drawn, or destination-in to invert the mask.
Example
Lets create a wrapper function that do all the steps for us
ONLINE DEMO HERE
function clipArc(ctx, x, y, r, f) { /// context, x, y, radius, feather size
/// create off-screen temporary canvas where we draw in the shadow
var temp = document.createElement('canvas'),
tx = temp.getContext('2d');
temp.width = ctx.canvas.width;
temp.height = ctx.canvas.height;
/// offset the context so shape itself is drawn outside canvas
tx.translate(-temp.width, 0);
/// offset the shadow to compensate, draws shadow only on canvas
tx.shadowOffsetX = temp.width;
tx.shadowOffsetY = 0;
/// black so alpha gets solid
tx.shadowColor = '#000';
/// "feather"
tx.shadowBlur = f;
/// draw the arc, only the shadow will be inside the context
tx.beginPath();
tx.arc(x, y, r, 0, 2 * Math.PI);
tx.closePath();
tx.fill();
/// now punch a hole in main canvas with the blurred shadow
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
ctx.drawImage(temp, 0, 0);
ctx.restore();
}
That's all there is to it.
USAGE
clipArc(context, centerX, centerY, radius, featherSize);
With demo background (see fiddle):
ctx.fillStyle = '#ffa';
ctx.fillRect(0, 0, demo.width, demo.height);
clipArc(ctx, 200, 200, 150, 40);
Result:
If you want to keep center intact just replace composite mode with destination-in.
Demo for inverted feathered mask
I'm attempting to make a program that takes the information gathered from some calculations and plots it on a canvas graph. I need to scale the graph, however, so that it can accommodate larger numbers. But every time I put ctx.scale(); the whole canvas blanks out! I thought I could stop this by scaling the canvas first, but nothing is drawn on the canvas after I scale it.
Here's the coding for my canvas:
var c=document.getElementById("graph_");
var ctx=c.getContext("2d");
graph_.style.backgroundColor="white";
var z0=Math.max(Math.abs(a),Math.abs(b));
var z=Math.round(z0);
var z1=Math.round(z);
var z2=z*2
// alert(z1);
// alert(z2);
ctx.scale(3200/z,3200/z)
var xnew=360/2+360/2*a
var ynew=360/2-360/2*b
alert(xnew);
alert(ynew);
ctx.font = '10px Calibri';
ctx.fillText("( 0 , 0 )", 125, 85);
ctx.fillText(z1, 210, 87);
ctx.fillText(z2, 270, 87);
ctx.fillText(z1*-1, 75, 87);
ctx.fillText(z2*-1, 0, 87);
ctx.fillText(z1, 120, 43.5);
ctx.fillText(z2, 120, 10);
ctx.fillText(z1*-1, 120, 120);
ctx.fillText(z2*-1, 120, 145);
ctx.lineWidth = 1;
ctx.beginPath()
ctx.moveTo(150, 0);
ctx.lineTo(150, 400);
ctx.closePath();
ctx.lineWidth = .2;
ctx.moveTo(0, 75);
ctx.lineTo(400, 75);
ctx.strokeStyle = "#8B8682";
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(xnew, 180);
ctx.lineTo(180, ynew);
ctx.strokeStyle = "red";
ctx.stroke();
Actually, the stuff is being drawn to the canvas, you just can't see it because you're both too far zoomed in and still in the upper left corner of the graph since the default origin points for drawing are in the top left as 0,0.
So if you want to zoom in that far (even though you probably want to zoom out to show bigger numbers, i.e. larger drawings on the graph) you need to translate the canvas origin point to your new origin point (the top left of what you want to see) before you scale the context.
You can use the translate method like
ctx.translate(newX,newY);
But before you do you're going to what to save the context's state so you can revert back to it.
Say you wanted to zoom in on the center of the graph you would translate to the point that is:
ctx.translate((-c.width /2 * scale) + offsetX,(-c.height / 2 * scale) + offsetY);
where the offsetX is the canvas width / 2 and offsetY is the canvas height / 2 and the scale is by the amount that you're scaling in you ctx.scale call.
What is the value of 3200/z, exactly?
I'm guessing that you are scaling your canvas by an enormous amount, so much so that the only thing visible on your screen would be the first few pixels of the canvas. Since you don't draw anything in the top-left 5 pixels of the screen, you don't see anything.