Introduction
I'm trying to deal with blurry visuals on my canvas animation. The blurriness is especially prevalent on mobile-devices, retina and high-dpi (dots-per-inch) screens.
I'm looking for a way to ensure the pixels that are drawn using the canvas look their best on low-dpi screens and high-dpi screens. As a solution to this problem I red multiple articles about canvas-down-scaling and followed this tutorial:
https://www.kirupa.com/canvas/canvas_high_dpi_retina.htm
Integrating down-scaling in the project
The project in which I want to implement down-scaling can be found below and consists of a few important features:
There is a (big) main canvas. (Performance optimization)
There are multiple (pre-rendered) smaller canvasses that are used to draw and load a image into. (Performance optimization)
The canvas is animated. (In the code snippet, there is no visible animation but the animation function is intergrated.)
Question
What im trying to achieve: The problem I'm facing seems quite simple. When the website (with the canvas) is opened on a mobile device (eg. an Iphone, with more pixels per inch then a regular desktop). The images appear more blurry. What I'm actually trying to achieve is to remove this blurriness from the images. I red this and it stated that blurriness can be removed by downsampling. I tried to incorporate this technique in the code provided, but it did not work completely. The images just became larger and I was unable to scale the images back to the original size. snippet it is not implemented correctly, the output is still blurry. What did I do wrong and how am I able to fix this issue?
Explanation of the code snippet
The variable devicePixelRatio is set to 2 to simulate a high-dpi phone screen, low-dpi screens have a devicePixelRatio of 1.
Multiple pre-rendered canvasses generated is the function spawn is the snippet there are 5 different canvasses, but on the production environment there are 10's.
If there are any pieces of information missing or questions about this post, please let me know. Thanks a lot!
Code Snippet
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d' );
var circles = [];
//Simulate Retina screen = 2, Normal screen = 1
let devicePixelRatio = 2
function mainCanvasPixelRatio() {
// get current size of the canvas
let rect = canvas.getBoundingClientRect();
// increase the actual size of our canvas
canvas.width = rect.width * devicePixelRatio;
canvas.height = rect.height * devicePixelRatio;
// ensure all drawing operations are scaled
c.scale(devicePixelRatio, devicePixelRatio);
// scale everything down using CSS
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
}
// Initial Spawn
function spawn() {
for (let i = 0; i < 2; i++) {
//Set Radius
let radius = parseInt(i*30);
//Give position
let x = Math.round((canvas.width/devicePixelRatio) / 2);
let y = Math.round((canvas.height /devicePixelRatio) / 2);
//Begin Prerender canvas
let PreRenderCanvas = document.createElement('canvas');
const tmp = PreRenderCanvas.getContext("2d");
//Set PreRenderCanvas width and height
let PreRenderCanvasWidth = ((radius*2)*1.5)+1;
let PreRenderCanvasHeight = ((radius*2)*1.5)+1;
//Increase the actual size of PreRenderCanvas
PreRenderCanvas.width = PreRenderCanvasWidth * devicePixelRatio;
PreRenderCanvas.height = PreRenderCanvasHeight * devicePixelRatio;
//Scale PreRenderCanvas down using CSS
PreRenderCanvas.style.width = PreRenderCanvasWidth + 'px';
PreRenderCanvas.style.height = PreRenderCanvasHeight + 'px';
//Ensure PreRenderCanvas drawing operations are scaled
tmp.scale(devicePixelRatio, devicePixelRatio);
//Init image
const image= new Image();
//Get center of PreRenderCanvas
let m_canvasCenterX = (PreRenderCanvas.width/devicePixelRatio) * .5;
let m_canvasCenterY = (PreRenderCanvas.height/devicePixelRatio) * .5;
//Draw red circle on PreRenderCanvas
tmp.strokeStyle = "red";
tmp.beginPath();
tmp.arc((m_canvasCenterX), (m_canvasCenterY), ((PreRenderCanvas.width/devicePixelRatio)/3) , 0, 2 * Math.PI);
tmp.lineWidth = 2;
tmp.stroke();
tmp.restore();
tmp.closePath()
//Set Image
image .src= "https://play-lh.googleusercontent.com/IeNJWoKYx1waOhfWF6TiuSiWBLfqLb18lmZYXSgsH1fvb8v1IYiZr5aYWe0Gxu-pVZX3"
//Get padding
let paddingX = (PreRenderCanvas.width/devicePixelRatio)/5;
let paddingY = (PreRenderCanvas.height/devicePixelRatio)/5;
//Load image
image.onload = function () {
tmp.beginPath()
tmp.drawImage(image, paddingX,paddingY, (PreRenderCanvas.width/devicePixelRatio)-(paddingX*2),(PreRenderCanvas.height/devicePixelRatio)-(paddingY*2));
tmp.closePath()
}
let circle = new Circle(x, y, c ,PreRenderCanvas);
circles.push(circle)
}
}
// Circle parameters
function Circle(x, y, c ,m_canvas) {
this.x = x;
this.y = y;
this.c = c;
this.m_canvas = m_canvas;
}
//Draw circle on canvas
Circle.prototype = {
//Draw circle on canvas
draw: function () {
this.c.drawImage( this.m_canvas, (this.x - (this.m_canvas.width)/2), (this.y - this.m_canvas.height/2));
}
};
// Animate
function animate() {
//Clear canvas each time
c.clearRect(0, 0, (canvas.width /devicePixelRatio), (canvas.height /devicePixelRatio));
//Draw in reverse for info overlap
circles.slice().reverse().forEach(function( circle ) {
circle.draw();
});
requestAnimationFrame(animate);
}
mainCanvasPixelRatio()
spawn()
animate()
#mainCanvas {
background:blue;
}
<canvas id="mainCanvas"></canvas>
<br>
<!DOCTYPE html>
<html>
<body>
<p>Image to use:</p>
<img id="scream" width="220" height="277"
src="pic_the_scream.jpg" alt="The Scream">
<p>Canvas:</p>
<canvas id="myCanvas" width="240" height="297"
style="border:1px solid #d3d3d3;">
</canvas>
<script>
window.onload = function() {
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var img = document.getElementById("scream");
ctx.drawImage(img, 10, 10);
};
</script>
</body>
Related
This question already has answers here:
Canvas is stretched when using CSS but normal with "width" / "height" properties
(10 answers)
Real mouse position in canvas
(5 answers)
Closed 6 months ago.
need help!!! i am following this new javascript 2D gaming video and i am currently stuck on 2:32:29 where we are making an explosion effect on canvas where the mouse clicked. https://www.youtube.com/watch?v=GFO_txvwK_c&t=9365s.
problem: whenever i click on the canvas, no sqaure shapes shows up, in the tutorial it mentioned you will have to offset the e.x and e.y to make it relative the canvas. but i realized whenever i click the top left corner of the screen. sometimes an elongated rectangular shape does appear but when i offset my e.x and e.y. i can not get the correct offset to make it work. could anyone help explain why i am getting this weird offset effect on my event.x and event.y values and how to fix it? thanks a lot!!! you can see the thrid picture attached for my code adding the offset suggested by video and it still does not work.
const canvas=document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = 500;
canvas,height = 700;
const explosion =[];
class Explosion {
constructor(x,y){
this.x = x;
this.y = y;
this.spriteWidth = 200;
this.spriteHeight = 179;
this.width = this.spriteWidth/2;
this.height = this.spriteHeight/2;
this.image = new this.image();
this.image.src = "boom.png";
this.frame=0;
}
update(){
this.frame++;
}
draw(){
ctx.drawImage(this.image,this.spriteWidth*this.frame,0,this.spriteWidth,this.spriteHeight,this.x,this.y,this.width, this.height);
}
}
window.addEventListener('click', function(e){
ctx.fillStyle= 'white';
ctx.fillRect(e.x,e.y,50,50);
})
enter image description here
enter image description here
enter image description here
First, I wouldn't attach the click listener to the global window object. As your merely interested in clicks happening inside the <canvas> element, attach it to this instead.
Second, the x and y properties returned by the mouseevent don't match what you might expect because it does not take into account the offset of the HTML element on the page. Try using the offsetX and offsetY properties instead.
Here's an example:
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = 500;
canvas.height = 700;
const explosion = [];
class Explosion {
constructor(x, y) {
this.x = x;
this.y = y;
this.spriteWidth = 200;
this.spriteHeight = 179;
this.width = this.spriteWidth / 2;
this.height = this.spriteHeight / 2;
this.image = new this.image();
this.image.src = "boom.png";
this.frame = 0;
}
}
canvas.addEventListener('click', function(e) {
ctx.fillStyle = 'blue';
ctx.fillRect(e.offsetX, e.offsetY, 50, 50);
});
<canvas id="canvas1" style="background: #eeeeee"></canvas>
Website: http://minimedit.com/
Currently implementing an eye dropper. It works fine in my normal resolution of 1080p, but when testing on a higher or lower resolution it doesn't work.
This is the basics of the code:
var ctx = canvas.getContext("2d");
canvas.on('mouse:down', function(e) {
var newColor = dropColor(e, ctx);
}
function dropColor(e, ctx) {
var mouse = canvas.getPointer(e.e),
x = parseInt(mouse.x),
y = parseInt(mouse.y),
px = ctx.getImageData(x, y, 1, 1).data;
return rgb2hex('rgba('+px+')');
}
When I first initiate the canvas I have it resize to fit resolution:
setResolution(16/9);
function setResolution(ratio) {
var conWidth = ($(".c-container").css('width')).replace(/\D/g,'');
var conHeight = ($(".c-container").css('height')).replace(/\D/g,'');
var tempWidth = 0;
var tempHeight = 0;
tempHeight = conWidth / ratio;
tempWidth = conHeight * ratio;
if (tempHeight > conHeight) {
canvas.setWidth(tempWidth);
canvas.setHeight(conHeight);
} else {
canvas.setWidth(conWidth);
canvas.setHeight(tempHeight);
}
}
The x and y mouse coordinates work fine when zoomed in, but they don't line up with the returned image data. It seems as though the ctx isn't changing it's width and height and scaling along with the actual canvas size.
The canvas element is showing the correct width and height before using getContext as well.
Any ideas on a solution?
Feel free to check out the full scripts on the live website at: http://minimedit.com/
Try "fabric.devicePixelRatio" for calculating actual position, for example:
x = parseInt(mouse.x) * fabric.devicePixelRatio
Ok, I've noticed this bug happen not only in the following code(which is just to illustrate), but every code I've written for canvas;
basically, in chrome(I didn't test other browsers),
once you shift your current tab to either a different pc screen, or simply if you have a few tabs in a row and decide to make a new window out of your current tab,
the canvas itself fails to draw. it is stuck and no reason is given in the console.
Here is a GIF of it happening:
http://gifmaker.cc/PlayGIFAnimation.php?folder=2016081601ZEuvXaZnxC2T9fHNNqSW3l&file=output_f72O1H.gif
Just a simple canvas code if you want to try it out:
<canvas id="canvas" width="500" height="400" style="border:1px solid #000000;"></canvas>
<script>
// grab the canvas and context
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
// inital coordinates of the black square
var x = 0;
var y = 200;
// speed of the movement in x- and y-direction
// actual no movement in y-direction
var vX = 1;
var vY = 0;
// width and height of the square
var width = 10;
var height = 10;
function animate() {
// clear
// comment this clear function to see the plot of the sine movement
ctx.clearRect(0, 0, canvas.width, canvas.height);
x += vX;
y += vY;
// if the block leaves the canvas on the right side
// bring it back to the left side
if(x>500) {
x = 0;
}
ctx.fillRect(x, y, width, height);
setTimeout(animate, 33);
}
// call the animate function manually for the first time
animate();
</script>
Some sites fixed this bug but I don't know if I am allowed to link them here.
I have the following javascript code to draw a graph sheet. But the problem is when I take a printout, The thin lines are not appearing sharp. The problem is visible when you zoom the html page. I want the lines to be more sharp. But the width should be the same. Is it possible? Please help.
function drawBkg(canvasElem, squareSize, minorLineWidthStr, lineColStr)
{
var nLinesDone = 0;
var i, curX, curY;
var ctx = canvasElem.getContext('2d');
ctx.clearRect(0,0,canvasElem.width,canvasElem.height);
// draw the vertical lines
curX=0;
ctx.strokeStyle = lineColStr;
while (curX < canvasElem.width)
{
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(curX, 0);
ctx.lineTo(curX, canvasElem.height);
ctx.stroke();
curX += squareSize;
nLinesDone++;
}
// draw the horizontal lines
curY=0;
nLinesDone = 0;
while (curY < canvasElem.height)
{
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(0, curY);
ctx.lineTo(canvasElem.width, curY);
ctx.stroke();
curY += squareSize;
nLinesDone++;
}
}
drawBkg(byId('canvas'), 3.78, "0.35", "green");
What you are experiencing is the difference between your screen's PPI and your printer's DPI.
Canvas output is a raster image, if you set its size to be like 96px, a monitor with a resolution of 96ppi will output it as a one inch large image, but a printer with 300ppi will output it as a 3.125 inch image.
When doing so, the printing operation will downsample your image so it can fit into this new size. (each pixel will be multiplied so it covers a bigger area).
But the canvas context2d has a scale() method, so if all your drawings are vector based1, you can :
create a bigger canvas before printing,
set its context's scale to the wanted factor,
call the same drawing as on the smaller canvas
if you are printing directly from the browser's "print the page", set the bigger canvas style.width and style.height properties to the width and height properties of the smaller one,
replace the smaller canvas node with the bigger one,
print,
replace the bigger canvas with the original one
For this, you will need to rewrite a little bit your function so it doesn't take the passed canvas' width/height as values, but rather values that you have chosen.
function drawBkg(ctx, width, height, squareSize, minorLineWidthStr, lineColStr) {
var nLinesDone = 0;
var i, curX, curY;
ctx.clearRect(0, 0, width, height);
// draw the vertical lines
curX = 0;
ctx.strokeStyle = lineColStr;
while (curX < width) {
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(curX, 0);
ctx.lineTo(curX, height);
ctx.stroke();
curX += squareSize;
nLinesDone++;
}
// draw the horizontal lines
curY = 0;
nLinesDone = 0;
while (curY < height) {
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(0, curY);
ctx.lineTo(width, curY);
ctx.stroke();
curY += squareSize;
nLinesDone++;
}
}
// your drawings
var smallCanvas = document.getElementById('smallCanvas');
var smallCtx = smallCanvas.getContext('2d');
drawBkg(smallCtx, smallCanvas.width, smallCanvas.height, 3.78, "0.35", "green");
// a function to get the screen's ppi
function getPPI() {
var test = document.createElement('div');
test.style.width = "1in";
test.style.height = 0;
document.body.appendChild(test);
var dpi = devicePixelRatio || 1;
var ppi = parseInt(getComputedStyle(test).width) * dpi;
document.body.removeChild(test);
return ppi;
}
function scaleAndPrint(outputDPI) {
var factor = outputDPI / getPPI();
var bigCanvas = smallCanvas.cloneNode();
// set the required size of our "printer version" canvas
bigCanvas.width = smallCanvas.width * factor;
bigCanvas.height = smallCanvas.height * factor;
// set the display size the same as the original one to don't brake the page's layout
var rect = smallCanvas.getBoundingClientRect();
bigCanvas.style.width = rect.width + 'px';
bigCanvas.style.height = rect.height + 'px';
var bigCtx = bigCanvas.getContext('2d');
// change the scale of our big context
bigCtx.scale(factor, factor);
// tell the function we want the height and width of the small canvas
drawBkg(bigCtx, smallCanvas.width, smallCanvas.height, 3.78, "0.35", "green");
// replace our original canvas with the bigger one
smallCanvas.parentNode.replaceChild(bigCanvas, smallCanvas);
// call the printer
print();
// set the original one back
bigCanvas.parentNode.replaceChild(smallCanvas, bigCanvas);
}
btn_o.onclick = function() { print(); };
btn_s.onclick = function() { scaleAndPrint(300);};
<button id="btn_o">print without scaling</button>
<button id="btn_s">print with scaling</button>
<br>
<canvas id="smallCanvas" width="250" height="500"></canvas>
1. all drawing operations on canvas are vector based, except for drawImage(), and putImageData()
Most simple way to achieve cripser lines is to use oversampling : you draw in a canvas which has a resolution bigger than the screen's resolution.
In Javascript if you want to oversample by a factor of X :
Change canvas's width and height to width*X and height*X
Scale the canvas's context by a factor of X
Fix Css width and height to inital width and height to keep same size on screen.
In the below sample i first downsampled the canvas to make it easier to see. You have to zoom quite a lot to see the difference between no upsampling, 2 X and 4X.
function overSampleCanvas(tgtCanvas, ctx, factor) {
var width = tgtCanvas.width;
var height = tgtCanvas.height;
tgtCanvas.width = 0 | (width * factor);
tgtCanvas.height = 0 | (height * factor);
tgtCanvas.style.width = width + 'px';
tgtCanvas.style.height = height + 'px';
ctx.scale(factor, factor);
}
// -------------------- example
var $ = document.getElementById.bind(document);
var cv05 = $('cv05'),
ctx05 = cv05.getContext('2d');
var cv = $('cv'),
ctx = cv.getContext('2d');
var cv2X = $('cv2X'),
ctx2X = cv2X.getContext('2d');
var cv4X = $('cv4X'),
ctx4X = cv4X.getContext('2d');
overSampleCanvas(cv05, ctx05, 0.5);
overSampleCanvas(cv2X, ctx2X, 2);
overSampleCanvas(cv4X, ctx4X, 4);
function drawCircle(ctx) {
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 6.28);
ctx.fillStyle = '#AB6';
ctx.fill();
}
drawCircle(ctx05);
drawCircle(ctx);
drawCircle(ctx2X);
drawCircle(ctx4X);
canvas downsampled by 2X, normal, then upsampled by 2X, then 4X. <br>
<canvas id="cv05" width="100" height="100"></canvas>
<canvas id="cv" width="100" height="100"></canvas>
<canvas id="cv2X" width="100" height="100"></canvas>
<canvas id="cv4X" width="100" height="100"></canvas>
I'm experimenting with HTML5 canvas, and am having some issues with drawing Circle / Arc, when ever I make a circle, it appears to be more of a distorted oval. I'm not sure what I am doing wrong.
Here is my js, fiddle below the code.
<script>
$(document).ready(function drawOnCanvas() {
$("a#draw").click(function(){
var canvas = document.getElementById("canvas_1");
if ( canvas.getContext ) {
var canvas_context = canvas.getContext("2d");
console.log("2D context supported");
// Drawing circles
var start_degrees = 0;
var end_degrees = 360;
var start_angle = ( Math.PI/180 ) * start_degrees;
var end_angle = ( Math.PI/180 ) * end_degrees;
canvas_context.beginPath();
canvas_context.arc(100,100,25,start_angle, end_angle, true);
canvas_context.strokeStyle="rgb(0,222,0)";
canvas_context.fill();
}
else{
alert("Not Working");
}
});
});
</script>
Fiddle:
http://jsfiddle.net/hLfHy/
The problem is that the canvas inner size isn't accorded to the viewport in which it's rendered.
Add this in the initialization :
var canvas = document.getElementById('canvas_1');
canvas.height = canvas.clientHeight;
canvas.width = canvas.clientWidth
Demonstration
You should set the canvas size to have the correct aspect ratio
Example
<canvas id="canvas_1" width="500" height="500>
You need to set your canvas width and height so you get your calculations right. Try the below it seems to be working now in your fiddle http://jsfiddle.net/hLfHy/10/
canvas.height = $("#canvas_1").clientHeight;
canvas.width = $("#canvas_1").clientWidth;