使用全新的 View Transitions API,实现 B 站 PC 客户端的深色主题切换效果

原理分析

目前深色模式基本是通过prefers-color-scheme这个系统预设属性来初始化的,如需具体切换时,可以通过JS给html根节点增加class的方式来脱离系统预设(如:tailwindcss中的darkTheme: 'class'这个配置)。

具体分析到这个客户端的华丽切换效果,如此细粒度的动画这应该不是单纯的css或者js能控制的。个人猜测应该是通过electron对切换前的当前APP屏幕截图进行保存,然后将旧页面图片和切换后的页面进行叠加,当切换为dark时,新页面(黑色)在上并且逐渐增大,当切换为light时,旧页面(黑色)在上,并且逐渐缩小。这种方法由于截图的存在,应该只能在electron环境中使用。

View Transition API 介绍

这个API主要用做对不同时间处于不同状态的DOM进行过渡处理,常用于SPA页面转场时的过渡效果,如下这类效果:

需要注意的是,这个API目前还处于试验阶段(预计edge 版本 111 发布),截止目前(edge 版本 110)浏览器还没有实装,但可以在edge dev channel中试验。具体可以查看Document.startViewTransition\(\) \- Web APIs | MDN \(mozilla.org\)[1]

代码的实现

CSS部分

View Transition API产生过渡时,会生成一组伪元素,分别代表着状态变换前的图像和状态变换后的图像,并且默认就带有过滤渐隐渐现的过渡效果,我们主题切换不需要这种效果,将其默认效果关闭,同时也关闭过渡自带的mix-blend-mode效果。

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* 进入dark模式和退出dark模式时,两个图像的位置顺序正好相反 */
.dark::view-transition-old(root) {
  z-index: 1;
}
.dark::view-transition-new(root) {
  z-index: 999;
}

::view-transition-old(root) {
  z-index: 999;
}
::view-transition-new(root) {
  z-index: 1;
}
复制代码

JS部分

  1. 观察这个过渡动过可以发现,新产生的页面由点到圆直至最终覆盖完了整个页面。所以首先我们需要获取鼠标点击的位置,然后计算出过滤效果最终的位置圆的半径大小。
const toggleTheme = (event: MouseEvent) => {
    const x = event.clientX;
    const y = event.clientY;
    const endRadius = Math.hypot(
      Math.max(x, innerWidth - x),
      Math.max(y, innerHeight - y)
    );
}
复制代码
  1. 使用`document.startViewTransition`,并传入更新DOM的方法,使浏览器记录两次状态时DOM的快照,即`::view-transition-old(root)`和`::view-transition-new(root)`:
const toggleTheme = (event: MouseEvent) => {
  const x = event.clientX;
  const y = event.clientY;
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

+  let isDark: boolean;

+  // @ts-ignore
+  const transition = document.startViewTransition(() => {
+    const root = document.documentElement;
+    isDark = root.classList.contains("dark");
+    root.classList.remove(isDark ? "dark" : "light");
+    root.classList.add(isDark ? "light" : "dark");
+  });
};
复制代码
  1. 等待浏览器准备好了后,使用`element.animate`来驱动动画(当然也可以使用`gsap`或者`anime.js`等库或者自己去写缓动效果来实现\)
const toggleTheme = (event: MouseEvent) => {
  const x = event.clientX;
  const y = event.clientY;
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  let isDark: boolean;

  // @ts-ignore
  const transition = document.startViewTransition(() => {
    const root = document.documentElement;
    isDark = root.classList.contains("dark");
    root.classList.remove(isDark ? "dark" : "light");
    root.classList.add(isDark ? "light" : "dark");
  });

+  transition.ready.then(() => {
+    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: 500,
+        easing: "ease-in",
+        pseudoElement: isDark ? "::view-transition-old(root)" : "::view-transition-new(root)",
+      }
+    );
+  });
};
复制代码
  1. 最后补充完DOM结构即可。
<div class="flex h-screen flex-col justify-between bg-white p-4 dark:bg-gray-800">
  <div class="text-gray-800 dark:text-gray-100">
    填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字
  </div>
  <div class="text-gray-800 dark:text-gray-100">
    填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字
  </div>
  <div class="text-gray-800 dark:text-gray-100">
    填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字填充文字
  </div>
  <div>
    <button
      class="rounded-md border p-2 outline-none  dark:border-gray-900/50 dark:bg-gray-700 dark:text-gray-100"
      onClick={toggleTheme}
    >
      切换主题
    </button>
  </div>
</div>
复制代码

效果展示(注意edge/chrome版本至少为111,目前是最新版是110,建议使用dev版)

源码:yuhengshen/toggle-theme-demo \(github.com\)[2]

源码新增分支 gsap: 使用gsap操作CSS变量来驱动动画效果(关键点:需要注意在CSS中定义好过渡时间,否则过渡效果瞬间就结束了)

关于本文

作者:喻衡深

https://juejin.cn/post/7207810396420325413

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值