Background
My big-picture goal is to display an image such that the following conditions are satisfied:
- The entire viewport is filled by the image
- At least one dimension of the image fits within the viewport
- The image is not stretched
My solution is contained in the function updateRatioSettings
, which works as follows:
- Determine the aspect ratios of the source image (using
naturalWidth
andnaturalHeight
) and the viewport - If the viewport is wider than the image,
width = "100%"
andheight = "auto"
. - If the image is wider than the viewport,
height = "100%"
andwidth = "auto"
.
This works!
The Problem
Now I just need to get updateRatioSettings
to run when either of the aspect ratios changes. I have a working connection to the resize
event of window
, so now I just need to figure out how to make the function run when I change the image source.
Each of the below code snippets shows a square centered in a horizontal viewport. A working solution will show the entire viewport filled with this nice grey color.
Attempt #1
My first strategy was to call updateRatioSettings
immediately after setting img.src
. However, I ran into an issue where sourceRatio
, which is calculated as img.naturalWidth / img.naturalHeight
, printed as NaN
.
// function to listen for window size changes and move image accordingly
let img;
let windowWider = false; // assume the screen is some ratio narrower than the image... to start at least
let ratioUpdateReady = true; // debounce variable
function updateRatioSettings() {
// if the function is running, don't run the function
if (ratioUpdateReady) {
ratioUpdateReady = false;
// begin debounced code
const sourceRatio = img.naturalWidth / img.naturalHeight;
const windowRatio = window.innerWidth / window.innerHeight;
console.log("sourceRatio: " + sourceRatio, "windowRatio: " + windowRatio);
if (windowRatio > sourceRatio != windowWider) { // if something has changed
windowWider = !windowWider // log the new status
if (windowWider) {
// settings for window aspect ratio > source aspect ratio
img.style.width = "100%";
img.style.height = "auto";
} else {
// settings for source aspect ratio > window aspect ratio
img.style.height = "100%";
img.style.width = "auto";
}
}
// end debounced code
ratioUpdateReady = true;
}
}
document.addEventListener('DOMContentLoaded', function(){
// identify the element containing the image
img = document.getElementById("background");
// load an initial image
(function (){ // this normally executes after sending a request to another part of the chrome extension
const uri = "data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAAAAADDDgAAww4AAAAAAAAAAAAAQEBAAA=="; // this is the string returned by the extension in my test case
img.src = uri;
updateRatioSettings(); // this gets a sourceRatio that is NaN
})()
// set up an event to update the ratio settings when the window resizes
// commenting out for the purpose of this question, since it doesn't need debugging
// window.addEventListener('resize', updateRatioSettings);
});
html,body{
margin:0;
height:100%;
overflow:hidden;
}
img{
height:100%;
width:auto;
position:absolute;
top:-100%; bottom:-100%;
left:-100%; right:-100%;
margin:auto;
}
<html>
<head>
<title>New tab</title>
</head>
<body>
<img id="background" src="" alt="">
</body>
</html>
Attempt #2
Here I attempted to use image.onLoad
to run updateRatioSettings
when the image URI is loaded.
// function to listen for window size changes and move image accordingly
let img;
let windowWider = false; // assume the screen is some ratio narrower than the image... to start at least
let ratioUpdateReady = true; // debounce variable
function updateRatioSettings() {
// if the function is running, don't run the function
if (ratioUpdateReady) {
ratioUpdateReady = false;
// begin debounced code
const sourceRatio = img.naturalWidth / img.naturalHeight;
const windowRatio = window.innerWidth / window.innerHeight;
console.log("sourceRatio: " + sourceRatio, "windowRatio: " + windowRatio);
if (windowRatio > sourceRatio != windowWider) { // if something has changed
windowWider = !windowWider // log the new status
if (windowWider) {
// settings for window aspect ratio > source aspect ratio
img.style.width = "100%";
img.style.height = "auto";
} else {
// settings for source aspect ratio > window aspect ratio
img.style.height = "100%";
img.style.width = "auto";
}
}
// end debounced code
ratioUpdateReady = true;
}
}
document.addEventListener('DOMContentLoaded', function(){
// identify the element containing the image
img = document.getElementById("background");
// load an initial image
(function (){ // this normally executes after sending a request to another part of the chrome extension
const uri = "data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAAAAADDDgAAww4AAAAAAAAAAAAAQEBAAA=="; // this is the string returned by the extension in my test case
img.onLoad = updateRatioSettings; // this is the function I want to run when naturalWidth and naturalHeight have updated
img.src = uri; // updateRatioSettings is not called
})()
// set up an event to update the ratio settings when the window resizes
// commenting out for the purpose of this question, since it doesn't need debugging
// window.addEventListener('resize', updateRatioSettings);
});
html,body{
margin:0;
height:100%;
overflow:hidden;
}
img{
height:100%;
width:auto;
position:absolute;
top:-100%; bottom:-100%;
left:-100%; right:-100%;
margin:auto;
}
<html>
<head>
<title>New tab</title>
</head>
<body>
<img id="background" src="" alt="">
</body>
</html>
Attempt #3
Next, I tried using a MutationObserver
to watch for the point at which naturalWidth
and naturalHeight
actually update, then run updateRatioSettings
in repsonse. Like in Attempt #2, updateRatioSettings
never actually ran.
// function to listen for window size changes and move image accordingly
let img;
let windowWider = false; // assume the screen is some ratio narrower than the image... to start at least
let ratioUpdateReady = true; // debounce variable
function updateRatioSettings() {
// if the function is running, don't run the function
if (ratioUpdateReady) {
ratioUpdateReady = false;
// begin debounced code
const sourceRatio = img.naturalWidth / img.naturalHeight;
const windowRatio = window.innerWidth / window.innerHeight;
console.log("sourceRatio: " + sourceRatio, "windowRatio: " + windowRatio);
if (windowRatio > sourceRatio != windowWider) { // if something has changed
windowWider = !windowWider // log the new status
if (windowWider) {
// settings for window aspect ratio > source aspect ratio
img.style.width = "100%";
img.style.height = "auto";
} else {
// settings for source aspect ratio > window aspect ratio
img.style.height = "100%";
img.style.width = "auto";
}
}
// end debounced code
ratioUpdateReady = true;
}
}
const watchingAttributes = ["naturalWidth", "naturalHeight"];
document.addEventListener('DOMContentLoaded', function(){
// identify the element containing the image
img = document.getElementById("background");
// set up the image resize listener
function processMutation(mutationRecord) {
if (watchingAttributes.includes(mutationRecord.attributeName)) {
updateRatioSettings();
}
}
let watcher = new MutationObserver((mutations) => {
mutations.forEach(processMutation);
});
watcher.observe(img, {
attributes: true
});
// load an initial image
(function (){ // this normally executes after sending a request to another part of the chrome extension
const uri = "data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAAAAADDDgAAww4AAAAAAAAAAAAAQEBAAA=="; // this is the string returned by the extension in my test case
img.src = uri;
})()
// set up an event to update the ratio settings when the window resizes
// commenting out for the purpose of this question, since it doesn't need debugging
// window.addEventListener('resize', updateRatioSettings);
});
html,body{
margin:0;
height:100%;
overflow:hidden;
}
img{
height:100%;
width:auto;
position:absolute;
top:-100%; bottom:-100%;
left:-100%; right:-100%;
margin:auto;
}
<html>
<head>
<title>New tab</title>
</head>
<body>
<img id="background" src="" alt="">
</body>
</html>
Conclusion
So… what do I do now?