최근에 흑백 전환 애니메이션을 구현하고 싶었습니다. 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>