Instantly share code, notes, and snippets.
Created
May 29, 2025 10:32
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save ruucm-working/01a423aa894e7a679e412135daf1958b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useRef, useEffect } from "react" | |
import * as THREE from "three" | |
export default function Like3D(props) { | |
const [likes, setLikes] = useState(42) | |
const [isLiked, setIsLiked] = useState(false) | |
const mountRef = useRef(null) | |
const sceneRef = useRef(null) | |
const heartRef = useRef(null) | |
const rendererRef = useRef(null) | |
const animationIdRef = useRef(null) | |
// 하트 모양 생성 함수 | |
const createHeartShape = () => { | |
const heartShape = new THREE.Shape() | |
const x = 0, | |
y = 0 | |
heartShape.moveTo(x + 5, y + 5) | |
heartShape.bezierCurveTo(x + 5, y + 5, x + 4, y, x, y) | |
heartShape.bezierCurveTo(x - 6, y, x - 6, y + 3.5, x - 6, y + 3.5) | |
heartShape.bezierCurveTo(x - 6, y + 5.5, x - 4, y + 7.7, x, y + 9.5) | |
heartShape.bezierCurveTo(x + 4, y + 7.7, x + 6, y + 5.5, x + 6, y + 3.5) | |
heartShape.bezierCurveTo(x + 6, y + 3.5, x + 6, y, x, y) | |
heartShape.bezierCurveTo(x + 4, y, x + 5, y + 5, x + 5, y + 5) | |
return heartShape | |
} | |
// 3D 씬 초기화 | |
useEffect(() => { | |
if (!mountRef.current) return | |
// 씬 설정 | |
const scene = new THREE.Scene() | |
const camera = new THREE.PerspectiveCamera(75, 200 / 200, 0.1, 1000) | |
const renderer = new THREE.WebGLRenderer({ | |
alpha: true, | |
antialias: true, | |
}) | |
renderer.setSize(200, 200) | |
renderer.setClearColor(0x000000, 0) | |
mountRef.current.appendChild(renderer.domElement) | |
// 하트 지오메트리 생성 | |
const heartShape = createHeartShape() | |
const extrudeSettings = { | |
depth: 2, | |
bevelEnabled: true, | |
bevelSegments: 5, | |
steps: 2, | |
bevelSize: 0.5, | |
bevelThickness: 0.3, | |
} | |
const heartGeometry = new THREE.ExtrudeGeometry( | |
heartShape, | |
extrudeSettings | |
) | |
// 머티리얼 설정 | |
const heartMaterial = new THREE.MeshPhongMaterial({ | |
color: isLiked ? 0xff6b6b : 0x666666, | |
shininess: 30, | |
transparent: true, | |
opacity: 0.9, | |
}) | |
const heart = new THREE.Mesh(heartGeometry, heartMaterial) | |
heart.position.set(0, -5, 0) | |
heart.rotation.set(0, 0, Math.PI) | |
heart.scale.set(0.8, 0.8, 0.8) | |
scene.add(heart) | |
// 조명 설정 | |
const ambientLight = new THREE.AmbientLight(0x404040, 0.6) | |
scene.add(ambientLight) | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8) | |
directionalLight.position.set(10, 10, 5) | |
scene.add(directionalLight) | |
// 포인트 라이트 (반짝임 효과용) | |
const pointLight = new THREE.PointLight(0xff69b4, 1, 100) | |
pointLight.position.set(0, 0, 10) | |
scene.add(pointLight) | |
camera.position.z = 20 | |
// 참조 저장 | |
sceneRef.current = scene | |
heartRef.current = heart | |
rendererRef.current = renderer | |
// 기본 애니메이션 루프 | |
const animate = () => { | |
animationIdRef.current = requestAnimationFrame(animate) | |
if (heartRef.current) { | |
heartRef.current.rotation.y += 0.01 | |
// 호버 효과를 위한 부드러운 스케일 변화 | |
const targetScale = isLiked ? 0.9 : 0.8 | |
heartRef.current.scale.lerp( | |
new THREE.Vector3(targetScale, targetScale, targetScale), | |
0.1 | |
) | |
} | |
renderer.render(scene, camera) | |
} | |
animate() | |
// 클린업 함수 | |
return () => { | |
if (animationIdRef.current) { | |
cancelAnimationFrame(animationIdRef.current) | |
} | |
if (mountRef.current && renderer.domElement) { | |
mountRef.current.removeChild(renderer.domElement) | |
} | |
renderer.dispose() | |
} | |
}, []) | |
// 좋아요 상태 변경 시 애니메이션 | |
useEffect(() => { | |
if (heartRef.current) { | |
const heart = heartRef.current | |
// 색상 변경 | |
heart.material.color.setHex(isLiked ? 0xff6b6b : 0x666666) | |
if (isLiked) { | |
// 클릭 애니메이션 | |
const originalScale = heart.scale.clone() | |
// 펄스 애니메이션 | |
const pulseAnimation = () => { | |
let time = 0 | |
const pulse = () => { | |
time += 0.1 | |
const scale = 0.9 + Math.sin(time * 8) * 0.2 | |
heart.scale.set(scale, scale, scale) | |
if (time < Math.PI / 4) { | |
requestAnimationFrame(pulse) | |
} else { | |
heart.scale.copy(originalScale) | |
} | |
} | |
pulse() | |
} | |
pulseAnimation() | |
// 파티클 효과 | |
createParticles() | |
} | |
} | |
}, [isLiked]) | |
// 파티클 효과 생성 | |
const createParticles = () => { | |
if (!sceneRef.current) return | |
const particleCount = 20 | |
const particles = new THREE.Group() | |
for (let i = 0; i < particleCount; i++) { | |
const particleGeometry = new THREE.SphereGeometry(0.1, 8, 8) | |
const particleMaterial = new THREE.MeshBasicMaterial({ | |
color: new THREE.Color().setHSL( | |
Math.random() * 0.1 + 0.9, | |
1, | |
0.7 | |
), | |
transparent: true, | |
opacity: 0.8, | |
}) | |
const particle = new THREE.Mesh(particleGeometry, particleMaterial) | |
particle.position.set( | |
(Math.random() - 0.5) * 5, | |
(Math.random() - 0.5) * 5, | |
(Math.random() - 0.5) * 5 | |
) | |
particle.velocity = new THREE.Vector3( | |
(Math.random() - 0.5) * 0.3, | |
Math.random() * 0.3 + 0.1, | |
(Math.random() - 0.5) * 0.3 | |
) | |
particles.add(particle) | |
} | |
sceneRef.current.add(particles) | |
// 파티클 애니메이션 | |
let time = 0 | |
const animateParticles = () => { | |
time += 0.05 | |
particles.children.forEach((particle) => { | |
particle.position.add(particle.velocity) | |
particle.material.opacity *= 0.95 | |
particle.scale.multiplyScalar(0.95) | |
}) | |
if (time < 2) { | |
requestAnimationFrame(animateParticles) | |
} else { | |
sceneRef.current.remove(particles) | |
} | |
} | |
animateParticles() | |
} | |
const handleLike = () => { | |
setIsLiked(!isLiked) | |
setLikes((prev) => (isLiked ? prev - 1 : prev + 1)) | |
} | |
return ( | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
alignItems: "center", | |
justifyContent: "center", | |
padding: "20px", | |
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", | |
borderRadius: "20px", | |
boxShadow: "0 20px 40px rgba(0,0,0,0.1)", | |
fontFamily: "Arial, sans-serif", | |
minHeight: "300px", | |
position: "relative", | |
overflow: "hidden", | |
}} | |
> | |
{/* 배경 장식 */} | |
<div | |
style={{ | |
position: "absolute", | |
top: "-50%", | |
left: "-50%", | |
width: "200%", | |
height: "200%", | |
background: | |
"radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px)", | |
backgroundSize: "30px 30px", | |
animation: "float 20s infinite linear", | |
pointerEvents: "none", | |
}} | |
/> | |
<style> | |
{` | |
@keyframes float { | |
0% { transform: translate(0, 0) rotate(0deg); } | |
100% { transform: translate(-30px, -30px) rotate(360deg); } | |
} | |
.like-button:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 10px 25px rgba(0,0,0,0.2); | |
} | |
`} | |
</style> | |
{/* 3D 하트 */} | |
<div | |
ref={mountRef} | |
style={{ | |
width: "200px", | |
height: "200px", | |
cursor: "pointer", | |
transition: "transform 0.2s ease", | |
borderRadius: "15px", | |
overflow: "hidden", | |
}} | |
onClick={handleLike} | |
className="like-button" | |
/> | |
{/* 좋아요 카운터 */} | |
<div | |
style={{ | |
marginTop: "15px", | |
display: "flex", | |
alignItems: "center", | |
gap: "10px", | |
background: "rgba(255,255,255,0.2)", | |
padding: "12px 20px", | |
borderRadius: "25px", | |
backdropFilter: "blur(10px)", | |
border: "1px solid rgba(255,255,255,0.3)", | |
}} | |
> | |
<span | |
style={{ | |
fontSize: "24px", | |
fontWeight: "bold", | |
color: "#fff", | |
textShadow: "0 2px 4px rgba(0,0,0,0.3)", | |
}} | |
> | |
{likes.toLocaleString()} | |
</span> | |
<span | |
style={{ | |
fontSize: "16px", | |
color: "rgba(255,255,255,0.9)", | |
fontWeight: "500", | |
}} | |
> | |
{likes === 1 ? "like" : "likes"} | |
</span> | |
</div> | |
{/* 상태 표시 */} | |
<div | |
style={{ | |
marginTop: "10px", | |
fontSize: "14px", | |
color: "rgba(255,255,255,0.8)", | |
transition: "all 0.3s ease", | |
}} | |
> | |
{isLiked ? "💖 Liked!" : "Click the heart to like"} | |
</div> | |
</div> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment