Search code examples
cssvue.jsanimationvue-component

Why is a third image briefly shown during flip animation in Vue.js?


I’m working on a Vue.js component where I’m creating a flip animation with images that change every few seconds. The goal is to randomly change the image, but there is an issue during the transition.

What I want:

  • Possible images are loaded from a folder. We start with one image and which will be the next image is determined randomly.
  • There is a flip animation between the images.

What happens:

  • The logic has one flaw. The animation between image 1 and image 2 works as desired. But in the animation from image 2 to image 3, image 1 reappears for a short moment. (And this happens in all succeeding animations, too.)

  • You can find a screen recording of this issue here.

Code:

Template:

<template>
  <div class="start-screen">
    <div class="container">
      <div id="card">
        <div
          v-for="(image, index) in images"
          :key="index"
          :class="['flip-page', { active: index === currentIndex, turnedLeft: index === previousIndex }]"
          :style="{ backgroundImage: `url(${image})` }"
        ></div>
      </div>
    </div>
  </div>
</template>

Script:

<script>
export default {
  name: "StartScreen",
  data() {
    return {
      images: [],
      currentIndex: 0,
      previousIndex: null, // Track the last image for smooth transition
    };
  },
  methods: {
    loadImages() {
      const context = require.context("../assets/icons", false, /\.(png|jpe?g|svg)$/);
      this.images = context.keys().map(context);
    },
    getRandomIndex() {
      if (this.images.length <= 1) return 0;

      let newIndex;
      do {
        newIndex = Math.floor(Math.random() * this.images.length);
      } while (newIndex === this.currentIndex); // Avoid immediate repetition

      return newIndex;
    },
    nextSlide() {
      this.previousIndex = this.currentIndex; // Store the previous image index
      this.currentIndex = this.getRandomIndex(); // Get a new random image
    },
  },
  mounted() {
    this.loadImages();
    setInterval(this.nextSlide, 6000); // Flip every 6 seconds
  },
};
</script>

Style:

<style scoped>
.start-screen {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

/* Container for the flip effect */
.container {
  width: 260px;
  height: 260px;
  position: relative;
  perspective: 800px;
}

/* Stacked images */
#card {
  width: 100%;
  height: 100%;
  position: absolute;
}

/* Flip animation for each page */
.flip-page {
  width: 100%;
  height: 100%;
  position: absolute;
  background-size: cover;
  background-position: center;
  border-radius: 10px;
  backface-visibility: hidden;
  transition: transform 3s;
  transform: rotateY(180deg);
}

/* Flip the active page forward */
.active {
  transform: rotateY(0deg);
  z-index: 2;
}

/* Only show the most recent previous page */
.turnedLeft {
  transform: rotateY(-180deg);
  z-index: 1;
}
</style>

Question: Can anyone explain why this happens and how I can fix it to ensure only the current and next images are visible and there is no flickering of a third image? Thank you very much!


Solution

  • Just hide the .flip-page elements that are neither .active nor .turnedLeft, like this:

    .flip-page:not(.active):not(.turnedLeft) {
      opacity: 0;
    }
    

    Or simply put, the same:

    .flip-page:not(.active, .turnedLeft) {
      opacity: 0;
    }
    

    const { createApp, ref, onMounted } = Vue;
    
    createApp({
      setup() {
        const images = ref([
          "https://picsum.photos/260/260?1",
          "https://picsum.photos/260/260?2",
          "https://picsum.photos/260/260?3",
          "https://picsum.photos/260/260?4",
        ]); // Example images listed instead of loading for CDN Example
    
        const currentIndex = ref(0);
        const previousIndex = ref(null); // Track the last image for smooth transition
    
        const getRandomIndex = () => {
          if (images.value.length <= 1) return 0;
    
          let newIndex;
          do {
            newIndex = Math.floor(Math.random() * images.value.length);
          } while (newIndex === currentIndex.value); // Avoid immediate repetition
    
          return newIndex;
        };
    
        const nextSlide = () => {
          previousIndex.value = currentIndex.value; // Store the previous image index
          currentIndex.value = getRandomIndex(); // Get a new random image
        };
    
        onMounted(() => {
          setInterval(nextSlide, 3000); // Flip every 6 seconds (changed to 3s for test)
        });
    
        return { images, currentIndex, previousIndex };
      }
    }).mount("#app");
    .start-screen {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    
    /* Container for the flip effect */
    .container {
      width: 260px;
      height: 260px;
      position: relative;
      perspective: 800px;
    }
    
    /* Stacked images */
    #card {
      width: 100%;
      height: 100%;
      position: absolute;
    }
    
    /* Flip animation for each page */
    .flip-page {
      width: 100%;
      height: 100%;
      position: absolute;
      background-size: cover;
      background-position: center;
      border-radius: 10px;
      backface-visibility: hidden;
      transition: transform 3s;
      transform: rotateY(180deg);
    }
    .flip-page:not(.active, .turnedLeft) {
      opacity: 0;
    }
    
    /* Flip the active page forward */
    .active {
      transform: rotateY(0deg);
      z-index: 2;
    }
    
    /* Only show the most recent previous page */
    .turnedLeft {
      transform: rotateY(-180deg);
      z-index: 1;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.4/vue.global.prod.js"></script>
    
    <div id="app">
      <div class="start-screen">
        <div class="container">
          <div id="card">
            <div
              v-for="(image, index) in images"
              :key="index"
              :class="['flip-page', { active: index === currentIndex, turnedLeft: index === previousIndex }]"
              :style="{ backgroundImage: `url(${image})` }"
            ></div>
          </div>
        </div>
      </div>
    </div>