Why do stroked rectangles tend to go outside the canvas? - javascript

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:

Related

HTML5 Canvas rotate function

function drawNumbers(){
var rad, num;
cx.font= "30px Arial";
cx.textAlign = "center";
cx.textBaseline = "middle";
//numbers around the inner circumference
for(num=1; num < 13; num++){
rad = num * Math.PI/6; //angle for every number
cx.rotate(rad);
cx.translate(0, -175);
cx.rotate(-rad);
cx.fillText(num.toString(),0,0);
cx.rotate(rad);
cx.translate(0, 175);
cx.rotate(-rad);
}
}
function drawHands(){
//getting the time
var time = new Date();
var hours = time.getHours();
var minutes = time.getMinutes();
var seconds = time.getSeconds();
//setting the radians based on the time
//hour hand
hours %= 12;
hours = (hours * Math.PI/6) + (minutes * Math.PI/360) + (seconds * Math.PI/21600);
hands(hours, radius * 0.04, radius * 0.5);
//minute hand
minutes = (minutes * Math.PI/30) + (seconds * Math.PI/1800);
hands(minutes, radius * 0.03, radius * 0.65);
//second hand
seconds = (seconds * Math.PI/30);
hands(seconds, radius * 0.01, radius * 0.68);
}
function hands(ang, width, length){
cx.beginPath();
cx.lineWidth = width;
cx.lineJoin = "round";
cx.lineCap = "round";
cx.moveTo(0, 0);
cx.rotate(ang);
cx.lineTo(0, -length);
cx.stroke();
cx.rotate(-ang);
}
I was learning the HTML5 canvas in W3Schools and the tutorial was teaching how to make a working clock.
1. I just don't understand how the extra rotates work in the functions.
2. When applying a rotate function, does it always rotate from the center of origin (0, 0) of the canvas?
When you call the rotate function it rotates the entire canvas, imagine holding a painting and then tilting it. It happens around the origin always. The way to rotate around a different point is to translate the entire canvas first, then rotate it.
Going back to the painting analogy, if we have rotated our painting, once we have drawn our line, we need to then restore the painting to being upright. Thus we rotate(-ang). If we had translated we would also have to undo our transation in a similar manner.
Rotating and why we undo rotates
In the code below you can see I'm drawing a base black rectangle, and then calling a function which rotates the canvas by 0.5 radians and draws another rectangle twice. I haven't undone my rotation so the 3rd rectangle is actually rotated at 1 radian.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
function drawRotatedRectangle() {
ctx.rotate(0.5);
ctx.fillRect(0, 0, 60, 50);
}
// Draw base rectangle
ctx.fillRect(0, 0, 60, 50);
// Rotate and draw second rectangle
ctx.fillStyle = "#FF0000";
drawRotatedRectangle();
// rotate and draw third rectangle
ctx.fillStyle = "#00FF00";
drawRotatedRectangle();
<canvas id="canvas"></canvas>
To fix this we modify the drawRotatedRectangle() function to undo all translations and rotations that it made:
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
function drawRotatedRectangle() {
ctx.rotate(0.5);
ctx.fillRect(0, 0, 60, 50);
ctx.rotate(-0.5);
}
// Draw base rectangle
ctx.fillRect(0, 0, 60, 50);
// Rotate and draw second rectangle
ctx.fillStyle = "#FF0000";
drawRotatedRectangle();
// rotate and draw third rectangle
ctx.fillStyle = "#00FF00";
drawRotatedRectangle();
<canvas id="canvas"></canvas>
Now we see the red (hidden) rectangle and the green rectangle are at the same angle.
Rotating around a different point to the origin
To demonstrate how we can rotate around a different location to the origin, we can first translate where our context origin is and then rotate our canvas. Below I move the origin to the center of the base rectangle, rotate the canvas and draw a rotated rectangle ontop of the base rectangle. Again the translations and rotations are restored in order of most recently applied.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
function drawRotatedRectangle() {
// Move origin to center of rectangle
ctx.translate(30, 25);
// Rotate 0.5 radians
ctx.rotate(0.5);
// Draw rectangle where the center of the rectangle is the origin
ctx.fillRect(-30, -25, 60, 50);
// Undo our rotate
ctx.rotate(-0.5);
// Undo our translate
ctx.translate(-30, -25);
}
// Draw base rectangle
ctx.fillRect(0, 0, 60, 50);
// Rotate and draw second rectangle
ctx.fillStyle = "#FF0000";
drawRotatedRectangle();
// rotate and draw third rectangle
ctx.fillStyle = "#00FF00";
drawRotatedRectangle();
<canvas id="canvas"></canvas>
Edit - Handling Rounding Errors
As Kaiido mentioned in the comments, the rotate(a) function will round the input it is given so simply doing the reverse rotate(-a) function will not return you to the original transformation.
The solution they suggested is to set the transformation matrix to the desired location with setTransform(), in these example we are only returning to the original transform of the canvas so we can use the identity matrix:
ctx.setTransform(1,0,0,1,0,0)
Alternitively, you can also use save() and restore() methods. These will act like pushing the current state of the canvas context to a stack and when you restore() it will pop the latest state from the stack returning you to the previous transform. This article by Jakob Jenkov explains this method further with some examples.

Javascript canvas. Fixed linewidth independent of the transformation

I am trying to draw a graph in a simple (x, y) plane.
I am using canvas transform to transform my coordinate system.
I am looking for a method to draw the 'linewidth' as a fixed pixel width independent of the x/y scale.
The problem is when x/y is large, the line width along x or y disappears or it is deformed because the line width is scaled relative to the x and y scale.
See example:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var y_scale=10
ctx.transform(1, 0, 0, 1/y_scale, 0, 0);
ctx.lineWidth = 10;
ctx.strokeRect(20, 20, 80, 100*y_scale);
Is there a way to fix the width independent of the transformation?
Of course I can do my own transformation matrix but I would rather not.
Same question: html5 canvas prevent linewidth scaling
Solution:
Apply transformation
Define path
Restore transformation
Stroke path
Code:
var y_scale=10;
ctx.beginPath();
ctx.save(); //save context
ctx.transform(1, 0, 0, 1 / y_scale, 0, 0);
ctx.rect(20, 20, 80, 100 * y_scale); // define rect
ctx.restore(); //restore context
ctx.lineWidth = 10;
ctx.stroke(); //stroke path
See example: https://jsfiddle.net/aj3sn7yv/2/
Instead of scaling the context you could scale your point series - no need for a complete matrix solution. This way you are just altering the "path" and not the rendering result.
var scale = 10;
var newPoints = scalePoints(points, scale); // scale or inverse: 1/scale
... plot new points here at the line-width you wish
// example function
function scalePoints(points, scale) {
var newPoints = [];
points.forEach(function(p) { newPoints.push(p * scale) });
return newPoints
}
Modify as needed.

globalCompositeOperation and concentric, hollow, moving shapes

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.

Canvas clipping with "feather" edges effect

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

How to make the scale() function top priority?

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.

Categories