Miu
Miu

Reputation: 844

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

What I want to do

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

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>

Upvotes: 1

Views: 2404

Answers (2)

Yoshi
Yoshi

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

Kaiido
Kaiido

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

Related Questions