What's the math behind CSS's background-size:cover - css

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

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.

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).

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);
}

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.
: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.stack.imgur.com/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>

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).

Related

How can I scale all images to the same dimensional area? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 10 months ago.
Improve this question
I'm loading in several images. They are various lengths and widths, but I would like for them all to feel as though they are the same "size". So, if one image is 200x100 and another image is 200x400, I would like for them both to scale in such a way that they take up the same amount of space on the screen. If I fix the width to be 200, then the second element is 4 times the size of the first.
For example:
.my_img {
width: 200px;
}
produces this behavior
How can I use css to fix the area of an image or other element? Where by area, I mean literally length times width. I would like to load in an image of arbitrary dimensions, and scale it (preserving aspect ratio) so that its area is fixed to be a given value.
I don't believe you can do this with CSS. While you can calculate square root with CSS in various ways, getting natural dimensions may be problematic. You'd need that in order to find the smallest image.
For a JavaScript solution, you'd have to first establish the smallest image area, then resize each down according to initial proportion, maintaining aspect ratio.
const images = document.querySelectorAll('img');
let smallestArea = 999999999;
const getSmallestImageByArea = () => {
images.forEach(image => {
const width = image.naturalWidth;
const height = image.naturalHeight;
if (width * height < smallestArea) {
smallestArea = width * height;
}
});
};
const sizeImagesToSmallestArea = () => {
images.forEach(image => {
let width = image.naturalWidth;
let height = image.naturalHeight;
const area = width * height;
if (area > smallestArea) {
const areaRoot = Math.sqrt(area);
const proportion = areaRoot / Math.sqrt(smallestArea);
const aspectRoot = Math.sqrt(width / height);
width = areaRoot / proportion * aspectRoot;
height = areaRoot / proportion / aspectRoot;
image.style.width = width + 'px';
image.style.height = height + 'px';
}
// show hidden images
image.style.display = 'inline';
console.log('Initial area:', area, '| Final area:', width * height);
});
};
// wait for images: https://stackoverflow.com/a/60949881/1264804
Promise.all(Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise(resolve => {
img.onload = img.onerror = resolve;
}))).then(() => {
getSmallestImageByArea();
sizeImagesToSmallestArea();
});
/* hide images to prevent jumping effect */
img {
display: none;
}
<img src="https://via.placeholder.com/165x250" />
<img src="https://via.placeholder.com/100x50" />
<img src="https://via.placeholder.com/200x200" />
<img src="https://via.placeholder.com/1900x300" />
Maybe you can try something like this. if you want them to fit on a specific container you have to wrap them and set their height and width and let them fit on it using obejct-fit:cover. This sample have 2 different size images but they fit exactly the way I would do it no matter how big they are. Let me know if this is what you are looking at??
.my_img {
justify-content:space-around;
display:flex;
}
.my_img > img {
object-fit: fill;
height: 300px;
width: 300px;
border: 1px solid gray;
margin: 10px;
overflow:visible;
}
<div class="my_img">
<img src="https://www.kindpng.com/picc/m/227-2275045_daisy-yellow-bloom-frame-flower-border-flowers-transparent.png">
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.clir.org%2Fwp-content%2Fuploads%2Fsites%2F6%2F2016%2F09%2FWelcome-banner.png&f=1&nofb=1">
</div>

CSS: Make Canvas as big as possible while keeping aspect ratio

