@luanlu/mk-motion 1.2.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.
- package/package.json +2 -4
- package/src/a11y/focus-trap.ts +64 -0
- package/src/a11y/keyboard.ts +43 -0
- package/src/components/alert/alert.css +111 -0
- package/src/components/alert/alert.ts +107 -0
- package/src/components/avatar/avatar.css +112 -0
- package/src/components/avatar/avatar.ts +175 -0
- package/src/components/breadcrumb/breadcrumb.css +31 -0
- package/src/components/breadcrumb/breadcrumb.ts +71 -0
- package/src/components/button/button.css +108 -0
- package/src/components/button/button.ts +140 -0
- package/src/components/card/card.css +52 -0
- package/src/components/card/card.ts +87 -0
- package/src/components/collapse/collapse.css +76 -0
- package/src/components/collapse/collapse.ts +168 -0
- package/src/components/dialog/dialog.css +78 -0
- package/src/components/dialog/dialog.ts +164 -0
- package/src/components/drawer/drawer.css +73 -0
- package/src/components/drawer/drawer.ts +131 -0
- package/src/components/empty/empty.css +36 -0
- package/src/components/empty/empty.ts +85 -0
- package/src/components/form/checkbox.css +56 -0
- package/src/components/form/checkbox.ts +119 -0
- package/src/components/form/radio.css +57 -0
- package/src/components/form/radio.ts +153 -0
- package/src/components/form/select.css +91 -0
- package/src/components/form/select.ts +174 -0
- package/src/components/form/slider.css +56 -0
- package/src/components/form/slider.ts +148 -0
- package/src/components/input/input.css +92 -0
- package/src/components/input/input.ts +162 -0
- package/src/components/layout/divider.css +32 -0
- package/src/components/layout/divider.ts +42 -0
- package/src/components/layout/row.css +64 -0
- package/src/components/layout/row.ts +57 -0
- package/src/components/layout/space.css +14 -0
- package/src/components/layout/space.ts +48 -0
- package/src/components/loading/loading.css +37 -0
- package/src/components/loading/loading.ts +46 -0
- package/src/components/menu/menu.css +121 -0
- package/src/components/menu/menu.ts +187 -0
- package/src/components/message/message.css +64 -0
- package/src/components/message/message.ts +96 -0
- package/src/components/popover/popover.css +73 -0
- package/src/components/popover/popover.ts +279 -0
- package/src/components/progress/progress.css +112 -0
- package/src/components/progress/progress.ts +171 -0
- package/src/components/steps/steps.css +127 -0
- package/src/components/steps/steps.ts +102 -0
- package/src/components/styles/components.css +28 -0
- package/src/components/styles/reset.css +24 -0
- package/src/components/styles/tokens.css +248 -0
- package/src/components/styles/variables.css +24 -0
- package/src/components/switch/switch.css +53 -0
- package/src/components/switch/switch.ts +103 -0
- package/src/components/table/table.css +192 -0
- package/src/components/table/table.ts +370 -0
- package/src/components/tabs/tabs.css +138 -0
- package/src/components/tabs/tabs.ts +211 -0
- package/src/components/tag/tag.css +123 -0
- package/src/components/tag/tag.ts +112 -0
- package/src/components/tooltip/tooltip.css +66 -0
- package/src/components/tooltip/tooltip.ts +185 -0
- package/src/core/animator.ts +124 -0
- package/src/core/timeline.ts +128 -0
- package/src/core/utils.ts +47 -0
- package/src/effects/glitch.ts +99 -0
- package/src/effects/particle.ts +134 -0
- package/src/effects/text-split.ts +95 -0
- package/src/effects/wave-text.ts +88 -0
- package/src/gesture/draggable.ts +130 -0
- package/src/gesture/spring.ts +152 -0
- package/src/index.ts +162 -0
- package/src/interactive/coverflow.ts +100 -0
- package/src/interactive/cursor-trail.ts +113 -0
- package/src/interactive/flip-card.ts +114 -0
- package/src/interactive/magnetic.ts +121 -0
- package/src/micro/hover-lift.ts +94 -0
- package/src/micro/ripple.ts +130 -0
- package/src/motion/component-motion.ts +177 -0
- package/src/presets/index.ts +69 -0
- package/src/scroll/scroll-trigger.ts +104 -0
- package/src/styles/animations.css +135 -0
- package/src/styles/element-plus.css +174 -0
- package/src/text/count-up.ts +108 -0
- package/src/text/typewriter.ts +109 -0
- package/src/theme/dark.css +19 -0
- package/src/theme/light.css +19 -0
- package/src/theme/theme.ts +65 -0
- package/src/transitions/blur-reveal.ts +92 -0
- package/src/transitions/collapse.ts +112 -0
- package/src/transitions/lazy-image.ts +87 -0
- package/src/transitions/list.ts +75 -0
- package/src/transitions/loading.ts +95 -0
- package/src/transitions/parallax.ts +60 -0
- package/src/transitions/shimmer.ts +105 -0
- package/src/transitions/toast.ts +151 -0
- package/src/types.d.ts +4 -0
|
@@ -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
|
+
}
|