点击元素时CSS Transition与Canvas坐标更新不同步问题求助
问题描述
我正在开发一个Web项目,包含用户可点击的元素列表。点击元素时,该元素需变为激活状态,触发200ms的CSS Transition(如transform),同时要同步更新该元素在HTML5 Canvas上的坐标。
当前JS代码逻辑是:点击元素时添加active类触发CSS过渡,随后调用updateItemCoordinates()更新坐标,但坐标更新要么滞后于过渡完成,要么保留旧坐标,导致两者不同步。
尝试过的方案:
- 使用
transitionend事件,但引入了延迟; - 用
requestAnimationFrame在过渡期间更新坐标,但未实现正确同步。
当前使用的JavaScript代码如下:
// 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);
解决方案
核心问题在于你提前计算了newCoordinates,但此时CSS过渡尚未开始,元素位置没有变化,导致Canvas动画没有对应到实际的元素移动。正确的做法是在动画过程中实时获取元素的当前位置,而非预计算目标坐标。
修改后的updateItemCoordinates函数及点击事件逻辑如下:
// 修改点击事件逻辑 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'); // 启动同步动画,无需提前计算新坐标 syncCanvasWithTransition(); }); }) function syncCanvasWithTransition() { const startTime = performance.now(); // 记录过渡开始时的初始坐标 const startCoordinates = items.map(getCanvasCoordinates); function animateFrame(timestamp) { const elapsed = timestamp - startTime; const progress = Math.min(1, elapsed / animationDuration); ctx.clearRect(0, 0, canvas.width, canvas.height); // 实时获取当前元素的坐标(CSS过渡中的实时位置) const currentCoordinates = items.map(getCanvasCoordinates); // 绘制所有连线,使用当前实时坐标 for (let i = 0; i < items.length - 1; i++) { drawLine(currentCoordinates[i].x, currentCoordinates[i].y, currentCoordinates[i+1].x, currentCoordinates[i+1].y, 1); } if (progress < 1) { requestAnimationFrame(animateFrame); } else { // 动画结束后更新基准坐标 itemCoordinates = currentCoordinates; } } requestAnimationFrame(animateFrame); }
关键改进点:
- 实时获取坐标:在每一动画帧中调用
getCanvasCoordinates获取元素的当前位置,确保Canvas绘制的位置与CSS过渡中的元素位置完全同步; - 取消预计算目标坐标:不再提前计算
newCoordinates,而是直接使用元素的实时位置,避免因过渡未开始导致坐标无变化的问题; - 动画结束更新基准:过渡完成后,将当前坐标更新为基准坐标,保证后续操作的准确性。
额外优化建议:
如果担心getBoundingClientRect的性能,可以在动画开始时记录元素的初始位置和目标位置(通过计算CSS变换后的最终位置),然后用progress值插值计算中间位置,减少DOM查询次数:
// 计算目标坐标的示例(假设active类的transform是translateY(-10px)) function getTargetCoordinates(element) { const rect = element.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect(); // 根据active类的transform计算目标位置 const targetY = rect.top + rect.height / 2 - canvasRect.top - 10; return { x: rect.left + rect.width / 2 - canvasRect.left, y: targetY }; } // 在syncCanvasWithTransition中使用插值计算 function syncCanvasWithTransition() { const startTime = performance.now(); const startCoordinates = items.map(getCanvasCoordinates); const targetCoordinates = items.map(getTargetCoordinates); function animateFrame(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 currentX = startCoordinates[i].x + (targetCoordinates[i].x - startCoordinates[i].x) * progress; const currentY = startCoordinates[i].y + (targetCoordinates[i].y - startCoordinates[i].y) * progress; const nextX = startCoordinates[i+1].x + (targetCoordinates[i+1].x - startCoordinates[i+1].x) * progress; const nextY = startCoordinates[i+1].y + (targetCoordinates[i+1].y - startCoordinates[i+1].y) * progress; drawLine(currentX, currentY, nextX, nextY, 1); } if (progress < 1) { requestAnimationFrame(animateFrame); } else { itemCoordinates = targetCoordinates; } } requestAnimationFrame(animateFrame); }
这种方式适合已知变换规则的场景,性能更优,但需要硬编码或动态获取CSS变换值。
内容的提问来源于stack exchange,提问作者Kilian
相关产品推荐
相关产品推荐

