注册
web

这么炫酷的换肤动画,看一眼你就会爱上

动画.gif


实现过程


我们先创建下 vue 项目


npm init vite-app vue3-vite-animation

进入文件夹中


cd vue3-vite-animation

安装下依赖


npm install

启动


npm run dev

image-20240503171537954.png


重新修改 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>


基本样式出来了,但是页面出现了滚动条,我们需要去掉原有样式


image-20240503175456039.png


src/index.css,里的所有样式都删除了,再到 index.html 中将 bodymargin 属性去掉


<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')
}

image-20240503180914393.png


按钮背景颜色、边框、字体颜色都没有改变


调整下按钮样式,把背景颜色、边框、字体颜色这些都用 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);
}

image-20240503181138545.png


这个效果不是我们想要的,需要一个过渡动画对不对


使用 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)'
})
})

动画-1714752074132-6.gif


为什么动画效果和预期的不一样


因为,默认的动画效果,把当前动画覆盖了,我们把默认动画效果去掉


/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}

动画-1714752309164-8.gif


效果出来了,但是圆的扩散不是从按钮中心扩散的


那么,通过 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;
}

动画这个圆的半径不对,导致动画到快末尾的时候,直接就结束了


动画-1714753474905-10.gif


动画的圆的半径 = 按钮中心坐标 到 对角点的坐标


可以使用三角函数计算,两短边平方 = 斜边平方


image-20240504002759638.png


// 计算展开圆的半径
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)',
})
})

动画-1714754131456-15.gif


如果是右上角呢


.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);
}

动画-1714754468881-23.gif


在右边的话,使用三角函数计算,其中一个短边就不能是 屏幕宽度 - 按钮x坐标,直接是 x 坐标就对了


那要怎么实现呢,直接取 屏幕宽度 - 按钮x坐标 与 按钮x坐标 的最大值就可以了


y 也是同理


const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)

动画-1714754788538-25.gif


你可以试试其他位置,是否也是可行的


完整代码


<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

0 个评论

要回复文章请先登录注册