自定义事件 水波纹效果vue原生
这个交互有点意思
基本逻辑
确定作用目标范围
水波纹指令需要作用在一个块级元素,元素要有宽度和高度,每当用户点击,获取用户点击坐标到块级元素的四个直角顶点的距离其中距离最远的顶点作为ripple半径画圆
确定交互事件-pointEvent
交互事件对于ripple的生成与移除,我们使用pointEvent指针事件
指针事件 - Pointer events 是一类可以为定点设备所触发的DOM事件。它们被用来创建一个可以有效掌握各类输入设备(鼠标、触控笔和单点或多点的手指触摸)的统一的DOM事件模型。 所谓指针 是指一个可以明确指向屏幕上某一组坐标的硬件设备。建立这样一个单独的事件模型可以有效的简化Web站点与应用所需的工作,同时也便于提供更加一致与良好的用户体验,无需关心不同用户和场景在输入硬件上的差异。而且,对于某些特定设备才可处理的交互情景,指针事件也定义了一个 pointerType 属性以使开发者可以知晓该事件的触发设备,以有针对性的加以处理。 也就是说本来web端我们会使用mousedown、mouseover、mouseup等来监听鼠标时间、移动端我们会用touchstart、touchmove、touchend监听触摸时间,但是使用了指针事件Pointer events 就不用这样区分了,它会自动兼容web端还是移动端的事件,也会返回pointerType属性表明触发设备。
元素生成效果
因为水波纹实际上是一个圆形,最终实现的大小会比当前点击元素大很多
原生交互效果
自定义事件封装 v-ripple
之前的实现用的over-flow: “hidden” 一种解决办法是我们给父元素添加over-flow: “hidden”,但是这样也会存在着一个问题,我们给父元素动态添加css属性属于修改了用户层面的样式,这就会导致各种隐藏bug,引起不必要的麻烦 这种解决方法,我们采用父元素与ripple之间在嵌套一层元素,作为ripple的父元素,并且对最外层的父元素也就是用户点击的元素做绝对定位就可以解决问题
什么是自定义事件
设置配置项
//自定义指令的总配置属性
interface IRippleDirectiveOptions {
/**
*
* @remarks
* Y* 你可以设置 ·currentColor· to 能够自动使用元素的文本颜色
*
* @default
* 'currentColor'
*/
color: string
/**
* 第一次出现的透明度
*
* @default
* 0.2 默认opacity 0.2
*/
initialOpacity: number
/**
* 在透明度 结束的时候 stopped 的时候 我们设置透明度的大小
*
* @default
* 0.1
*/
finalOpacity: number
/**
* 动画持续事件
*
* @default
* 400ms
*/
duration: number
/**
* css 动画 从开始到结束 以相同的时间来执行动画
*
* @default
* 'ease-out'
*/
easing: string
/**
* 取消延迟时间
*
* @note
* 类似于 debounceTime
* @default
* 75ms
*/
delay: number
/**
* 禁止 水波
*
* @note
* 类似于 点击禁止ripple效果
* @default
* false
*/
disabled: boolean
/**
* ripple展示方式
*
* @note
* ripple在center中间开始动画
* @default
* false
*/
center: boolean
}
//辅助补充配置
interface IRipplePluginOptions extends IRippleDirectiveOptions {
/**
* 用于覆盖指令的名称
* 默认指令 ripple
*/
directive: string
}
// 给可预见值 value 添加类型
interface IRippleDirectiveOptionWithBinding {
value: IRippleDirectiveOptions
}
interface HTMLElementRectType {
width: number
height: number
top: number
left: number
bottom: number
right: number
}
// 初始化配置,default_plugin_options
const DEFAULT_PLUGIN_OPTIONS: IRipplePluginOptions = {
directive: 'ripple',
color: 'currentColor', // 可选,默认当前文本颜色
initialOpacity: 0.1, //可选,初始交互效果透明度大小
finalOpacity: 0.2, //可选,结束交互效果长按透明度大小
duration: 350, // 可选,持续时间
easing: 'ease-out', //可选,缓动动画
delay: 60, // 可选,延迟 debouceTime 时间后调用
disabled: false, // 可选,禁止水波效果
center: false // 是否从中间开始
}
export {
DEFAULT_PLUGIN_OPTIONS,
IRipplePluginOptions,
HTMLElementRectType,
IRippleDirectiveOptions,
IRippleDirectiveOptionWithBinding
}
创建水波纹父组件容器
export const createContainer = ({
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius
}: CSSStyleDeclaration): HTMLElement => {
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'
// 兼容 ie 苹果
rippleContainer.style.webkitMaskImage =
'-webkit-radial-gradient(white, black)'
return rippleContainer
}
设置水波纹子组件
import { IRippleDirectiveOptions, HTMLElementRectType } from '../options'
export const createRippleElement = (
x: number,
y: number,
size: number,
options: IRippleDirectiveOptions,
rect: HTMLElementRectType
): HTMLElement => {
const rippleElement = document.createElement('div')
rippleElement.style.position = 'absolute'
rippleElement.style.width = options.center
? `${Math.sqrt(rect.width * rect.width + rect.height * rect.height)}px`
: `${size * 2}px`
// rippleElement.style.height = options.center ? `${size}px` : `${size}px`;
rippleElement.style.height = options.center
? `${Math.sqrt(rect.width * rect.width + rect.height * rect.height)}px`
: `${size * 2}px`
rippleElement.style.top = options.center ? `${rect.height / 2}px` : `${y}px`
rippleElement.style.left = options.center ? `${rect.width / 2}px` : `${x}px`
rippleElement.style.background = options.color
rippleElement.style.borderRadius = '50%'
rippleElement.style.opacity = `${options.initialOpacity}`
rippleElement.style.transform = `translate(-50%,-50%) scale(0)`
rippleElement.style.transition = `transform ${
options.duration / 1000
}s cubic-bezier(0, 0.5, 0.25, 1)
, opacity ${options.duration / 1000}s
cubic-bezier(0.0, 0, 0.2, 1)
`
return rippleElement
}
// Standard 默认效果
// cubic-bezier(0.0, 0, 0.2, 1)
获取作用源圈的半径
// 毕达哥拉斯定理
export function getPythagoreanDistance(
x1: number,
y1: number,
x2: number,
y2: number
): number {
const deltaX = x1 - x2
const deltaY = y1 - y2
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
}
interface DOMRectValue {
x: number
y: number
diameter: number
}
// 获取点击目标的位置到块级作用域直线边界的距离
export function getDistanceToFurthestCorner(
event: PointerEvent,
{ width, height, left, top }: DOMRect
): DOMRectValue {
const x = event.clientX - left
const y = event.clientY - top
const topLeft = getPythagoreanDistance(x, y, 0, 0)
const topRight = getPythagoreanDistance(x, y, width, 0)
const bottomLeft = getPythagoreanDistance(x, y, 0, height)
const bottomRight = getPythagoreanDistance(x, y, width, height)
const diameter = Math.max(topLeft, topRight, bottomLeft, bottomRight)
return {
x,
y,
diameter
}
}
配置入口
/*
* @Date: 2022-03-29 11:33:22
* @LastEditTime: 2022-09-04 10:19:15
* @Author:webkubor
*/
import {
DEFAULT_PLUGIN_OPTIONS,
IRippleDirectiveOptions,
IRippleDirectiveOptionWithBinding
} from './options'
import { getHooks } from './utils/hooks'
import { ripple } from './v-ripple'
const optionMap = new WeakMap<
HTMLElement,
Partial<IRippleDirectiveOptions> | false
>()
const globalOptions = { ...DEFAULT_PLUGIN_OPTIONS }
export default {
install: (app) => {
const hooks = getHooks(app)
app.directive('ripple', {
[hooks.mounted](
el: HTMLElement,
binding: IRippleDirectiveOptionWithBinding
) {
optionMap.set(el, binding.value ?? {})
el.addEventListener('pointerdown', (event) => {
const options = optionMap.get(el)
if (binding.value?.disabled) return
if (options === false) return
ripple(event, el, {
...globalOptions,
...options
})
})
},
[hooks.updated](
el: HTMLElement,
binding: IRippleDirectiveOptionWithBinding
) {
optionMap.set(el, binding.value ?? {})
}
})
}
}