需求
在div或者button中,触发点击时,出现水波纹效果
思路
当鼠标点击时,触发从当前点向外层最远距离发散一个圆,圆的半径达到最远距离后消失。最远距离是从点击处到达矩形角的距离。
代码实现1
解题
当鼠标点击时,新建一个当前元素的内部span元素,此元素是绝对定位,初始宽高为0,整体是一个圆形。从点击的那一刻开始,宽高不断增加,而透明度不断减小,当此元素的半径大于最远距离时,此元素消失。
代码
<template>
<div class="content">
<div class="ripple-content" v-ripple="{color: color, duration: duration}"></div>
</div>
</template>
<script>
export default {
name: "ripple-page",
data() {
return {
color: '#00ff00',
duration: 700
}
}
}
</script>
export default {
inserted: (el, binding) => {
el.addEventListener('pointerdown', event => {
// 设置外层元素相对定位且隐藏多余部分
el.style.position = 'relative';
el.style.overflow = 'hidden';
const rect = el.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const rectHeight = rect.height;
const rectWidth = rect.width;
//在鼠标位置增加一个span标签,将此标签插入当前元素内部
let span = document.createElement("span")
span.style.position = "absolute"
span.style.background = binding.value.color || '#5e7ce0'
span.style.borderRadius = '50%'
el.append(span)
// 初始化元素的宽、高、透明度
let width = 0;
let height = 0;
let opacity = 1;
let diameter = getMaxRadius(x, y, rectWidth, rectHeight) * 2;
// 通过定时器不断增大宽高,减小透明度
let time = setInterval(() => {
width += 5;
height += 5;
opacity -= 0.01;
//判断超出最大值时,清除定时,并且删除span
if (width < diameter) {
span.style.width = width + 'px'
span.style.height = height + 'px'
span.style.opacity = opacity;
span.style.left = x - span.offsetWidth / 2 + 'px'
span.style.top = y - span.offsetHeight / 2 + 'px'
} else {
clearInterval(time)
time = null;
span.remove()
}
}, binding.value.duration / 100 || 5)
})
}
}
/**
* 计算当前点到达角的距离
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* */
function getDistance(x1, y1, x2, y2) {
const deltaX = x1 - x2;
const deltaY = y1 - y2;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
/**
* 计算当前最大的半径
* @param {Number} x 点击处到外层元素左上角的横向距离
* @param {Number} y 点击处到外层元素左上角的纵向距离
* @param {Number} width 外层元素的宽度
* @param {Number} height 外层元素的高度
* */
function getMaxRadius(x, y, width, height) {
let topLeft = getDistance(x, y, 0, 0);
let topRight = getDistance(x, y, width, 0);
let bottomLeft = getDistance(x, y, 0, height);
let bottomRight = getDistance(x, y, width, height);
let radius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
return radius;
}
效果
问题
- 此种方法生成出来的水波纹显得比较生硬。
- 连续点击时,中心区域是一个实质性的点,显得不美观。
- 在最外层控制了用户自己的元素定位方式,可能出现其他问题。
- 通过定时器调整存在一定程度的性能浪费。
代码实现2
解题
此方法源自于 devui 框架方案,写法参考于此篇文章 《Ripple:这个广受好评的水波纹组件,你不打算了解下怎么实现的吗?》
ps: 原版使用的 ts ,虽然兼容 vue2 ,但是我没详细用,这里是大致模仿的效果。
在当前元素的内部复制一个样式相同的元素,同时创建一个水波纹元素插入此元素中,形成三级嵌套的DOM结构。设置水波纹元素的 translate 为 -50% -50% scale(0),让此元素存在但无法显示出来。点击外层元素时,将水波纹元素设置为translate:scale(1),通过 transform 设置缓慢显示出来,达到水波效果。
代码
export default {
inserted: (el, binding) => {
el.addEventListener('pointerdown', event => {
let rect = el.getBoundingClientRect();
let rectWidth = rect.width,
rectHeight = rect.height,
x = event.clientX - rect.left,
y = event.clientY - rect.top;
let radius = getMaxRadius(x, y, rectWidth, rectHeight);
// 复制一个外层元素
const computedStyles = window.getComputedStyle(el);
const {
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius
} = computedStyles;
const rippleContainer = document.createElement('div');
rippleContainer.style.top = '0';
rippleContainer.style.left = '0';
rippleContainer.style.width = '100%';
rippleContainer.style.height = '100%';
rippleContainer.style.position = 'absolute';
rippleContainer.style.borderRadius =
`${borderTopLeftRadius} ${borderTopRightRadius} ${borderBottomRightRadius} ${borderBottomLeftRadius}`;
rippleContainer.style.overflow = 'hidden';
rippleContainer.style.pointerEvents = 'none';
// 创建一个内部水波纹元素
const rippleElement = document.createElement('div');
rippleElement.style.position = 'absolute';
rippleElement.style.width = `${radius * 2}px`;
rippleElement.style.height = `${radius * 2}px`;
rippleElement.style.top = `${y}px`;
rippleElement.style.left =`${x}px`;
rippleElement.style.background = binding.value.color || '#5e7ce0';
rippleElement.style.borderRadius = '50%';
rippleElement.style.opacity = 0.1;
rippleElement.style.transform = `translate(-50%,-50%) scale(0)`;
rippleElement.style.transition = `transform ${binding.value.duration / 1000}s cubic-bezier(0, 0.5, 0.25, 1), opacity ${binding.value.duration / 1000}s cubic-bezier(0.0, 0, 0.2, 1)`;
// 将元素组合插入最外层元素内
rippleContainer.append(rippleElement);
el.append(rippleContainer);
setTimeout(()=>{
rippleElement.style.transform = 'translate(-50%,-50%) scale(1)';
rippleElement.style.opacity = 0.2;
setTimeout(()=>{
rippleElement.style.transition = 'opacity 120ms ease in out';
rippleElement.style.opacity = '0';
setTimeout(()=>{
rippleContainer.remove();
}, 120)
}, 700)
}, 0)
})
}
}
/**
* 计算当前点到达角的距离
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* */
function getDistance(x1, y1, x2, y2) {
const deltaX = x1 - x2;
const deltaY = y1 - y2;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
/**
* 计算当前最大的半径
* @param {Number} x 点击处到外层元素左上角的横向距离
* @param {Number} y 点击处到外层元素左上角的纵向距离
* @param {Number} width 外层元素的宽度
* @param {Number} height 外层元素的高度
* */
function getMaxRadius(x, y, width, height) {
let topLeft = getDistance(x, y, 0, 0);
let topRight = getDistance(x, y, width, 0);
let bottomLeft = getDistance(x, y, 0, height);
let bottomRight = getDistance(x, y, width, height);
let radius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
return radius;
}
效果
问题
- 此方法需要创建多重DOM,效率不高
- 感觉改了贝塞尔曲线以后效果也并没有太好
- 使用了三重定时器,效率不高
总结
在绑定点击事件时,可以看到并未绑定 click 事件,而是绑定了 pointerdown 方法,原因是因为这个方法对于各种硬件设备的适配更好,可以有效响应鼠标点击,手指点击,触控笔等各类效果。
研究了大半天这个效果, devui 这个框架里面的 v-ripple 这个效果其实写的很好,但是ts代码我现在看的还是有点儿云里雾里,回头再看吧。