I'm working on a web project where I have a list of elements that the user can click. When an element is clicked, it should become active, triggering a CSS transition (e.g., a transform) that lasts for 200ms. At the same time, I want to update the coordinates of that element on an HTML5 canvas.
Currently, I have JavaScript code that adds the "active" class to the clicked element, triggering the CSS transition, and then it calls the updateItemCoordinates() function to update the coordinates. However, the coordinate update occurs after the CSS transition has finished, resulting in a lag between the transition and the coordinate update. Or it keeps the previous coordinates.
I want the coordinate update to happen simultaneously with the CSS transition.
Here's my example on codepen: https://codepen.io/kilian-m/pen/QWzXmEr
Here's the JavaScript code I'm currently using:
// Lines animation
const canvas = document.getElementById('line-canvas');
const ctx = canvas.getContext('2d');
const list = document.querySelector('.ressource-search__parent-terms');
const items = Array.from(list.querySelectorAll('.ressource-search__parent-term'));
const parent = document.querySelector('.ressource-search');
canvas.width = list.offsetWidth;
canvas.height = parent.offsetHeight;
const animationDuration = 200;
function getCanvasCoordinates(element) {
const rect = element.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
return {
x: rect.left + rect.width / 2 - canvasRect.left,
y: rect.top + rect.height / 2 - canvasRect.top
};
}
let itemCoordinates = items.map(getCanvasCoordinates);
// Click sur l'element
items.forEach((item, key) => {
item.addEventListener('click', () => {
items.forEach(otherItem => {
otherItem.classList.remove('active');
otherItem.classList.add('not-active');
});
item.classList.remove('not-active');
item.classList.add('active');
updateItemCoordinates();
});
})
function updateItemCoordinates() {
const newCoordinates = items.map(getCanvasCoordinates);
const startTime = performance.now();
function animateTransition(timestamp) {
const elapsed = timestamp - startTime;
const progress = Math.min(1, elapsed / animationDuration);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < items.length - 1; i++) {
const x1 = itemCoordinates[i].x;
const y1 = itemCoordinates[i].y;
const x2 = itemCoordinates[i + 1].x;
const y2 = itemCoordinates[i + 1].y;
const newX1 = x1 + (newCoordinates[i].x - x1) * progress;
const newY1 = y1 + (newCoordinates[i].y - y1) * progress;
const newX2 = x2 + (newCoordinates[i + 1].x - x2) * progress;
const newY2 = y2 + (newCoordinates[i + 1].y - y2) * progress;
drawLine(newX1, newY1, newX2, newY2, 1);
}
if (progress < 1) {
requestAnimationFrame(animateTransition);
} else {
itemCoordinates = newCoordinates;
cancelAnimationFrame(animateTransition);
}
}
requestAnimationFrame(animateTransition);
}
function animateLine(index, startTime) {
const x1 = itemCoordinates[index].x;
const y1 = itemCoordinates[index].y;
const x2 = itemCoordinates[index + 1].x;
const y2 = itemCoordinates[index + 1].y;
return function(timestamp) {
const elapsed = timestamp - startTime;
const progress = Math.min(1, elapsed / animationDuration);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < index; i++) {
const prevX1 = itemCoordinates[i].x;
const prevY1 = itemCoordinates[i].y;
const prevX2 = itemCoordinates[i + 1].x;
const prevY2 = itemCoordinates[i + 1].y;
drawLine(prevX1, prevY1, prevX2, prevY2, 1);
}
drawLine(x1, y1, x2, y2, progress);
if (progress < 1) {
requestAnimationFrame(animateLine(index, startTime));
} else {
if (index + 2 < items.length) {
animateLine(index + 1, performance.now())(performance.now());
}
}
};
}
function drawLine(x1, y1, x2, y2, progress) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x1 + (x2 - x1) * progress, y1 + (y2 - y1) * progress);
ctx.lineWidth = 1;
ctx.strokeStyle = "#707070";
ctx.stroke();
}
function startAnimation() {
if (items.length > 1) {
animateLine(0, performance.now())(performance.now());
}
}
window.addEventListener('resize', () => {
itemCoordinates = items.map(getCanvasCoordinates);
startAnimation();
});
setTimeout(() => {
startAnimation();
}, 50);
I've tried using the transitionend event, but it introduced a delay. I've also attempted to use requestAnimationFrame to update the coordinates during the transition, but I'm struggling to properly synchronize the two.
I added transparents clones without transition to resolve my problem