1

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.

  1. Simulation background-size: cover in canvas
  2. 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>
Miu
  • 846
  • 8
  • 22

2 Answers2

1

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>
Yoshi
  • 54,081
  • 14
  • 89
  • 103
  • 1
    This code is perfect! It's very simple and short. Moreover, it is exactly the same as `background-size: cover`. Thank you for showing the comparison. – Miu Mar 10 '21 at 12:59
1

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>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thank you for your kind explanation. It's very easy to understand. Now thanks to you, I understand why my code didn't work. – Miu Mar 10 '21 at 12:55