Resize canvas to viewport without loosing quality - javascript

I am trying to have a canvas fill the entire viewport and resize, whenever the user resizes the browser window.
However, when I try to use a size of 100%, it looses quality.
I am kind of a newb in JavaScript programming.
Can someone please help me?

I figured out a way:
I calculate a scaling factor, by which I scale the size, but not the position of each element drawn.
I do not want to scale the position, because then some elements might be off screen.
Thus I can not use setTransform, as suggested in the comments.
Here is the code I used for calculating the scaling factor.
If someone has improvements, please share them in the comments below!
if (window.screen.width > window.screen.height)
scalingFactor = window.screen.width * window.devicePixelRatio / 1280;
else
scalingFactor = window.screen.height * window.devicePixelRatio / 720;

The easiest way to resize a canvas with losing the data currently on it is to store the data, then resize the canvas (which clears it), then redraw the data into the canvas. That would look something like this:
const data = ctx.getImageData( 0, 0, canvas.width, canvas.height );
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.putImageData( data, 0, 0 );
In its purest, simplest example it would look something like this:
/* This method does the resize by storing the data and redrawing it. It loses any data outside of the screen, though. */
function onResize( event ){
const data = ctx.getImageData( 0, 0, canvas.width, canvas.height );
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.putImageData( data, 0, 0 );
}
/* This method is for drawing some content to help visualisation. */
function onRedraw(){
const r = v => Math.floor(Math.random() * v);
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.fillStyle = `rgb(${r(255)},${r(255)},${r(255)})`;
ctx.fillRect( 0, 0, innerWidth, innerHeight );
ctx.fillStyle = `rgb(${r(255)},${r(255)},${r(255)})`;
ctx.fillRect( 0, 0, innerWidth / 2, innerHeight / 2 );
ctx.fillRect( innerWidth / 2, innerHeight / 2, innerWidth / 2, innerHeight / 2 );
}
window.addEventListener( 'resize', onResize );
const canvas = document.createElement( 'canvas' );
const ctx = canvas.getContext( '2d' );
const reset = document.getElementById( 'reset' );
document.body.appendChild( canvas );
onRedraw();
redraw.addEventListener( 'click', onRedraw );
body { padding: 0; margin: 0; overflow: hidden; }
button { position: fixed; top: 20px; left: 20px; z-index: 1; }
<button id="redraw">Redraw</button>
However, you lose data drawn outside the canvas when your window grows and shrinks, as the data is never saved. We could hold a buffer with our graphics, which could simplify things a bit but makes the code slightly more complicated. In this case, we can use a second canvas that holds all our previously drawn maximum sized screen data, just for reference:
/* This method does the resize by storing the data and redrawing it. It retains data outside the screen in a buffer that holds the maximum size ever drawn. */
function onResize( event ){
const data = bufferCtx.getImageData( 0, 0, bufferCanvas.width, bufferCanvas.height );
bufferCanvas.width = Math.max( bufferCanvas.width, innerWidth );
bufferCanvas.height = Math.max( bufferCanvas.height, innerHeight );
bufferCtx.putImageData( data, 0, 0 );
bufferCtx.drawImage( canvas, 0, 0 );
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.drawImage( bufferCanvas, 0, 0 );
}
/* This method is for drawing some content to help visualisation. */
function onRedraw(){
const r = v => Math.floor(Math.random() * v);
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.fillStyle = `rgb(${r(255)},${r(255)},${r(255)})`;
ctx.fillRect( 0, 0, innerWidth, innerHeight );
ctx.fillStyle = `rgb(${r(255)},${r(255)},${r(255)})`;
ctx.fillRect( 0, 0, innerWidth / 2, innerHeight / 2 );
ctx.fillRect( innerWidth / 2, innerHeight / 2, innerWidth / 2, innerHeight / 2 );
}
window.addEventListener( 'resize', onResize );
const canvas = document.createElement( 'canvas' );
const ctx = canvas.getContext( '2d' );
const bufferCanvas = document.createElement( 'canvas' );
const bufferCtx = bufferCanvas.getContext( '2d' );
const reset = document.getElementById( 'reset' );
document.body.appendChild( canvas );
onRedraw();
redraw.addEventListener( 'click', onRedraw );
body { padding: 0; margin: 0; overflow: hidden; }
button { position: fixed; top: 20px; left: 20px; z-index: 1; }
<button id="redraw">Redraw</button>
If you'de rather use lose no data at all and 'rescale' the canvas the best it can, perhaps best to use something like this:
/* This method does the resize by just redrawing the canvas from a buffer: */
function onResize( event ){
bufferCanvas.width = canvas.width;
bufferCanvas.height = canvas.height;
bufferCtx.drawImage( canvas, 0, 0 );
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.drawImage( bufferCanvas, 0, 0, canvas.width, canvas.height );
}
/* This method is for drawing some content to help visualisation. */
function onRedraw(){
const r = v => Math.floor(Math.random() * v);
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.fillStyle = `rgb(${r(255)},${r(255)},${r(255)})`;
ctx.fillRect( 0, 0, innerWidth, innerHeight );
ctx.fillStyle = `rgb(${r(255)},${r(255)},${r(255)})`;
ctx.fillRect( 0, 0, innerWidth / 2, innerHeight / 2 );
ctx.fillRect( innerWidth / 2, innerHeight / 2, innerWidth / 2, innerHeight / 2 );
}
window.addEventListener( 'resize', onResize );
const canvas = document.createElement( 'canvas' );
const ctx = canvas.getContext( '2d' );
const bufferCanvas = document.createElement( 'canvas' );
const bufferCtx = bufferCanvas.getContext( '2d' );
const reset = document.getElementById( 'reset' );
document.body.appendChild( canvas );
onRedraw();
redraw.addEventListener( 'click', onRedraw );
body { padding: 0; margin: 0; overflow: hidden; }
button { position: fixed; top: 20px; left: 20px; z-index: 1; }
<button id="redraw">Redraw</button>

Related

Canvas fill viewport and keep image ratio

I am trying to make the canvas area and image fill the viewport without the image stretching and retain its aspect ratio when you resize the page. Below is my current code.
var ctx = $("#demo")[0].getContext("2d"),
img = new Image(),
radius = 35,
blurryImageSrc = "https://s9.postimg.cc/u9nsmzlwf/image.jpg";
img.src = blurryImageSrc;
$(img).on("load", function() {
resizeCanvas();
$("#demo").on("mousemove", function(e) {
erase(getXY(e));
});
$("#reset").on("click", function() {
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = "destination-out";
});
ctx.drawImage(img, 0, 0, window.innerWidth, window.innerHeight);
ctx.globalCompositeOperation = "destination-out";
});
function getXY(e) {
var r = $("#demo")[0].getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top };
}
function erase(pos) {
ctx.beginPath();
ctx.arc(pos.x, pos.y, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
var can = document.getElementById('demo');
function resizeCanvas() {
can.style.width = window.innerWidth + 'px';
setTimeout(function() {
can.style.height = window.innerHeight + 'px';
}, 0);
}
window.onresize = resizeCanvas;
resizeCanvas();
https://jsfiddle.net/65fph0mn/8/
To retain the image's correct proportions, you need to query it's 'natural' width and height. The image object itself has two properties for this purpose: naturalWidth and naturalHeight.
As the image finished loading you must decide by which factor to scale the image based on the dimensions of the browser window and the longer side of your image.
Let's have a look at a simple example. Say your browser window's width is 1200 pixel and the image 250. If we now divide 1200 by 250 we get 4.8 - that's the factor we need to multiply the image's width and height to fill the current browser window in one direction while maintaining it's correct aspect ratio.
Here's an example:
var ctx = $("#demo")[0].getContext("2d"),
img = new Image(),
radius = 35,
blurryImageSrc = "https://s9.postimg.cc/u9nsmzlwf/image.jpg";
/// setup logic
img.src = blurryImageSrc;
$(img).on("load", function() {
resizeCanvas();
});
var can = document.getElementById('demo');
function resizeCanvas() {
can.width = window.innerWidth;
can.height = window.innerHeight;
if (img.width > 0) {
let factor = can.width / img.naturalWidth * img.naturalHeight > window.innerHeight ? can.height / img.naturalHeight : can.width / img.naturalWidth;
ctx.drawImage(img, 0, 0, img.naturalWidth * factor, img.naturalHeight * factor);
}
}
window.onresize = resizeCanvas;
resizeCanvas();
body {
background: lightgrey;
margin: 0;
}
.container {
position: relative;
background: blue;
width: 100vw;
height: 100vh;
}
#demo {
cursor: crosshair;
width: 100vw;
height: 100vh;
}
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<div class="container">
<canvas id="demo" width=640 height=640></canvas>
</div>
Here I'm waiting for the image to load. Then I can add the eventlistener for window resize and call the function resizeCanvas. This function will then resize the canvas and paint the image in the canvas.
The image must be repainted every time the window is resized.
To keep the aspect ratio the height of the image is calculated from the scaled width.
const canvas = document.getElementById('demo');
var ctx = canvas.getContext("2d"),
img = new Image(),
blurryImageSrc = "https://s9.postimg.cc/u9nsmzlwf/image.jpg";
img.src = blurryImageSrc;
img.addEventListener("load", e => {
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
});
function paint() {
var newHeight = Math.round(img.width/canvas.width*img.height);
ctx.drawImage(img, 0, 0, img.width, newHeight, 0, 0, canvas.width, canvas.height);
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
paint();
}
body {
background: lightgrey;
margin: 0;
}
.container {
position: relative;
background: blue;
width: 100vw;
height: 100vh;
}
#demo {
cursor: crosshair;
width: 100vw;
height: 100vh;
}
<div class="container">
<canvas id="demo" width="640" height="640"></canvas>
</div>

