这么炫酷的换肤动画,看一眼你就会爱上
实现过程
我们先创建下 vue 项目
npm init vite-app vue3-vite-animation
进入文件夹中
cd vue3-vite-animation
安装下依赖
npm install
启动
npm run dev
重新修改 App.vue
<template>
<div class="info-box">
<div class="change-theme-btn">改变主题</div>
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>
</template>
<script setup lang="ts">
</script>
<style>
.change-theme-btn {
width: 80px;
height: 40px;
background-color: #fff;
text-align: center;
line-height: 40px;
color: #282c34;
cursor: pointer;
border-radius: 8px;
border: 2px solid #282c34;
}
.info-box {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
基本样式出来了,但是页面出现了滚动条,我们需要去掉原有样式
将 src/index.css
,里的所有样式都删除了,再到 index.html
中将 body
的margin
属性去掉
<body style="margin: 0;">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
接下来,我们来实现下换肤功能
使用 css 变量,先定义下一套黑暗主题、一套白色主题
:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}
:root.dark {
--background-color: #282c34;
--color: #fff;
}
再定义点击事件 changeColor
,点击 "改变主题" 就会改变主题颜色
classList.toggle
这个方法的第一个参数是类名,第二个参数是布尔值,表示是否添加类
如果第二个参数为 true
,则添加类;如果第二个参数为 false
,则移除类
<div class="change-theme-btn" @click="changeColor">改变主题</div>
/* 改变颜色 */
const changeColor = () => {
document.documentElement.classList.toggle('dark')
}
按钮背景颜色、边框、字体颜色都没有改变
调整下按钮样式,把背景颜色、边框、字体颜色这些都用 css 变量代替
.change-theme-btn {
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}
这个效果不是我们想要的,需要一个过渡动画对不对
使用 startViewTransition
,这个 API 会生成一个屏幕截图,将新旧屏幕截图进行替换
截图分别对应两个伪元素 ::view-transition-new(root)
、::view-transition-old(root)
// 创建一个过渡对象
document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
可以看到,一个淡入淡出的效果,但是我们需要的是一个圆向外扩散的效果
用剪切效果就可以实现,其中 circle(动画进度 at 动画初始x坐标 动画初始y坐标)
设置动画时间为 1秒,作用在新的伪元素上,也即是作用在新的截图上
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
transition.ready.then(() => {
document.documentElement.animate({
clipPath: ['circle(0% at 50% 50%)', 'circle(100% at 100% 100%)']
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)'
})
})
为什么动画效果和预期的不一样
因为,默认的动画效果,把当前动画覆盖了,我们把默认动画效果去掉
/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}
效果出来了,但是圆的扩散不是从按钮中心扩散的
那么,通过 ref="btn"
来获取 “改变主题” 按钮的坐标位置
再获取按钮坐标减去宽高,就能得到按钮的中心坐标了
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<script setup>
import { ref } from 'vue';
const btn = ref<any>(null)
/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(100% at ${x}px ${y}px)`]
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>
扩展,如果,我不要从中心扩展,要从左上角开始动画呢,右上角呢...
我们把按钮放在左上角,看看效果
修改下样式、与模板
<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>
</template>
.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
动画这个圆的半径不对,导致动画到快末尾的时候,直接就结束了
动画的圆的半径 = 按钮中心坐标 到 对角点的坐标
可以使用三角函数计算,两短边平方 = 斜边平方
// 计算展开圆的半径
const tragetRadius = Math.hypot(
window.innerWidth - x,
innerHeight - y
)
// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})
如果是右上角呢
.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}
在右边的话,使用三角函数计算,其中一个短边就不能是 屏幕宽度 - 按钮x坐标,直接是 x 坐标就对了
那要怎么实现呢,直接取 屏幕宽度 - 按钮x坐标 与 按钮x坐标 的最大值就可以了
y 也是同理
const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)
你可以试试其他位置,是否也是可行的
完整代码
<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const btn = ref<any>(null)
/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标
// 计算展开圆的半径
const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)
// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>
<style>
:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}
:root.dark {
--background-color: #282c34;
--color: #fff;
}
/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}
.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}
.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
小结
换肤功能,主要靠 css 变量 与 classList.toggle
startViewTransition
这个 API 来实现过渡动画效果,注意需要清除默认动画
圆点扩散效果,主要运用剪切的方式进行实现,计算过程运用了三角函数运算
来源:juejin.cn/post/7363836438935552035