Alexander Zaytsev
Posted on September 28, 2021
In this post, we look at concurrent hide/show animation behavior in Velo APIs. When one of the animation effects can't start because a previous one isn't finished yet.
What is the issue?
Let's suppose that we have to animate an image by mouse event. For example, we want to create a hover effect that combines mouse in/out events.
Our realization will be very trivial. We have an image that has two event listeners on onMouse{In/Out}
and a vector image that will be shown or hidden.
It's the next code snippet:
$w.onReady(() => {
const fadeOptions = {
duration: 300,
};
$w('#imageParrot')
.onMouseIn(() => {
$w('#vectorHat').show('fade', fadeOptions);
})
.onMouseOut(() => {
$w('#vectorHat').hide('fade', fadeOptions);
});
});
As you can see above, the animation has a duration of 300 ms. What happens if we move the cursor through in/out the image faster than 300 ms?
Yes, there is an issue. The next in the queue animation doesn't run if the previous one is going at the moment.
Why does it happen?
Let's visualize a timeline of the animation's execution. For example, we have three events, in -> out -> in
. The first animation starts at 0 it will be finishing at 300 ms. The second animation starts at 200 ms, but it will be skip because this element is animating at this moment. The third one starts at 300 ms that will successfully run because the first one has finished, the second one skipped so the element can animate again.
Visualization of mouse events in a timeline
0 ms -- 100 ms -- 200 ms -- 300 ms -- 400 ms -- 500 ms -- 600 ms -- 700 ms
----------------------------------------------------------------------------
.show() -------------------│ ✅ Done
│
.hide() ......│ ❌ 🪲 Skipped
│
│ .show() -------------------│ ✅ Done
The Wix elements can't be animating with two concurrent (hide/show) animations on the same element at one time.
How can we fix it?
We have to wait for the animation's end before running a new one. For this, we create a queue. We push each animation request to this queue, where animations will be calling one by one.
Visualization of the Promise queue in a timeline
0 ms -- 100 ms -- 200 ms -- 300 ms -- 400 ms -- 500 ms -- 600 ms -- 700 ms
----------------------------------------------------------------------------
.show() -------------------│ ✅ Done
│
.hide() ......└--------------------│ ✅ Done
│
.show() ............└--------------------│ ✅ Done
Create a queue
Create a queue.js
file in the public
folder. In this file, we implement the queue logic.
First, we implement a mechanism for adding actions to the queue.
public/queue.js
export const createQueue = () => {
// Action list
const actions = [];
return (action) => {
// Adds action to the end of the list
actions.push(action);
};
};
Тurn your attention we don't run animation when mouse event fired. Instead, we wrap it to function and push it to the array.
Let's upgrade the code on the page to see how it works.
HOME Page (code)
import { createQueue } from 'public/queue.js';
$w.onReady(() => {
// Initializing queue
const queue = createQueue();
const fadeOptions = {
duration: 300,
};
$w('#imageParrot')
.onMouseIn(() => {
// Add actions to queue
queue(() => $w('#vectorHat').show('fade', fadeOptions));
})
.onMouseOut(() => {
// Add actions to queue
queue(() => $w('#vectorHat').hide('fade', fadeOptions));
});
});
Great, we have a list of actions. The next step, run the queue.
It will be an auxiliary function for the queue start.
public/queue.js
export const createQueue = () => {
const actions = [];
const runQueue = () => {
// Check: are we have any actions in queue
if (actions.length > 0) {
// Removes the first action from the queue
// and returns that removed action
const action = actions.shift();
// Waits the promise
action().then(() => {
// When the Promise resolves
// then it runs the queue to the next action
runQueue();
});
}
};
return (action) => {
actions.push(action);
// Runs the queue when adding a new action
runQueue();
};
};
The runQueue()
is the recursive function it runs itself after the promise has been resolved. Also, we trigger runQueue()
by adding a new action. We have to limit the trigger it should run only once at the queue start.
Further, we add the flag for closing the runQueue()
if the queue is active.
public/queue.js
export const createQueue = () => {
// Flag
let isActive = false;
const actions = [];
const runQueue = () => {
// Check: if the queue is running
if (isActive) {
// Stop this call
return;
}
if (actions.length > 0) {
const action = actions.shift();
// Before: closes the queue
isActive = true;
action().then(() => {
// After: opens the queue
isActive = false;
runQueue();
});
}
};
return (action) => {
actions.push(action);
runQueue();
};
};
When a new action is adding to the list, we check the queue is active. If the queue is not active, we run it. If the queue is active, we do nothing.
Queue length
The last thing we need is control of the queue length. We can create a lot of animation actions that could lead to a blink effect.
Velo: blink effect by a long Promise queue
The algorithm is simple. If the queue has a max length then we remove the last action before adding a new one.
Visualization of removing queue actions in a timeline
0 ms -- 100 ms -- 200 ms -- 300 ms -- 400 ms -- 500 ms -- 600 ms -- 700 ms
----------------------------------------------------------------------------
.show() -------------------│ ✅ Done
│
.hide() ..............│ 🗑️ Removed
│
.show() ..........│ 🗑️ Removed
│
.hide() ....└--------------------│ ✅ Done
Let's set a max length by default as one. I think it covered 99% of use cases.
public/queue.js
// By default, the queue has one action
export const createQueue = (maxLength = 1) => {
let isActive = false;
const actions = [];
const runQueue = () => {…};
return (action) => {
// Check: if the queue has max length
if (actions.length >= maxLength) {
// Removes the last action from the queue
// before adds a new one
actions.pop();
}
actions.push(action);
runQueue();
};
};
Check how it works on Live Demo
That's it! I hope it could be helpful to your projects. Thanks for reading.
Code Snippets
Here are the whole code snippet plus JSDoc types.
public/queue.js
/**
* Create a promise queue
*
* @typedef {() => Promise<unknown>} Action
*
* @param {number} [maxLength] - max count actions in the queue
* @returns {(action: Action) => void}
*/
export const createQueue = (maxLength = 1) => {
/** @type {boolean} */
let isActive = false;
/** @type {Action[]} */
const actions = [];
const runQueue = () => {
if (isActive) {
return;
}
if (actions.length > 0) {
const action = actions.shift();
isActive = true;
action().then(() => {
isActive = false;
runQueue();
});
}
};
return (action) => {
if (actions.length >= maxLength) {
actions.pop();
}
actions.push(action);
runQueue();
};
};
Example of using:
HOME Page (code)
import { createQueue } from 'public/queue.js';
$w.onReady(() => {
const queue = createQueue();
const fadeOptions = {
duration: 300,
};
$w('#imageParrot')
.onMouseIn(() => {
queue(() => $w('#vectorHat').show('fade', fadeOptions));
})
.onMouseOut(() => {
queue(() => $w('#vectorHat').hide('fade', fadeOptions));
});
});
Resources
Posts
Posted on September 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.