You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
6.6 KiB
243 lines
6.6 KiB
import classNames from 'classnames';
|
|
import { CSSMotionList } from 'rc-motion';
|
|
import raf from 'rc-util/lib/raf';
|
|
import { render, unmount } from 'rc-util/lib/React/render';
|
|
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(() => {
|
|
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;
|
|
|
|
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 ? globalToken.colorPrimary : globalToken.colorPrimaryBgHover,
|
|
};
|
|
}),
|
|
);
|
|
}, 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;
|
|
|