html5 canvas prevent linewidth scaling - html

If I draw a rectangle of say linewidth=2 and then scale it to double the size of the rectangle, I get a rectangle that has its border double the size of the initial linewidth.
Is there a way to keep the linewidth to the perceived size of 2 or the original size.
In short, I want to just scale the size of the rectangle but keep the linewidth visually of size 2.
I tried setting the linewidth before and after the scale(2,2) command but the border width also increases.
One option is to divide the linewidth by the scale factor and this will work if the x and y scale factors are the same.
I don't have the option to scale the rectangle width and height and I need to use the scale command as I have other objects within the rectangle that need the scaling.

You can define path with transformation and stroke it without one. That way line width won't be transformed.
Example:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.save(); //save context without transformation
ctx.scale(2, 0.5); //make transformation
ctx.beginPath(); //define path
ctx.arc(100, 75, 50, 0, 2 * Math.PI);
ctx.restore(); //restore context without transformation
ctx.stroke(); //stroke path
<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">

The lineWidth should be scaled down beforehand.
ctx.lineWidth = 2 / Math.max(scaleX, scaleY);
ctx.scale(scaleX, scaleY);
ctx.fillRect(50, 50, 50, 50);

Ok, you have a couple of options:
You can do your own scaling of coordinates and dimensions, e.g.
ctx.strokeRect( scaleX * x, scaleY * y, scaleX * width, scaleY * height) ;
And you'll need to apply the scaling to all the other objects too.
Alternatively you could apply the scaling but not rely on lineWidth for drawing the border of the rectangle. A simple method would be to draw the rectangle by filling the correct region and then removing the interior, minus the border, like so:
var scaleX = 1.5, scaleY = 2;
var lineWidth = 2;
ctx.scale(scaleX, scaleY);
ctx.fillStyle = "#000";
ctx.fillRect(100, 50, 100,
ctx.clearRect(100+lineWidth/scaleX, 50+lineWidth/scaleY, 100-(2*lineWidth)/scaleX, 60-(2*lineWidth)/scaleY);

Related

Why are artifacts visible in a scaled html5 canvas?

I've seen this and this discussion about removing antialiasing in canvases, but I don't think this is the same thing.
After scaling an html5 canvas by an arbitrary value (i.e., making it responsive), I've noticed that if I draw two rectangles of the same size and in the same location, the edges of the scaled side of the first rectangle remain visible.
I've included an example snippet where I draw a grey rectangle, then draw an red rectangle on top of it. There's a one-pixel red vertical line on the left and right edges of the grey rectangle. I know it may seem trivial, but it's very noticeable in my situation.
How do I fix this? Thanks!
var example = document.getElementById("example");
var ctx = example.getContext('2d');
ctx.scale(1.13,1);
ctx.fillStyle = "LightGrey";
ctx.fillRect(10,10,50,30);
ctx.fillStyle = "Black";
ctx.font = "20px Arial";
ctx.fillText("< Looks good.",70,30);
ctx.fillStyle = "Red";
ctx.fillRect(10,50,50,30);
// This light grey rectangle should completely cover the previous red one, but it doesn't!
ctx.fillStyle = "LightGrey";
ctx.fillRect(10,50,50,30);
ctx.fillStyle = "Black";
ctx.font = "20px Arial";
ctx.fillText("< Do you see red?",70,70);
<canvas id="example"></canvas>
You are scaling the transform matrix by a factor of 1.13 on the X axis.
So your coordinate 10, will actually end up on at coordinate 11.3 on the real pixels matrix.
You can't draw on fraction of pixels, so indeed antialiasing will kick in here.
So why does the first one looks better?
Because the mix between grey and white* is more neutral than the one between red grey and white. But even your first rect is antialiased.
Just zoom in your canvas and you'll see it, there is a one pixel band on both sides that is actually semi-transparent.
* "White" here is the one of the page's background
var example = document.createElement("canvas");
var ctx = example.getContext('2d');
ctx.scale(1.13,1);
ctx.fillStyle = "LightGrey";
ctx.fillRect(10,10,50,30);
ctx.fillStyle = "Red";
ctx.fillRect(10,50,50,30);
ctx.fillStyle = "LightGrey";
ctx.fillRect(10,50,50,30);
// draw bigger with no antialiasing
var z_ctx = zoomed.getContext('2d');
zoomed.width = example.width * 10;
zoomed.height = example.height * 10;
z_ctx.imageSmoothingEnabled = false;
z_ctx.drawImage(example, 0,0, zoomed.width, zoomed.height);
<canvas id="zoomed"></canvas>
So how to avoid this?
Well simply avoid filling at non integer pixel coordinates. This means you have to be constantly aware of your context transformation matrix too, not only of the values you pass to the drawing functions.
(Ps: also remember that stroke is an even eviler beast since it start drawing from the middle of the line, so in this case, you even have to take into considerations the lineWidth, see this Q/A on the matter).

HTML canvas: Why does a large shadow blur not show up for small objects?

Here's a demonstration:
var ctx = document.getElementById("test").getContext("2d");
ctx.shadowColor = "black";
ctx.fillStyle = "white";
ctx.shadowBlur = 10;
ctx.fillRect(10, 10, 10, 10);
ctx.shadowBlur = 50;
ctx.fillRect(70, 10, 10, 10);
ctx.fillRect(70, 70, 70, 70);
<canvas id="test" width="200" height="200"></canvas>
If I set shadowBlur=10 and then draw a small 10x10 square, I get a nice, strong shadow. The same if I set shadowBlur=50 and draw a big 70x70 square. But if I set shadowBlur=50 and then draw a small 10x10 square, I get a very faint, barely visible shadow.
Instead I would have expected a small center square and a large dark shadow all around it.
Obviously I misunderstand how the shadow blur works, so - how does it work, and how do I get a large dark shadow around a small object?
The shadowBlur uses Gaussian blur to produce the shadow internally. The object is drawn to a separate bitmap as stencil in the shadow-color and then blurred using the radius. It does not use the original shape after this step. The result is composited back (as a side-note: there was previously a disagreement on how to composite shadows so Firefox and Chrome/Opera rendered them differently - I think they have landed on source-over in both camps by now though).
If the object is very small and the blur radius very big, the averaging will be thinned by the empty remaining space around the object leaving a more faint shadow.
The only way to get a more visible shadow with the built-in method is to use a smaller radius. You can also "cheat" using a radial gradient, or draw a bigger object with shadow applied to an off-screen canvas but offset relative to the shadow itself so the object doesn't overlap it, then draw the shadow only (using clipping arguments with drawImage()) back to main canvas at desired size before drawing main object.
In newer versions of the browsers you can also produce Gaussian blurred shadows manually using the new filter property on the context with CSS filters. It do require some extra compositing steps and most likely an off-screen canvas for most scenarios, but you can with this method overdraw shadows in multiple steps with variable radii from small to bigger producing a more pronounced shadow at the cost of some performance.
Example of manually generated shadow using filter:
This allow for more complex shapes like with the built-in shadow, but offer more control of the end result. "Falloff" in this case can be controlled by using a easing-function with an initial normalized radius value inside the loop.
// note: requires filter support on context
var ctx = c.getContext("2d");
var iterations = 16, radius = 50,
step = radius / iterations;
for(var i = 1; i < iterations; i++) {
ctx.filter = "blur(" + (step * i) + "px)";
ctx.fillRect(100, 50, 10, 10);
}
ctx.filter = "none";
ctx.fillStyle = "#fff";
ctx.fillRect(100, 50, 10, 10);
<canvas id=c></canvas>
Example of gradient + filter:
This is a more cross-browser friendly solutions as if filter is not supported, at least the gradient comes close to an acceptable shadow. The only drawback is it is more limited in regards to complex shapes.
Additionally, using a variable center point for the gradient allows for mimicking fall-off, light size, light type etc.
Based on #Kaiido's example/mod in comment -
// note: requires filter support on context
var ctx = c.getContext("2d");
var grad = ctx.createRadialGradient(105,55,50,105,55,0);
grad.addColorStop(0,"transparent");
grad.addColorStop(0.33,"rgba(0,0,0,0.5)"); // extra point to control "fall-off"
grad.addColorStop(1,"black");
ctx.fillStyle = grad;
ctx.filter = "blur(10px)";
ctx.fillRect(0, 0, 300, 150);
ctx.filter = "none";
ctx.fillStyle = "#fff";
ctx.fillRect(100, 50, 10, 10);
<canvas id=c></canvas>

Canvas inner stroke

I studied strokeStyle a bit but I cant find how to control the position of the stroke from inner/center/outer. It seems all stroke is outside the rectangle I draw. Is there anyway make the stroke be inner? (or even centered on the rectangle bounds)?
Thanks
Hope this helps!
Instead of doing:
ctx.fill();
ctx.stroke();
DO:
ctx.save();
ctx.clip();
ctx.lineWidth *= 2;
ctx.fill();
ctx.stroke();
ctx.restore();
Edit
For me I believe this works because the clip method removes any fill and stroke around the already present fill area, meaning the only place the stroke can go is on the inside because else it would be clipped off.
The default stroke do use centered stroke but there is unfortunately no parameter to control the alignment of the stroke so you would either have to calculate an offset value for the rectangle's position and size, or combine two rectangles and use for example the fill-rule evenodd:
var ctx = c.getContext("2d");
// default centered
ctx.lineWidth = 10;
ctx.strokeRect(10, 10, 100, 100);
ctx.lineWidth = 1;
ctx.strokeStyle = "red";
ctx.strokeRect(10, 10, 100, 100); // show main path
// inner
ctx.rect(150, 10, 100, 100);
ctx.rect(150+10, 10+10, 100-20, 100-20); // offset position and size
ctx.fill("evenodd"); // !important
ctx.strokeRect(150, 10, 100, 100);
<canvas id=c></canvas>
This answer "Draw outer and inner border around any canvas shape" shows how to use masking and compositing to precisely control the offset, both inwards and outwards of a stroke without the need to manipulate paths. It can be used for any canvas path no matter how complex.

Getting the right width and height of a canvas element

I am having difficulties with setting the correct width and height of my canvas element.
I have a ball, that I'd like to bounce back whenever it hits a screen boundary by changing it's vertical velocity. It works, but instead of moving back as soon as it hits the edge of the screen, it goes on for a couple of seconds and THEN moves back. I have these variables to determine the viewport's size:
var left = 0,
right = canvas.width,
top = 0,
bottom = canvas.height;
If my ball's x or y positions are outside these boundaries, the velocity should be changed to a negative one. However, during my animation I console.log it's x position and by the time it reaches the right edge of the screen the value is around 600, which is really strange, since I'm on a 1366x768px monitor.
Also, it doesnt't fully reach the left screen edge, but bounces off like 50px from it.
Any ideas are really appreciated, because I've been stuck on this for quite some time.
You can see a working example here: http://codepen.io/gbnikolov/pen/puiwk
Update your draw to the following.
Ball.prototype.draw = function(ctx) {
ctx.save();
// you've translated to the x and y position of the ball.
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.scale(this.scaleX, this.scaleY);
ctx.lineWidth = this.lineWidth;
ctx.fillStyle = this.color;
ctx.strokeStyle = this.strokeColor;
ctx.beginPath();
// Draw at 0,0 since we are already translated to the x and y.
ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
Live Demo
Your problem is in the draw method, you're translating the context and then making the arc at the x and y of the ball so if you translate to 20, 20 for example and then draw at 20,20 your ball is actually at 40,40.

Write text on canvas with background

Is it possible to write image on canvas and write text with background?
For example like this:
How text works in canvas
Unfortunately no, you can't produce text with background with the text methods - only fill or outline the text itself.
This is because the glyphs from the typeface (font) are converted to individual shapes or paths if you want, where the background of it would be the inner part of the glyph itself (the part you see when using fill). There is no layer for the black-box (the rectangle which the glyph fits within) the glyph is using besides from using its geometric position, so we need to provide a sort-of black-box and bearings ourselves.
On the old computer systems most fonts where binary font which where setting or clearing a pixels. Instead of just clearing the background one could opt to provide a background instead. This is not the case with vector based typefaces by default (a browser has direct access to the glyphs geometry and can therefor provide a background this way).
Creating custom background
In order to create a background you would need to draw it first using other means such as shapes or an image.
Examples:
ctx.fillRect(x, y, width, height);
or
ctx.drawImage(image, x, y [, width, height]);
then draw the text on top:
ctx.fillText('My text', x, y);
You can use measureText to find out the width of the text (in the future also the height: ascend + descend) and use that as a basis:
var width = ctx.measureText('My text').width; /// width in pixels
You can wrap all this in a function. The function here is basic but you can expand it with color and background parameters as well as padding etc.
/// expand with color, background etc.
function drawTextBG(ctx, txt, font, x, y) {
/// lets save current state as we make a lot of changes
ctx.save();
/// set font
ctx.font = font;
/// draw text from top - makes life easier at the moment
ctx.textBaseline = 'top';
/// color for background
ctx.fillStyle = '#f50';
/// get width of text
var width = ctx.measureText(txt).width;
/// draw background rect assuming height of font
ctx.fillRect(x, y, width, parseInt(font, 10));
/// text color
ctx.fillStyle = '#000';
/// draw text on top
ctx.fillText(txt, x, y);
/// restore original state
ctx.restore();
}
ONLINE DEMO HERE
Just note that this way of "measuring" height is not accurate. You can measure height of a font by using a temporary div/span element and get the calculated style from that when font and text is set for it.
I simpler solution is to call fillText twice. First a string of Unicode+2588 █ which is a black rectangle repeated the same length as the text using the background color. And then call fillText as normal with the foreground color.
This function gives you vertically and horizontally centered text with a background. It only works well with monospaced fonts (characters with the same width). The function counts the number of character in the string you which to print and multiplies them with 0.62 (assuming that the width of the font is slightly less than 0.62 times the height). The background is 1.5 times bigger than the font size. Change this to fit your needs.
function centeredText(string, fontSize, color) {
var i = string.length;
i = i*fontSize*0.62;
if (i > canvas.width) {
i = canvas.width;
}
ctx.fillStyle = "RGBA(255, 255, 255, 0.8)";
ctx.fillRect(canvas.width / 2 - i / 2,canvas.height / 2 - (fontSize * 1.5) / 2, i, (fontSize * 1.5) );
ctx.font = fontSize.toString() + "px monospace";
ctx.fillStyle = color;
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillText(string, canvas.width / 2, canvas.height / 2);
}
So calling the function would look something like this.
centeredText("Hello World", 30, "red");