跳转到主要内容
crayonxiaoxin .
ARTICLE JavaScript
6 分钟阅读

用 View Transitions 做日出日落式主题切换

Avatar photo 小鑫 4 views
用 View Transitions 做日出日落式主题切换

Tension 主题把明暗切换做成了「日出日落」式的整页帷幕:浅色从上往下收走,深色从下往上展开。文字、边框和纸面颜色跟着裁切走,而不是在内容上面盖一层黑幕。这篇文章记录从需求到落地、再到几次闪屏和按钮动效踩坑的过程。

真正要扫过的是整套配色

一开始很容易做成全屏 overlay:浅转深就从上往下拉一块黑色。问题立刻出现——幕布盖在 live 文字上面,字还是浅色的,等幕布过完才整页变色。图如果再用 mix-blend-mode: difference 假反相,封面和截图也会一起反色,和「图片保持全彩」冲突。

单棵 DOM 同一时刻只能有一套 data-lc-color-scheme,没法让上半页已经是暗色令牌、下半页还是浅色令牌。所以要扫过的不是一层颜色,而是两张已经画好的完整页面

参考 Element Plus,只换裁切形状

Element Plus 文档站的主题切换用的是同文档 document.startViewTransition:先拍切换前整页,写入新主题后再拍一张,用 clip-path 裁其中一张。他们从按钮做圆形扩散;Tension 改成全宽水平帷幕。

  • 浅 → 深:旧快照(浅色)叠在上面,inset(0) 收到 inset(100% 0 0 0),浅色从上往下消失,底下露出暗色。
  • 深 → 浅:新快照(浅色)叠在上面,从 inset(100% 0 0 0) 展到 inset(0),浅色从下往上长出来。

必须关掉 View Transitions 默认的 cross-fade 和 mix-blend-mode: plus-lighter,否则会发灰、发亮,图也会糊。站内导航已经用了根层 0.2s 淡入,主题帷幕要用 html[data-lc-theme-dir] 单独作用域,避免点太阳月亮也变成整页淡变。

javascript
html.setAttribute('data-lc-theme-dir', scheme === 'dark' ? 'to-dark' : 'to-light');
const transition = document.startViewTransition(() => {
  commitScheme(scheme); // 写入 data-lc-color-scheme、cookie、图表重绘
});
transition.finished.then(() => releaseThemeCurtain(html));
css
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

html[data-lc-theme-dir="to-dark"]::view-transition-old(root) {
  z-index: 2147483646;
  animation-name: lc-theme-wipe-down;
  animation-duration: 500ms;
  animation-timing-function: ease-in;
  animation-fill-mode: both;
}

html[data-lc-theme-dir="to-light"]::view-transition-new(root) {
  z-index: 2147483646;
  animation-name: lc-theme-wipe-up;
  animation-duration: 500ms;
  animation-timing-function: ease-in;
  animation-fill-mode: both;
}

@keyframes lc-theme-wipe-down {
  from { clip-path: inset(0); }
  to { clip-path: inset(100% 0 0 0); }
}

@keyframes lc-theme-wipe-up {
  from { clip-path: inset(100% 0 0 0); }
  to { clip-path: inset(0); }
}

startViewTransitionprefers-reduced-motion: reduce 时,走原来的 0.3s 颜色 fade。About 页 Chart.js 是画布像素,MutationObserver 来不及,要在写入配色后同步 chart.update('none'),否则新快照里图表还是旧色。

闪屏:fill-mode、卸层时序、别给裁切加透明度

Element Plus 修过一件事:animation-fill-mode: both。浅转深若只动画旧快照,没有 both,第一帧或最后一帧 clip-path 对不上,会闪回浅色。简写 animation: none 会把 fill-mode 重置掉,所以裁切动画要用长属性写 animation-fill-mode: both

更隐蔽的是收尾。过渡 finished 有时比快照层卸掉更早一帧。那一帧里如果立刻摘掉 data-lc-theme-dir,导航用的 0.2s fade 会套到还在的旧快照上:切到暗色结束闪白,切到浅色结束闪黑。多切几次更容易撞上。做法是根层默认 animation: none,等双 requestAnimationFrame 再加短延迟再清属性。

还有一次误伤:为了防闪,给浅→深的旧快照加了 opacity: 1 → 0。裁切还没到底,整张浅色快照已经在变透明,底下暗色透出来,看起来像底部提前 fade。深→浅没有改透明度,所以只有一边有问题。后来裁切只动 clip-path,透明度留给真正的淡入场景。

太阳月亮:不要挖整颗按钮

按钮希望用太阳 / 月亮,还要旋转切换。View Transitions 期间直播 DOM 是藏着的,普通 CSS transition 看不见,于是给按钮加了 view-transition-name。整颗按钮(含底和边)从根快照里挖走后,根层对不齐,收尾又开始整页闪。

延后到帷幕结束再转图标,能避开闪屏,但会慢半拍,和帷幕脱节。最后只给透明的图标图层命名,按钮底和边框仍跟着整页裁切;图标组和帷幕同时 500ms。组动画设 animation: none,避免插值出一个很大的脏矩形。

方向也要对称。浅→深:太阳顺时针转出,月亮从另一侧转入。深→浅若还用同一套关键帧,月亮会往同一边转,像转反了。要做成回转:月亮逆时针转出,太阳从白天那一侧转回来,静止姿态和 keyframes 对齐。

小结

  • 主题切换要扫「两张完整页」,不要在文字上盖幕,也不要用 blend 假反相。
  • 同文档 View Transitions + clip-path: inset() 就能做方向帷幕;叠层要和 Element Plus 一样分清裁旧还是裁新。
  • animation-fill-mode: both、卸层后再清标记、裁切不要顺带改 opacity,是去掉闪屏和提前 fade 的三件事。
  • 按钮动效不要 view-transition-name 整颗控件;只命名图标,并且浅深两向旋转要互为逆过程。
  • 减动效和无 VT 的浏览器,老老实实 fade 或瞬间切换即可。

这套动效已经打进 Tension v1.0.5。点导航栏太阳 / 月亮就能看到:墨从顶上拉下来,纸从底下托上去,字色贴着裁切走。

RELATED

相关文章

DISCUSS

讨论

DISCUSS 无留言

还没有留言,来留下第一条评论吧!

COMMENT

发表留言