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

发布于:2024-05-06 ⋅ 阅读:(18) ⋅ 点赞:(0)

动画.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 来实现过渡动画效果,注意需要清除默认动画

圆点扩散效果,主要运用剪切的方式进行实现,计算过程运用了三角函数运算