返回

CSS动画多元素同步:解决鼠标悬停重置问题

javascript

CSS动画多元素同步:解决鼠标悬停后重置问题

在网页开发中,CSS动画是一种常用的增强用户体验的方式。当多个元素同时执行相同的动画时,保持动画同步是一个重要考量。特别是在鼠标悬停交互时,动画重置可能导致元素间的不同步,影响视觉效果的整体协调性。本文针对这一问题,提出几种解决方案。

问题分析

问题的根源在于,CSS动画在鼠标悬停时被重置。当鼠标移出元素后,动画重新开始,导致元素动画不同步。

解决方案

方案一:使用CSS变量(自定义属性)

这种方案的核心是利用CSS变量存储动画的初始状态或进度,避免动画在鼠标悬停时被直接重置。

原理:

  1. 定义一个CSS变量 --animation-play-state,用于控制动画的播放状态。
  2. 鼠标悬停时,设置变量为 paused,暂停动画。
  3. 鼠标移出时,设置变量为 running,恢复动画。
  4. 动画的关键帧定义使用这个CSS变量来动态控制背景颜色。

实现步骤:

  1. .box 元素中添加CSS变量 --animation-play-state 定义动画播放状态和 --animation-start-color 定义动画起始颜色:

    .box {
      width: 50px;
      height: 50px;
      border: solid 1px black;
      animation: fade 2s ease-in-out infinite;
      --animation-play-state: running;
      --animation-start-color: blue;
      background-color: var(--animation-start-color);
      animation-play-state: var(--animation-play-state);
    }
    
  2. 修改动画 :hover 状态的样式:

    .hoverable:hover {
      background-color: red;
      --animation-play-state: paused;
    }
    
  3. 更新 @keyframes 的定义, 使其使用定义的CSS变量:

    @keyframes fade {
        0%   { background-color: var(--animation-start-color); }
        50%  { background-color: #FFFFFF; }
        100% { background-color: var(--animation-start-color); }
    }
    

代码示例:

.box {
  width: 50px;
  height: 50px;
  border: solid 1px black;
  animation: fade 2s ease-in-out infinite;
  --animation-play-state: running;
  --animation-start-color: blue;
  background-color: var(--animation-start-color);
  animation-play-state: var(--animation-play-state);
}

.hoverable:hover {
  background-color: red;
  --animation-play-state: paused;
}

.row {
  display: flex;
  flex-direction: row;
}

@keyframes fade {
    0%   { background-color: var(--animation-start-color); }
    50%  { background-color: #FFFFFF; }
    100% { background-color: var(--animation-start-color); }
}

优势:

  • 实现简单,性能良好。
  • 代码简洁易懂。
  • 兼容性较好,主流浏览器都支持CSS变量。

方案二: 使用JavaScript控制动画

通过JavaScript可以更精细地控制动画的播放和同步。这种方案适用于更复杂的动画场景。

原理:

  1. 获取所有需要同步动画的元素。
  2. 计算动画的当前进度,并在鼠标悬停时保存当前进度。
  3. 鼠标移出时,根据保存的进度重新开始动画。

实现步骤:

  1. 为所有动画元素添加一个自定义属性,用于存储动画起始时间。

        let boxes = document.querySelectorAll(".box");
        let animationDuration = 2000; // 动画持续时间,单位毫秒
    
        boxes.forEach(box => {
            let startTime = Date.now();
            box.dataset.startTime = startTime; // 使用dataset存储动画开始时间
            updateAnimation(box);
        });
    
  2. 创建一个 updateAnimation 函数,用于更新动画:

        function updateAnimation(box) {
            let startTime = parseInt(box.dataset.startTime);
            let elapsedTime = Date.now() - startTime;
            let normalizedTime = (elapsedTime % animationDuration) / animationDuration; // 计算动画循环周期内的时间
    
            let color;
            if (normalizedTime < 0.5) {
                color = interpolateColor(0x0000FF, 0xFFFFFF, normalizedTime * 2); // 从蓝色到白色
            } else {
                color = interpolateColor(0xFFFFFF, 0x0000FF, (normalizedTime - 0.5) * 2); // 从白色到蓝色
            }
            box.style.backgroundColor = rgbToHex(color);
    
            requestAnimationFrame(() => updateAnimation(box));
        }
    
        function interpolateColor(rgbStart, rgbEnd, t) {
             let r1 = (rgbStart >> 16) & 0xFF;
            let g1 = (rgbStart >> 8) & 0xFF;
            let b1 = rgbStart & 0xFF;
    
            let r2 = (rgbEnd >> 16) & 0xFF;
            let g2 = (rgbEnd >> 8) & 0xFF;
            let b2 = rgbEnd & 0xFF;
    
             let r = Math.round(r1 + (r2 - r1) * t);
            let g = Math.round(g1 + (g2 - g1) * t);
             let b = Math.round(b1 + (b2 - b1) * t);
    
            return (r << 16) | (g << 8) | b;
        }
    
        function rgbToHex(rgb) {
            return '#' + (rgb & 0xFFFFFF).toString(16).padStart(6, '0');
          }
    
    
  3. 移除元素上的动画,仅保留基础样式:

    .box {
      /*background-color: blue;*/
      width: 50px;
      height: 50px;
      border: solid 1px black;
    }
    
    .hoverable:hover {
      background-color: red;
    }
    

代码示例:

HTML 和 CSS代码如上,JavaScript代码如下:

        let grid = document.getElementById("grid");

        for (i = 0; i < 4; i++){
        let row = document.createElement("div");
        row.className = "row";
        
        for (j = 0; j < 4; j++){
            let box = document.createElement("div");
            box.className = "box";
            box.classList.add("hoverable");
            row.appendChild(box);
        }
        grid.appendChild(row);
        }

        let boxes = document.querySelectorAll(".box");
        let animationDuration = 2000;

        boxes.forEach(box => {
            let startTime = Date.now();
            box.dataset.startTime = startTime;
            updateAnimation(box);
            box.addEventListener('mouseenter', function () {
                this.style.backgroundColor = 'red';
              });
        });

        function updateAnimation(box) {
            let startTime = parseInt(box.dataset.startTime);
            let elapsedTime = Date.now() - startTime;
            let normalizedTime = (elapsedTime % animationDuration) / animationDuration;

            let color;
            if (normalizedTime < 0.5) {
                color = interpolateColor(0x0000FF, 0xFFFFFF, normalizedTime * 2);
            } else {
                color = interpolateColor(0xFFFFFF, 0x0000FF, (normalizedTime - 0.5) * 2);
            }
            box.style.backgroundColor = rgbToHex(color);

            requestAnimationFrame(() => updateAnimation(box));
        }

        function interpolateColor(rgbStart, rgbEnd, t) {
             let r1 = (rgbStart >> 16) & 0xFF;
            let g1 = (rgbStart >> 8) & 0xFF;
            let b1 = rgbStart & 0xFF;
        
            let r2 = (rgbEnd >> 16) & 0xFF;
            let g2 = (rgbEnd >> 8) & 0xFF;
            let b2 = rgbEnd & 0xFF;
        
             let r = Math.round(r