Skip to content

Commit ae41143

Browse files
committed
feat: 3d card
1 parent 6c891f3 commit ae41143

3 files changed

Lines changed: 240 additions & 3 deletions

File tree

src/assets/images/3dchihiro.png

87.4 KB
Loading
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import React from 'react'
2+
import clsx from 'clsx'
3+
4+
const ThreeDCard = ({
5+
title,
6+
imageUrl,
7+
backgroundUrl,
8+
className,
9+
children,
10+
variant = 'default',
11+
disabled = false,
12+
...props
13+
}) => {
14+
const cardRef = React.useRef(null)
15+
const [rotation, setRotation] = React.useState({ x: 0, y: 0 })
16+
const [position, setPosition] = React.useState({ x: 0, y: 0 })
17+
const [isHovered, setIsHovered] = React.useState(false)
18+
const [isInitialRender, setIsInitialRender] = React.useState(true)
19+
20+
React.useEffect(() => {
21+
setIsInitialRender(false)
22+
}, [])
23+
24+
const handleMouseMove = React.useCallback(
25+
(e) => {
26+
if (!cardRef.current || disabled) return
27+
28+
const rect = cardRef.current.getBoundingClientRect()
29+
const centerX = rect.left + rect.width / 2
30+
const centerY = rect.top + rect.height / 2
31+
const mouseX = e.clientX - centerX
32+
const mouseY = e.clientY - centerY
33+
34+
const rotateY = (mouseX / (rect.width / 2)) * 25
35+
const rotateX = -(mouseY / (rect.height / 2)) * 25
36+
37+
const moveX = (mouseX / rect.width) * 10
38+
const moveY = (mouseY / rect.height) * 10
39+
40+
requestAnimationFrame(() => {
41+
setRotation({ x: rotateX, y: rotateY })
42+
setPosition({ x: moveX, y: moveY })
43+
})
44+
},
45+
[disabled]
46+
)
47+
48+
const handleMouseLeave = React.useCallback(() => {
49+
if (disabled) return
50+
requestAnimationFrame(() => {
51+
setRotation({ x: 0, y: 0 })
52+
setPosition({ x: 0, y: 0 })
53+
setIsHovered(false)
54+
})
55+
}, [disabled])
56+
57+
const handleMouseEnter = React.useCallback(() => {
58+
if (disabled) return
59+
setIsHovered(true)
60+
}, [disabled])
61+
62+
const transitionSettings = isInitialRender
63+
? 'none'
64+
: isHovered
65+
? 'transform 0.1s ease-out'
66+
: 'transform 0.5s ease-out'
67+
68+
const cardStyle = {
69+
transform: `
70+
perspective(2000px)
71+
rotateX(${disabled ? 0 : rotation.x}deg)
72+
rotateY(${disabled ? 0 : rotation.y}deg)
73+
scale(${isHovered && !disabled ? 1.05 : 1})
74+
${disabled ? 'translateZ(0)' : ''}
75+
`,
76+
transformStyle: 'preserve-3d',
77+
transition: transitionSettings,
78+
transformOrigin: 'center center',
79+
filter: disabled ? 'grayscale(1) brightness(0.8)' : 'none',
80+
willChange: 'transform',
81+
}
82+
83+
return (
84+
<div
85+
ref={cardRef}
86+
className={clsx(
87+
'group relative h-[280px] w-[200px] cursor-pointer overflow-hidden rounded-xl',
88+
'transform-gpu shadow-2xl',
89+
variant === 'border' && [
90+
'before:absolute before:inset-0 before:z-20 before:rounded-xl before:border-2',
91+
'before:border-white/20 before:transition-colors before:duration-700',
92+
'hover:before:border-white/40',
93+
],
94+
variant === 'shine' && [
95+
'after:absolute after:inset-0 after:z-20',
96+
'after:bg-linear-to-br after:from-white/0 after:to-white/20',
97+
'after:transition-opacity after:duration-700',
98+
'hover:after:opacity-100',
99+
],
100+
disabled && 'cursor-not-allowed',
101+
className
102+
)}
103+
style={cardStyle}
104+
onMouseMove={handleMouseMove}
105+
onMouseLeave={handleMouseLeave}
106+
onMouseEnter={handleMouseEnter}
107+
{...props}
108+
>
109+
{backgroundUrl && (
110+
<div
111+
className={clsx('absolute inset-0 scale-110 bg-cover bg-center', disabled && 'brightness-75 grayscale')}
112+
style={{
113+
backgroundImage: `url(${backgroundUrl})`,
114+
transform: `
115+
translateZ(-75px)
116+
translateX(${position.x * 2}px)
117+
translateY(${position.y * 2}px)
118+
scale(${isHovered && !disabled ? 1.15 : 1.1})
119+
`,
120+
transition: transitionSettings,
121+
willChange: 'transform',
122+
}}
123+
/>
124+
)}
125+
126+
{!disabled && (
127+
<div
128+
className="pointer-events-none absolute inset-0 h-full w-full"
129+
style={{
130+
background: `linear-gradient(
131+
${105 + rotation.x}deg,
132+
transparent 20%,
133+
rgba(255, 255, 255, ${isHovered ? 0.1 : 0}) 35%,
134+
rgba(255, 255, 255, ${isHovered ? 0.2 : 0}) 50%,
135+
transparent 80%
136+
)`,
137+
transform: 'translateZ(1px)',
138+
opacity: isHovered ? 1 : 0,
139+
transition: 'opacity 0.5s ease-out',
140+
}}
141+
/>
142+
)}
143+
144+
{imageUrl && (
145+
<div className="relative h-full w-full">
146+
<img
147+
src={imageUrl}
148+
alt={title || 'Card image'}
149+
className={clsx(
150+
'relative z-10 h-full w-full object-contain drop-shadow-2xl',
151+
disabled && 'brightness-75 grayscale',
152+
isHovered && !disabled && 'drop-shadow-[0_20px_30px_rgba(0,0,0,0.3)]'
153+
)}
154+
style={{
155+
transform: `
156+
translateZ(${isHovered ? 120 : 75}px)
157+
translateX(${position.x * -2}px)
158+
translateY(${position.y * -2}px)
159+
scale(${isHovered && !disabled ? 1.2 : 1.1})
160+
`,
161+
transition: transitionSettings,
162+
willChange: 'transform',
163+
}}
164+
/>
165+
</div>
166+
)}
167+
168+
<div
169+
className="absolute -bottom-3 z-20 w-full rounded-b-xl bg-linear-to-t from-black/90 via-black/50 to-transparent p-4"
170+
style={{
171+
transform: `
172+
translateZ(50px)
173+
translateX(${position.x * -1.5}px)
174+
translateY(${position.y * -1.5}px)
175+
`,
176+
transition: transitionSettings,
177+
willChange: 'transform',
178+
}}
179+
>
180+
{title && (
181+
<h3
182+
className={clsx('text-lg font-bold text-white', disabled && 'text-white/70')}
183+
style={{
184+
textShadow: '2px 2px 4px rgba(0,0,0,0.5)',
185+
transform: `translateZ(25px)`,
186+
transition: transitionSettings,
187+
}}
188+
>
189+
{title}
190+
</h3>
191+
)}
192+
{children}
193+
</div>
194+
195+
<div
196+
className={clsx('absolute inset-0 rounded-xl ring-2 ring-white/0', isHovered && !disabled && 'ring-white/20')}
197+
style={{
198+
transform: 'translateZ(100px)',
199+
transition: 'ring-color 0.5s ease-out',
200+
}}
201+
/>
202+
</div>
203+
)
204+
}
205+
206+
export default ThreeDCard