Adjust canvas image size like 'background-size: cover' and responsive

What I want to do
Set background-size:cover-like effect on a canvas image
Re-draw a canvas image as a window is resized (responsive)
What I tried
I tried 2 ways below. Neither works.
Simulation background-size: cover in canvas
How to set background size cover on a canvas
Issue
The image's aspect ratio is not fixed.
I am not sure but the image does not seem to be re-rendered as a window is resized.
My code
function draw() {
// Get <canvas>
const canvas = document.querySelector('#canvas');
// Canvas
const ctx = canvas.getContext('2d');
const cw = canvas.width;
const ch = canvas.height;
// Image
const img = new Image();
img.src = 'https://source.unsplash.com/WLUHO9A_xik/1600x900';
img.onload = function() {
const iw = img.width;
const ih = img.height;
// 'background-size:cover'-like effect
const aspectHeight = ih * (cw / iw);
const heightOffset = ((aspectHeight - ch) / 2) * -1;
ctx.drawImage(img, 0, heightOffset, cw, aspectHeight);
};
}
window.addEventListener('load', draw);
window.addEventListener('resize', draw);
canvas {
display: block;
/* canvas width depends on parent/window width */
width: 90%;
height: 300px;
margin: 0 auto;
border: 1px solid #ddd;
}
<canvas id="canvas"></canvas>
The canvas width / height attributes do not reflect it's actual size in relation to it being sized with CSS. Given your code, cw/ch are fixed at 300/150. So the whole calculation is based on incorrect values.
You need to use values that actually reflect it's visible size. Like clientWidth / clientHeight.
A very simple solution is to update the canvas width/height attributes before using them for any calculations. E.g.:
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
Full example:
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
// only load the image once
const img = new Promise(r => {
const img = new Image();
img.src = 'https://source.unsplash.com/WLUHO9A_xik/1600x900';
img.onload = () => r(img);
});
const draw = async () => {
// resize the canvas to match it's visible size
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const loaded = await img;
const iw = loaded.width;
const ih = loaded.height;
const cw = canvas.width;
const ch = canvas.height;
const f = Math.max(cw/iw, ch/ih);
ctx.setTransform(
/* scale x */ f,
/* skew x */ 0,
/* skew y */ 0,
/* scale y */ f,
/* translate x */ (cw - f * iw) / 2,
/* translate y */ (ch - f * ih) / 2,
);
ctx.drawImage(loaded, 0, 0);
};
window.addEventListener('load', draw);
window.addEventListener('resize', draw);
.--dimensions {
width: 90%;
height: 300px;
}
.--border {
border: 3px solid #333;
}
canvas {
display: block;
margin: 0 auto;
}
#test {
background: url(https://source.unsplash.com/WLUHO9A_xik/1600x900) no-repeat center center;
background-size: cover;
margin: 1rem auto 0;
}
<canvas id="canvas" class="--dimensions --border"></canvas>
<div id="test" class="--dimensions --border"></div>
First, load only once your image, currently you are reloading it every time the page is resized.
Then, your variables cw and ch will always be 300 and 150 since you don't set the size of your canvas. Remember, the canvas has two different sizes, its layout one (controlled by CSS) and its buffer one (controlled by its .width and .height attributes).
You can retrieve the layout values through the element's .offsetWidth and .offsetHeight properties.
Finally, your code does a contain image-sizing. To do a cover, you can refer to the answers you linked to, and particularly to K3N's one
{
// Get <canvas>
const canvas = document.querySelector('#canvas');
// Canvas
const ctx = canvas.getContext('2d');
// Image
const img = new Image();
img.src = 'https://source.unsplash.com/WLUHO9A_xik/1600x900';
function draw() {
// get the correct dimension as calculated by CSS
// and set the canvas' buffer to this dimension
const cw = canvas.width = canvas.offsetWidth;
const ch = canvas.height = canvas.offsetHeight;
if( !inp.checked ) {
drawImageProp(ctx, img, 0, 0, cw, ch, 0, 0);
}
}
img.onload = () => {
window.addEventListener('resize', draw);
draw();
};
inp.oninput = draw;
}
// by Ken Fyrstenberg https://stackoverflow.com/a/21961894/3702797
function drawImageProp(ctx, img, x, y, w, h, offsetX, offsetY) {
if (arguments.length === 2) {
x = y = 0;
w = ctx.canvas.width;
h = ctx.canvas.height;
}
// default offset is center
offsetX = typeof offsetX === "number" ? offsetX : 0.5;
offsetY = typeof offsetY === "number" ? offsetY : 0.5;
// keep bounds [0.0, 1.0]
if (offsetX < 0) offsetX = 0;
if (offsetY < 0) offsetY = 0;
if (offsetX > 1) offsetX = 1;
if (offsetY > 1) offsetY = 1;
var iw = img.width,
ih = img.height,
r = Math.min(w / iw, h / ih),
nw = iw * r, // new prop. width
nh = ih * r, // new prop. height
cx, cy, cw, ch, ar = 1;
// decide which gap to fill
if (nw < w) ar = w / nw;
if (Math.abs(ar - 1) < 1e-14 && nh < h) ar = h / nh; // updated
nw *= ar;
nh *= ar;
// calc source rectangle
cw = iw / (nw / w);
ch = ih / (nh / h);
cx = (iw - cw) * offsetX;
cy = (ih - ch) * offsetY;
// make sure source rectangle is valid
if (cx < 0) cx = 0;
if (cy < 0) cy = 0;
if (cw > iw) cw = iw;
if (ch > ih) ch = ih;
// fill image in dest. rectangle
ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
}
canvas {
display: block;
/* canvas width depends on parent/window width */
width: 90%;
height: 300px;
margin: 0 auto;
border: 1px solid #ddd;
background-image: url('https://source.unsplash.com/WLUHO9A_xik/1600x900');
background-size: cover;
background-repeat: no-repeat;
}
<label><input id="inp" type="checkbox">show CSS rendering</label>
<canvas id="canvas"></canvas>

