只需要通过 View Transitions API 切换不同的类名,即可实现流畅的切换动画

2025-03-16T07:07:52.png

命名 view-transition-name
在 View Transitions 动画执行的过程中,默认会在页面根节点下自动创建一组伪元素:

::view-transition  // 视图过渡根元素,包含所有视图过渡组,且位于其他页面内容的顶部
├─ ::view-transition-group(root)  // 默认视图过渡组 (root)
    └─ ::view-transition-image-pair(root)  // 承载一个过渡中旧视图状态和新视图状态的容器
        ├─ ::view-transition-old(root)  // 旧视图状态
        └─ ::view-transition-new(root)  // 新视图状态

通过调用 API,让浏览器为新旧两种不同视图分别捕获并建立了快照 (即 ::view-transition-old 旧快照 和 ::view-transition-new 新快照),而后新旧两快照在 ::view-transition-image-pair 容器中完成转场动画的过渡。动画结束后则删除其相关伪元素 (快照和容器)。
动画执行的基本过程如下图所示:
2025-03-16T07:15:01.png

  • 开发者通过 document.startViewTransition(callback) 启动转场动画,其中 callback
    函数是用来更新 DOM 状态 (即更新为新视图状态)
  • 捕获当前状态为旧视图状态
  • 暂停 DOM 树渲染
  • 回调函数 callback 被调用,用来更新文档状态 (可以是异步函数,返回 Promise)
  • 回调函数 callback 成功后,transition.updateCallbackDone 被执行 (即 promise is
    resolved)
  • 恢复 DOM 树渲染,而后捕获当前状态为新视图状态
  • 创建过渡伪元素 (即 ::view-transition-old::view-transition-new ...等)
  • 渲染未暂停,显示过渡伪元素
  • transition.ready 被执行 (即 promise is resolved)
  • 伪元素开始动画,直至动画完成
  • 删除了过渡伪元素
  • transition.finished 被执行 (即 promise is resolved)

若需要使某个元素执行过渡动画,需要给每个元素添加一个自定义属性:view-transition-name,且每个元素的 view-transition-name 必须唯一,即同一个页面上渲染的元素(display 非 none) view-transition-name 不同重复。

<!DOCTYPE html>
<html lang=zh-CN>
<head>
  <meta charset=utf-8>
  <meta http-equiv=X-UA-Compatible content="IE=edge">
  <title></title>
  <script>
    function readly(){
      const btn = document.querySelector('#btn')
      btn.addEventListener('click', function(ev) {
        document.documentElement.style.setProperty('--x', ev.clientX + 'px');
        document.documentElement.style.setProperty('--y', ev.clientY + 'px');
        if (document.startViewTransition) {
          const vt = document.startViewTransition(() => {
            document.documentElement.classList.toggle('dark');
          });
          console.log('View Transition Instance', vt);
          vt.ready.then(() => {
            console.log('Transition Ready');
          });
          vt.updateCallbackDone.then(() => {
            console.log('Callback Done');
          });
          vt.finished.then(() => {
            console.log('Transition Finished');
          });
        } else {
          document.documentElement.classList.toggle('dark');
        }
      });
    }
  </script>
  <style>
    html,
    body {
      margin: 0;
      height: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    html {
      background-color: #fff;
    }
    button {
      padding: 5px 16px;
      margin-top: 20px;
      margin-left:auto;
      background-color: transparent;
      border-radius: 8px;
      line-height: 1.4;
      box-shadow: 0 2px #00000004;
      cursor: pointer;
      transition: .3s;
      transform: scale(1);
      border-color: transparent;
      background-color: royalblue;
      color: #fff;
      text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
      box-shadow: 0 2px #0000000b;
      user-select: none;
    }
    button:hover {
      filter: brightness(1.1);
    }
    .dark {
      background-color: #111;
    }
    .list {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 15px;
      margin-top: 20px;
    }
    .item {
      width: 200px;
      height: 150px;
      background-color: royalblue;
      border-radius: 10px;
    }
    ::view-transition-old(*) {
      animation: none;
    }
    ::view-transition-new(*) {
      animation: clip .5s ease-in-out;
    }
    @keyframes clip {
      from {
        clip-path: circle(0% at var(--x) var(--y));
      }
      to {
        clip-path: circle(100% at var(--x) var(--y));
      }
    }
  </style>
</head>

<body onload="readly()">
  <button class="button" id="btn" href="javascript:;">切换</button>
  <div class="list" id="list">
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
  </div>
</body>

</html>

标签: none

添加新评论