实现ElementPlus的主题切换动画

3,085 阅读3分钟

具体效果

element-plus的效果

element-theme.gif

实现的效果

如图所示,点击主题切换按钮后出现一个圆形的扩散动画并具有这些特点

首先先分析动画效果, 通过点击右上角的切换按钮

如果要切换到深色(dark)模式, 程序会在点击位置渲染一个圆, 圆初始很小, 随着时间流动逐渐放大, 直到占满整个屏幕, 并且被圆覆盖的部分会显示夜间模式的效果, 从而完成一个用户友好的主题切换效果.

如果要切换到亮色(light)模式, 程序会渲染一个占满屏幕的圆, 圆初始很大, 随着时间流动逐渐缩小, 直到缩小到点击的位置, 并且没有被圆覆盖的部分会显示亮色模式的效果, 从而完成一个用户友好的主题切换效果.

扩散的圆形里面和外面分别是不同主题色的页面,里圆可以仅遮盖一部分文字, 仅让文字的一部分变成新主题的颜色.

实现分析

要想达到效果,最容易想到的就是有两张一模一样的页面重叠在一起

  • 其中用旧主题的截图定位覆盖在最上层
  • 在切换主题对截图进行裁剪动画,新主题随着上层完成动画后完全出现,并且截图层隐藏

image.png

裁剪的效果使用css的clip-path属性

 /* 放大缩小动画 */
      @keyframes scaleGrowAnimation {
        0% {
            clip-path: circle(0  at 200px 50px);
        }
        100% {
            clip-path: circle(200%  at 200px 50px);
        }
      }

clip-path属性说明

而这个对旧主题截图的操作正好有一个Web APi实现了,他就是 View Transitions API,并且他直接支持不同 DOM 状态之间的动画过渡。同时还可以在单个步骤中更新 DOM 内容。

View Transitions API属性说明,具体的属性说明和使用案例这里都有详细的说明,这里就不赘述了

直接上最后实现的代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>切换主题</title>
    <style>
      /* 默认主题样式 */
      html {
        margin: 0;
        padding: 0;
        background-color: white;
      }
      html.dark {
        background-color: #1b1b1b;
      }
      header {
        text-align: left;
        padding: 10px;
      }
      img {
        width: auto;
      }
      /* Alternative custom animation style */
      ::view-transition-old(root),
      ::view-transition-new(root) {
        height: auto;
        width: 100vw;
        animation: none;
        mix-blend-mode: normal;
      }
      html.dark::view-transition-old(root) {
        z-index: 2147483646;
      }
      html.dark::view-transition-new(root) {
        z-index: 1;
      }
      html::view-transition-old(root) {
        z-index: 1;
      }
      html::view-transition-new(root) {
        z-index: 2147483646;
      }

      button {
        background-color: blue;
        color: white;
      }
      button.dark {
        background-color: white;
        color: #000000;
      }
      .dark #light {
        display: none;
      }
      .dark #dark {
        display: block;
      }
      #dark {
        display: none;
      }
      #light {
        display: block;
      }
    </style>
  </head>
  <body>
    <header>
      <button id="themeButton">切换主题</button>
    </header>
    <div class="box">
      <img src="./light.png" id="light" />
      <img src="./dark.png" id="dark" />
    </div>
    <script>
      // 获取按钮元素
      const themeButton = document.getElementById("themeButton");
      let isDark = false;
      themeButton.textContent = isDark ? "白天" : "晚上";

      function changeTheme() {
        // 切换页面主题
        isDark = !isDark;
        document.documentElement.classList.toggle("dark");
        themeButton.textContent = isDark ? "白天" : "晚上";
      }
      function updateView(event) {
         //在不支持的浏览器里不做动画
        if (!document.startViewTransition) {
          changeTheme();
          return;
        }
        // 开始一次视图过渡:
        const transition = document.startViewTransition(() => changeTheme());
        transition.ready.then(() => {
          const x = event.clientX;
          const y = event.clientY;
           //计算按钮到最远点的距离用作裁剪圆形的半径
          const endRadius = Math.hypot(
            Math.max(x, innerWidth - x),
            Math.max(y, innerHeight - y)
          );
          const clipPath = [
            `circle(0px at ${x}px ${y}px)`,
            `circle(${endRadius}px at ${x}px ${y}px)`,
          ];
          //开始动画
          document.documentElement.animate(
            {
              clipPath: isDark ? [...clipPath].reverse() : clipPath,
            },
            {
              duration: 400,
              easing: "ease-in",
              pseudoElement: isDark
                ? "::view-transition-old(root)"
                : "::view-transition-new(root)",
            }
          );
        });
      }
      // 添加点击事件监听器
      themeButton.addEventListener("click", updateView);
    </script>
  </body>
</html>

以上就实现和elementplus切换主题效果一样了,可以直接拷贝运行看看,有不懂的看注释