Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Display image only when it's completely decoded not working

Tags:

javascript

I'm running a Raspberry Pi 3 in Chromium for a custom Spotify interface which displays the current song that's played. While that works in itself, the transition between one song to another is extremely choppy and I just don't know what to do anymore. What's supposed to happen is that the image and background do not fade-in until the image is completely loaded, as in, it should not be cut in half.

But as you can see here, that is not the case (it's only for a few frames, but you can clearly see how it first only shows a fifth of the cover art and then jumps to the full image): https://i.imgur.com/pQsQ26r.mp4

For reference, this is what is should look like: https://i.imgur.com/QWY1J38.gif

On a regular PC this is already super smooth, but I assume the RP3 is just too slow to get it all decoded in time. So, naturally, I thought my problem would be solved like this, but the results are still what you can see in the first video:

function changeImage(newImage) {
    var preloadImg = new Image();
    preloadImg.src = newImage;
    preloadImg.decode().then(() => {
        let artworkUrl = makeUrl(preloadImg.src);
        document.getElementById("artwork-img").style.backgroundImage = artworkUrl;
        document.getElementById("background-img").style.backgroundImage = artworkUrl;
        setArtworkOpacity("1");
    });
    setArtworkOpacity("0");
}

function setArtworkOpacity(value) {
    // the smooth fade itself is done via CSS "transition: opacity 1s;"
    document.getElementById("artwork-img").style.opacity = value;
    document.getElementById("background-img").style.opacity = value;
}

I've also tried img.onload, same result.

Is this the wrong approach? Ideally there would be a function that goes "do not execute until image has been fully painted" in which case I'd move the setArtworkOpacity("1") into its callback, but I couldn't find anything like that.

like image 877
Selbi Avatar asked Feb 15 '21 22:02

Selbi


Video Answer


1 Answers

If I may still answer this... You said you tried with img.onload but didn't show your implementation.

I use this one a lot and it's very effective, so have a look at it. It basically relies on css with an HTML attribute binding.

const image = document.querySelector(".image");

function setLoaded(e) {
  e.target.setAttribute('loaded', true);
}

image.addEventListener("load", setLoaded);

setTimeout(() => {
  image.removeAttribute("loaded");
  image.setAttribute("src", "https://source.unsplash.com/1600x900/?test");
}, 2000);
html,
body {
  margin: 0;
}

.image {
  opacity: 0;
  transition: opacity 0.3s ease;
  width: 100vw;
  height: 100vh;
  object-fit: cover;
}

.image[loaded] {
  opacity: 1;
}
<img src="https://source.unsplash.com/random" class="image" alt="some image" />

EDIT:

Just to make sure I'm clear enough, here's a more complete answer.

I can see you're relying on backgroundImage to display your data. But the thing about the background image in css is that it doesn't react the same way as a proper img tag. You make use of the Image constructor, but you loose all its benefits once you finally put only the url in the css property. Since you only pass the newImage url, all that javascript image processing is gone and has been done for nothing. The css is reading the url and tries to display your image as fast as it can, no Js trickery can save you at this point and it creates little glitch you hate so much.

You could save yourself a lot of trouble by simply use the default HTML img tag and adding your newImage url in their respective src attributes. You will also gain a little bit of accessibility by using a proper semantic tag instead of simple div's background image.

Please consider the code below making use of a changeImage function: You can see the loaded boolean is used in the css to handle the transition without using JS.

// get your elements ref in here
const images = document.querySelectorAll("img");

function setLoaded(e) {
  e.target.setAttribute('loaded', true);
  // check the CSS now
}

images.forEach(image => {
  image.addEventListener("load", setLoaded);
});

function changeImage(newImage) {
  const background = document.querySelector(".background");
  const cover = document.querySelector(".cover");

  background.removeAttribute("loaded");
  cover.removeAttribute("loaded");

  background.setAttribute("src", newImage);
  cover.setAttribute("src", newImage);
}

setTimeout(() => {
  changeImage("https://source.unsplash.com/1600x900/");
}, 2000);

setTimeout(() => {
  changeImage("https://source.unsplash.com/1920x1080/");
}, 4000);
html,
body {
  margin: 0;
}

.header {
  position: relative;
  padding-top: 240px;
  background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
  display: flex;
  justify-content: center;
}

img {
  opacity: 0;
  transition: opacity .5s ease;
}


/* Here's where the loaded boolean does its magic */

img[loaded] {
  opacity: 1;
}

.background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -1;
}

.cover {
  width: 120px;
  height: 120px;
  box-shadow: 0 8px 16px rgb(33 33 33 / 50%);
  object-fit: cover;
}
<header class="header">
  <img src="https://source.unsplash.com/random" class="background" />
  <img src="https://source.unsplash.com/random" class="cover" />
</header>
like image 83
Pierre Burton Avatar answered Nov 15 '22 04:11

Pierre Burton