Product Hunt Gold Foil / Holographic Badge

February 28, 2025

834 words by Pascal Pixel

Product Hunt Gold Foil / Holographic Badge

Author
Pascal
Date
Feb 28, 2025 07:34 PM
Slug
holographic-badge
Tags
Blog
Description
"use client"; import { useState, useEffect, useRef, memo, useMemo } from "react"; interface BadgeProps { url?: string; width?: number; height?: number; textColor?: string; borderColor?: string; bgColor?: string; maxRotationX?: number; maxRotationY?: number; title?: string; subtitle?: string; } type Point = { x: number; y: number; }; const ANIMATION_CONFIG = { SPRING_STIFFNESS: 200, SPRING_DAMPING: 10, IDLE_ANIMATION_DURATION: 8, MAX_DELTA_TIME: 0.016, } as const; const RAINBOW_COLORS = [ "hsl(358, 100%, 62%)", "hsl(30, 100%, 50%)", "hsl(60, 100%, 50%)", "hsl(96, 100%, 50%)", "hsl(233, 85%, 47%)", "hsl(271, 85%, 47%)", "hsl(300, 20%, 35%)", "transparent", "transparent", "white", ] as const; const createMatrix = ({ x, y }: Point) => ({ scaleX: Math.cos((y * Math.PI) / 180), shearY1: 0, rotationY: Math.sin((y * Math.PI) / 180), perspective1: 0, rotationCompoundXY: Math.sin((x * Math.PI) / 180) * Math.sin((y * Math.PI) / 180), scaleY: Math.cos((x * Math.PI) / 180), rotationCompound: -Math.sin((x * Math.PI) / 180) * Math.cos((y * Math.PI) / 180), perspective2: 0, rotationX: -Math.sin((y * Math.PI) / 180), rotationZ: Math.sin((x * Math.PI) / 180), scaleZ: Math.cos((x * Math.PI) / 180) * Math.cos((y * Math.PI) / 180), perspective3: 0, translateX: 0, translateY: 0, translateZ: 0, perspectiveFactor: 1, }); function useAnimation(target: Point, isHovering: boolean) { const speed = useRef<Point>({ x: 0, y: 0 }); const [rotation, setRotation] = useState<Point>({ x: 0, y: 0 }); const frameRef = useRef<number>(null); const lastTimeRef = useRef(performance.now()); useEffect(() => { function animate() { const now = performance.now(); const delta = Math.min( (now - lastTimeRef.current) / 1000, ANIMATION_CONFIG.MAX_DELTA_TIME, ); lastTimeRef.current = now; // Calculate the current target based on interaction state const currentTarget = isHovering ? target : { // Create a figure-8 motion path for idle state x: Math.sin((now / 4000) * Math.PI), y: Math.sin((now / 2000) * Math.PI), }; // Spring physics const springForceX = (currentTarget.x - rotation.x) * ANIMATION_CONFIG.SPRING_STIFFNESS; const springForceY = (currentTarget.y - rotation.y) * ANIMATION_CONFIG.SPRING_STIFFNESS; const dampingForceX = -speed.current.x * ANIMATION_CONFIG.SPRING_DAMPING; const dampingForceY = -speed.current.y * ANIMATION_CONFIG.SPRING_DAMPING; // Update speed with spring and damping forces speed.current = { x: speed.current.x + (springForceX + dampingForceX) * delta, y: speed.current.y + (springForceY + dampingForceY) * delta, }; // Update position setRotation((prev) => ({ x: prev.x + speed.current.x * delta, y: prev.y + speed.current.y * delta, })); frameRef.current = requestAnimationFrame(animate); } frameRef.current = requestAnimationFrame(animate); return () => { if (frameRef.current) { cancelAnimationFrame(frameRef.current); } }; }, [target, rotation, isHovering]); return rotation; } function AwardLogo({ color }: { color: string }) { return ( <g transform="translate(8, 9)"> <path fill={color} d="M14.963 9.075c.787-3-.188-5.887-.188-5.887S12.488 5.175 11.7 8.175c-.787 3 .188 5.887.188 5.887s2.25-1.987 3.075-4.987m-4.5 1.987c.787 3-.188 5.888-.188 5.888S7.988 14.962 7.2 11.962c-.787-3 .188-5.887.188-5.887s2.287 1.987 3.075 4.987m.862 10.388s-.6-2.962-2.775-5.175C6.337 14.1 3.375 13.5 3.375 13.5s.6 2.962 2.775 5.175c2.213 2.175 5.175 2.775 5.175 2.775m3.3 3.413s-1.988-2.288-4.988-3.075-5.887.187-5.887.187 1.987 2.287 4.988 3.075c3 .787 5.887-.188 5.887-.188Zm6.75 0s1.988-2.288 4.988-3.075c3-.826 5.887.187 5.887.187s-1.988 2.287-4.988 3.075c-3 .787-5.887-.188-5.887-.188ZM32.625 13.5s-2.963.6-5.175 2.775c-2.213 2.213-2.775 5.175-2.775 5.175s2.962-.6 5.175-2.775c2.175-2.213 2.775-5.175 2.775-5.175M28.65 6.075s.975 2.887.188 5.887c-.826 3-3.076 4.988-3.076 4.988s-.974-2.888-.187-5.888c.788-3 3.075-4.987 3.075-4.987m-4.5 7.987s.975-2.887.188-5.887c-.788-3-3.076-4.988-3.076-4.988s-.974 2.888-.187 5.888c.788 3 3.075 4.988 3.075 4.988ZM18 26.1c.975-.225 3.113-.6 5.325 0 3 .788 5.063 3.038 5.063 3.038s-2.888.975-5.888.187a13 13 0 0 1-1.425-.525c.563.788 1.125 1.425 2.288 1.913l-.863 2.062c-2.063-.862-2.925-2.137-3.675-3.262-.262-.375-.525-.713-.787-1.05-.26.293-.465.586-.686.903l-.102.147-.048.068c-.775 1.108-1.643 2.35-3.627 3.194l-.862-2.062c1.162-.488 1.725-1.125 2.287-1.913-.45.225-.938.375-1.425.525-3 .788-5.887-.187-5.887-.187s1.987-2.288 4.987-3.075c2.212-.563 4.35-.188 5.325.037" /> </g> ); } function ShinyEffect({ width, height, shineAngle, }: { width: number; height: number; shineAngle: number; }) { return ( <g style={{ mixBlendMode: "overlay" }} mask="url(#badgeMask)"> {RAINBOW_COLORS.map((color, i) => ( <g key={i} style={{ transform: `rotate(${shineAngle * 3.2 + i * 10 - 20}deg)`, transformOrigin: "center center", }} > <polygon points={`0,0 ${width},${height} ${width},0 0,${height}`} fill={color} filter="url(#blur1)" opacity="0.5" /> </g> ))} </g> ); } export default function GoldenKittyBadge({ url = "https://www.producthunt.com/golden-kitty-awards/hall-of-fame?year=2024#maker-of-the-year-10", width = 260, height = 54, bgColor = "hsl(54, 100%, 49%)", borderColor = "hsl(54, 100%, 45%)", textColor = "hsl(40, 100%, 30%)", maxRotationX = 15, maxRotationY = 15, title = "PRODUCT HUNT", subtitle = "Maker of the Year 2024", }: BadgeProps) { const [isHovering, setIsHovering] = useState(false); const [target, setTarget] = useState<Point>({ x: 0, y: 0 }); const rotation = useAnimation(target, isHovering); const matrix = useMemo(() => createMatrix(rotation), [rotation]); const shineAngle = useMemo( () => Math.hypot(rotation.x, rotation.y) * 2, [rotation.x, rotation.y], ); const handleMouseMove = useMemo( () => (e: React.MouseEvent<HTMLAnchorElement>) => { if (!isHovering) return; const rect = e.currentTarget.getBoundingClientRect(); setTarget({ x: ((e.clientY - rect.top) / rect.height - 0.5) * 2 * maxRotationX, y: -((e.clientX - rect.left) / rect.width - 0.5) * 2 * maxRotationY, }); }, [isHovering, maxRotationX, maxRotationY], ); const handleMouseLeave = useMemo( () => () => { setIsHovering(false); setTarget({ x: 0, y: 0 }); }, [], ); return ( <a href={url} target="_blank" rel="noopener" className="block relative" onMouseMove={handleMouseMove} onMouseEnter={() => setIsHovering(true)} onMouseLeave={handleMouseLeave} > <div style={{ transform: `perspective(500px) matrix3d(${Object.values(matrix).join( ",", )})`, transformOrigin: "center center", }} > <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${width} ${height}`} className="w-[180px] sm:w-[260px] h-auto" > <defs> <filter id="blur1"> <feGaussianBlur in="SourceGraphic" stdDeviation="3" /> </filter> <mask id="badgeMask"> <rect width={width} height={height} fill="white" rx="10" /> </mask> </defs> <rect width={width} height={height} rx="10" fill={bgColor} /> <rect x="4" y="4" width={width - 8} height={height - 8} rx="8" fill="transparent" stroke={borderColor} strokeWidth="1" /> <text fontFamily="Helvetica-Bold, Helvetica" fontSize="9" fontWeight="bold" fill={textColor} x="53" y="20" > {title} </text> <text fontFamily="Helvetica-Bold, Helvetica" fontSize="16" fontWeight="bold" fill={textColor} x="52" y="40" > {subtitle} </text> <AwardLogo color={textColor} /> <ShinyEffect width={width} height={height} shineAngle={shineAngle} /> </svg> </div> </a> ); }

Get Horse Browser

The browser designed for ADHD minds and research workflows. Organize your browsing with Trails® and stay focused on what matters.

Try 7 days free
Then $60/YEAR
Japanese Green TeasGoogle Search
Japanese Green TeaWikipedia
MatchaWikipedia
SenchaWikipedia

Join Our Community

Stay connected with updates, participate in discussions, and help shape the future of Horse Browser through our community channels.

Handled through Mailchimp. Unsubscribe anytime.
Horse Browser NewsletterIssue #12

Turn your Browser into the ultimate Research system.

You don't need a todo list, or a notes app. Your browser can do these things. But it should be more integrated than simply loading a website. This is where Horse Browser comes in, with built-in productivity features that make your browser a powerful tool.

Need Help?

Access your account, manage billing, get support, and find answers to frequently asked questions about Horse Browser.

Pascal and Elly at Disneysea Tokyo

Explore Resources

Access our comprehensive knowledge base, user manual, affiliate program details, and learn more about the team behind Horse Browser.

Pascal and Elly at Disneysea Tokyo