Skip to content

Instantly share code, notes, and snippets.

@ruucm-working
Created May 29, 2025 10:32
Show Gist options
  • Save ruucm-working/01a423aa894e7a679e412135daf1958b to your computer and use it in GitHub Desktop.
Save ruucm-working/01a423aa894e7a679e412135daf1958b to your computer and use it in GitHub Desktop.
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