HTML 5 canvas lines appear distorted - html

I am trying to make a grid on my 500px x 500px canvas:
<canvas id="area" style="width: 500px; height: 500px;"></canvas>
var canvas = document.getElementById('area');
var context = canvas.getContext('2d');
for (var x = 0.5; x < 500; x += 10) {
context.moveTo(x, 0);
context.lineTo(x, 500);
}
for (var y = 0.5; y < 500; y += 10) {
context.moveTo(0, y);
context.lineTo(500, y);
}
context.strokeStyle = "#eee";
context.stroke();
The code looks correct to me but for some reason its coming out elongated and pixelated:
http://jsfiddle.net/DK4m7/1/
Would anyone know why this occurring?

Avoid using CSS to set the canvas size, do instead:
<canvas id="area" width=500 height=500></canvas>
Using CSS will just stretch the current size of the canvas' bitmap which defaults to 350 x 150 pixels. You need to specifically define the bitmap size using the width and height attributes.
Modified fiddle

Related

Display pixel-perfect canvas on all devices

I have some canvases that I want to be displayed pixel-perfect in every (modern) browser. By default, devices with high-DPI screens are scaling my page so that everything looks the right size, but it's breaking* the appearance of my canvases.
How can I ensure that one pixel in my canvas = one pixel on the screen? Preferably this wouldn't affect other elements on the page, since I still want e.g. text to be scaled appropriately for the device.
I already tried styling the canvas dimensions based on window.devicePixelRatio. That makes the canvases the right size but the contents look even worse. I'm guessing that just scales them down after they're already scaled up incorrectly.
*If you care, because the canvases use dithering and the browsers are doing some kind of lerp rather than nearest-neighbor
For me, only a combination of different 'pixel perfect' techniques helped to archive the results:
Get and scale canvas with a pixel ratio:
pixelRatio = window.devicePixelRatio/ctx.backingStorePixelRatio
Scale the canvas on the resize (avoid canvas default stretch scaling).
multiple the lineWidth with pixelRatio to find proper 'real' pixel line thickness:
context.lineWidth = thickness * pixelRatio;
NOTE: not sure wheter it's valid for all devices
Check whether the thickness of the line is odd or even. add half of the pixelRatio to the line position for the odd thickness values.
x = x + pixelRatio/2;
The odd line will be placed in the middle of the pixel. The line above is used to move it a little bit.
use image-rendering: pixelated;
function getPixelRatio(context) {
dpr = window.devicePixelRatio || 1,
bsr = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return dpr / bsr;
}
var canvas = document.getElementById('canvas');
var context = canvas.getContext("2d");
var pixelRatio = getPixelRatio(context);
var initialWidth = canvas.clientWidth * pixelRatio;
var initialHeight = canvas.clientHeight * pixelRatio;
window.addEventListener('resize', function(args) {
rescale();
redraw();
}, false);
function rescale() {
var width = initialWidth * pixelRatio;
var height = initialHeight * pixelRatio;
if (width != context.canvas.width)
context.canvas.width = width;
if (height != context.canvas.height)
context.canvas.height = height;
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
}
function pixelPerfectLine(x1, y1, x2, y2) {
context.save();
context.beginPath();
thickness = 1;
// Multiple your stroke thickness by a pixel ratio!
context.lineWidth = thickness * pixelRatio;
context.strokeStyle = "Black";
context.moveTo(getSharpPixel(thickness, x1), getSharpPixel(thickness, y1));
context.lineTo(getSharpPixel(thickness, x2), getSharpPixel(thickness, y2));
context.stroke();
context.restore();
}
function pixelPerfectRectangle(x, y, w, h, thickness, useDash) {
context.save();
// Pixel perfect rectange:
context.beginPath();
// Multiple your stroke thickness by a pixel ratio!
context.lineWidth = thickness * pixelRatio;
context.strokeStyle = "Red";
if (useDash) {
context.setLineDash([4]);
}
// use sharp x,y and integer w,h!
context.strokeRect(
getSharpPixel(thickness, x),
getSharpPixel(thickness, y),
Math.floor(w),
Math.floor(h));
context.restore();
}
function redraw() {
context.clearRect(0, 0, canvas.width, canvas.height);
pixelPerfectLine(50,50,250,250);
pixelPerfectLine(120,0,120,250);
pixelPerfectLine(122,0,122,250);
pixelPerfectRectangle(10, 11, 200.3, 43.2, 1, false);
pixelPerfectRectangle(41, 42, 150.3, 43.2, 1, true);
pixelPerfectRectangle(102, 100, 150.3, 243.2, 2, true);
}
function getSharpPixel(thickness, pos) {
if (thickness % 2 == 0) {
return pos;
}
return pos + pixelRatio / 2;
}
rescale();
redraw();
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 100vh;
height: 100vh;
}
<canvas id="canvas"></canvas>
NOTE: Resize event is not fired in the snipped, so might be not accurate.
The currently marked answer is wrong
There is no way to get a pixel perfect canvas on all devices across all browsers in 2022.
You might think it's working using
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
}
But all you have to do to see it fail is press Zoom-In/Zoom-Out. Zooming is not the only place it will fail. Many devices and many OSes have fractional devicePixelRatio.
In other words. Let's say you make a 100x100 canvas (enlarged).
The user's devixePixelRatio is 1.25 (which is what my Desktop PC is). The canvas will then display at 125x125 pixels and your 100x100 pixel canvas will get scaled to 125x125 with nearest neighbor filtering (image-rendering: pixelated) and look really crappy as some pixels are 1x1 pixel big and others 2x2 pixels big.
So no, using
image-rendering: pixelated;
image-rendering: crisp-edges;
by itself is not a solution.
Worse. The browser works in fractional sizes but the device does not. Example:
let totalWidth = 0;
let totalDeviceWidth = 0;
document.querySelectorAll(".split>*").forEach(elem => {
const rect = elem.getBoundingClientRect();
const width = rect.width;
const deviceWidth = width * devicePixelRatio;
totalWidth += width;
totalDeviceWidth += deviceWidth;
log(`${elem.className.padEnd(6)}: width = ${width}, deviceWidth: ${deviceWidth}`);
});
const elem = document.querySelector('.split');
const rect = elem.getBoundingClientRect();
const width = rect.width;
const deviceWidth = width * devicePixelRatio;
log(`
totalWidth : ${totalWidth}
totalDeviceWidth: ${totalDeviceWidth}
elemWidth : ${width}
elemDeviceWidth : ${deviceWidth}`);
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = args.join(' ');
document.body.appendChild(elem);
}
.split {
width: 299px;
display: flex;
}
.split>* {
flex: 1 1 auto;
height: 50px;
}
.left {
background: pink;
}
.middle {
background: lightgreen;
}
.right {
background: lightblue;
}
pre { margin: 0; }
<div class="split">
<div class="left"></div>
<div class="middle"></div>
<div class="right"></div>
</div>
The sample above there is 299 CSS pixel wide div and inside there are 3 child divs each taking 1/3 of their parent. Asking for sizes by calling getBoundingClientRect on my MacBook Pro in Chrome 102 I get
left : width = 99.6640625, deviceWidth: 199.328125
middle: width = 99.6640625, deviceWidth: 199.328125
right : width = 99.6640625, deviceWidth: 199.328125
totalWidth : 298.9921875
totalDeviceWidth: 597.984375
elemWidth : 299
elemDeviceWidth : 598
Add them up and you might see a problem. According to getBoundingClientRect each one is about 1/3 width in device pixels (199.328125). You can't actually have 0.328125 device pixels so they'll need to be converted to integers. Let's use Math.round so they all become 199.
199 + 199 + 199 = 597
But according to the browser the size of the parent is 598 device pixels big. Where is the missing pixel?
Let's ask
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
let good = false;
if (entry.devicePixelContentBoxSize) {
// NOTE: Only this path gives the correct answer
// The other paths are imperfect fallbacks
// for browsers that don't provide anyway to do this
width = entry.devicePixelContentBoxSize[0].inlineSize;
height = entry.devicePixelContentBoxSize[0].blockSize;
good = true;
} else {
if (entry.contentBoxSize) {
if (entry.contentBoxSize[0]) {
width = entry.contentBoxSize[0].inlineSize;
height = entry.contentBoxSize[0].blockSize;
} else {
width = entry.contentBoxSize.inlineSize;
height = entry.contentBoxSize.blockSize;
}
} else {
width = entry.contentRect.width;
height = entry.contentRect.height;
}
width *= devicePixelRatio;
height *= devicePixelRatio;
}
log(`${entry.target.className.padEnd(6)}: width = ${width} measure = ${good ? "good" : "bad (not accurate)"}`);
}
});
document.querySelectorAll('.split>*').forEach(elem => {
observer.observe(elem);
});
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = args.join(' ');
document.body.appendChild(elem);
}
.split {
width: 299px;
display: flex;
}
.split>* {
flex: 1 1 auto;
height: 50px;
}
.left {
background: pink;
}
.middle {
background: lightgreen;
}
.right {
background: lightblue;
}
pre { margin: 0; }
<div class="split">
<div class="left"></div>
<div class="middle"></div>
<div class="right"></div>
</div>
The code above asks using ResizeObserver and devicePixelContentBoxSize which is only supported on Chromium browsers and Firefox at the moment. For me, the middle div got the extra pixel
left : width = 199
middle: width = 200
right : width = 199
The point of all of that is
You can't just naively set image-rendering: pixelated; image-rendering: crisp-edges
If you want to know how many pixels are in the canvas you may have to ask and you can only find out on Chrome/Firefox ATM
Solutions: TL;DR THERE IS NO CROSS BROWSER SOLUTION IN 2022
In Chrome/Firefox you can use the ResizeObserver solution above.
In Chrome and Firefox you can also compute a fractional sized canvas
In other words. A typical "fill the page" canvas like
<style>
html, body, canvas { margin: 0; width: 100%; height: 100%; display: block; }
<style>
<body>
<canvas>
</canvas>
<body>
will not work. See reasons above. But you can do this
get the size of the container (the body in this case)
multiply by devicePixelRatio and round down
divide by the devicePixelRatio
Use that value for the canvas CSS width and height and the #2 value for the canvas's width and heigh.
This will end up leaving a trailing gap on the right and bottom edges
of 1 to 3 device pixels but it should give you a canvas that is 1x1 pixels.
const px = v => `${v}px`;
const canvas = document.querySelector('canvas');
resizeCanvas(canvas);
draw(canvas);
window.addEventListener('resize', () => {
resizeCanvas(canvas);
draw(canvas);
});
function resizeCanvas(canvas) {
// how many devicePixels per pixel in the canvas we want
// you can set this to 1 if you always want 1 device pixel to 1 canvas pixel
const pixelSize = Math.max(1, devicePixelRatio) | 0;
const rect = canvas.parentElement.getBoundingClientRect();
const deviceWidth = rect.width * devicePixelRatio | 0;
const deviceHeight = rect.height * devicePixelRatio | 0;
const pixelsAcross = deviceWidth / pixelSize | 0;
const pixelsDown = deviceHeight / pixelSize | 0;
const devicePixelsAcross = pixelsAcross * pixelSize;
const devicePixelsDown = pixelsDown * pixelSize;
canvas.style.width = px(devicePixelsAcross / devicePixelRatio);
canvas.style.height = px(devicePixelsDown / devicePixelRatio);
canvas.width = pixelsAcross;
canvas.height = pixelsDown;
}
function draw(canvas) {
const ctx = canvas.getContext('2d');
for (let x = 0; x < canvas.width; ++x) {
let h = x % (canvas.height * 2);
if (h > canvas.height) {
h = 2 * canvas.height - h;
}
ctx.fillStyle = 'lightblue';
ctx.fillRect(x, 0, 1, h);
ctx.fillStyle = 'black';
ctx.fillRect(x, h, 1, 1);
ctx.fillStyle = 'pink';
ctx.fillRect(x, h + 1, 1, canvas.height - h);
}
}
html, body {
margin: 0;
width: 100%;
height: 100%;
/* set to red we can see the gap */
/* set to some other color to hide the gap */
background: red;
}
canvas {
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
<canvas></canvas>
In Safari there are no solutions: Safari provides neither support for devicePixelContentBoxSize nor does it adjust the devicePixelRatio in response to zooming. On the plus side, Safari never returns a fractional devicePixelRatio, at least as of 2022 but you won't get 1x1 device pixels on Safari if the user zooms.
It's easy:
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
}
Browser support isn't great, but so far it's worked on all of the modern browsers I've tested, so I'm happy. I've tested this with Chrome, Firefox, and Safari on Windows, Mac, Linux, iOS, and Android.
This has no effect on normal DPI devices as long as your canvases are being displayed at their normal size. Also, depending on the screen, you get interesting artifacts on high DPI devices, but I think that's unavoidable due to how those displays work. They're still better than the artifacts you get without the style.
I also tried upscaling all of my canvases 200% with nearest-neighbor in javascript and then setting them as background images of normal-sized divs. This is how Apple recommends displaying images for retina devices, so I thought it would give a great result. But you still end up with artifacts and they aren't fixed by zooming in the page so it's actually worse that the simple solution above.