src/pages/tilt/index.jsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,47 @@
11
import React from 'react'
22
import FixTabPanel from '@stateless/FixTabPanel'
33
import Tilt from 'react-parallax-tilt'
4+
import ThreeDCard from '@stateless/ThreeDCard'
45

56
import wkylinPng from '@assets/images/wkylin.png'
7+
import SpringPng from '@assets/images/spring.png'
8+
import HePng from '@assets/images/he.png'
9+
import SongPng from '@assets/images/song.png'
10+
import XuePng from '@assets/images/xue.png'
11+
import MacPng from '@assets/images/3dchihiro.png'
612

713
const ReactTilt = () => (
814
<FixTabPanel>
915
<h3>Welcome to react tilt!</h3>
10-
<section style={{ width: 200, height: 200 }}>
11-
<Tilt tiltMaxAngleX={40} tiltMaxAngleY={40} perspective={1000} scale={1.1} glareEnable={true}>
12-
<img src={wkylinPng} alt="wkylin.w" width="200" height="200" />
16+
<section className="flex flex-wrap items-center justify-start gap-8 p-8">
17+
<Tilt tiltMaxAngleX={40} tiltMaxAngleY={40} perspective={1000} scale={1.05} glareEnable={true}>
18+
<section className="h-[150px] w-[300px] overflow-hidden">
19+
<img src={SpringPng} alt="wkylin.w" />
20+
</section>
1321
</Tilt>
22+
<Tilt tiltMaxAngleX={40} tiltMaxAngleY={40} perspective={1000} scale={1.05} glareEnable={true}>
23+
<section className="h-[150px] w-[300px] overflow-hidden">
24+
<img src={HePng} alt="wkylin.w" />
25+
</section>
26+
</Tilt>
27+
<Tilt tiltMaxAngleX={40} tiltMaxAngleY={40} perspective={1000} scale={1.05} glareEnable={true}>
28+
<section className="h-[150px] w-[300px] overflow-hidden">
29+
<img src={SongPng} alt="wkylin.w" />
30+
</section>
31+
</Tilt>
32+
<Tilt tiltMaxAngleX={40} tiltMaxAngleY={40} perspective={1000} scale={1.05} glareEnable={true}>
33+
<section className="h-[150px] w-[300px] overflow-hidden">
34+
<img src={XuePng} alt="wkylin.w" />
35+
</section>
36+
</Tilt>
37+
</section>
38+
39+
<h3 style={{ margin: '30px 0', fontSize: 18 }}>3D Card</h3>
40+
<section className="flex flex-wrap items-center justify-start gap-8 p-8">
41+
<ThreeDCard title="小荷才露尖尖角,早有蜻蜓立上头" backgroundUrl={HePng} imageUrl={MacPng} variant="shine" />
42+
<ThreeDCard title="不知细叶谁裁出,二月春风似剪刀" backgroundUrl={SpringPng} variant="border" />
43+
<ThreeDCard title="明月松间照,清泉石上流" backgroundUrl={SongPng} />
44+
<ThreeDCard title="柴门闻犬吠,风雪夜归人" backgroundUrl={XuePng} />
1445
</section>
1546
</FixTabPanel>
1647
)

0 commit comments

Comments
 (0)