What's the math behind CSS's background-size:cover
Asked Answered
S

6

32

I'm creating an "image generator" where users can upload an image and add text and/or draw on it. The outputted image is a fixed size (698x450).

On the client side, when the user uploads their image it is set as the background of a div that's 698x450 with background-size:cover. This makes it fill the area nicely.

The final combined image is generated by PHP using GD functions. My question is, how can I get the image to scale in PHP the same way it does in CSS. I want the result of the PHP script to look the same as if the image was set in CSS as it was above. Does anyone know how browsers using background-size:cover calculate how to scale the image appropriately? I want to translate this into PHP.

Thanks

Seepage answered 23/4, 2012 at 17:18 Comment(0)
A
58

Here's a logic behind cover calculations.

You have four base values :

imgWidth // your original img width
imgHeight

containerWidth // your container  width (here 698px)
containerHeight

Two ratios derived from these values :

imgRatio = (imgHeight / imgWidth)       // original img ratio
containerRatio = (containerHeight / containerWidth)     // container ratio

You want to find two new values :

finalWidth // the scaled img width
finalHeight

So :

if (containerRatio > imgRatio) 
{
    finalHeight = containerHeight
    finalWidth = (containerHeight / imgRatio)
} 
else 
{
    finalWidth = containerWidth 
    finalHeight = (containerWidth / imgRatio)
}

... and you have the equivalent of a background-size : cover.

Allegorize answered 23/4, 2012 at 17:45 Comment(4)
Not sure why but I had to change last line as finalHeight = (containerWidth * imgRatio) to work properly for my codeKiethkiev
@Ergec, exactly! imgRatio is height / width. We need to multiply height / width by width to get height. @mddw, fix your answer, plaeaseQuass
Is this the official implementation or reverse engineering?Cutcheon
The math of this did not work for me. The answer by Antriver did.Pentlandite
J
16

I know this is a very old question, but the answer I wrote is actually cleaner by using max and mins on the ratios between the images instead of each image with itself:

var originalRatios = {
  width: containerWidth / imageNaturalWidth,
  height: containerHeight / imageNaturalHeight
};

// formula for cover:
var coverRatio = Math.max(originalRatios.width, originalRatios.height); 

// result:
var newImageWidth = imageNaturalWidth * coverRatio;
var newImageHeight = imageNaturalHeight * coverRatio;

I like this approach because it is very systematic — maybe it's the wrong word —. What I mean is you can get rid of the if statements and make it work in a more "math formula" kind of way (input = output, if that makes sense):

var ratios = {
  cover: function(wRatio, hRatio) {
    return Math.max(wRatio, hRatio);
  },

  contain: function(wRatio, hRatio) {
    return Math.min(wRatio, hRatio);
  },

  // original size
  "auto": function() {
    return 1;
  },

  // stretch
  "100% 100%": function(wRatio, hRatio) {
    return { width:wRatio, height:hRatio };
  }
};

function getImageSize(options) {
  if(!ratios[options.size]) {
    throw new Error(options.size + " not found in ratios");
  }

  var r = ratios[options.size](
    options.container.width / options.image.width,
    options.container.height / options.image.height
  );

  return {
    width: options.image.width * (r.width || r),
    height: options.image.height * (r.height || r)
  };
}

Usage

const { width, height } = getImageSize({
  container: {width: 100, height: 100},
  image: {width: 200, height: 50},
  size: 'cover' // 'contain' | 'auto' | '100% 100%'
});

Playground

I created a jsbin here if you want to take a look at what I mean with systematic (it also has a scale method that I thought was not needed in this answer but very useful for something other than the usual).

Johathan answered 24/8, 2016 at 19:31 Comment(2)
I think the word you were looking for is "semantic" maybe?Umbilicus
Could be. Now that I read it again, maybe the correct word is "mathematization", or just "simplified"/"streamlined"? In mathematics, 'systematic' usually refers to a process that follows a specific & organized pattern/plan/system, but this code isn't a process, per se. IMO, it is just a simplification to remove the if statements and transform it into more of a "formula" approach.Johathan
S
5

Thanks to mdi for pointing me in the right direction, but that didn't seem quite right. This is the solution that worked for me:

    $imgRatio = $imageHeight / $imageWidth;
    $canvasRatio = $canvasHeight / $canvasWidth;

    if ($canvasRatio > $imgRatio) {
        $finalHeight = $canvasHeight;
        $scale = $finalHeight / $imageHeight;
        $finalWidth = round($imageWidth * $scale , 0);
    } else {
        $finalWidth = $canvasWidth;
        $scale = $finalWidth / $imageWidth;
        $finalHeight = round($imageHeight * $scale , 0);
    }
Seepage answered 24/4, 2012 at 12:10 Comment(0)
C
3

I stumbled across this QA after a long search for a way how to scale and position a background image on a div to match an html background image while also supporting browser resizing and ad-hoc positioning of the div and I came up with this.

background image of a div positioned to match html background

:root {
  /* background image size (source) */

  --bgw: 1920;
  --bgh: 1080;

  /* projected background image size and position */

  --bgscale: max(calc(100vh / var(--bgh)), calc(100vw / var(--bgw)));

  --pbgw: calc(var(--bgw) * var(--bgscale)); /* projected width */
  --pbgh: calc(var(--bgh) * var(--bgscale)); /* projected height */

  --bgLeftOverflow: calc((var(--pbgw) - 100vw) / 2); 
  --bgTopOverflow: calc((var(--pbgh) - 100vh) / 2);
}

JS equivalent

window.onresize = () => {
  const vw100 = window.innerWidth
  const vh100 = window.innerHeight

  /* background image size (source) */

  const bgw = 1920
  const bgh = 1080

  /* projected background image size and position */

  const bgscale = Math.max(vh100 / bgh, vw100 / bgw)

  const projectedWidth  = bgw * bgscale | 0
  const projectedHeight = bgh * bgscale | 0

  const leftOverflow = (projectedWidth  - vw100) / 2 | 0
  const topOverflow  = (projectedHeight - vh100) / 2 | 0

  console.log(bgscale.toFixed(2), projectedWidth, projectedHeight, leftOverflow, topOverflow)
}

Try resizing a window with this snippet to see the result.
Best viewed in Full page view. tip: Open console.

window.onresize = () => {
  const vw100 = window.innerWidth
  const vh100 = window.innerHeight
  const bgw = 1920
  const bgh = 1080

  const bgscale = Math.max(vh100 / bgh, vw100 / bgw)

  const projectedWidth = bgw * bgscale | 0
  const projectedHeight = bgh * bgscale | 0

  const leftOverflow = (projectedWidth - vw100) / 2 | 0
  const topOverflow = (projectedHeight - vh100) / 2 | 0

  console.log(bgscale.toFixed(2), projectedWidth, projectedHeight, leftOverflow, topOverflow)
}
:root {
  /* background image size */
  --bgurl: url('https://i.sstatic.net/3iy4y.jpg');
  --bgw: 1000;
  --bgh: 600;
  
  --bgscale: max(calc(100vh / var(--bgh)), calc(100vw / var(--bgw)));
  
  --pbgw: calc(var(--bgw) * var(--bgscale));
  --pbgh: calc(var(--bgh) * var(--bgscale));
  
  --bgLeftOverflow: calc((var(--pbgw) - 100vw) / 2);
  --bgTopOverflow: calc((var(--pbgh) - 100vh) / 2);
}

html {
  background: #000 var(--bgurl) no-repeat center center fixed;
  background-size: cover;
  overflow: hidden;
}

#panel {
  --x: 100px;
  --y: 100px;
  --w: 200px;
  --h: 150px;
  
  position: absolute;
  left: var( --x);
  top: var( --y);
  width: var(--w);
  height: var(--h);
  
  background-image: var(--bgurl);
  background-repeat: no-repeat;
  background-position: calc(0px - var(--bgLeftOverflow) - var(--x)) calc(0px - var(--bgTopOverflow) - var(--y));
  background-size: calc(var( --bgscale) * var(--bgw));
  filter: invert(1);
}
<div id="panel"></div>
Cutcheon answered 16/3, 2021 at 18:2 Comment(0)
R
2

When using background-size: cover, it is scaled to the smallest size that covers the entire background.

So, where it is thinner than it is tall, scale it until its width is the same as the area. Where it is taller than it is thin, scale it until its height is the same as the area.

When it is larger than the area to cover, scale it down until it fits (if there is less overflow in height, scale until the same height, if there is less overflow in width, scale until the same width).

Rosinweed answered 23/4, 2012 at 17:42 Comment(0)
C
0

The above answers are missing a step.

Step 1. Scale the image depending on the aspect ratio of the canvass or the image. The smallest size that covers the entire canvas. Thanks to anrtiver

Step 2. Crop the image, otherwise it wont be central.

 function ImageCover($img1, $canvass_width, $canvass_height) {
    
      $img1_width = imagesx($img1);
      $img1_height = imagesy($img1);
      $img1_ratio = $img1_height / $img1_width;
      
      $canvass_ratio = $canvass_width / $canvass_height;
    
    //scale 
      if ($canvass_ratio < $img1_ratio) {
        $finalHeight = $canvass_height;
        $scale = $finalHeight / $img1_height;
        $finalWidth = $img1_width * $scale;
      } else {
        $finalWidth = $canvass_width;
        $scale = $finalWidth / $img1_width;
        $finalHeight = $img1_height * $scale;
      }
      
      $img1_scalled = imagescale($img1, $finalWidth, $finalHeight);
    
      $img1_scalled_width = imagesx($img1_scalled);
      $img1_scalled_height = imagesy($img1_scalled);
    
      $img1 = $img1_scalled;
      $img1_width = $img1_scalled_width;
      $img1_height = $img1_scalled_height;
    
    
    //crop
      $x = 0;
      $y = 0;
      if($img1_scalled_width > $canvass_width) {
        $difference_x = $img1_scalled_width - $canvass_width;
        $x = $difference_x / 2;
      } else {
        $difference_y = $img1_scalled_height - $canvass_height;   
        $y = ($difference_y / 2);
      }
    
    
    
      $img1 = imagecrop($img1, ['x' => $x, 'y' => $y, 'width' => $canvass_width, 'height' => $canvass_height]);
      
      return $img1;
    
    
    } //ImageCover
Complaint answered 28/6, 2023 at 10:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.