Canvas createPattern() and fill() with an offset

I'm using the canvas to draw simple rectangles and fill them with a flooring pattern (PNG). But if I utilize a "Camera" script to handle transform offsets for the HTML5 canvas, the rectangle shape moves appropriately, but the fill pattern seems to always draw from a fixed point on the screen (I'm assuming the top left). Is there a way to "nail down" the fill pattern so it always lines up with the rectangle, no matter the canvas transform, or a way to add an offset on fill() that can be calculated elsewhere in the code? I could just use drawImage() of course, but drawing the rectangles is more versatile for my purposes.
sampleDrawFunction(fillTexture, x1, y1, x2, y2) {
// This is oversimplified, but best I can do with a ~10k lines of code
x1 -= Camera.posX;
x2 -= Camera.posX;
y1 = -1 * y1 - Camera.posY;
y2 = -1 * y2 - Camera.posY;
// Coordinates need to be adjusted for where the camera is positioned
var img = new Image();
img.src = fillTexture;
var pattern = this.ctx.createPattern(img, "repeat");
this.ctx.fillStyle = pattern;
// Translate canvas's coordinate pattern to match what the camera sees
this.ctx.save();
this.ctx.translate(Camera.posX - Camera.topLeftX, Camera.posY - Camera.topLeftY);
this.ctx.fillStyle = pattern;
this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
this.ctx.restore();
}
Thank you.
Canvas fills (CanvasPatterns and CanvasGradients) are always relative to the context's transformation matrix, so they do indeed default at top left corner of the canvas, and don't really care about where the path using them will be be.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = begin; //!\ ALWAYS WAIT FOR YOUR IMAGE TO LOAD BEFORE DOING ANYTHING WITH IT!
img.crossOrigin = "anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
function begin() {
const rect_size = 20;
ctx.fillStyle = ctx.createPattern( img, 'no-repeat' );
// drawing a checkerboard of several rects shows that the pattern doesn't move
for ( let y = 0; y < canvas.height; y += rect_size ) {
for ( let x = (y / rect_size % 2) ? rect_size : 0 ; x < canvas.width; x += rect_size * 2 ) {
ctx.fillRect( x, y, rect_size, rect_size );
}
}
}
<canvas id="canvas" width="500" height="500"></canvas>
Now, because they are relative to the context transform matrix, this means we can also move them by changing that transformation matrix:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = begin; //!\ ALWAYS WAIT FOR YOUR IMAGE TO LOAD BEFORE DOING ANYTHING WITH IT!
img.crossOrigin = "anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/f/f7/Cool_bunny_sprite.png";
function begin() {
const rect_size = 244;
const max_speed = 5;
let x_speed = Math.random() * max_speed;
let y_speed = Math.random() * max_speed;
ctx.fillStyle = ctx.createPattern( img, 'repeat' );
let x = 0;
let y = 0;
requestAnimationFrame( anim );
function anim( now ) {
clear();
x = (x + x_speed);
y = (y + y_speed);
if( x > canvas.width || x < -rect_size ) {
x_speed = Math.random() * max_speed * -Math.sign( x_speed );
x = Math.min( Math.max( x, -rect_size ), canvas.width );
}
if( y > canvas.height || y < -rect_size ) {
y_speed = Math.random() * max_speed * -Math.sign( y_speed )
y = Math.min( Math.max( y, -rect_size ), canvas.height );
}
// we change the transformation matrix of our context
ctx.setTransform( 1, 0, 0, 1, x, y );
// we thus always draw at coords 0,0
ctx.fillRect( 0, 0, rect_size, rect_size );
ctx.strokeRect( 0, 0, rect_size, rect_size );
requestAnimationFrame( anim );
}
function clear() {
// since we changed the tranform matrix we need to reset it to identity
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, canvas.width, canvas.height );
}
}
<canvas id="canvas" width="300" height="300"></canvas>
We can even detach the path declaration from the filling by changing the transformation matrix after we declared the sub-path and of course by replacing the shorthand fillRect() with beginPath(); rect(); fill()
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = begin; //!\ ALWAYS WAIT FOR YOUR IMAGE TO LOAD BEFORE DOING ANYTHING WITH IT!
img.crossOrigin = "anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/f/f7/Cool_bunny_sprite.png";
function begin() {
const rect_size = 244;
const max_speed = 5;
let x_speed = Math.random() * max_speed;
let y_speed = Math.random() * max_speed;
ctx.fillStyle = ctx.createPattern( img, 'repeat' );
let x = 0;
let y = 0;
requestAnimationFrame( anim );
function anim( now ) {
clear();
x = (x + x_speed);
y = (y + y_speed);
if( x > canvas.width || x < -rect_size ) {
x_speed = Math.random() * max_speed * -Math.sign( x_speed );
x = Math.min( Math.max( x, -rect_size ), canvas.width );
}
if( y > canvas.height || y < -rect_size ) {
y_speed = Math.random() * max_speed * -Math.sign( y_speed )
y = Math.min( Math.max( y, -rect_size ), canvas.height );
}
// we declare the sub-path first, with identity matrix applied
ctx.beginPath();
ctx.rect( 50, 50, rect_size, rect_size );
// we change the transformation matrix of our context
ctx.setTransform( 1, 0, 0, 1, x, y );
// and now we fill
ctx.fill();
ctx.stroke();
requestAnimationFrame( anim );
}
function clear() {
// since we changed the tranform matrix we need to reset it to identity
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, canvas.width, canvas.height );
}
}
<canvas id="canvas" width="300" height="300"></canvas>
But in your case, it sounds that transforming the whole drawing is the easiest and most idiomatic way to follow. Not too sure why you are modifying your x and y coordinates in relation to the camera. Generally, if we use a camera object it's for the objects in the scene don't have to care about it, and stay relative to the world.

