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?
@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!
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;
}
...