Canvas image: center wide image and hide overflow

I have a very wide image that exceeds mostly viewing widths and must be rendered using a <canvas> tag. How would I hide the overflow and also center the image?
In other words, I'm looking for the canvas equivalent to background-position: center.
Ideally, this would be done in a way which is responsive - so if the viewing window is resized, the image stays centered.
Here's an example:
window.onload = function() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = document.getElementById("image");
ctx.drawImage(img, 0, 0);
};
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
canvas {
overflow: hidden;
}
.canvasContainer {
width: 100%;
overflow-x: hidden;
}
img {
display: none;
}
<div class="container">
<div class="canvasContainer">
<img id="image" src="http://via.placeholder.com/2400x800?text=center" />
<canvas id="canvas" width="2400" height="800" />
</div>
</div>
Note: There is a text Center present in the placeholder, but is currently not visible
This code should do what you need.
The image width should be set to the canvas.width to avoid the image overflowing the canvas.
The image height is now relative to the image width so the ratio stays the same.
I have included an event listener which will resize your canvas/image to the size of the window.
function init() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = document.getElementById("image");
var canHeight = window.innerWidth / 4;
canvas.width = window.innerWidth;
canvas.height = canHeight;
var width = canvas.width;
ctx.drawImage(img, 0, 0, width, canHeight);
}
init();
window.addEventListener('resize', function() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
init();
});
For the canvas you just draw the image where you want it. It will not add scroll bars. The example loads the image and centers it on the canvas. You can click to see the image scaled to fit (see all the image), fill (see full height, or full width whichever fits best to fill the canvas) and click again to see at full resolution centered.
const image = new Image();
image.src = "http://via.placeholder.com/2400x800";
image.onload = showImage;
addEventListener("resize",showImageFit)
function drawText(text){
const ctx = canvas.getContext("2d");
ctx.font = "28px arial";
ctx.textAlign = "center";
ctx.fillText(text,canvas.width / 2, 28);
}
function showImage(){
canvas.width = innerWidth - 8;
canvas.height = innerHeight - 8;
const x = (canvas.width / 2 - image.naturalWidth / 2) | 0;
const y = (canvas.height / 2 - image.naturalHeight / 2) | 0;
canvas.getContext("2d").drawImage(image,x,y);
drawText("Click to scale image to fit");
canvas.onclick = showImageFit;
}
function showImageFit(){
canvas.width = innerWidth - 8;
canvas.height = innerHeight - 8;
const scale = Math.min( canvas.width /image.naturalWidth , canvas.height / image.naturalHeight );
const x = (canvas.width / 2 - (image.naturalWidth / 2) * scale) | 0;
const y = (canvas.height / 2 - (image.naturalHeight / 2) * scale) | 0;
canvas.getContext("2d").drawImage(image,x,y,image.naturalWidth * scale, image.naturalHeight * scale);
drawText("Click to scale image to fill");
canvas.onclick = showImageFill;
}
function showImageFill(){
canvas.width = innerWidth - 8;
canvas.height = innerHeight - 8;
const scale = Math.max( canvas.width /image.naturalWidth , canvas.height / image.naturalHeight );
const x = (canvas.width / 2 - (image.naturalWidth / 2) * scale) | 0;
const y = (canvas.height / 2 - (image.naturalHeight / 2) * scale) | 0;
canvas.getContext("2d").drawImage(image,x,y,image.naturalWidth * scale, image.naturalHeight * scale);
drawText("Click to see image at full resolution and centered");
canvas.onclick = showImage;
}
canvas {
border : 2px solid black;
}
body {
margin : 0px;
}
<canvas id="canvas"></canvas>
Thanks for the feedback - the suggested solutions work with solving the canvas problem.
I found another solution, which was to treat the canvas as I would any other oversized element and use CSS.
+-------------------------------------------+
| page container |
+---------------------------------------------------------+
| |
| canvas |
+---------------------------------------------------------+
| |
+-------------------------------------------+
Solution (and illustration) taken from here:
center oversized image in div
margin-left: 50%;
transform: translateX(-50%);

