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.
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:
I'm trying to achieve the following:
A number of concentric circles (or rings) are drawn on a canvas. Each circle has a "hole" in it, so the smaller circles, drawn behind it are partially visible. Each frame (we're using window.requestAnimationFrame to render) the radius of each circle/shape/ring is slightly increased.
A scenario with two rings is depicted in the image here.
The code:
function draw() {
drawBgr();
for (var i = 0, len = rings.length; i < len; i++) {
rings[i].draw();
}
}
function drawBgr() {
context.globalCompositeOperation = "source-over";
context.clearRect(0, 0, WIDTH, HEIGHT);
context.rect(0, 0, WIDTH, HEIGHT);
context.fillStyle = '#FFFFFF';
context.fill();
}
function squareRing(ring) { //called by rings[i].draw();
context.globalCompositeOperation = "source-over";
context.fillRect(ring.centerX - ring.radius / 2, ring.centerY - ring.radius / 2, ring.radius, ring.radius);
context.globalCompositeOperation = "source-out";
context.beginPath();
context.arc(CENTER_X, CENTER_Y, ring.radius, 0, 2 * Math.PI, false);
//context.lineWidth = RING_MAX_LINE_WIDTH * (ring.radius / MAX_SIDE);
context.fillStyle = '#000000';
context.fill();
context.globalCompositeOperation = "source-over";
}
What exactly is the problem here? I'm calling clearRect before the circles are drawn. See "What I'm actually getting" image. This is the result of a SINGLE RING being drawn over a number of frames. I shouldn't be getting anything different than a black circle with a hollow square in the middle. (Note that radius is increasing each frame.)
I do realize switching globalCompositeOperation might not suffice for the effect I desire. How can I draw a "hole" in an object drawn on the canvas without erasing everything in the "hole" underneath the object I'm trying to modify?
This is the tutorial I used as a reference for the globalCompositeOperation values.
I'm using Firefox 28.0.
I would not try to use globalCompositeOperation, since i find it hard to figure out what will happen after several iterations, and even harder if the canvas was not cleared before.
I prefer to use clipping, which gets me to that :
http://jsbin.com/guzubeze/1/edit?js,output
So, to build a 'hole' in a draw, how to use clipping ?
-->> Define a positive clipping sub-path, and within this area, cut off a negative part, using this time a clockwise sub-path :
Clipping must be done with one single path, so rect() cannot be used : it does begin a path each time, and does not allow to choose clockwisity (:-)), so you have to define those two functions which will just create the desired sub-paths :
// clockwise sub-path of a rect
function rectPath(x,y,w,h) {
ctx.moveTo(x,y);
ctx.lineTo(x+w,y);
ctx.lineTo(x+w,y+h);
ctx.lineTo(x,y+h);
}
// counter-clockwise sub-path of a rect
function revRectPath(x,y,w,h) {
ctx.moveTo(x,y);
ctx.lineTo(x,y+h);
ctx.lineTo(x+w,y+h);
ctx.lineTo(x+w,y);
}
then you can write your drawing code :
function drawShape(cx, cy, d, scale, rotation) {
ctx.save();
ctx.translate(cx,cy);
scale = scale || 1;
if (scale !=1) ctx.scale(scale, scale);
rotation = rotation || 0;
if (rotation) ctx.rotate(rotation);
// clip with rectangular hole
ctx.beginPath();
var r=d/2;
rectPath(-r,-r, d, d);
revRectPath(-0.25*r,-0.8*r, 0.5*r, 1.6*r);
ctx.closePath();
ctx.clip();
ctx.beginPath();
// we're clipped !
ctx.arc(0,0, r, 0, 2*Math.PI);
ctx.closePath();
ctx.fill();
ctx.restore();
}
Edit :
For the record, there is a simpler way to draw the asked scheme : just draw a circle, then draw counter clockwise a rect within. What you fill will be the part inside the circle that is outside the rect, which is what you want :
function drawTheThing(x,y,r) {
ctx.beginPath();
ctx.arc(x ,y, r, 0, 2*Math.PI);
revRectPath(x-0.25*r, y-0.8*r, 0.5*r, 1.6*r);
ctx.fill();
ctx.closePath();
}
(i do not post image : it is the same).
Depending on your need if you change the draw or if you want to introduce some kind of genericity, use first or second one.
If you do not change the scheme later, the second solution is simpler => better.
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
Here is an example!
I am trying to reset the green arc inside drawValueArc() so that each time you click the change button, the green arc is removed and redrawn. How can I remove it without removing the entire canvas? Also, as an aside, I have noticed that Math.random() * 405 * Math.PI / 180 doesn't actually always result in an arc that fits inside the gray arc, sometimes it is larger than the gray arc, why is this?
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cx = 150;
var cy = 150;
var startRadians = 135 * Math.PI / 180;
var endRadians = 405 * Math.PI / 180;
//main arc
ctx.beginPath();
ctx.arc(cx, cy, 58, startRadians, endRadians, false);
ctx.strokeStyle="rgb(220,220,220)";
ctx.lineWidth = 38;
ctx.stroke();
$('#setRandomValue').click(function(){
drawValueArc(Math.random() * 405 * Math.PI / 180);
});
function drawValueArc(val){
//ctx.clearRect(0, 0, W, H);
ctx.beginPath();
ctx.arc(cx, cy, 58, startRadians, val, false);
ctx.strokeStyle = "green";
ctx.lineWidth = 38;
ctx.stroke();
}
Drawing past boundary
The problem you are facing is in first instance the fact you are drawing before and after a 0-degree on the circle. This can be complicated to handle as you need to split in two draws: one for the part up to 0 (360) and one 0 to the remaining part.
There is a simple trick you can use to make this easier to deal with and that is to deal with all angles from 0 and use an offset when you draw.
Demo using redraw base (I moved it to jsfiddle as jsbin did not work for me):
http://jsfiddle.net/3dGLR/
Demo using off-screen canvas
http://jsfiddle.net/AbdiasSoftware/Dg9Jj/
First, some optimizations and settings for the offset:
var startRadians = 0; //just deal with angles
var endRadians = 300;
var deg2rad = Math.PI / 180; //pre-calculate this to save some cpu cycles
var offset = 122; //adjust this to modify rotation
We will now let the main function, drawArc() do all calculations for us so we can focus on the numbers - here we also offset the values:
function drawArc(color, start, end) {
ctx.beginPath();
ctx.arc(cx, cy, 58,
(startRadians + offset) * deg2rad,
(end + offset) * deg2rad, false);
ctx.strokeStyle = color;
ctx.lineWidth = 38;
ctx.stroke();
}
Clearing the previous arc
There are several techniques to clear the previous drawn arc:
You can draw the base arc to an off-screen canvas and use drawImage() to erase the old.
You can do as in the following example, just re-draw it with the base color
As with 2. but subtracting the green arc and draw the base color from the end of the green arc to the end of the base arc.
clearing the whole canvas with fillRect or clearRect.
1 and 3 are the fastest, while 4 is the slowest.
With out re-factored function (drawArc) it's as easy as this:
function drawValueArc(val) {
drawArc("rgb(220,220,220)", startRadians, endRadians);
drawArc("green", startRadians, val);
}
As everything now is 0-based concerning start we really don't need to give any other argument than 0 to the drawArc instead of startRadians. Use the new offset to offset the start position and adjust the endRadians to where you want it to stop.
As you can see in the demo, using this technique keeps everything in check without the need to draw in split.
Tip: if you notice green artifacts on the edges: this is due to anti-alias. Simply reduce the line width for the green color by 2 pixels (see demo 2, off-screen canvas).
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.