6

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

Selbi
  • 813
  • 7
  • 23
  • 1
    Your code looks almost perfect. The problem is that your code sets `background-image` and `opacity` simultaniously, so you don't give it enough time to paint. You need to set `background-image`, wait a bit, and **then** set `opacity`. (From a hardware rendering perspective, `opacity` is much faster than most things.) – Eliezer Berlin Feb 19 '21 at 08:24
  • 1
    @EliezerBerlin Ahh, so the image data isn't incomplete, it's just the Raspi struggling to paint images this quickly. I suppose there is no way to check for paint completion though? – Selbi Feb 19 '21 at 19:51
  • Ah whatever, it's not pretty but I put a setTimeout of one second for the fade-in. Thanks for the tip! – Selbi Feb 19 '21 at 23:51
  • Does it need to be a background image? What if instead you append the that you asked to be decoded? Also you could try with a 2d context, [`drawImage` is synchronous.](https://stackoverflow.com/questions/39543290/how-to-know-when-browser-finish-to-process-an-image-after-loading-it/39543762#39543762) and if it needs to be a background, then maybe the css paint API can help. – Kaiido Feb 20 '21 at 02:00
  • @Kaiido Unfortunately, yes. The background is a composite with a gradient overlay to be used with `background-blend-mode` to give that light-glow effect. I'm gonna take a look at that drawImage thing though, maybe I can rework this. Thanks! – Selbi Feb 22 '21 at 18:14

3 Answers3

2

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>
Pierre Burton
  • 1,954
  • 2
  • 13
  • 27
  • For my implementation regarding the onload, imagine the `preloadImg.decode().then(() => {});` is simply replaced with `preloadImg.onload = {};`. Maybe that was wrong, I dunno. As for your sample, I don't quite see how this is any different from simply using the `load` event directly, because you don't seem to do anything with the `loaded` boolean. I'm gonna give it a shot though, thanks! – Selbi Feb 27 '21 at 14:21
  • Thank you so much! I had no idea background images would work differently. For that reason alone I'm definitely gonna switch to img. The only tough part will be the actual background, since that one is actually overlayed using a custom image rather than a CSS gradient (too expensive for a Raspi). Seems like you found a way around it though. So in any case, this is definitely a step in the right direction! Shame the bounty already expired, sorry bout that... – Selbi Mar 01 '21 at 21:54
  • 1
    It's okay, I'm more than happy to help you and win the accepted answer ;-) – Pierre Burton Mar 02 '21 at 08:17
  • Hey, just wanted to check in and confirm that this indeed did the trick. Thank you so much, once again! – Selbi Mar 05 '21 at 00:19
0
window.addEventListener("load", event => {
  var img = document.querySelector('img');
  var isLoaded = img.complete && img.naturalHeight !== 0;
  alert(isLoaded); //do what you want to do after artwork image loaded
});
Jejun
  • 410
  • 2
  • 13
  • 3
    Please provide a short description for your answer. – Rahul Bhobe Feb 19 '21 at 08:55
  • Wouldn't this result in a race condition if the `load` event triggers before img.complete is true? From what I can tell in the docs, `complete` is simply a boolean, not a method to _wait_ for completion. – Selbi Feb 19 '21 at 19:49
  • 1
    Tried it, sadly `complete` and `naturalHeight` also get set before the image finished paining. – Selbi Feb 20 '21 at 00:05
0

Try to use WebP image (.webp) .

  • This does not provide an answer to the question. To critique or request clarification from an author, leave a comment below their post. - [From Review](/review/low-quality-posts/28397490) – casraf Feb 24 '21 at 09:33
  • @casraf Thanks for your comment. I just want to offer a solution. – 小聪聪到此一游 Feb 24 '21 at 10:02