Reputation: 844
background-size:cover
-like effect on a canvas imageI tried 2 ways below. Neither works.
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>
Upvotes: 1
Views: 2404
Reputation: 54649
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>
Upvotes: 1
Reputation: 136986
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>
Upvotes: 1