最近、白黒切り替えアニメーションを実装したいと思いました。antfuさんのブログでその効果を見て、とても良いと思いました。ソースコードを見て、ちょうどnuxt3を使っているので、そのまま使いました。
具体的なコードの場所: https://github.com/antfu/antfu.me/blob/main/src/logics/index.ts
完全なコード
<script setup lang="ts">
const mode = useColorMode()
const isDark = computed<boolean>({
get() {
return mode.value === 'dark'
},
set() {
mode.preference = isDark.value ? 'light' : 'dark'
},
})
/**
* クレジット: [@hooray](https://github.com/hooray)
* @see https://github.com/vuejs/vitepress/pull/2347
*/
function toggle(event?: MouseEvent) {
const isAppearanceTransition = typeof document !== 'undefined'
// @ts-expect-error document.startViewTransition can be undefined
&& document.startViewTransition
&& !window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!isAppearanceTransition || !event) {
isDark.value = !isDark.value
return
}
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const transition = document.startViewTransition(async () => {
isDark.value = !isDark.value
await nextTick()
})
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.value
? clipPath.reverse()
: clipPath,
},
{
duration: 400,
easing: 'ease-out',
pseudoElement: isDark.value
? '::view-transition-old(root)'
: '::view-transition-new(root)',
},
)
})
}
const context = {
mode,
isDark,
toggle,
}
</script>
<template>
<ClientOnly placeholder-tag="span">
<slot v-bind="context" />
</ClientOnly>
</template>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 9999;
}
.dark::view-transition-old(root) {
z-index: 9999;
}
.dark::view-transition-new(root) {
z-index: 1;
}
</style>
コードの説明
mode
は useColorMode
フックを使用して現在のカラーモードを取得します。
isDark
は現在のモードがダークモードかどうかを判断し、モードを切り替える機能を提供する計算プロパティです。
toggle
関数はダークモードとライトモードを切り替えるために使用されます。
isAppearanceTransition
はビュー遷移アニメーションがサポートされているかどうか、およびユーザーがモーション効果を減らすことを選択していないかどうかを確認します。 ビュー遷移アニメーションがサポートされていない場合やイベントが渡されていない場合は、直接モードを切り替えます。 ビュー遷移アニメーションがサポートされている場合は、クリック位置と遷移アニメーションの終了半径を計算し、ビュー遷移アニメーションを開始して、アニメーション中にモードを切り替えます。
Math.hypot
は直角三角形の斜辺の長さを計算し、アニメーションがビューポート全体をカバーすることを保証します。
innerWidth
は読み取り専用プロパティで、ウィンドウのドキュメント表示領域の幅をピクセル単位で返します。スクロールバーの幅も含まれます。アニメーションの拡散半径を計算する際に、innerWidth
を使用して、トリガーポイントからビューポートの端までの最遠距離を決定します。
transition.ready.then
では、clipPath
を使用して円形の拡張アニメーション効果を実現するために、遷移アニメーションの効果を定義します。
ダークモードへの切り替え:clipPath.reverse()
は大から小へ拡張します。
ライトモードへの切り替え:clipPath
は小から大へ拡張します。
使用方法
<template>
...others
<DarkToggle v-if="isDesktop">
<template #default="{ toggle }">
<div cursor="pointer" @click="toggle">
<Icon size="20" name="material-symbols:light-mode-outline-rounded" dark="hidden" />
<Icon size="20" name="material-symbols:nightlight-outline-rounded" light="hidden" />
</div>
</template>
</DarkToggle>
...others
</template>