Search code examples
vuetify.jscss-animations

Vuetify: how to bounce an image


I have a project which uses v-chip components and when I move them around and drop them elsewhere, I have used a 'bounce' transition to animate them:

<Transition name="bounce">
        <v-chip  ......

and the css:

.bounce-enter-active {
  animation: bounce-in 0.5s;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}

This works well, but if I try the same technique with a v-img there is no bounce. So, how do I bounce an image?

Later edit

@Moritz 's answer below was very helpful, improved my 'bounce' and is a great answer to the question I originally posed. However, it was @yoduh 's comment that led me to the key problem. I had changed the way the images were being moved, so in effect the reactive v-img didn't get a look-in to the DOM change. So I now have simply:

<v-chip  class="bounce-enter-active" ......

instead of:

<Transition name="bounce">
        <v-chip  ......

Maybe I should have added this as an answer, but it doesn't actually answer my original question!


Solution

  • Most likely, you don't see the animation because it happens while the image still loads. Have a look at the snippet to see the difference:

    const { createApp, ref } = Vue;
    const { createVuetify } = Vuetify
    const vuetify = createVuetify()
    
    const app = {
      setup() {
        const isOn = ref(false)
        const loadUncached = ref(false)
        const showPreloaded = ref(false)
        const nonEmpty = ref(false)
        const rawSrc = 'https://www.neonpoodle.co.uk/cdn/shop/products/HeadInCloudsPink_620x.jpg?v=1554576513'
        const src = ref(rawSrc)
    
        const loadSrc = () => {
          const src = rawSrc + (loadUncached.value ? '?asfd=' + Math.random() : '')
          return !showPreloaded.value ? src : new Promise(resolve => {
            const img = new Image()
            img.onload = () => resolve(src)
            img.src = src
          })
        }
        setInterval(async() => {
          const doShow = !isOn.value
          if (doShow) {
            src.value = await loadSrc()
          }
          isOn.value = doShow
    
        }, 2000)
    
        return {
          isOn,
          loadUncached,
          showPreloaded,
          nonEmpty,
          src
        }
      }
    
    }
    createApp(app).use(vuetify).mount('#app')
    .bounce-enter-active {
      animation: bounce-in 0.5s;
    }
    
    @keyframes bounce-in {
      0% {
        transform: scale(0);
        image-rendering: crisp-edges;
      }
      50% {
        transform: scale(1.25);
      }
      100% {
        transform: scale(1);
      }
    }
    
    .non-empty{
      width: 100px;
      height: 100px;
      background: purple;
    }
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/vuetify@3/dist/vuetify.min.css" />
    <link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
    <div id="app">
      <v-app>
        <v-main class="ma-4">
          <div class="d-flex">
            <v-switch v-model="loadUncached" label="load uncached (leads to stunted transition)"></v-switch>
            <v-switch v-model="showPreloaded" label="preload image (restores transition)" :disabled="!loadUncached"></v-switch>
            <v-switch v-model="nonEmpty" label="set background"></v-switch>
          </div>
          <Transition name="bounce">
            <v-img v-if="isOn" :src="src" width="100" :class="{'non-empty' : nonEmpty}"></v-img>
          </Transition>
        </v-main>
      </v-app>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@3/dist/vuetify.min.js"></script>

    If you want to see the full animation, make sure that the image is loaded into cache before you show it. A simple way to do it is to put the url into an Image and wait for the onloaded event, at which point the image is loaded into cache:

      const preloadSrc = (src) => {
        return new Promise(resolve => {
          const img = new Image()
          img.onload = () => resolve(true)
          img.src = src
        })
      }
      showVImg.value = await preloadSrc('/the/image/url')
    

    There is also a @load event on v-img, but it is quite annoying to get it to work with a combination of <Transition> and v-if. But since you are using a real CSS animation, you could use it without <Transition> (if it is acceptable to leave the class after the animation finished):

    const { createApp, ref } = Vue;
    const { createVuetify } = Vuetify
    const vuetify = createVuetify()
    
    const app = {
      setup() {
        const rawSrc = 'https://www.quilkin.co.uk/shared/topright.png'
        const src = ref(null)
        const loaded = ref(false)
    
        setInterval(async() => {
          src.value = src.value ? null : rawSrc + '?asfd=' + Math.random()
          loaded.value = false
        }, 2000)
    
        return {src, loaded}
      }
    
    }
    createApp(app).use(vuetify).mount('#app')
    .bounce {
      animation: bounce-in 1s;
    }
    
    @keyframes bounce-in {
      0% {
        transform: scale(0);
        image-rendering: crisp-edges;
      }
      50% {
        transform: scale(1.25);
      }
      100% {
        transform: scale(1);
      }
    }
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/vuetify@3/dist/vuetify.min.css" />
    <link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
    <div id="app">
      <v-app>
        <v-main class="ma-4">
    
          <v-img
            v-if="src"
            :src="src"
            width="100"
            :class="{'bounce' : loaded}"
            @load="loaded = true"
          ></v-img>
        </v-main>
      </v-app>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@3/dist/vuetify.min.js"></script>


    Alternatively, you could also set a background color and fixed height and width on the v-img, then you see that background animated while the image loads.


    Finally, to make sure the animation uses the image and not a scaled-down version, set image-rendering to crisp-edges in your CSS. It seems to be enough to set it on the 0% keyframe of the animation:

    @keyframes bounce-in {
      0% {
        transform: scale(0);
        image-rendering: crisp-edges;
      }
      ...