Display pixel-perfect canvas on all devices - html
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.
Related
how to make cicular border corners (not rounding corners)
how do change border corners style on a font? -webkit-text-stroke: 3px blue; this sets the border, it's applied on font's contour. As you can see in my case I have very sharp thingy on the contour and I would prefer a bit more orthogonal style. As the line would end like on my sketch with red pencil. Or if someone doesn't like this style, can they change to round stroke?
I don't think what you are trying to do is possible. You could do it on a <canvas>: var canvas = document.getElementById("myCanvas"); var ctx = canvas.getContext("2d"); var dpr = window.devicePixelRatio; function paint() { canvas.width = 500 * dpr; canvas.height = 120 * dpr; ctx.lineJoin = "bevel"; ctx.lineWidth = 3 * dpr; ctx.strokeStyle = "blue"; ctx.font = (140 * dpr) + "px serif"; ctx.strokeText("Hello!", 20 * dpr, 110 * dpr); } paint(); window.addEventListener("resize", function() { if (dpr !== window.devicePixelRatio) { dpr = window.devicePixelRatio; paint(); } }); #myCanvas { width: 500px; height: 120px; } <canvas id="myCanvas"></canvas> This is what it looks like when you zoom in:
HTML5 Canvas DrawImage Safari 12.0 bug (tested on iOS 12.1/Mac OS Mojave)
Found this bug recently while dealing with CanvasRenderingContext2D.drawImage() method of the Canvas 2D API void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); The sx and sy parameters which are supposed to be the x-axis and y-axis coordinates of the top-left corner of the sub-rectangle of the source image, to draw into the destination context. (source: Mozilla) work differently on Safari browsers when provided with a value less than 0 (negative values). Negative values for sx and syon other browsers (tested Chrome and Microsoft Edge) work as expected, by re-positioning the mentioned top-left corner along the negative sides of the x-axis and y-axis But on Safari, negative values for sx and sy reset the top left coordinates to 0,0 and affect the RIGHT BOTTOM CORNER of the sub-rectangle of the source image, to draw into the destination context. This is how the attached demo snippet of canvas renders on Chrome and Safari (outline being the boundary of the canvas): const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const image = document.getElementById('source'); onload = e => ctx.drawImage(image, -40, -160, 300, 290, 0, 0, 300, 290); canvas { border: 1px solid black; } <canvas id="canvas" width="300" height="290"></canvas> <div style="display:none;"> <img id="source" src="https://i.stack.imgur.com/NS35G.jpg"> </div> Any work around for this will definitely help. EDIT: Source width and source height doesn’t exceed the actual image’s width or height.
This is what I have experienced. My code was created based on Chrome renderImage(ctx) { if (this.$$.source) { const { image } = ... // html img tag with src const { viewport } = this.$$; ctx.drawImage( image, viewport.x, viewport.y, viewport.width, viewport.height, 0, 0, this.canvasWidth, this.canvasHeight ); } viewport(x, y, width,height) - partial area of img to be rendered 0, 0, canvasWidth, canvasHeight - canvas area The code works well in chrome, but not in safari Workaround is to offset destination dimension if sx or sy is negative renderImage(ctx) { if (this.$$.source) { const { image } = ... // html img tag with src const { viewport } = this.$$; const offsetX = viewport.x < 0 ? -viewport.x : 0; const offsetY = viewport.y < 0 ? -viewport.y : 0; ctx.drawImage( image, viewport.x + offsetX, // set sx to 0 if negative viewport.y + offsetY, // set sy to 0 if negative viewport.width, viewport.height, offsetX, // shift dx if negative offsetY, // shift dy if negative this.canvasWidth, this.canvasHeight ); } It works in chrome and safari. My test image(400px, 400px) Chrome vieport=(x: -100, y:-100, width:400, height: 400) Safari vieport=(x: -100, y:-100, width:400, height: 400)
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%);
HTML 5 canvas lines appear distorted
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
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 :)