Compare commits
18 Commits
Author | SHA1 | Date |
---|---|---|
二货机器人 | 920864e303 | 2 years ago |
二货机器人 | 77f22d5d0d | 2 years ago |
二货机器人 | 4b7967eeb9 | 2 years ago |
二货机器人 | 75929d07d5 | 2 years ago |
二货机器人 | 78630f28fc | 2 years ago |
二货机器人 | 6e4a6df77c | 2 years ago |
二货机器人 | 985e0c73d0 | 2 years ago |
二货机器人 | cb5f4ddb51 | 2 years ago |
二货机器人 | 7d243d23a6 | 2 years ago |
二货机器人 | 6eb620f079 | 2 years ago |
二货机器人 | b6b7ab562b | 2 years ago |
二货机器人 | f08c980eb9 | 2 years ago |
二货机器人 | 8a729d6642 | 2 years ago |
二货机器人 | 192c9332fa | 2 years ago |
二货机器人 | e21f44e56f | 2 years ago |
二货机器人 | 966ba02bf9 | 2 years ago |
二货机器人 | 7e4139815e | 2 years ago |
二货机器人 | 1d7af30f28 | 2 years ago |
20 changed files with 555 additions and 145 deletions
@ -1,14 +1,19 @@ |
|||
import useEvent from 'rc-util/lib/hooks/useEvent'; |
|||
import type { ConfigProviderProps } from '../../config-provider'; |
|||
import showWaveEffect from './WaveEffect'; |
|||
import type { useToken } from '../../theme/internal'; |
|||
|
|||
export default function useWave( |
|||
nodeRef: React.RefObject<HTMLElement>, |
|||
className: string, |
|||
token: ReturnType<typeof useToken>, |
|||
wave: ConfigProviderProps['wave'], |
|||
): VoidFunction { |
|||
function showWave() { |
|||
const node = nodeRef.current!; |
|||
|
|||
showWaveEffect(node, className); |
|||
showWaveEffect(node, className, token, wave); |
|||
} |
|||
|
|||
return showWave; |
|||
return useEvent(showWave); |
|||
} |
|||
|
@ -0,0 +1,9 @@ |
|||
import type { OverrideToken } from '../../interface'; |
|||
import wave from './wave'; |
|||
|
|||
const token: OverrideToken = {}; |
|||
|
|||
export default { |
|||
token, |
|||
wave, |
|||
}; |
@ -0,0 +1,100 @@ |
|||
import { Keyframes, useStyleRegister } from '@ant-design/cssinjs'; |
|||
import type { UseTokenType } from '../wave'; |
|||
|
|||
export const TARGET_ATTR = 'data-happy-wave-target'; |
|||
|
|||
const antWaveTargetEffect = new Keyframes('antWaveTargetEffect', { |
|||
'0%': { |
|||
transform: 'scale(1)', |
|||
}, |
|||
'10%': { |
|||
transform: 'scale(1.1)', |
|||
}, |
|||
'35%': { |
|||
transform: 'scale(0.94)', |
|||
}, |
|||
'60%': { |
|||
transform: 'scale(1.05)', |
|||
}, |
|||
'85%': { |
|||
transform: 'scale(0.97)', |
|||
}, |
|||
'100%': { |
|||
transform: 'scale(1)', |
|||
}, |
|||
}); |
|||
|
|||
const antWaveDotEffect = new Keyframes('antWaveDotEffect', { |
|||
'0%': { |
|||
opacity: 0, |
|||
left: `var(--start-x)`, |
|||
top: `var(--start-y)`, |
|||
width: `var(--start-size)`, |
|||
height: `var(--start-size)`, |
|||
background: `var(--background)`, |
|||
border: `var(--border)`, |
|||
}, |
|||
'25%': { |
|||
opacity: 1, |
|||
}, |
|||
'50%': { |
|||
opacity: 0.8, |
|||
}, |
|||
'100%': { |
|||
opacity: 0, |
|||
left: `var(--end-x)`, |
|||
top: `var(--end-y)`, |
|||
width: `var(--end-size)`, |
|||
height: `var(--end-size)`, |
|||
background: `var(--background)`, |
|||
border: `var(--border)`, |
|||
}, |
|||
}); |
|||
|
|||
export default function useStyle(tokenConfig: UseTokenType) { |
|||
const [theme, token, hashId] = tokenConfig; |
|||
|
|||
useStyleRegister({ theme, token, hashId, path: ['Customize', 'Wave', 'Effect'] }, () => { |
|||
const { motionDurationSlow } = token; |
|||
|
|||
const prefixCls = '.happy-wave'; |
|||
|
|||
return [ |
|||
{ |
|||
// ======================== Target ========================
|
|||
[`[${TARGET_ATTR}], & [${TARGET_ATTR}]`]: { |
|||
animationName: antWaveTargetEffect, |
|||
animationDuration: `0.45s`, |
|||
animationTimingFunction: 'ease-in-out', |
|||
animationFillMode: 'backwards', |
|||
}, |
|||
|
|||
// ========================= Dots =========================
|
|||
[prefixCls]: { |
|||
position: 'fixed', |
|||
pointerEvents: 'none', |
|||
zIndex: 999, |
|||
|
|||
[`${prefixCls}-dot`]: { |
|||
boxSizing: 'border-box', |
|||
position: 'absolute', |
|||
borderRadius: '100%', |
|||
opacity: 0, |
|||
transform: 'translate(-50%, -50%)', |
|||
|
|||
// Start Position
|
|||
zIndex: 999, |
|||
|
|||
// =================== Basic Motion ===================
|
|||
'&.happy-in-out': { |
|||
animationName: antWaveDotEffect, |
|||
animationDuration: motionDurationSlow, |
|||
animationTimingFunction: 'linear', |
|||
animationFillMode: 'backwards', |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
]; |
|||
}); |
|||
} |
@ -0,0 +1,253 @@ |
|||
import { TinyColor } from '@ctrl/tinycolor'; |
|||
import classNames from 'classnames'; |
|||
import { CSSMotionList } from 'rc-motion'; |
|||
import { render, unmount } from 'rc-util/lib/React/render'; |
|||
import raf from 'rc-util/lib/raf'; |
|||
import * as React from 'react'; |
|||
import type { ConfigProviderProps } from '../../../config-provider'; |
|||
import useWaveStyle, { TARGET_ATTR } from './style/wave'; |
|||
|
|||
export type WaveRender = Required<Required<ConfigProviderProps>['wave']>['render']; |
|||
export type UseTokenType = Parameters<WaveRender>[0]['token']; |
|||
|
|||
interface HappyWaveProps { |
|||
target: HTMLElement; |
|||
token: UseTokenType; |
|||
onFinish: VoidFunction; |
|||
} |
|||
|
|||
const DOT_COUNT = 7; |
|||
const DOT_COUNT_LG = 10; |
|||
|
|||
interface DotInfo { |
|||
key: number; |
|||
startSize: string; |
|||
endSize: string; |
|||
type: 'fill' | 'outlined'; |
|||
color: string; |
|||
startX: string; |
|||
startY: string; |
|||
endX: string; |
|||
endY: string; |
|||
} |
|||
|
|||
function inRange(x: number, y: number, left: number, top: number, right: number, bottom: number) { |
|||
return x >= left && x <= right && y >= top && y <= bottom; |
|||
} |
|||
|
|||
function HappyWave({ target, token, onFinish }: HappyWaveProps) { |
|||
const prefixCls = 'happy-wave'; |
|||
const dotPrefixCls = `${prefixCls}-dot`; |
|||
|
|||
const [dots, setDots] = React.useState<DotInfo[] | null>(null); |
|||
const [left, setLeft] = React.useState(0); |
|||
const [top, setTop] = React.useState(0); |
|||
|
|||
const [, globalToken, hashId] = token; |
|||
useWaveStyle(token); |
|||
|
|||
// ========================= Dots =========================
|
|||
React.useEffect(() => { |
|||
const id = raf(() => { |
|||
if (['-dangerous', '-error'].some((skipCls) => target.className.includes(skipCls))) { |
|||
return; |
|||
} |
|||
|
|||
const rect = target.getBoundingClientRect(); |
|||
const { width, height } = rect; |
|||
|
|||
setLeft(rect.left + width / 2); |
|||
setTop(rect.top + height / 2); |
|||
setDots([]); |
|||
|
|||
const minSize = Math.min(width, height); |
|||
const maxSize = Math.max(width, height); |
|||
const halfMinSize = minSize / 2; |
|||
const halfMaxSize = maxSize / 2; |
|||
const halfWidth = width / 2; |
|||
const halfHeight = height / 2; |
|||
|
|||
const OFFSET_MIN = 15; |
|||
const OFFSET_MAX = 30; |
|||
const halfOffsetMinWidth = halfWidth + OFFSET_MIN; |
|||
const halfOffsetMinHeight = halfHeight + OFFSET_MIN; |
|||
const halfOffsetMaxWidth = halfWidth + OFFSET_MAX; |
|||
const halfOffsetMaxHeight = halfHeight + OFFSET_MAX; |
|||
|
|||
const dotCount = minSize >= 20 ? DOT_COUNT_LG : DOT_COUNT; |
|||
|
|||
// Delay to start dot motion
|
|||
setTimeout(() => { |
|||
const offsetAngle = Math.random() * 360; |
|||
|
|||
// Color
|
|||
const { colorPrimary } = globalToken; |
|||
const colorHsv = new TinyColor(colorPrimary).toHsv(); |
|||
colorHsv.h -= 30; |
|||
const colorPrimaryWeak = new TinyColor(colorHsv).toHexString(); |
|||
|
|||
setDots( |
|||
new Array(dotCount).fill(null).map((_, index) => { |
|||
const rotate: number = 360 / dotCount; |
|||
const randomAngle = offsetAngle + rotate * index; |
|||
|
|||
// Get start XY (Which should align the rect edge)
|
|||
let startX = 0; |
|||
let startY = 0; |
|||
|
|||
for (let startDist = halfMinSize - 1; startDist <= halfMaxSize; startDist += 1) { |
|||
const x = Math.cos((randomAngle * Math.PI) / 180) * startDist; |
|||
const y = Math.sin((randomAngle * Math.PI) / 180) * startDist; |
|||
|
|||
if (!inRange(x, y, -halfWidth, -halfHeight, halfWidth, halfHeight)) { |
|||
break; |
|||
} |
|||
|
|||
startX = x; |
|||
startY = y; |
|||
} |
|||
|
|||
// Get end XY
|
|||
let endX = startX; |
|||
let endY = startY; |
|||
let endDist = halfMinSize; |
|||
|
|||
const endHalfWidth = |
|||
Math.random() * (halfOffsetMaxWidth - halfOffsetMinWidth) + halfOffsetMinWidth; |
|||
const endHalfHeight = |
|||
Math.random() * (halfOffsetMaxHeight - halfOffsetMinHeight) + halfOffsetMinHeight; |
|||
|
|||
do { |
|||
endX = Math.cos((randomAngle * Math.PI) / 180) * endDist; |
|||
endY = Math.sin((randomAngle * Math.PI) / 180) * endDist; |
|||
endDist += 1; |
|||
} while ( |
|||
inRange(endX, endY, -endHalfWidth, -endHalfHeight, endHalfWidth, endHalfHeight) |
|||
); |
|||
|
|||
let size = Math.random() * 3 + 3; |
|||
if (height >= 20) { |
|||
size = Math.random() * 4 + 6; |
|||
} |
|||
|
|||
return { |
|||
key: index + 1, |
|||
startX: `${startX}px`, |
|||
startY: `${startY}px`, |
|||
endX: `${endX}px`, |
|||
endY: `${endY}px`, |
|||
startSize: `${size}px`, |
|||
endSize: `${Math.random() > 0.75 ? size : 0}px`, |
|||
type: Math.random() > 0.6 ? 'outlined' : 'fill', |
|||
color: Math.random() > 0.5 ? colorPrimary : colorPrimaryWeak, |
|||
}; |
|||
}), |
|||
); |
|||
}, 50); |
|||
|
|||
target.setAttribute(TARGET_ATTR, 'true'); |
|||
}); |
|||
|
|||
return () => { |
|||
raf.cancel(id); |
|||
}; |
|||
}, []); |
|||
|
|||
// ======================== Clean =========================
|
|||
React.useEffect(() => { |
|||
const id = setTimeout(() => { |
|||
target.removeAttribute(TARGET_ATTR); |
|||
onFinish(); |
|||
}, 600); |
|||
|
|||
return () => { |
|||
clearTimeout(id); |
|||
}; |
|||
}, []); |
|||
|
|||
// ======================== Render ========================
|
|||
if (!dots) { |
|||
return null; |
|||
} |
|||
|
|||
return ( |
|||
<div |
|||
className={classNames(prefixCls, hashId)} |
|||
style={{ |
|||
left, |
|||
top, |
|||
}} |
|||
> |
|||
<CSSMotionList component={false} keys={dots} motionAppear motionName="happy-in-out"> |
|||
{({ |
|||
className: motionCls, |
|||
style: motionStyle, |
|||
key, |
|||
startX, |
|||
startY, |
|||
endX, |
|||
endY, |
|||
startSize, |
|||
endSize, |
|||
type, |
|||
color, |
|||
}) => { |
|||
const name = `${dotPrefixCls}-${key}`; |
|||
|
|||
const dotCls = classNames(dotPrefixCls, motionCls, name); |
|||
|
|||
const dotStyle: Record<string, string> = { |
|||
[`--start-x`]: startX, |
|||
[`--start-y`]: startY, |
|||
[`--end-x`]: endX, |
|||
[`--end-y`]: endY, |
|||
[`--start-size`]: startSize, |
|||
[`--end-size`]: endSize, |
|||
}; |
|||
if (type === 'fill') { |
|||
dotStyle['--background'] = color; |
|||
} else { |
|||
dotStyle['--border'] = `1px solid ${color}`; |
|||
} |
|||
|
|||
return ( |
|||
<div |
|||
className={dotCls} |
|||
style={{ |
|||
...motionStyle, |
|||
...dotStyle, |
|||
}} |
|||
/> |
|||
); |
|||
}} |
|||
</CSSMotionList> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
const renderWave: WaveRender = (props) => { |
|||
// Create holder
|
|||
const holder = document.createElement('div'); |
|||
holder.style.position = 'absolute'; |
|||
holder.style.left = `0px`; |
|||
holder.style.top = `0px`; |
|||
document.body.appendChild(holder); |
|||
|
|||
render( |
|||
<HappyWave |
|||
{...props} |
|||
onFinish={() => { |
|||
unmount(holder).then(() => { |
|||
holder.parentElement?.removeChild(holder); |
|||
}); |
|||
}} |
|||
/>, |
|||
holder, |
|||
); |
|||
}; |
|||
|
|||
const Wave: ConfigProviderProps['wave'] = { |
|||
render: renderWave, |
|||
}; |
|||
|
|||
export default Wave; |
Loading…
Reference in new issue