@luanlu/mk-motion 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/package.json +14 -2
  2. package/src/a11y/focus-trap.ts +64 -0
  3. package/src/a11y/keyboard.ts +43 -0
  4. package/src/components/alert/alert.css +111 -0
  5. package/src/components/alert/alert.ts +107 -0
  6. package/src/components/avatar/avatar.css +112 -0
  7. package/src/components/avatar/avatar.ts +175 -0
  8. package/src/components/breadcrumb/breadcrumb.css +31 -0
  9. package/src/components/breadcrumb/breadcrumb.ts +71 -0
  10. package/src/components/button/button.css +108 -0
  11. package/src/components/button/button.ts +140 -0
  12. package/src/components/card/card.css +52 -0
  13. package/src/components/card/card.ts +87 -0
  14. package/src/components/collapse/collapse.css +76 -0
  15. package/src/components/collapse/collapse.ts +168 -0
  16. package/src/components/dialog/dialog.css +78 -0
  17. package/src/components/dialog/dialog.ts +164 -0
  18. package/src/components/drawer/drawer.css +73 -0
  19. package/src/components/drawer/drawer.ts +131 -0
  20. package/src/components/empty/empty.css +36 -0
  21. package/src/components/empty/empty.ts +85 -0
  22. package/src/components/form/checkbox.css +56 -0
  23. package/src/components/form/checkbox.ts +119 -0
  24. package/src/components/form/radio.css +57 -0
  25. package/src/components/form/radio.ts +153 -0
  26. package/src/components/form/select.css +91 -0
  27. package/src/components/form/select.ts +174 -0
  28. package/src/components/form/slider.css +56 -0
  29. package/src/components/form/slider.ts +148 -0
  30. package/src/components/input/input.css +92 -0
  31. package/src/components/input/input.ts +162 -0
  32. package/src/components/layout/divider.css +32 -0
  33. package/src/components/layout/divider.ts +42 -0
  34. package/src/components/layout/row.css +64 -0
  35. package/src/components/layout/row.ts +57 -0
  36. package/src/components/layout/space.css +14 -0
  37. package/src/components/layout/space.ts +48 -0
  38. package/src/components/loading/loading.css +37 -0
  39. package/src/components/loading/loading.ts +46 -0
  40. package/src/components/menu/menu.css +121 -0
  41. package/src/components/menu/menu.ts +187 -0
  42. package/src/components/message/message.css +64 -0
  43. package/src/components/message/message.ts +96 -0
  44. package/src/components/popover/popover.css +73 -0
  45. package/src/components/popover/popover.ts +279 -0
  46. package/src/components/progress/progress.css +112 -0
  47. package/src/components/progress/progress.ts +171 -0
  48. package/src/components/steps/steps.css +127 -0
  49. package/src/components/steps/steps.ts +102 -0
  50. package/src/components/styles/components.css +28 -0
  51. package/src/components/styles/reset.css +24 -0
  52. package/src/components/styles/tokens.css +248 -0
  53. package/src/components/styles/variables.css +24 -0
  54. package/src/components/switch/switch.css +53 -0
  55. package/src/components/switch/switch.ts +103 -0
  56. package/src/components/table/table.css +192 -0
  57. package/src/components/table/table.ts +370 -0
  58. package/src/components/tabs/tabs.css +138 -0
  59. package/src/components/tabs/tabs.ts +211 -0
  60. package/src/components/tag/tag.css +123 -0
  61. package/src/components/tag/tag.ts +112 -0
  62. package/src/components/tooltip/tooltip.css +66 -0
  63. package/src/components/tooltip/tooltip.ts +185 -0
  64. package/src/core/animator.ts +124 -0
  65. package/src/core/timeline.ts +128 -0
  66. package/src/core/utils.ts +47 -0
  67. package/src/effects/glitch.ts +99 -0
  68. package/src/effects/particle.ts +134 -0
  69. package/src/effects/text-split.ts +95 -0
  70. package/src/effects/wave-text.ts +88 -0
  71. package/src/gesture/draggable.ts +130 -0
  72. package/src/gesture/spring.ts +152 -0
  73. package/src/index.ts +162 -0
  74. package/src/interactive/coverflow.ts +100 -0
  75. package/src/interactive/cursor-trail.ts +113 -0
  76. package/src/interactive/flip-card.ts +114 -0
  77. package/src/interactive/magnetic.ts +121 -0
  78. package/src/micro/hover-lift.ts +94 -0
  79. package/src/micro/ripple.ts +130 -0
  80. package/src/motion/component-motion.ts +177 -0
  81. package/src/nuxt/module.ts +46 -0
  82. package/src/presets/index.ts +69 -0
  83. package/src/scroll/scroll-trigger.ts +104 -0
  84. package/src/styles/animations.css +135 -0
  85. package/src/styles/element-plus.css +174 -0
  86. package/src/text/count-up.ts +108 -0
  87. package/src/text/typewriter.ts +109 -0
  88. package/src/theme/dark.css +19 -0
  89. package/src/theme/light.css +19 -0
  90. package/src/theme/theme.ts +65 -0
  91. package/src/transitions/blur-reveal.ts +92 -0
  92. package/src/transitions/collapse.ts +112 -0
  93. package/src/transitions/lazy-image.ts +87 -0
  94. package/src/transitions/list.ts +75 -0
  95. package/src/transitions/loading.ts +95 -0
  96. package/src/transitions/parallax.ts +60 -0
  97. package/src/transitions/shimmer.ts +105 -0
  98. package/src/transitions/toast.ts +151 -0
  99. package/src/types.d.ts +4 -0
  100. package/src/vite/plugin.ts +45 -0
  101. package/src/vue/button.ts +28 -9
  102. package/src/vue/card.ts +28 -8
  103. package/src/vue/composables/index.ts +4 -0
  104. package/src/vue/composables/useLoading.ts +12 -0
  105. package/src/vue/composables/useMessage.ts +16 -0
  106. package/src/vue/composables/useMotion.ts +19 -0
  107. package/src/vue/composables/useTheme.ts +12 -0
  108. package/src/vue/dialog.ts +69 -17
  109. package/src/vue/index.ts +4 -21
  110. package/src/vue/input.ts +35 -11
  111. package/src/vue/slider.ts +22 -4
  112. package/src/vue/switch.ts +16 -9
  113. package/src/vue/alert.ts +0 -32
  114. package/src/vue/avatar.ts +0 -34
  115. package/src/vue/breadcrumb.ts +0 -32
  116. package/src/vue/checkbox.ts +0 -32
  117. package/src/vue/collapse.ts +0 -33
  118. package/src/vue/divider.ts +0 -32
  119. package/src/vue/drawer.ts +0 -33
  120. package/src/vue/empty.ts +0 -33
  121. package/src/vue/menu.ts +0 -33
  122. package/src/vue/popover.ts +0 -34
  123. package/src/vue/progress.ts +0 -33
  124. package/src/vue/row.ts +0 -32
  125. package/src/vue/select.ts +0 -33
  126. package/src/vue/space.ts +0 -32
  127. package/src/vue/steps.ts +0 -33
  128. package/src/vue/table.ts +0 -33
  129. package/src/vue/tabs.ts +0 -33
  130. package/src/vue/tag.ts +0 -33
  131. package/src/vue/tooltip.ts +0 -34
@@ -0,0 +1,113 @@
1
+ export interface CursorTrailOptions {
2
+ count?: number // 粒子数量
3
+ color?: string
4
+ size?: number
5
+ fadeSpeed?: number // 淡出速度
6
+ smoothing?: number // 平滑度 0~1
7
+ }
8
+
9
+ const DEFAULT_TRAIL: Required<CursorTrailOptions> = {
10
+ count: 15,
11
+ color: '#38bdf8',
12
+ size: 8,
13
+ fadeSpeed: 0.92,
14
+ smoothing: 0.15,
15
+ }
16
+
17
+ interface TrailDot {
18
+ el: HTMLElement
19
+ x: number
20
+ y: number
21
+ targetX: number
22
+ targetY: number
23
+ opacity: number
24
+ }
25
+
26
+ /**
27
+ * 鼠标跟随粒子拖尾
28
+ */
29
+ export function cursorTrail(options: CursorTrailOptions = {}): () => void {
30
+ const opts = { ...DEFAULT_TRAIL, ...options }
31
+
32
+ const dots: TrailDot[] = []
33
+ const container = document.createElement('div')
34
+ container.style.cssText = `
35
+ position: fixed;
36
+ inset: 0;
37
+ pointer-events: none;
38
+ z-index: 9998;
39
+ overflow: hidden;
40
+ `
41
+ document.body.appendChild(container)
42
+
43
+ for (let i = 0; i < opts.count; i++) {
44
+ const el = document.createElement('div')
45
+ el.style.cssText = `
46
+ position: absolute;
47
+ width: ${opts.size}px;
48
+ height: ${opts.size}px;
49
+ border-radius: 50%;
50
+ background: ${opts.color};
51
+ opacity: 0;
52
+ pointer-events: none;
53
+ `
54
+ container.appendChild(el)
55
+ dots.push({
56
+ el,
57
+ x: -100,
58
+ y: -100,
59
+ targetX: -100,
60
+ targetY: -100,
61
+ opacity: 0,
62
+ })
63
+ }
64
+
65
+ let mouseX = -100
66
+ let mouseY = -100
67
+ let running = true
68
+
69
+ const onMove = (e: MouseEvent) => {
70
+ mouseX = e.clientX
71
+ mouseY = e.clientY
72
+ }
73
+
74
+ document.addEventListener('mousemove', onMove)
75
+
76
+ const tick = () => {
77
+ if (!running) return
78
+
79
+ dots.forEach((dot, i) => {
80
+ if (i === 0) {
81
+ dot.targetX = mouseX
82
+ dot.targetY = mouseY
83
+ } else {
84
+ dot.targetX = dots[i - 1].x
85
+ dot.targetY = dots[i - 1].y
86
+ }
87
+
88
+ dot.x += (dot.targetX - dot.x) * opts.smoothing
89
+ dot.y += (dot.targetY - dot.y) * opts.smoothing
90
+
91
+ if (mouseX > 0) {
92
+ dot.opacity = Math.min(1, dot.opacity + 0.1)
93
+ }
94
+ dot.opacity *= opts.fadeSpeed
95
+
96
+ const size = opts.size * (1 - i / opts.count * 0.6)
97
+ dot.el.style.transform = `translate(${dot.x - size / 2}px, ${dot.y - size / 2}px)`
98
+ dot.el.style.width = `${size}px`
99
+ dot.el.style.height = `${size}px`
100
+ dot.el.style.opacity = String(dot.opacity)
101
+ })
102
+
103
+ requestAnimationFrame(tick)
104
+ }
105
+
106
+ requestAnimationFrame(tick)
107
+
108
+ return () => {
109
+ running = false
110
+ document.removeEventListener('mousemove', onMove)
111
+ container.remove()
112
+ }
113
+ }
@@ -0,0 +1,114 @@
1
+ export interface FlipCardOptions {
2
+ axis?: 'x' | 'y' // 翻转轴
3
+ duration?: number // 翻转时长
4
+ perspective?: number // 透视距离
5
+ }
6
+
7
+ const DEFAULT_FLIP: Required<FlipCardOptions> = {
8
+ axis: 'y',
9
+ duration: 600,
10
+ perspective: 1000,
11
+ }
12
+
13
+ export class FlipCard {
14
+ private container: HTMLElement
15
+ private inner: HTMLElement
16
+ private front: HTMLElement
17
+ private back: HTMLElement
18
+ private flipped = false
19
+ private options: Required<FlipCardOptions>
20
+
21
+ constructor(
22
+ container: HTMLElement | string,
23
+ options: FlipCardOptions = {}
24
+ ) {
25
+ this.container =
26
+ typeof container === 'string'
27
+ ? document.querySelector<HTMLElement>(container)!
28
+ : container
29
+
30
+ if (!this.container) {
31
+ throw new Error('FlipCard: container not found')
32
+ }
33
+
34
+ this.options = { ...DEFAULT_FLIP, ...options }
35
+
36
+ // 自动创建内部结构
37
+ this.container.style.perspective = `${this.options.perspective}px`
38
+ this.container.style.cursor = 'pointer'
39
+
40
+ this.inner = document.createElement('div')
41
+ this.inner.style.cssText = `
42
+ position: relative;
43
+ width: 100%;
44
+ height: 100%;
45
+ transform-style: preserve-3d;
46
+ transition: transform ${this.options.duration}ms cubic-bezier(0.4, 0, 0.2, 1);
47
+ `
48
+
49
+ // 查找 front 和 back
50
+ this.front = this.container.querySelector('[data-flip="front"]') as HTMLElement
51
+ this.back = this.container.querySelector('[data-flip="back"]') as HTMLElement
52
+
53
+ if (!this.front || !this.back) {
54
+ throw new Error('FlipCard: 需要包含 data-flip="front" 和 data-flip="back" 的子元素')
55
+ }
56
+
57
+ const common = `
58
+ position: absolute;
59
+ inset: 0;
60
+ backface-visibility: hidden;
61
+ -webkit-backface-visibility: hidden;
62
+ `
63
+ this.front.style.cssText += common
64
+ this.back.style.cssText += common
65
+
66
+ if (this.options.axis === 'y') {
67
+ this.back.style.transform = 'rotateY(180deg)'
68
+ } else {
69
+ this.back.style.transform = 'rotateX(180deg)'
70
+ }
71
+
72
+ this.inner.appendChild(this.front)
73
+ this.inner.appendChild(this.back)
74
+ this.container.innerHTML = ''
75
+ this.container.appendChild(this.inner)
76
+
77
+ this.container.addEventListener('click', () => this.toggle())
78
+ }
79
+
80
+ toggle(): this {
81
+ this.flipped = !this.flipped
82
+ const rotate = this.flipped ? 180 : 0
83
+ if (this.options.axis === 'y') {
84
+ this.inner.style.transform = `rotateY(${rotate}deg)`
85
+ } else {
86
+ this.inner.style.transform = `rotateX(${rotate}deg)`
87
+ }
88
+ return this
89
+ }
90
+
91
+ flipToFront(): this {
92
+ this.flipped = false
93
+ this.inner.style.transform = ''
94
+ return this
95
+ }
96
+
97
+ flipToBack(): this {
98
+ this.flipped = true
99
+ const rotate = 180
100
+ if (this.options.axis === 'y') {
101
+ this.inner.style.transform = `rotateY(${rotate}deg)`
102
+ } else {
103
+ this.inner.style.transform = `rotateX(${rotate}deg)`
104
+ }
105
+ return this
106
+ }
107
+ }
108
+
109
+ export function createFlipCard(
110
+ container: HTMLElement | string,
111
+ options?: FlipCardOptions
112
+ ): FlipCard {
113
+ return new FlipCard(container, options)
114
+ }
@@ -0,0 +1,121 @@
1
+ export interface MagneticOptions {
2
+ strength?: number // 磁力强度 0~1
3
+ radius?: number // 作用半径像素
4
+ }
5
+
6
+ const DEFAULT_MAGNETIC: Required<MagneticOptions> = {
7
+ strength: 0.4,
8
+ radius: 150,
9
+ }
10
+
11
+ /**
12
+ * 磁性按钮:鼠标靠近时被吸引
13
+ */
14
+ export function magnetic(
15
+ element: HTMLElement | string,
16
+ options: MagneticOptions = {}
17
+ ): () => void {
18
+ const el =
19
+ typeof element === 'string'
20
+ ? document.querySelector<HTMLElement>(element)!
21
+ : element
22
+
23
+ if (!el) throw new Error('magnetic: element not found')
24
+
25
+ const opts = { ...DEFAULT_MAGNETIC, ...options }
26
+ el.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1)'
27
+
28
+ const onMove = (e: MouseEvent) => {
29
+ const rect = el.getBoundingClientRect()
30
+ const centerX = rect.left + rect.width / 2
31
+ const centerY = rect.top + rect.height / 2
32
+
33
+ const distX = e.clientX - centerX
34
+ const distY = e.clientY - centerY
35
+ const distance = Math.sqrt(distX * distX + distY * distY)
36
+
37
+ if (distance < opts.radius) {
38
+ const force = 1 - distance / opts.radius
39
+ const moveX = distX * opts.strength * force
40
+ const moveY = distY * opts.strength * force
41
+ el.style.transform = `translate(${moveX}px, ${moveY}px)`
42
+ } else {
43
+ el.style.transform = ''
44
+ }
45
+ }
46
+
47
+ const onLeave = () => {
48
+ el.style.transform = ''
49
+ }
50
+
51
+ document.addEventListener('mousemove', onMove)
52
+ el.addEventListener('mouseleave', onLeave)
53
+
54
+ return () => {
55
+ document.removeEventListener('mousemove', onMove)
56
+ el.removeEventListener('mouseleave', onLeave)
57
+ el.style.transform = ''
58
+ el.style.transition = ''
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 磁性文字:每个字符独立被吸引
64
+ */
65
+ export function magneticText(
66
+ element: HTMLElement | string,
67
+ options: MagneticOptions = {}
68
+ ): () => void {
69
+ const el =
70
+ typeof element === 'string'
71
+ ? document.querySelector<HTMLElement>(element)!
72
+ : element
73
+
74
+ if (!el) throw new Error('magneticText: element not found')
75
+
76
+ const text = el.textContent ?? ''
77
+ el.innerHTML = ''
78
+ el.style.display = 'inline-block'
79
+
80
+ const chars: HTMLElement[] = []
81
+ text.split('').forEach((char) => {
82
+ const span = document.createElement('span')
83
+ span.textContent = char === ' ' ? '\u00A0' : char
84
+ span.style.display = 'inline-block'
85
+ span.style.transition = 'transform 0.3s ease'
86
+ el.appendChild(span)
87
+ chars.push(span)
88
+ })
89
+
90
+ const opts = { ...DEFAULT_MAGNETIC, ...options }
91
+
92
+ const onMove = (e: MouseEvent) => {
93
+ chars.forEach((span) => {
94
+ const rect = span.getBoundingClientRect()
95
+ const cx = rect.left + rect.width / 2
96
+ const cy = rect.top + rect.height / 2
97
+ const dx = e.clientX - cx
98
+ const dy = e.clientY - cy
99
+ const dist = Math.sqrt(dx * dx + dy * dy)
100
+
101
+ if (dist < opts.radius) {
102
+ const force = 1 - dist / opts.radius
103
+ span.style.transform = `translate(${dx * opts.strength * force}px, ${dy * opts.strength * force}px)`
104
+ } else {
105
+ span.style.transform = ''
106
+ }
107
+ })
108
+ }
109
+
110
+ const onLeave = () => {
111
+ chars.forEach((span) => (span.style.transform = ''))
112
+ }
113
+
114
+ document.addEventListener('mousemove', onMove)
115
+ el.addEventListener('mouseleave', onLeave)
116
+
117
+ return () => {
118
+ document.removeEventListener('mousemove', onMove)
119
+ el.removeEventListener('mouseleave', onLeave)
120
+ }
121
+ }
@@ -0,0 +1,94 @@
1
+ export interface HoverLiftOptions {
2
+ y?: number // 悬浮时向上移动像素
3
+ scale?: number // 悬浮时放大比例
4
+ shadow?: string // 悬浮时阴影
5
+ duration?: number // 过渡时长毫秒
6
+ easing?: string // 缓动
7
+ }
8
+
9
+ const DEFAULT_HOVER: Required<HoverLiftOptions> = {
10
+ y: -6,
11
+ scale: 1.02,
12
+ shadow: '0 12px 24px rgba(0,0,0,0.15)',
13
+ duration: 250,
14
+ easing: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
15
+ }
16
+
17
+ /**
18
+ * 给元素添加悬浮抬升效果
19
+ */
20
+ export function hoverLift(
21
+ element: HTMLElement | string,
22
+ options: HoverLiftOptions = {}
23
+ ): () => void {
24
+ const el =
25
+ typeof element === 'string'
26
+ ? document.querySelector<HTMLElement>(element)!
27
+ : element
28
+
29
+ if (!el) throw new Error('hoverLift: element not found')
30
+
31
+ const opts = { ...DEFAULT_HOVER, ...options }
32
+
33
+ el.style.transition = `transform ${opts.duration}ms ${opts.easing}, box-shadow ${opts.duration}ms ${opts.easing}`
34
+
35
+ const onEnter = () => {
36
+ el.style.transform = `translateY(${opts.y}px) scale(${opts.scale})`
37
+ el.style.boxShadow = opts.shadow
38
+ }
39
+
40
+ const onLeave = () => {
41
+ el.style.transform = ''
42
+ el.style.boxShadow = ''
43
+ }
44
+
45
+ el.addEventListener('mouseenter', onEnter)
46
+ el.addEventListener('mouseleave', onLeave)
47
+
48
+ return () => {
49
+ el.removeEventListener('mouseenter', onEnter)
50
+ el.removeEventListener('mouseleave', onLeave)
51
+ el.style.transition = ''
52
+ el.style.transform = ''
53
+ el.style.boxShadow = ''
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 给元素添加悬浮发光效果
59
+ */
60
+ export function hoverGlow(
61
+ element: HTMLElement | string,
62
+ color: string = 'rgba(56, 189, 248, 0.4)',
63
+ options?: Pick<HoverLiftOptions, 'duration' | 'easing'>
64
+ ): () => void {
65
+ const el =
66
+ typeof element === 'string'
67
+ ? document.querySelector<HTMLElement>(element)!
68
+ : element
69
+
70
+ if (!el) throw new Error('hoverGlow: element not found')
71
+
72
+ const duration = options?.duration ?? 250
73
+ const easing = options?.easing ?? 'ease'
74
+
75
+ el.style.transition = `box-shadow ${duration}ms ${easing}`
76
+
77
+ const onEnter = () => {
78
+ el.style.boxShadow = `0 0 20px ${color}`
79
+ }
80
+
81
+ const onLeave = () => {
82
+ el.style.boxShadow = ''
83
+ }
84
+
85
+ el.addEventListener('mouseenter', onEnter)
86
+ el.addEventListener('mouseleave', onLeave)
87
+
88
+ return () => {
89
+ el.removeEventListener('mouseenter', onEnter)
90
+ el.removeEventListener('mouseleave', onLeave)
91
+ el.style.transition = ''
92
+ el.style.boxShadow = ''
93
+ }
94
+ }
@@ -0,0 +1,130 @@
1
+ export interface RippleOptions {
2
+ color?: string // 波纹颜色
3
+ duration?: number // 动画时长毫秒
4
+ maxScale?: number // 最大缩放倍数
5
+ }
6
+
7
+ const DEFAULT_RIPPLE: Required<RippleOptions> = {
8
+ color: 'rgba(255, 255, 255, 0.35)',
9
+ duration: 600,
10
+ maxScale: 2.5,
11
+ }
12
+
13
+ /**
14
+ * 给元素添加 Material Design 风格的水波纹点击效果
15
+ */
16
+ export function addRipple(
17
+ element: HTMLElement | string,
18
+ options: RippleOptions = {}
19
+ ): () => void {
20
+ const el =
21
+ typeof element === 'string'
22
+ ? document.querySelector<HTMLElement>(element)!
23
+ : element
24
+
25
+ if (!el) throw new Error('addRipple: element not found')
26
+
27
+ el.style.position = 'relative'
28
+ el.style.overflow = 'hidden'
29
+
30
+ const opts = { ...DEFAULT_RIPPLE, ...options }
31
+
32
+ const handler = (e: MouseEvent) => {
33
+ const rect = el.getBoundingClientRect()
34
+ const size = Math.max(rect.width, rect.height)
35
+ const x = e.clientX - rect.left - size / 2
36
+ const y = e.clientY - rect.top - size / 2
37
+
38
+ const ripple = document.createElement('span')
39
+ ripple.style.cssText = `
40
+ position: absolute;
41
+ border-radius: 50%;
42
+ background: ${opts.color};
43
+ width: ${size}px;
44
+ height: ${size}px;
45
+ left: ${x}px;
46
+ top: ${y}px;
47
+ pointer-events: none;
48
+ transform: scale(0);
49
+ opacity: 1;
50
+ `
51
+
52
+ el.appendChild(ripple)
53
+
54
+ const anim = ripple.animate(
55
+ [
56
+ { transform: 'scale(0)', opacity: 1 },
57
+ { transform: `scale(${opts.maxScale})`, opacity: 0 },
58
+ ],
59
+ {
60
+ duration: opts.duration,
61
+ easing: 'ease-out',
62
+ }
63
+ )
64
+
65
+ anim.onfinish = () => {
66
+ ripple.remove()
67
+ }
68
+ }
69
+
70
+ el.addEventListener('click', handler)
71
+
72
+ return () => {
73
+ el.removeEventListener('click', handler)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 一次性水波纹(不绑定事件,立即播放)
79
+ */
80
+ export function rippleEffect(
81
+ element: HTMLElement | string,
82
+ options: RippleOptions = {}
83
+ ): Promise<void> {
84
+ const el =
85
+ typeof element === 'string'
86
+ ? document.querySelector<HTMLElement>(element)!
87
+ : element
88
+
89
+ if (!el) throw new Error('rippleEffect: element not found')
90
+
91
+ const opts = { ...DEFAULT_RIPPLE, ...options }
92
+ const rect = el.getBoundingClientRect()
93
+ const size = Math.max(rect.width, rect.height)
94
+
95
+ const ripple = document.createElement('span')
96
+ ripple.style.cssText = `
97
+ position: absolute;
98
+ border-radius: 50%;
99
+ background: ${opts.color};
100
+ width: ${size}px;
101
+ height: ${size}px;
102
+ left: ${rect.width / 2 - size / 2}px;
103
+ top: ${rect.height / 2 - size / 2}px;
104
+ pointer-events: none;
105
+ transform: scale(0);
106
+ opacity: 1;
107
+ `
108
+
109
+ el.style.position = 'relative'
110
+ el.style.overflow = 'hidden'
111
+ el.appendChild(ripple)
112
+
113
+ return new Promise((resolve) => {
114
+ const anim = ripple.animate(
115
+ [
116
+ { transform: 'scale(0)', opacity: 1 },
117
+ { transform: `scale(${opts.maxScale})`, opacity: 0 },
118
+ ],
119
+ {
120
+ duration: opts.duration,
121
+ easing: 'ease-out',
122
+ }
123
+ )
124
+
125
+ anim.onfinish = () => {
126
+ ripple.remove()
127
+ resolve()
128
+ }
129
+ })
130
+ }