Why does Canvas's putImageData not work when I specify target location?

In trying to find documentation for Canvas context's putImageData() method, I've found things like this:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
(from http://www.w3schools.com/tags/canvas_putimagedata.asp)
According to the documentation I've read, x and y are an index into the source image, whereas dirtyX and dirtyY specify coordinates in the target canvas where to draw the image. Yet, as you'll see from the example below (and JSFiddle) a call to putImageData(imgData,x,y) works while putImageData(imgData, 0, 0, locX, locY) doesn't. I'm not sure why.
EDIT:
I guess my real question is why the top row of the image is black, and there are only 7 rows, not 8. The images should start at the top-left of the Canvas. They DO start at the left (and have 8 columns). Why do they not start at the top?
Answer: that's due to divide by 0 on this line when yLoc is 0:
xoff = imgWidth / (yLoc/3);
The JSFiddle:
http://jsfiddle.net/WZynM/
Code:
<html>
<head>
<title>Canvas tutorial</title>
<script type="text/javascript">
var canvas;
var context; // The canvas's 2d context
function setupCanvas()
{
canvas = document.getElementById('myCanvas');
if (canvas.getContext)
{
context = canvas.getContext('2d');
context.fillStyle = "black"; // this is default anyway
context.fillRect(0, 0, canvas.width, canvas.height);
}
}
function init()
{
loadImages();
startGating();
}
var images = new Array();
var gatingTimer;
var curIndex, imgWidth=0, imgHeight;
// Load images
function loadImages()
{
for (n = 1; n <= 16; n++)
{
images[n] = new Image();
images[n].src = "qxsImages/frame" + n + ".png";
// document.body.appendChild(images[n]);
console.log("width = " + images[n].width + ", height = " + images[n].height);
}
curIndex = 1;
imgWidth = images[1].width;
imgHeight = images[1].height;
}
function redrawImages()
{
if (imgWidth == 0)
return;
curIndex++;
if (curIndex > 16)
curIndex = 1;
// To do later: use images[1].width and .height to layout based on image size
for (var x=0; x<8; x++)
{
for (var y=0; y<8; y++)
{
//if (x != 1)
// context.drawImage(images[curIndex], x*150, y*100);
// context.drawImage(images[curIndex], x*150, y*100, imgWidth/2, imgHeight/2); // scale
// else
self.drawCustomImage(x*150, y*100);
}
}
}
function drawCustomImage(xLoc, yLoc)
{
// create a new pixel array
imageData = context.createImageData(imgWidth, imgHeight);
pos = 0; // index position into imagedata array
xoff = imgWidth / (yLoc/3); // offsets to "center"
yoff = imgHeight / 3;
for (y = 0; y < imgHeight; y++)
{
for (x = 0; x < imgWidth; x++)
{
// calculate sine based on distance
x2 = x - xoff;
y2 = y - yoff;
d = Math.sqrt(x2*x2 + y2*y2);
t = Math.sin(d/6.0);
// calculate RGB values based on sine
r = t * 200;
g = 125 + t * 80;
b = 235 + t * 20;
// set red, green, blue, and alpha:
imageData.data[pos++] = Math.max(0,Math.min(255, r));
imageData.data[pos++] = Math.max(0,Math.min(255, g));
imageData.data[pos++] = Math.max(0,Math.min(255, b));
imageData.data[pos++] = 255; // opaque alpha
}
}
// copy the image data back onto the canvas
context.putImageData(imageData, xLoc, yLoc); // Works... kinda
// context.putImageData(imageData, 0, 0, xLoc, yLoc, imgWidth, imgHeight); // Doesn't work. Why?
}
function startGating()
{
gatingTimer = setInterval(redrawImages, 1000/25); // start gating
}
function stopGating()
{
clearInterval(gatingTimer);
}
</script>
<style type="text/css">
canvas { border: 1px solid black; }
</style>
</head>
<body onload="setupCanvas(); init();">
<canvas id="myCanvas" width="1200" height="800"></canvas>
</body>
</html>
http://jsfiddle.net/WZynM/
You just had your coordinates backwards.
context.putImageData(imageData, xLoc, yLoc, 0, 0, imgWidth, imgHeight);
Live Demo
xLoc, and yLoc are where you are putting it, and 0,0,imgWidth,imgHeight is the data you are putting onto the canvas.
Another example showing this.
A lot of the online docs seem a bit contradictory but for the seven param version
putImageData(img, dx, dy, dirtyX, dirtyY, dirtyRectWidth, dirtyRectHeight)
the dx, and dy are your destination, the next four params are the dirty rect parameters, basically controlling what you are drawing from the source canvas. One of the most thorough descriptions I can find was in the book HTML5 Unleashed by Simon Sarris (pg. 165).
Having been using this recently, I've discovered that Loktar above has hit upon a VERY important issue. Basically, some documentation of this method online is incorrect, a particularly dangerous example being W3Schools, to which a number of people will turn to for reference.
Their documentation states the following:
Synopsis:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas
x : The x-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
y : The y-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
dirtyX : Optional. The horizontal (x) value, in pixels, where to place the image on the canvas [WRONG]
dirtyY : Optional. The vertical (y) value, in pixels, where to place the image on the canvas [WRONG]
dirtyWidth : Optional. The width to use to draw the image on the canvas
dirtyHeight: Optional. The height to use to draw the image on the canvas
As Loktar states above, the CORRECT synopsis is as follows:
Correct Synopsis:
context.putImageData(imgData, canvasX, canvasY, srcX ,srcY, srcWidth, srcHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas (as before);
canvasX : The x coordinate of the location on the CANVAS where you are plotting your imageData;
canvasY : The y coordinate of the location on the CANVAS where you are plotting your ImageData;
srcX : Optional. The x coordinate of the top left hand corner of your ImageData;
srcY : Optional. The y coordinate of the top left hand corner of your ImageData;
srcWidth : Optional. The width of your ImageData;
srcHeight : Optional. The height of your ImageData.
Use the correct synopsis above, and you won't have the problems that have been encountered above.
I'll give a big hat tip to Loktar for finding this out initially, but I thought it apposite to provide an expanded answer in case others run into the same problem.

Canvas items not rendering properly

I have defined the two functions to render a circle and a triangle. Very straight forward stuff.
function circle(offset, size){
var color = $("#color option:selected").val();
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
radius = size * 1;
context.beginPath();
context.arc(offset, 2, radius, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
}
function triangle(offset, size){
var color = $("#color option:selected").val();
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var width = size * 6;
var height = size * 5;
var padding = 0;
// Draw a path
context.beginPath();
context.moveTo(offset + width/2, padding);
context.lineTo(offset + width, height + padding);
context.lineTo(offset, height + padding);
context.closePath();
// Fill the path
context.fillStyle = color;
context.fill();
}
I am have added the canvas to my page with:
<canvas id="canvas"></canvas>
For some reason I can see the circle and square a not rendering correctly. See attached screen shots.
I can almost guarantee that it is because you are setting the width and height of the Canvas using CSS width and height and not the <canvas> html attributes.
You need to define the width/height either in the canvas tag:<canvas width="500" height="500">
or in code:
var canvas = document.getElementById("canvas");
canvas.width = 500;
canvas.height = 500;
And not by CSS. If you did this:
<canvas style="width: 500px; height: 500px;">
Then you would have a 300x150 canvas (the default size) that was scaled/warped to be 500x500, which is almost certainly what you're getting.
(I wrote the above freehand so there might be a typo, but you get the idea)

Canvas drawings, like lines, are blurry

I have a <div style="border:1px solid border;" /> and canvas, which is drawn using:
context.lineWidth = 1;
context.strokeStyle = "gray";
The drawing looks quite blurry (lineWidth less than one creates even worse picture), and nothing near to the div's border. Is it possible to get the same quality of drawing as HTML using canvas?
var ctx = document.getElementById("canvas").getContext("2d");
ctx.lineWidth = 1;
ctx.moveTo(2, 2);
ctx.lineTo(98, 2);
ctx.lineTo(98, 98);
ctx.lineTo(2, 98);
ctx.lineTo(2, 2);
ctx.stroke();
div {
border: 1px solid black;
width: 100px;
height: 100px;
}
canvas, div {background-color: #F5F5F5;}
canvas {border: 1px solid white;display: block;}
<table>
<tr><td>Line on canvas:</td><td>1px border:</td></tr>
<tr><td><canvas id="canvas" width="100" height="100"/></td><td><div> </div></td></tr>
</table>
I found that setting the canvas size in CSS caused my images to be displayed in a blurry manner.
Try this:
<canvas id="preview" width="640" height="260"></canvas>
as per my post: HTML Blurry Canvas Images
When drawing lines in canvas, you actually need to straddle the pixels. It was a bizarre choice in the API in my opinion, but easy to work with:
Instead of this:
context.moveTo(10, 0);
context.lineTo(10, 30);
Do this:
context.moveTo(10.5, 0);
context.lineTo(10.5, 30);
Dive into HTML5's canvas chapter talks about this nicely
Even easier fix is to just use this:
context = canvas.context2d;
context.translate(0.5, 0.5);
From here on out your coordinates should be adjusted by that 0.5 pixel.
I use a retina display and I found a solution that worked for me here.
Small recap :
First you need to set the size of your canvas twice as large as you want it, for example :
canvas = document.getElementById('myCanvas');
canvas.width = 200;
canvas.height = 200;
Then using CSS you set it to the desired size :
canvas.style.width = "100px";
canvas.style.height = "100px";
And finally you scale the drawing context by 2 :
const dpi = window.devicePixelRatio;
canvas.getContext('2d').scale(dpi, dpi);
The Mozilla website has example code for how to apply the correct resolution in a canvas:
https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
// Set display size (css pixels).
var size = 200;
canvas.style.width = size + "px";
canvas.style.height = size + "px";
// Set actual size in memory (scaled to account for extra pixel density).
var scale = window.devicePixelRatio; // Change to 1 on retina screens to see blurry canvas.
canvas.width = size * scale;
canvas.height = size * scale;
// Normalize coordinate system to use css pixels.
ctx.scale(scale, scale);
ctx.fillStyle = "#bada55";
ctx.fillRect(10, 10, 300, 300);
ctx.fillStyle = "#ffffff";
ctx.font = '18px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var x = size / 2;
var y = size / 2;
var textString = "I love MDN";
ctx.fillText(textString, x, y);
<canvas id="canvas"></canvas>
Lines are blurred because the canvas virtual size is zoomed to its HTML element actual size. To overcome this issue you need to adjust canvas virtual size before drawing:
function Draw () {
var e, surface;
e = document.getElementById ("surface");
/* Begin size adjusting. */
e.width = e.offsetWidth;
e.height = e.offsetHeight;
/* End size adjusting. */
surface = e.getContext ("2d");
surface.strokeRect (10, 10, 20, 20);
}
window.onload = Draw ()
<!DOCTYPE html>
<html>
<head>
<title>Canvas size adjusting demo</title>
</head>
<body>
<canvas id="surface"></canvas>
</body>
</html>
HTML:
Ok, I've figured this out once and for all. You need to do two things:
place any lines on 0.5 px. Refer to this, which provides a great explanation:
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_lineWidth_example
There are essentially two heights and two widths associated with the canvas. There is the canvas height and width and then there is the css style height and width of the element. These need to be in sync.
To do this, you need to calculate the css height and width as:
var myCanvasEl = document.getElementById('myCanvas');
var ctx = myCanvasEl.getContext('2d');
myCanvasEl.style.height = myCanvasEl.height / window.devicePixelRatio + "px";
myCanvasEl.style.width = myCanvasEl.width / window.devicePixelRatio + "px";
where myCanvasEl.style.height and myCanvasEl.style.widthis the css styling height and width of the element, while myCanvasEl.height and myCanvasEl.width is the height and width of the canvas.
OLD ANSWER (superseded by above):
This is the best solution I've found in 2020. Notice I've multiplied the devicePixelRatio by 2:
var size = 100;
var scale = window.devicePixelRatio*2;
context.width = size * scale;
cartesian_001El.style.height = cartesian_001El.height / window.devicePixelRatio + "px";
cartesian_001El.style.height = cartesian_001El.height / window.devicePixelRatio + "px";
context.height = size * scale;
context.scale(scale, scale);
Something else that nobody talked about here when images are scaled (which was my issue) is imageSmoothingEnabled.
The imageSmoothingEnabled property of the CanvasRenderingContext2D interface, part of the Canvas API, determines whether scaled images are smoothed (true, default) or not (false). On getting the imageSmoothingEnabled property, the last value it was set to is returned.
This property is useful for games and other apps that use pixel art. When enlarging images, the default resizing algorithm will blur the pixels. Set this property to false to retain the pixels' sharpness.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled
To disable it, simply set the properity to false:
ctx.imageSmoothingEnabled = false;
canvas.width=canvas.clientWidth
canvas.height=canvas.clientHeight
To avoid this issue in animation I would like to share a small demo.
Basically I am checking increment values each time & jumping in a set of 1px by removing float values.
HTML:
<canvas id="canvas" width="600" height="600"></canvas>
CSS:
html, body{
height: 100%;
}
body{
font-family: monaco, Consolas,"Lucida Console", monospace;
background: #000;
}
canvas{
position: fixed;
top: 0;
left: 0;
transform: translateZ(0);
}
JS:
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
ctx.translate(0.5, 0.5);
var i = 0;
var iInc = 0.005;
var range = 0.5;
raf = window.requestAnimationFrame(draw);
function draw() {
var animInc = EasingFunctions.easeInQuad(i) * 250;
ctx.clearRect(0, 0, 600, 600);
ctx.save();
ctx.beginPath();
ctx.strokeStyle = '#fff';
var rectInc = 10 + animInc;
// Avoid Half Pixel
rectIncFloat = rectInc % 1; // Getting decimal value.
rectInc = rectInc - rectIncFloat; // Removing decimal.
// console.log(rectInc);
ctx.rect(rectInc, rectInc, 130, 60);
ctx.stroke();
ctx.closePath();
ctx.font = "14px arial";
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText("MAIN BUTTON", 65.5 + rectInc, 35.5 + rectInc);
i += iInc;
if (i >= 1) {
iInc = -iInc;
}
if (i <= 0) {
iInc = Math.abs(iInc);
}
raf = window.requestAnimationFrame(draw);
}
// Easing
EasingFunctions = {
// no easing, no acceleration
linear: function(t) {
return t
},
// accelerating from zero velocity
easeInQuad: function(t) {
return t * t
},
// decelerating to zero velocity
easeOutQuad: function(t) {
return t * (2 - t)
},
// acceleration until halfway, then deceleration
easeInOutQuad: function(t) {
return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
},
// accelerating from zero velocity
easeInCubic: function(t) {
return t * t * t
},
// decelerating to zero velocity
easeOutCubic: function(t) {
return (--t) * t * t + 1
},
// acceleration until halfway, then deceleration
easeInOutCubic: function(t) {
return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
},
// accelerating from zero velocity
easeInQuart: function(t) {
return t * t * t * t
},
// decelerating to zero velocity
easeOutQuart: function(t) {
return 1 - (--t) * t * t * t
},
// acceleration until halfway, then deceleration
easeInOutQuart: function(t) {
return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
},
// accelerating from zero velocity
easeInQuint: function(t) {
return t * t * t * t * t
},
// decelerating to zero velocity
easeOutQuint: function(t) {
return 1 + (--t) * t * t * t * t
},
// acceleration until halfway, then deceleration
easeInOutQuint: function(t) {
return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
}
}
A related issue could be that you're setting the <canvas>'s height and width from CSS or other sources. I'm guessing it scales the canvas and associated drawings. Setting the <canvas> size using the height and width property (either from the HTML tag or a JS script) resolved the error for me.
Here is my solution: set width and height for canvas
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Also set in css, so it will not overflow from its parent
canvas {
width: 100%
height: 100%
}
Although LittleJoe's solution worked perfect on desktop it didn't work on mobile because on iphone 11 pro for example the dpi is 3 so I had to set width/height based on dpi. At the end it worked:
let width = 100, height = 100;
const dpi = window.devicePixelRatio;
canvas = document.getElementById('myCanvas');
canvas.width = width * dpi;
canvas.height = height * dpi;
canvas.style.width = width + "px";
canvas.style.height = width + "px";
canvas.getContext('2d').scale(dpi, dpi);
in order to get rid of the blurryness you need to set the size of the canvas in two manners:
first withcanvas.width = yourwidthhere;
and canvas.height = yourheighthere;
second by setting the css attribute either by js or a stylesheet
HTML:
<canvas class="canvas_hangman"></canvas>
JS:
function setUpCanvas() {
canvas = document.getElementsByClassName("canvas_hangman")[0];
ctx = canvas.getContext('2d');
ctx.translate(0.5, 0.5);
// Set display size (vw/vh).
var sizeWidth = 80 * window.innerWidth / 100,
sizeHeight = 100 * window.innerHeight / 100 || 766;
// console.log(sizeWidth, sizeHeight);
// Setting the canvas height and width to be responsive
canvas.width = sizeWidth;
canvas.height = sizeHeight;
canvas.style.width = sizeWidth;
canvas.style.height = sizeHeight;
}
window.onload = setUpCanvas();
This perfectly sets up your HTML canvas to draw on, and in a responsive manner too :)