forked from 77media/video-flow
229 lines
5.6 KiB
TypeScript
229 lines
5.6 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import styled from 'styled-components';
|
|
|
|
// 导入图片资源
|
|
import mono from '@/assets/3dr_mono.png';
|
|
import chihiro from '@/assets/3dr_chihiro.png';
|
|
import howlcastle from '@/assets/3dr_howlcastle.png';
|
|
import monobg from '@/assets/3dr_monobg.jpg';
|
|
import spirited from '@/assets/3dr_spirited.jpg';
|
|
import howlbg from '@/assets/3dr_howlbg.jpg';
|
|
|
|
const Parallax = () => {
|
|
const pageXRef = useRef<HTMLDivElement>(null);
|
|
const cardsRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const pageX = pageXRef.current;
|
|
const cards = cardsRef.current;
|
|
const images = document.querySelectorAll('.card__img') as NodeListOf<HTMLImageElement>;
|
|
const backgrounds = document.querySelectorAll('.card__bg') as NodeListOf<HTMLDivElement>;
|
|
|
|
let timeout: number | undefined;
|
|
const range = 40;
|
|
|
|
// 计算旋转角度
|
|
const calcValue = (a: number, b: number): string => ((a / b) * range - range / 2).toFixed(1);
|
|
|
|
// 视差动画函数
|
|
const parallax = (e: MouseEvent): void => {
|
|
const x = e.clientX;
|
|
const y = e.clientY;
|
|
|
|
if (timeout) {
|
|
window.cancelAnimationFrame(timeout);
|
|
}
|
|
|
|
timeout = window.requestAnimationFrame(() => {
|
|
const xValue = calcValue(x, window.innerWidth);
|
|
const yValue = calcValue(y, window.innerHeight);
|
|
|
|
// 设置卡片容器的旋转角度
|
|
if (cards) {
|
|
cards.style.transform = `rotateX(${yValue}deg) rotateY(${xValue}deg)`;
|
|
}
|
|
|
|
// 设置所有图片的位移
|
|
images.forEach(item => {
|
|
item.style.transform = `translateX(${-xValue}px) translateY(${yValue}px)`;
|
|
});
|
|
|
|
// 设置所有背景的位置
|
|
backgrounds.forEach(item => {
|
|
item.style.backgroundPosition = `${Number(xValue) * 0.45}px ${-Number(yValue) * 0.45}px`;
|
|
});
|
|
});
|
|
};
|
|
|
|
if (pageX) {
|
|
pageX.addEventListener('mousemove', parallax as EventListener, false);
|
|
|
|
return () => {
|
|
pageX.removeEventListener('mousemove', parallax as EventListener);
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}, []);
|
|
|
|
return (
|
|
<PageX ref={pageXRef}>
|
|
<Cards ref={cardsRef}>
|
|
<h3>Movies</h3>
|
|
<h1>Popular</h1>
|
|
|
|
{/* 幽灵公主 */}
|
|
<Card className="princess-mononoke">
|
|
<CardBg className="card__bg" bgUrl={monobg.src} />
|
|
<CardImg className="card__img" src={mono.src} alt="Princess Mononoke" />
|
|
<CardText>
|
|
<CardTitle>Princess Mononoke</CardTitle>
|
|
</CardText>
|
|
</Card>
|
|
|
|
{/* 千与千寻 */}
|
|
<Card className="spirited-away">
|
|
<CardBg className="card__bg" bgUrl={spirited.src} />
|
|
<CardImg className="card__img" src={chihiro.src} alt="Spirited Away" />
|
|
<CardText>
|
|
<CardTitle>Spirited Away</CardTitle>
|
|
</CardText>
|
|
</Card>
|
|
|
|
{/* 哈尔的移动城堡 */}
|
|
<Card className="howl-s-moving-castle">
|
|
<CardBg className="card__bg" bgUrl={howlbg.src} />
|
|
<CardImg className="card__img" src={howlcastle.src} alt="Howl's Moving Castle" />
|
|
<CardText>
|
|
<CardTitle>Howl's Moving Castle</CardTitle>
|
|
</CardText>
|
|
</Card>
|
|
</Cards>
|
|
</PageX>
|
|
);
|
|
};
|
|
|
|
// 样式定义
|
|
const PageX = styled.div`
|
|
width: 1000px;
|
|
height: 700px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
overflow: hidden;
|
|
perspective: 1800px;
|
|
background: #642b73;
|
|
background: linear-gradient(to bottom, #c6426e, #642b73);
|
|
`;
|
|
|
|
const Cards = styled.div`
|
|
display: inline-block;
|
|
min-width: 595px;
|
|
padding: 30px 35px;
|
|
perspective: 1800px;
|
|
transform-origin: 50% 50%;
|
|
transform-style: preserve-3d;
|
|
border-radius: 15px;
|
|
text-align: left;
|
|
background: #fff;
|
|
box-shadow: 0px 10px 20px 20px rgba(0, 0, 0, 0.17);
|
|
|
|
h1 {
|
|
margin-bottom: 30px;
|
|
transform: translateZ(35px);
|
|
letter-spacing: -1px;
|
|
font-size: 32px;
|
|
font-weight: 800;
|
|
color: #3e3e42;
|
|
}
|
|
|
|
h3 {
|
|
margin-bottom: 6px;
|
|
transform: translateZ(25px);
|
|
font-size: 16px;
|
|
color: #eb285d;
|
|
}
|
|
`;
|
|
|
|
const Card = styled.div`
|
|
display: inline-block;
|
|
width: 175px;
|
|
height: 250px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
perspective: 1200px;
|
|
transform-style: preserve-3d;
|
|
transform: translatez(35px);
|
|
transition: transform 200ms ease-out;
|
|
text-align: center;
|
|
border-radius: 15px;
|
|
box-shadow: 5px 5px 20px -5px rgba(0, 0, 0, 0.6);
|
|
|
|
&:not(:last-child) {
|
|
margin-right: 30px;
|
|
}
|
|
|
|
&.princess-mononoke .card__img {
|
|
top: 14px;
|
|
right: -10px;
|
|
height: 110%;
|
|
}
|
|
|
|
&.spirited-away .card__img {
|
|
top: 25px;
|
|
}
|
|
|
|
&.howl-s-moving-castle .card__img {
|
|
top: 5px;
|
|
left: -4px;
|
|
height: 110%;
|
|
}
|
|
`;
|
|
|
|
interface CardBgProps {
|
|
bgUrl: string;
|
|
}
|
|
|
|
const CardBg = styled.div<CardBgProps>`
|
|
bottom: -50px;
|
|
left: -50px;
|
|
position: absolute;
|
|
right: -50px;
|
|
top: -50px;
|
|
transform-origin: 50% 50%;
|
|
transform: translateZ(-50px);
|
|
z-index: 0;
|
|
background: ${props => `url(${props.bgUrl}) center/cover no-repeat`};
|
|
`;
|
|
|
|
const CardImg = styled.img`
|
|
position: relative;
|
|
height: 100%;
|
|
`;
|
|
|
|
const CardText = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
width: 100%;
|
|
height: 70px;
|
|
position: absolute;
|
|
z-index: 2;
|
|
bottom: 0;
|
|
background: linear-gradient(
|
|
to bottom,
|
|
rgba(0, 0, 0, 0) 0%,
|
|
rgba(0, 0, 0, 0.55) 100%
|
|
);
|
|
`;
|
|
|
|
const CardTitle = styled.p`
|
|
margin-bottom: 3px;
|
|
padding: 0 10px;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
`;
|
|
|
|
export default Parallax; |