Image following a mouse on a canvas

I want to have an image follow the mouse around the canvas, which is fairly easy, but the catch is that I want my canvas to change with screen resolution (it is set using CSS to be 70vw).
When the resolution decreases and the window becomes smaller this means that using a normal method of using clientX doesn't work.
My code so far is this:
var mouseX = e.clientX/document.documentElement.clientWidth * 1920;
var mouseY = e.clientY/document.documentElement.clientHeight * 943;
This tries to convert the users clientX into the value it would be on a 1920x1080 monitor.
However, this isn't really accurate and doesn't work very well on even 1920x1080 monitors. Any help would be appreciated.
You can't scale the canvas using CSS in the way that you think. A canvas is basically a more advanced image. Scaling the canvas via CSS just stretches the canvas the same way an image would stretch. To change the canvas height and width, you need to change it's height and width attributes in the tag or via code. This will physically change the canvas to the size that you want without scaling and/or stretching.
That being said, we can use this to watch for window size changes and resize the canvas when the window changes.
window.addEventListener('resize', e => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
})
With some basic math, we can calculate what a 70% width would be, it would be done like this
window.addEventListener('resize', e => {
canvas.width = window.innerWidth * 0.7
canvas.height = window.innerHeight
})
The next thing we need to do is get the local position of the mouse on the canvas, which can be done using mousePosition - canvasOffset like this
let x = e.clientX - canvas.offsetLeft
let y = e.clientY - canvas.offsetTop
When all is said and done, we end up with something like this (To see it in action press run then click on Full Page and you will see the canvas resize):
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
// Set the inital height and width of the canvas
canvas.width = window.innerWidth
canvas.height = window.innerHeight
canvas.addEventListener('mousemove', e => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Get the local x/y coordinates of the mouse on the canvas
let x = e.clientX - canvas.offsetLeft
let y = e.clientY - canvas.offsetTop
// Draw a dot where the mouse is
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI, false);
ctx.fillStyle = 'white';
ctx.fill();
})
// Update the height and width when the window size changes
window.addEventListener('resize', e => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
})
body {
padding: 0;
margin: 0;
}
canvas {
background-color: black;
display: block;
}
<canvas></canvas>
In this example below, we use a canvas that is 70% the width and height of the screen and center it with CSS. However, we never touch the height/width with css because it will mess up the canvas' coordinate system. This part is done with JavaScript.
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
// Set the inital height and width of the canvas
canvas.width = window.innerWidth * 0.7
canvas.height = window.innerHeight * 0.7
canvas.addEventListener('mousemove', e => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Get the local x/y coordinates of the mouse on the canvas
let x = e.clientX - canvas.offsetLeft
let y = e.clientY - canvas.offsetTop
// Draw a dot where the mouse is
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI, false);
ctx.fillStyle = 'white';
ctx.fill();
})
// Update the height and width when the window size changes
window.addEventListener('resize', e => {
canvas.width = window.innerWidth * 0.7
canvas.height = window.innerHeight * 0.7
})
body {
padding: 0;
margin: 0;
}
canvas {
background-color: black;
display: block;
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
margin: auto;
}
<canvas></canvas>
I took my snippet from my answer to create a full screen canvas.
I added this for mouse movement:
let User = { x: 0, y: 0 };
//controles if the mouse is moving
window.addEventListener(
"mousemove",
e => {
User.x = e.clientX;
User.y = e.clientY;
},
false
);
Uncomment: cvs.ctx.drawImage(image, User.x, User.y); in the ShowImage() function to draw an image at the mouse x and y position.
Mind to replace the path of the image source: image.src = "Your/Path/To/Image.png";
/**
* #author RensvWalstijn. GitHub: https://github.com/RensvWalstijn
* Sets the canvas properties.
* #param {object} Cvs Give the html canvas Id.
* #param {boolean} Fullscreen Change the canvas fullscreen default false.
* #param {string} Dimension Change the canvas dimension default "2d".
* #return {object}
*/
function NewCanvas(cvs, fullscreen, dimension) {
if (!dimension) dimension = "2d";
var ctx = cvs.getContext(dimension);
if (fullscreen) {
cvs.style.position = "fixed";
cvs.style.left = cvs.x = 0;
cvs.style.top = cvs.y = 0;
} else {
var rect = cvs.getBoundingClientRect();
cvs.x = rect.left;
cvs.y = rect.top;
}
cvs.ctx = ctx;
cvs.dimension = dimension;
cvs.fullscreen = fullscreen;
return cvs;
}
/**
* #author RensvWalstijn. GitHub: https://github.com/RensvWalstijn
* Updates the canvas width and hight.
* #param {object} Cvs NewCanvas() object.
* #param {boolean} Clear Change the canvas clear default true.
*/
function UpdateCvs(cvs) {
if (cvs.fullscreen) {
//if the width is not the same resize the canvas width
if (window.innerWidth != cvs.width) {
cvs.width = window.innerWidth;
}
//if the height is not the same resize the canvas height
if (window.innerHeight != cvs.height) {
cvs.height = window.innerHeight;
}
} else {
let rect = cvs.getBoundingClientRect();
cvs.x = rect.left;
cvs.y = rect.top;
}
}
function ClearCvs(cvs) {
if (cvs.dimension == "2d")
// set fillRect to clearRect to clear all of the canvas
// fillRect is used here to show the full canvas
cvs.ctx.fillRect(0, 0, cvs.width, cvs.height);
}
/**
* #author RensvWalstijn. GitHub: https://github.com/RensvWalstijn
* get html element by id.
* #param {string} id give the html element id.
* #return {object} document.getElementById(id);
*/
function GetId(id) { return document.getElementById(id) }
// To create your canvas object.
var canvas = NewCanvas(GetId("yourCanvasId"), true);
// If you want to update your canvas size use this:
window.addEventListener("resize", function() {
UpdateCvs(canvas);
});
let User = { x: 0, y: 0 };
//controles if the mouse is moving
window.addEventListener(
"mousemove",
e => {
User.x = e.clientX;
User.y = e.clientY;
},
false
);
// Set it to current width
UpdateCvs(canvas);
ClearCvs(canvas);
// create an image
let image = new Image();
image.src = "Your/Path/To/Image.png";
function ShowImage(cvs) {
// Use this line to draw your image.
// cvs.ctx.drawImage(image, User.x, User.y);
// Shows where your image will be drawn.
cvs.ctx.clearRect(User.x, User.y, 100, 100);
}
function Update() {
ClearCvs(canvas);
ShowImage(canvas);
// keeps it looping
window.requestAnimationFrame(Update)
}
// Init the loop
Update();
<canvas id="yourCanvasId"></canvas>

