介绍
实现一个移动端app的水波效果
# 水波的效果
当用户点击时,会以点击中心为圆心产生一个水波扩散的涟漪效果,适用各个场景,美观又不浮夸,关键是可以给用户带来很直观的点击反馈。'
查看水波效果
实现思路
- 首先前端要实现这种效果的通常为矩形, 那么水波效果实际上是一个圆, 通过js获取到矩形的宽高, 然后把宽高看成一个三角形, 再通过勾股定理计算出三角形的斜边, 把这个斜边再当成圆的直径, 这样就能计算出属于自己矩形尺寸的一个圆
- 绘制出合适大小的圆后, 我们还需要对这个圆进行偏移/平移
translate
操作, 把鼠标点击的位置, 作为圆的圆心, 绘制完毕后的圆, 其圆心需要偏移到中心位置, 实现一个水波扩张波动的效果
- 通过以上的步骤, 我们就实现了一个视觉上的水波扩张的效果(其实是个圆), 最后我们不需要溢出的效果, 那么这个时候给元素设置为
overflow: hidden
这样就可以只显示元素内的水波效果啦
优化提示
will-change (opens new window) 这个css属性可以提前告诉浏览器哪些属性需要调用资源, 从而提升浏览器渲染性能, 特别适合非默认状态下的css中, 常用于translate
偏移(尤其是3D)和opacity
显示隐藏,
使用方式: 通过js的mousedown
监听后通过classList.add()
添加其class类, 使用完毕后通过classList.remove()
删掉该class类, 或者直接设置style.willChange = 'auto'
的方式取消will-change
属性
- will-change属性不建议作为默认的css状态, 最好的使用方式是用时设置, 用完重置(值为
auto
), 或者直接删除掉
# 实现代码
- 第一步, 先写一个css样式, 作为生成水波(圆)的样式, 这里使用贝塞尔曲线定制,不清楚如何调试动画曲线的可以看这一篇文章 (opens new window)
- 水波效果的css可以作为全局css来使用, 在
main.ts
中直接导入
- 水波效果的css可以作为全局css来使用, 在
.my-ripple {
position: absolute;
top: 0;
left: 0;
z-index: 100;
border-radius: 50%;
background-color: currentColor;
opacity: 0;
transition: transform 0.2s cubic-bezier(0.68, 0.01, 0.62, 0.6), opacity 0.08s linear;
will-change: transform, opacity;
pointer-events: none;
}
通过上表面水波实现的原理, 先计算出水波的参数
动画结束时的水波的尺寸
=
圆的斜边创建时的(x,y)
=
用户点击的位置过度动画结束时的(x,y)
=
元素(0,0)点创建的水波进行x和y的偏移得到
// 计算出水波的参数
const computeRippleStyles = (el: HTMLElement, event: MouseEvent) => {
const { top, left } = el.getBoundingClientRect()
const { clientWidth, clientHeight } = el
// 计算出水波的半径
const radius = Math.sqrt(clientWidth ** 2 + clientHeight ** 2) / 2
// 计算出水波的大小
const size = radius * 2
// 计算出水波的位置
const localX = event.clientX - left
const localY = event.clientY - top
// 计算出水波的中心点
const centerX = (clientWidth - radius * 2) / 2
const centerY = (clientHeight - radius * 2) / 2
// 计算出水波的偏移量
const x = localX - radius
const y = localY - radius
return { x, y, centerX, centerY, size }
}
- 鼠标按下时创建水波
- 然后我们需要在鼠标按下时创建水波,监听鼠标按下的事件,这里以pc端为例子,刚创建水波时使用
transform
缩小到0.3
,这是作者尝试过相对合适的创建大小, 然后修改transform触发过度水波扩散动画,这里还加入了透明度的过度,可以使水波涟漪更有质感。
- 然后我们需要在鼠标按下时创建水波,监听鼠标按下的事件,这里以pc端为例子,刚创建水波时使用
// 创建水波
const createRipple = () => {
const { x, y, centerX, centerY, size } = computeRippleStyles(el, event)
const ripple = document.createElement('div')
ripple.classList.add('ripple-water-global')
ripple.style.opacity = '0'
ripple.style.transform = `translate(${x}px, ${y}px) scale3d(.3, .3, .3)`
ripple.style.width = `${size}px`
ripple.style.height = `${size}px`
ripple.dataset.createdAt = performance.now() + ''
const { position } = window.getComputedStyle(el)
el.style.overflow = 'hidden'
if (position === 'static') {
el.style.position = 'relative'
}
el.appendChild(ripple)
window.setTimeout(() => {
ripple.style.transform = `translate(${centerX}px, ${centerY}px) scale3d(1, 1, 1)`
ripple.style.opacity = '.25'
}, 0)
}
- 鼠标抬起时销毁水波
- 当鼠标抬起时,只需要找到这个生成的水波节点修改透明度,再等到透明度修改动画结束之后将水波纹节点移除即可
const removeRipple = () => {
const ripples = el.querySelectorAll('.ripple-water-global')
if (!ripples.length) {
return
}
const lastRipple = ripples[ripples.length - 1]
// 通过水波的创建时间计算出扩散动画还需要执行多久,确保每一个水波都完整的执行了扩散动画
const delay =
300 -
performance.now() +
Number((lastRipple as HTMLElement).dataset.createdAt)
setTimeout(() => {
;(lastRipple as HTMLElement).style.opacity = `0`
setTimeout(() => lastRipple.parentNode?.removeChild(lastRipple), 300)
}, delay)
}
# Vue3全局自定义水波指令
实现的框架是Vue3+TS, 通过Vue3的自定义全局指令 (opens new window), 直接给需要的元素添加指令即可, 非常的方便
- 水波效果的css可以作为全局css来使用, 在
main.ts
中直接导入
- 自定义一个Vue3全局自定义水波指令
export const waterClick = {
mounted(el: HTMLElement) {
el.addEventListener('mousedown', (event) => {
// 计算出水波的参数
const computeRippleStyles = (el: HTMLElement, event: MouseEvent) => {
const { top, left } = el.getBoundingClientRect()
const { clientWidth, clientHeight } = el
// 计算出水波的半径
const radius = Math.sqrt(clientWidth ** 2 + clientHeight ** 2) / 2
// 计算出水波的大小
const size = radius * 2
// 计算出水波的位置
const localX = event.clientX - left
const localY = event.clientY - top
// 计算出水波的中心点
const centerX = (clientWidth - radius * 2) / 2
const centerY = (clientHeight - radius * 2) / 2
// 计算出水波的偏移量
const x = localX - radius
const y = localY - radius
return { x, y, centerX, centerY, size }
}
// 创建水波
const createRipple = () => {
const { x, y, centerX, centerY, size } = computeRippleStyles(el, event)
const ripple = document.createElement('div')
ripple.classList.add('ripple-water-global')
ripple.style.opacity = '0'
ripple.style.transform = `translate(${x}px, ${y}px) scale3d(.3, .3, .3)`
ripple.style.width = `${size}px`
ripple.style.height = `${size}px`
ripple.dataset.createdAt = performance.now() + ''
const { position } = window.getComputedStyle(el)
el.style.overflow = 'hidden'
if (position === 'static') {
el.style.position = 'relative'
}
el.appendChild(ripple)
window.setTimeout(() => {
ripple.style.transform = `translate(${centerX}px, ${centerY}px) scale3d(1, 1, 1)`
ripple.style.opacity = '.25'
}, 0)
//执行完毕后移除水波
removeRipple()
}
const removeRipple = () => {
const ripples = el.querySelectorAll('.ripple-water-global')
if (!ripples.length) {
return
}
const lastRipple = ripples[ripples.length - 1]
// 通过水波的创建时间计算出扩散动画还需要执行多久,确保每一个水波都完整的执行了扩散动画
const delay =
300 -
performance.now() +
Number((lastRipple as HTMLElement).dataset.createdAt)
setTimeout(() => {
;(lastRipple as HTMLElement).style.opacity = `0`
setTimeout(() => lastRipple.parentNode?.removeChild(lastRipple), 300)
}, delay)
}
// 鼠标按下后, 执行水波
createRipple()
})
}
}
- 在main.ts入口文件中导入水波效果的css, 并注册全局自定义水波指令
- 自定义指令的名称为
v-指令名
- 自定义指令的名称为
// main.ts
// 导入Vue实例创建
import { createApp } from 'vue'
// 导入App.vue
import App from './App.vue'
// 导入vue全局样式(水波css)
import './styles/global.scss'
// 导入自定义指令
import { waterClick } from './directives'
// 创建Vue实例
const app = createApp(App)
// 注册全局自定义水波指令
app.directive(water, waterClick)
- 在Vue中使用
<div v-water />