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/c97b8189886af6eb9b375f592d54b205 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 { useState, useEffect, useRef, startTransition } from "react" | |
import { addPropertyControls, ControlType, useIsStaticRenderer } from "framer" | |
import { | |
motion, | |
useAnimation, | |
useMotionValue, | |
useTransform, | |
AnimatePresence, | |
} from "framer-motion" | |
/** | |
* 3D Heart Like Button with animation and counter | |
* | |
* @framerIntrinsicWidth 100 | |
* @framerIntrinsicHeight 100 | |
* | |
* @framerSupportedLayoutWidth fixed | |
* @framerSupportedLayoutHeight fixed | |
*/ | |
export default function LikeButton(props) { | |
const { | |
initialLikes = 0, | |
heartColor = "#FF5A5F", | |
counterColor = "#333333", | |
counterFont, | |
showCounter = true, | |
pulseEffect = true, | |
rotationEffect = true, | |
confettiEffect = true, | |
depth3D = 20, // New prop for controlling 3D depth | |
} = props | |
const [liked, setLiked] = useState(false) | |
const [likes, setLikes] = useState(initialLikes) | |
const [confetti, setConfetti] = useState([]) | |
const isStatic = useIsStaticRenderer() | |
const containerRef = useRef(null) | |
const controls = useAnimation() | |
const rotateY = useMotionValue(0) | |
const rotateX = useMotionValue(0) | |
const scale = useMotionValue(1) | |
const opacity = useMotionValue(1) | |
// Enhanced 3D effect with more dramatic color and lighting changes | |
const color = useTransform( | |
rotateY, | |
[-45, 0, 45], | |
[ | |
liked ? shadeColor(heartColor, -20) : "#CCCCCC", | |
liked ? heartColor : "#AAAAAA", | |
liked ? shadeColor(heartColor, -30) : "#DDDDDD", | |
] | |
) | |
const shadowX = useTransform( | |
rotateY, | |
[-45, 0, 45], | |
["12px", "0px", "-12px"] | |
) | |
const shadowBlur = useTransform( | |
rotateY, | |
[-45, 0, 45], | |
["16px", "24px", "16px"] | |
) | |
const shadowOpacity = useTransform(rotateY, [-45, 0, 45], [0.7, 0.5, 0.7]) | |
// Function to darken or lighten colors for 3D effect | |
function shadeColor(color, percent) { | |
if (!color.startsWith("#")) { | |
// If it's not a hex color, return as is | |
return color | |
} | |
let R = parseInt(color.substring(1, 3), 16) | |
let G = parseInt(color.substring(3, 5), 16) | |
let B = parseInt(color.substring(5, 7), 16) | |
R = Math.floor((R * (100 + percent)) / 100) | |
G = Math.floor((G * (100 + percent)) / 100) | |
B = Math.floor((B * (100 + percent)) / 100) | |
R = R < 255 ? R : 255 | |
G = G < 255 ? G : 255 | |
B = B < 255 ? B : 255 | |
R = R > 0 ? R : 0 | |
G = G > 0 ? G : 0 | |
B = B > 0 ? B : 0 | |
const RR = | |
R.toString(16).length === 1 ? "0" + R.toString(16) : R.toString(16) | |
const GG = | |
G.toString(16).length === 1 ? "0" + G.toString(16) : G.toString(16) | |
const BB = | |
B.toString(16).length === 1 ? "0" + B.toString(16) : B.toString(16) | |
return "#" + RR + GG + BB | |
} | |
const handleLike = () => { | |
if (isStatic) return | |
startTransition(() => { | |
setLiked(!liked) | |
setLikes((prev) => (!liked ? prev + 1 : prev - 1)) | |
}) | |
if (!liked) { | |
// Heart animation | |
controls.start({ | |
scale: [1, 1.2, 0.9, 1.1, 1], | |
transition: { duration: 0.6, times: [0, 0.2, 0.4, 0.6, 1] }, | |
}) | |
// Generate confetti if enabled | |
if (confettiEffect) { | |
const newConfetti = [] | |
for (let i = 0; i < 12; i++) { | |
newConfetti.push({ | |
id: Date.now() + i, | |
x: Math.random() * 60 - 30, | |
y: Math.random() * -60 - 20, | |
rotation: Math.random() * 360, | |
scale: Math.random() * 0.6 + 0.4, | |
color: [heartColor, "#FFD700", "#7FFFD4", "#FF69B4"][ | |
Math.floor(Math.random() * 4) | |
], | |
}) | |
} | |
startTransition(() => setConfetti(newConfetti)) | |
// Clear confetti after animation | |
setTimeout(() => { | |
startTransition(() => setConfetti([])) | |
}, 1000) | |
} | |
} | |
} | |
// Enhanced 3D rotation effect with more dramatic angles | |
const handleMouseMove = (e) => { | |
if (!rotationEffect || isStatic) return | |
const rect = containerRef.current.getBoundingClientRect() | |
const centerX = rect.left + rect.width / 2 | |
const centerY = rect.top + rect.height / 2 | |
const rotateYValue = ((e.clientX - centerX) / (rect.width / 2)) * 60 // Increased from 45 to 60 | |
const rotateXValue = ((centerY - e.clientY) / (rect.height / 2)) * 60 // Increased from 45 to 60 | |
rotateY.set(rotateYValue) | |
rotateX.set(rotateXValue) | |
} | |
const handleMouseLeave = () => { | |
if (isStatic) return | |
rotateY.set(0) | |
rotateX.set(0) | |
} | |
// Pulse animation | |
useEffect(() => { | |
if (isStatic || !pulseEffect) return | |
let interval | |
if (liked) { | |
interval = setInterval(() => { | |
controls.start({ | |
scale: [1, 1.08, 1], | |
transition: { duration: 1.5, ease: "easeInOut" }, | |
}) | |
}, 2000) | |
} | |
return () => clearInterval(interval) | |
}, [liked, pulseEffect, isStatic, controls]) | |
return ( | |
<div | |
ref={containerRef} | |
style={{ | |
position: "relative", | |
width: "100%", | |
height: "100%", | |
display: "flex", | |
flexDirection: "column", | |
alignItems: "center", | |
justifyContent: "center", | |
perspective: "800px", // Increased perspective for more dramatic 3D effect | |
cursor: "pointer", | |
}} | |
onMouseMove={handleMouseMove} | |
onMouseLeave={handleMouseLeave} | |
> | |
<motion.div | |
animate={controls} | |
style={{ | |
rotateY, | |
rotateX, | |
scale, | |
opacity, | |
transformStyle: "preserve-3d", | |
display: "flex", | |
justifyContent: "center", | |
alignItems: "center", | |
width: "60%", | |
height: "60%", | |
transformOrigin: "center center", | |
}} | |
onClick={handleLike} | |
> | |
{/* Back face (shadow) for 3D effect */} | |
<motion.div | |
style={{ | |
position: "absolute", | |
width: "100%", | |
height: "100%", | |
color: "#000000", | |
opacity: 0.2, | |
filter: "blur(2px)", | |
transform: `translateZ(-${depth3D / 2}px) scale(0.95)`, // Position shadow based on depth3D prop | |
}} | |
> | |
<svg | |
viewBox="0 0 24 24" | |
fill="currentColor" | |
style={{ width: "100%", height: "100%" }} | |
> | |
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" /> | |
</svg> | |
</motion.div> | |
{/* Main heart with 3D effects */} | |
<motion.div | |
style={{ | |
width: "100%", | |
height: "100%", | |
color, | |
filter: `drop-shadow(${shadowX} 5px ${shadowBlur} rgba(0,0,0,${shadowOpacity}))`, | |
transform: `translateZ(${depth3D}px)`, // Push forward in 3D space based on depth3D prop | |
backfaceVisibility: "hidden", | |
}} | |
> | |
<svg | |
viewBox="0 0 24 24" | |
fill="currentColor" | |
style={{ width: "100%", height: "100%" }} | |
> | |
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" /> | |
</svg> | |
</motion.div> | |
</motion.div> | |
{/* Confetti effect */} | |
<AnimatePresence> | |
{confetti.map((particle) => ( | |
<motion.div | |
key={particle.id} | |
initial={{ | |
x: 0, | |
y: 0, | |
opacity: 1, | |
scale: particle.scale, | |
rotate: 0, | |
}} | |
animate={{ | |
x: particle.x, | |
y: particle.y, | |
opacity: 0, | |
rotate: particle.rotation, | |
}} | |
exit={{ opacity: 0 }} | |
transition={{ duration: 1, ease: "easeOut" }} | |
style={{ | |
position: "absolute", | |
width: "8px", | |
height: "8px", | |
borderRadius: "50%", | |
backgroundColor: particle.color, | |
zIndex: -1, | |
}} | |
/> | |
))} | |
</AnimatePresence> | |
{/* Counter */} | |
{showCounter && ( | |
<motion.div | |
initial={{ opacity: 0, y: 10 }} | |
animate={{ opacity: 1, y: 0 }} | |
transition={{ delay: 0.2 }} | |
style={{ | |
marginTop: "8px", | |
color: counterColor, | |
...counterFont, | |
}} | |
> | |
{likes} | |
</motion.div> | |
)} | |
</div> | |
) | |
} | |
addPropertyControls(LikeButton, { | |
initialLikes: { | |
type: ControlType.Number, | |
title: "Initial Likes", | |
defaultValue: 0, | |
min: 0, | |
step: 1, | |
}, | |
heartColor: { | |
type: ControlType.Color, | |
title: "Heart Color", | |
defaultValue: "#FF5A5F", | |
}, | |
counterColor: { | |
type: ControlType.Color, | |
title: "Counter Color", | |
defaultValue: "#333333", | |
hidden: ({ showCounter }) => !showCounter, | |
}, | |
counterFont: { | |
type: ControlType.Font, | |
title: "Counter Font", | |
defaultValue: { | |
fontSize: "16px", | |
variant: "Bold", | |
textAlign: "center", | |
}, | |
controls: "extended", | |
defaultFontType: "sans-serif", | |
hidden: ({ showCounter }) => !showCounter, | |
}, | |
showCounter: { | |
type: ControlType.Boolean, | |
title: "Show Counter", | |
defaultValue: true, | |
enabledTitle: "Show", | |
disabledTitle: "Hide", | |
}, | |
pulseEffect: { | |
type: ControlType.Boolean, | |
title: "Pulse Effect", | |
defaultValue: true, | |
enabledTitle: "On", | |
disabledTitle: "Off", | |
}, | |
rotationEffect: { | |
type: ControlType.Boolean, | |
title: "3D Rotation", | |
defaultValue: true, | |
enabledTitle: "On", | |
disabledTitle: "Off", | |
}, | |
confettiEffect: { | |
type: ControlType.Boolean, | |
title: "Confetti", | |
defaultValue: true, | |
enabledTitle: "On", | |
disabledTitle: "Off", | |
}, | |
depth3D: { | |
type: ControlType.Number, | |
title: "3D Depth", | |
defaultValue: 20, | |
min: 5, | |
max: 50, | |
step: 1, | |
hidden: ({ rotationEffect }) => !rotationEffect, | |
}, | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment