video-flow-b/components/ui/ImageWave.tsx
2025-07-29 21:22:51 +08:00

208 lines
5.4 KiB
TypeScript

import React, { useState, useEffect, useCallback, useRef } from 'react';
import styled from 'styled-components';
interface ImageWaveProps {
// 图片列表数据
images: string[];
// 容器宽度
containerWidth?: string;
// 容器高度
containerHeight?: string;
// 单个图片宽度
itemWidth?: string;
// 单个图片高度
itemHeight?: string;
// 图片间距
gap?: string;
// 是否开启自动动画
autoAnimate?: boolean;
// 自动动画间隔时间(ms)
autoAnimateInterval?: number;
// 是否开启点击事件
onClick?: (index: number) => void;
}
const Wrapper = styled.div<{ width?: string; height?: string }>`
display: flex;
align-items: center;
justify-content: center;
width: ${props => props.width || '100vw'};
height: ${props => props.height || '100vh'};
background-color: transparent;
`;
const Items = styled.div<{ gap?: string }>`
display: flex;
gap: ${props => props.gap || '0.4rem'};
perspective: calc(var(--index) * 35);
&.has-expanded .item:not(.expanded) {
filter: grayscale(1) brightness(0.3);
}
&.has-expanded .item:hover {
filter: grayscale(1) brightness(0.3);
}
&.has-expanded .item.expanded:hover {
filter: inherit;
transform: translateZ(calc(var(--index) * 10));
}
`;
const Item = styled.div<{ width?: string; height?: string }>`
width: ${props => props.width || 'calc(var(--index) * 3)'};
height: ${props => props.height || 'calc(var(--index) * 12)'};
background-color: #222;
background-size: cover;
background-position: center;
cursor: pointer;
filter: grayscale(1) brightness(0.5);
transition: transform 1.25s var(--transition),
filter 3s var(--transition),
width 1.25s var(--transition),
margin 1.25s var(--transition);
will-change: transform, filter, rotateY, width;
&::before {
content: '';
position: absolute;
width: 20px;
height: 100%;
right: calc(var(--index) * -1);
}
&::after {
left: calc(var(--index) * -1);
}
&:hover {
filter: inherit;
transform: translateZ(calc(var(--index) * 7.8));
}
&:hover + * {
filter: inherit;
transform: translateZ(calc(var(--index) * 6.8)) rotateY(35deg);
z-index: -1;
}
&:hover + * + * {
filter: inherit;
transform: translateZ(calc(var(--index) * 5.6)) rotateY(40deg);
z-index: -2;
}
&:hover + * + * + * {
filter: inherit;
transform: translateZ(calc(var(--index) * 3.6)) rotateY(30deg);
z-index: -3;
}
&:hover + * + * + * + * {
filter: inherit;
transform: translateZ(calc(var(--index) * .6)) rotateY(15deg);
z-index: -4;
}
&:has(+ :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * 6.8)) rotateY(-35deg);
}
&:has(+ * + :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * 5.6)) rotateY(-40deg);
}
&:has(+ * + * + :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * 3.6)) rotateY(-30deg);
}
&:has(+ * + * + * + :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * .6)) rotateY(-15deg);
}
&.expanded {
width: 28vw;
filter: inherit;
z-index: 100;
transform: translateZ(calc(var(--index) * 7.8));
margin: 0.45vw;
}
&.selected {
filter: inherit;
}
`;
export const ImageWave: React.FC<ImageWaveProps> = ({
images,
containerWidth,
containerHeight,
itemWidth,
itemHeight,
gap,
autoAnimate = false,
autoAnimateInterval = 2000,
onClick,
}) => {
const [currentExpandedItem, setCurrentExpandedItem] = useState<number | null>(null);
const [currentSelectedIndex, setCurrentSelectedIndex] = useState<number | null>(null);
const itemsRef = useRef<HTMLDivElement>(null);
const autoAnimateRef = useRef<number | null>(null);
const currentIndexRef = useRef<number>(0);
const handleItemClick = (index: number) => {
if (currentExpandedItem === index) {
setCurrentExpandedItem(null);
} else {
setCurrentExpandedItem(index);
setCurrentSelectedIndex(index);
onClick?.(index);
}
};
const handleOutsideClick = useCallback((e: MouseEvent) => {
if (itemsRef.current && !itemsRef.current.contains(e.target as Node)) {
setCurrentExpandedItem(null);
}
}, []);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
setCurrentExpandedItem(null);
}
}, []);
useEffect(() => {
document.addEventListener('click', handleOutsideClick);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('click', handleOutsideClick);
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleOutsideClick, handleKeyDown]);
useEffect(() => {
return () => {
if (autoAnimateRef.current) {
clearTimeout(autoAnimateRef.current);
}
};
}, [autoAnimate]);
return (
<Wrapper width={containerWidth} height={containerHeight}>
<Items ref={itemsRef} gap={gap} className={currentExpandedItem !== null ? 'has-expanded' : ''}>
{images.map((image, index) => (
<Item
key={index}
width={itemWidth}
height={itemHeight}
className={`item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`}
style={{ backgroundImage: `url(${image})` }}
onClick={() => handleItemClick(index)}
tabIndex={0}
/>
))}
</Items>
</Wrapper>
);
};