Scaling an image to fit on canvas

I have a form that allows a user to upload an image.
Once the image is loaded, we perform some scaling on it in order to reduce its filesize before we pass it back to the server.
To do this, we place it on the canvas and manipulate it there.
This code will render the scaled image on the canvas, with the canvas of size 320 x 240px:
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
... where canvas.width and canvas.height is the image height and width x a scaling factor based on the size of the original image.
But when I go to use the code:
ctx.drawImage(img, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height
... I only get part of the image on the canvas, in this case the top left corner. I need the whole image 'scaled' to fit on the canvas, despite the actual image size being larger than the 320x240 canvas size.
So for the code above, the width and heights are 1142x856, as that is the final image size. I need to maintain that size to pass beck to the server when the form is submitted, but only want a smaller view of it to appear in the canvas for the user.
What am I missing here? Can anyone point me in the right direction please?
You made the error, for the second call, to set the size of source to the size of the target.
Anyway i bet that you want the same aspect ratio for the scaled image, so you need to compute it :
var hRatio = canvas.width / img.width ;
var vRatio = canvas.height / img.height ;
var ratio = Math.min ( hRatio, vRatio );
ctx.drawImage(img, 0,0, img.width, img.height, 0,0,img.width*ratio, img.height*ratio);
i also suppose you want to center the image, so the code would be :
function drawImageScaled(img, ctx) {
var canvas = ctx.canvas ;
var hRatio = canvas.width / img.width ;
var vRatio = canvas.height / img.height ;
var ratio = Math.min ( hRatio, vRatio );
var centerShift_x = ( canvas.width - img.width*ratio ) / 2;
var centerShift_y = ( canvas.height - img.height*ratio ) / 2;
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.drawImage(img, 0,0, img.width, img.height,
centerShift_x,centerShift_y,img.width*ratio, img.height*ratio);
}
you can see it in a jsbin here :
http://jsbin.com/funewofu/1/edit?js,output
Provide the source image (img) size as the first rectangle:
ctx.drawImage(img, 0, 0, img.width, img.height, // source rectangle
0, 0, canvas.width, canvas.height); // destination rectangle
The second rectangle will be the destination size (what source rectangle will be scaled to).
Update 2016/6: For aspect ratio and positioning (ala CSS' "cover" method), check out:
Simulation background-size: cover in canvas
I guess that you want the image to be scaled to a smaller size, without losing the ratio of the dimensions. I have a solution.
var ratio = image.naturalWidth / image.naturalHeight;
var width = canvas.width;
var height = width / ratio;
ctx.drawImage(image, 0, 0, width, height);
the ratio will be maintained. And the image drawn on the canvas will be of the same ratio. you can use the if loop if the height of the image is long, you can replace the canvas.width to some other width
You can call ctx.scale() before calling ctx.drawImage:
var factor = Math.min ( canvas.width / img.width, canvas.height / img.height );
ctx.scale(factor, factor);
ctx.drawImage(img, 0, 0);
ctx.scale(1 / factor, 1 / factor);
This should preserve the aspect ratio.
HTML:
<div id="root"></div>
JavaScript:
const images = [
'https://cdn.pixabay.com/photo/2022/07/25/15/18/cat-7344042_960_720.jpg',
'https://cdn.pixabay.com/photo/2022/06/27/08/37/monk-7287041_960_720.jpg',
'https://cdn.pixabay.com/photo/2022/07/18/19/57/dog-7330712_960_720.jpg',
'https://cdn.pixabay.com/photo/2022/05/22/18/25/spain-7214284_960_720.jpg',
];
const root = document.getElementById('root');
const image = new Image();
image.crossOrigin = 'anonumys';
image.src = images[3];
const canvas = document.createElement('canvas');
canvas.classList.add('track');
canvas.width = 600;
canvas.height = 400;
const ctx = canvas.getContext('2d');
const meta = {
ratio: image.width / image.height,
width: 0,
height: 0,
offsetX: 0,
offsetY: 0,
};
if (meta.ratio >= 1) {
meta.width = canvas.width > image.width ? image.width : canvas.width;
meta.height = meta.width / meta.ratio;
} else {
meta.height = canvas.height > image.height ? image.height : canvas.height;
meta.width = meta.height * meta.ratio;
}
meta.offsetX = canvas.width > meta.width ? (canvas.width - meta.width) / 2 : 0;
meta.offsetY = canvas.height > meta.height ? (canvas.height - meta.height) / 2 : 0;
image.addEventListener('load', () => {
ctx.drawImage(image, meta.offsetX, meta.offsetY, meta.width, meta.height);
root.append(canvas);
});
console.log(meta);

Categories