Skip to content

Instantly share code, notes, and snippets.

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