I have a Canvas element that is inside a container div. When the user selects an image from his machine, this image should be displayed on the canvas. I want the canvas to be big as possible but at the same time keep the aspect ratio of the image. I know neither the proportions of the image nor the size of the container div, as this is relative to the screen/window size of the user.
If I set max-width and max-height to e.g 100% the canvas will not fill the container if the selected image is smaller then the container. If I set width and height instead of max-width and max-height the canvas doesn't keep the aspect ratio.
Does anyone have an idea how to solve this?
If you're willing to use JQuery (or regular JavaScript), then a solution like this might work:
<script>
// Note: this uses jQuery.
// It makes getting/setting the dimensions easier,
// but you can do this with normal JavaScript
var img = $("#img");
var container = $("#container");
var width = img.width();
var height = img.height();
var maxWidth = container.width();
var maxHeight = container.height();
var ratio = maxWidth / width;
if(height * ratio > maxHeight) {
ratio = maxHeight / height;
}
img.width(width * ratio);
img.height(height * ratio);
</script>
What this does is that it finds the ratio to multiply the width and the height by, whichever one is smaller (so that it will always fit in the window).
Update: Tested on JSFiddle.net. See it here.
I hope this helps you!
After reading your clarification about video, check out the following:
https://jsfiddle.net/d0dox9xt/
body {
background: #eee;
}
#container {
margin:0 2% 0 2%;
}
#v {
position:absolute;
top:0;
left:0;
width:100%;
}
The only trick is setting width:100%; This will maintain aspect ratio.
Note that in the JS
document.addEventListener('DOMContentLoaded', function(){
var v = document.getElementById('v');
var canvas = document.getElementById('c');
var context = canvas.getContext('2d');
}
function draw(v) {
c.drawImage(v, 0, 0);
}
The drawImage function can take many arguments. The first argument is inserting the media, the next two are for positioning. There are many arguments you can have to position and change the height and width. I left them alone so it will follow the CSS rules.
Here is a link to more on placing in canvas: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage

Scrollbar scroll 'size'

How would I, instead of 'scrolling' as much height as I want, a fixed height?
I.e a div is 50px high and each time I scroll down I want to go down 50px instead of just 'stopping' where you want.
Thanks in advance.
You can override scrolling of the div in such a way:
$("#scrollableContainer").scroll(function(e) {
//measure how far are you from the top of the scrollable container
var top = $("#scrollableContainer").scrollTop();
var scrollIncrement = 50; //50px
if (top % scrollIncrement!= 0) {
var delta;
//calculate delta you need to align the position with
if(e.detail > 0) {
//scroll down
delta = ((top / scrollIncrement) + 1) * scrollIncrement) - top;
}else {
//scroll up
delta = ((top / scrollIncrement) - 1) * scrollIncrement) - top;
}
$("#scrollableContainer").scrollTop(delta);
}
});

Background-size cover jumping when background-position switches to fixed

I am working on a Parallax/Scrolling Timeline project and I am having a problem with the CSS3 Background-size cover property.
The div has these properties:
background: url(../images/timeline/back-6.jpg) no-repeat top center black;
background-size: cover;
padding-top: 90px;
height: 1855px;
position: relative;
Using jQuery I switch the background-attachment to fixed. When I do this the background image jumps "in" (meaning that parts of the image that were past the edge of the screen are now visible). Which isn't the desired result.
In testing I can switch the div to use background-size: 100% cover but it is causing different vertical jumping issues when scrolling.
Any ideas of how to prevent it from jumping in when I switch the background to fixed? (It also happens in reverse when I set the background to scroll).
I sadly can't link to a demo of this code as the page isn't ready to be deployed yet.
I had the same issue, when setting background-size to cover or contain
Setting a fixed height, in example for smaller screens via #media prevents the background-image from jumping. After my tests I came to the conclusion, that the jumping is due to the orientation of the element after setting background-attachment to fixed
Setting it to fixed, the size is calculated by the size of the viewport, not the element containing the background-image. This is where the jumping comes from and why setting a fixed height or width for the background-size solves this issue.
I had the same problem while creating a one page layout i wanted to use with a scrollTo-Plugin and so on....
The page layout was devided in two parts:
Left side for the background image which should change/scroll with the content on the right side.
So i used to make a kind of jquery Plugin to combine both "background-position: fixed" and "background-size: cover".
you just need to define the element by class/id for aligning the background-images.
dont complain about the code. im relatively new to javascript/jquery. but its working ;)
there it is:
function fixedResize() {
var targetEl = $('element where bg-images are in');
var targetWidth = targetEl.width();
var targetHeight = targetEl.height();
var targetPosX = targetEl.offset().left;
var targetPosY = targetEl.offset().top;
var leftRatio = targetWidth / targetHeight;
//console.log('TargetWidth', targetWidth, 'TargetHeight', targetHeight, 'Offset', targetPosX, targetPosY, 'leftRatio', leftRatio);
targetEl.each(function(){
var imgTarget = $(this);
var url = $(this).css('background-image').replace('url(', '').replace(')', '').replace("'", '').replace('"', '');
var bgImg = $('<img />'); // make background-image as image tag for getting width and height of the image
imgTarget.css('background-attachment','fixed');
bgImg.hide();
bgImg.bind('load', function(){
var imgHeight = $(this).height();
var imgWidth = $(this).width();
var imgRatio = imgWidth / imgHeight;
$(this).remove(); // remove img Tags again
// Calculate resize dimensions
if (imgRatio > leftRatio) {
var currentWidth = imgRatio * targetHeight; // image width after resize
var currentHeight = (currentWidth/imgWidth)*imgHeight;
var setToLeft = ((currentWidth - targetWidth)/2);
var imgPosX = targetPosX - setToLeft;
var imgPosY = (currentHeight - targetPosY - currentHeight/2 - targetHeight/2)* -1;
var resizeImg = 'background-size: auto '+ targetHeight +'px;';
} else if (imgRatio < leftRatio){
var currentWidth = targetWidth;
var currentHeight = (currentWidth/imgWidth)*imgHeight;
var imgPosX = targetPosX;
var imgPosY = (currentHeight - targetPosY - currentHeight/2 - targetHeight/2)* -1;
var resizeImg = 'background-size: '+ targetWidth +'px auto;'; // resize background
}
imgTarget.attr('style','background-attachment: fixed; background-position: '+ imgPosX +'px '+ imgPosY +'px;' + resizeImg);
console.log('imgWidth', imgWidth, 'imgHeight', imgHeight, 'imgRatio', imgRatio, 'currentWidth', currentWidth, 'currentHeight', currentHeight, 'setToLeft', setToLeft);
console.log('imgPos', imgPosX, imgPosY, 'setToLeft', setToLeft, targetPosX);
});
$(this).append(bgImg);
bgImg.attr('src', url);
});
}
fixedResize(); // initiate function
$(window).resize(function() {
fixedResize(); // initiate function for window resize (Fluid behavior)
});
or
jsfiddle.net/rowphant/eXb6e/14/

How to set offset on (selectmenu) overlays in jQuery Mobile [using a fixed header toolbar]?

Is there a way to define the offset JQM uses for selectmenu overlay?
Other options can be set via prototyping like this:
$.mobile.page.prototype.options.addBackBtn = true;
$.mobile.page.prototype.options.backBtnTheme = "a";
Problem description
jQuery Mobile determines the size of the screen and decides how to display the overlay for select menus. Unfortunately this seems to work only without using a fixed header toolbar, because JQM is generation the source over here always with the top-offset of 30px style="left: 741.65px; top: 30px;.
There is no ways to overwrite this with CSS only, because the specificity of the css rules are always lower than the ones of an style-attribute!
I don't want to change the JQM sourcecode, because I'd have to change it again with every release. And I don't use the uncompressed sources.
Generated source from JQM
<div class="ui-selectmenu ui-overlay-shadow ui-corner-all ui-body-a pop in"
style="left: 741.65px; top: 30px;">
Sample
http://jsfiddle.net/V8AAB/
JQM Source code
This is the corresponding code from jQuery Mobile 1.0RC2:
self.menuType = "overlay";
self.screen.height( $(document).height() ).removeClass( "ui-screen-hidden" );
// Try and center the overlay over the button
var roomtop = btnOffset - scrollTop,
roombot = scrollTop + screenHeight - btnOffset,
halfheight = menuHeight / 2,
maxwidth = parseFloat( self.list.parent().css( "max-width" ) ),
newtop, newleft;
if ( roomtop > menuHeight / 2 && roombot > menuHeight / 2 ) {
newtop = btnOffset + ( self.button.outerHeight() / 2 ) - halfheight;
} else {
// 30px tolerance off the edges
newtop = roomtop > roombot ? scrollTop + screenHeight - menuHeight - 30 : scrollTop + 30;
}
Suggested Fix:
.ui-selectmenu { z-index: 1100 !important; }

Resources