Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Animated Hugging Face (Single File)</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Babel for JSX compilation in browser --> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* Custom Animation Utilities */ | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-15px); } | |
| } | |
| .animate-float { | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| @keyframes breathe { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.02); } | |
| } | |
| .animate-breathe { | |
| animation: breathe 4s ease-in-out infinite; | |
| } | |
| /* Hand Animations */ | |
| @keyframes hand-idle-left { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 50% { transform: translate(0, -4px) rotate(-5deg); } | |
| } | |
| @keyframes hand-idle-right { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 50% { transform: translate(0, -4px) rotate(5deg); } | |
| } | |
| .animate-hand-idle-left { | |
| animation: hand-idle-left 3s ease-in-out infinite; | |
| transform-origin: 35px 150px; | |
| } | |
| .animate-hand-idle-right { | |
| animation: hand-idle-right 3s ease-in-out infinite; | |
| animation-delay: 1.5s; | |
| transform-origin: 165px 150px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-900 text-slate-100 antialiased overflow-hidden"> | |
| <div id="root"></div> | |
| <script type="text/babel" data-type="module"> | |
| import React, { useState, useEffect, useRef } from 'https://esm.sh/[email protected]'; | |
| import { createRoot } from 'https://esm.sh/[email protected]/client'; | |
| import { Activity, Eye, Zap, Wind, Github, Heart } from 'https://esm.sh/[email protected]'; | |
| // --- Types (Simulated as Constants for JS) --- | |
| const Mood = { | |
| HAPPY: 'HAPPY', | |
| EXCITED: 'EXCITED', | |
| SLEEPY: 'SLEEPY', | |
| SURPRISED: 'SURPRISED' | |
| }; | |
| // --- Components --- | |
| // 1. HuggingFaceLogo Component | |
| const HuggingFaceLogo = ({ mood, config, isClicking }) => { | |
| // Store raw cursor position relative to center of SVG | |
| const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 }); | |
| const [blinkOpen, setBlinkOpen] = useState(true); | |
| const svgRef = useRef(null); | |
| // Unified Mouse/Touch tracking | |
| useEffect(() => { | |
| const updateCursorPosition = (clientX, clientY) => { | |
| if (!svgRef.current) return; | |
| const rect = svgRef.current.getBoundingClientRect(); | |
| const centerX = rect.left + rect.width / 2; | |
| const centerY = rect.top + rect.height / 2; | |
| setCursorPos({ | |
| x: clientX - centerX, | |
| y: clientY - centerY | |
| }); | |
| }; | |
| const handleMouseMove = (e) => updateCursorPosition(e.clientX, e.clientY); | |
| const handleTouchMove = (e) => { | |
| if (e.touches.length > 0) { | |
| updateCursorPosition(e.touches[0].clientX, e.touches[0].clientY); | |
| } | |
| }; | |
| window.addEventListener('mousemove', handleMouseMove); | |
| window.addEventListener('touchmove', handleTouchMove); | |
| return () => { | |
| window.removeEventListener('mousemove', handleMouseMove); | |
| window.removeEventListener('touchmove', handleTouchMove); | |
| }; | |
| }, []); | |
| // Blinking logic | |
| useEffect(() => { | |
| if (!config.blink || mood === Mood.SLEEPY) return; | |
| const blinkInterval = setInterval(() => { | |
| setBlinkOpen(false); | |
| setTimeout(() => setBlinkOpen(true), 150); | |
| }, 4000 + Math.random() * 2000); | |
| return () => clearInterval(blinkInterval); | |
| }, [config.blink, mood]); | |
| // CSS transforms based on state | |
| const containerClasses = [ | |
| 'w-64 h-64 md:w-96 md:h-96 transition-transform duration-300 ease-out cursor-pointer', | |
| config.floating ? 'animate-float' : '', | |
| config.breathing ? 'animate-breathe' : '', | |
| isClicking ? 'scale-95' : 'hover:scale-105', | |
| ].join(' '); | |
| // SVG Parts | |
| const renderMouth = () => { | |
| switch (mood) { | |
| case Mood.SURPRISED: | |
| return <ellipse cx="100" cy="140" rx="12" ry="15" fill="#4B3621" />; | |
| case Mood.SLEEPY: | |
| return ( | |
| <path | |
| d="M 85 145 Q 100 140 115 145" | |
| stroke="#4B3621" | |
| strokeWidth="5" | |
| fill="none" | |
| strokeLinecap="round" | |
| opacity="0.8" | |
| /> | |
| ); | |
| case Mood.EXCITED: | |
| return ( | |
| <path | |
| d="M 70 135 Q 100 180 130 135 Z" | |
| stroke="#4B3621" | |
| strokeWidth="4" | |
| fill="#6D4C41" | |
| strokeLinejoin="round" | |
| /> | |
| ); | |
| default: // HAPPY | |
| return ( | |
| <path | |
| d="M 75 140 Q 100 160 125 140" | |
| stroke="#4B3621" | |
| strokeWidth="6" | |
| fill="none" | |
| strokeLinecap="round" | |
| /> | |
| ); | |
| } | |
| }; | |
| const renderEyes = () => { | |
| // 1. Calculate Eye Position (Clamped) | |
| let pupilX = 0; | |
| let pupilY = 0; | |
| if (config.followMouse) { | |
| const limit = 14; | |
| const angle = Math.atan2(cursorPos.y, cursorPos.x); | |
| const dist = Math.min(Math.sqrt(cursorPos.x * cursorPos.x + cursorPos.y * cursorPos.y) / 8, limit); | |
| pupilX = Math.cos(angle) * dist; | |
| pupilY = Math.sin(angle) * dist; | |
| } | |
| if (mood === Mood.SLEEPY) { | |
| return ( | |
| <g opacity="0.8"> | |
| <path d="M 60 95 Q 75 105 90 95" stroke="#4B3621" strokeWidth="5" fill="none" strokeLinecap="round" /> | |
| <path d="M 110 95 Q 125 105 140 95" stroke="#4B3621" strokeWidth="5" fill="none" strokeLinecap="round" /> | |
| </g> | |
| ); | |
| } | |
| const eyeScaleY = blinkOpen ? 1 : 0.1; | |
| return ( | |
| <g className="transition-all duration-100" style={{ transformOrigin: '100px 95px', transform: `scaleY(${eyeScaleY})` }}> | |
| {/* Left Eye */} | |
| <g transform={`translate(${pupilX * 0.1}, ${pupilY * 0.1})`}> | |
| <ellipse cx="75" cy="95" rx="13" ry="15" fill="#4B3621" /> | |
| <circle cx={75 + pupilX} cy={95 + pupilY} r="5" fill="white" opacity="0.9" /> | |
| <circle cx={79 + pupilX} cy={92 + pupilY} r="2" fill="white" opacity="0.6" /> | |
| </g> | |
| {/* Right Eye */} | |
| <g transform={`translate(${pupilX * 0.1}, ${pupilY * 0.1})`}> | |
| <ellipse cx="125" cy="95" rx="13" ry="15" fill="#4B3621" /> | |
| <circle cx={125 + pupilX} cy={95 + pupilY} r="5" fill="white" opacity="0.9" /> | |
| <circle cx={129 + pupilX} cy={92 + pupilY} r="2" fill="white" opacity="0.6" /> | |
| </g> | |
| </g> | |
| ); | |
| }; | |
| const renderHands = () => { | |
| // Hand Geometry | |
| const handPath = "M 0 25 Q 0 10 15 5 Q 30 0 45 10 Q 55 20 50 35 L 45 50 Q 35 60 15 55 Q 5 50 0 25 Z"; | |
| // Dynamic Hand Movement Logic | |
| const reachLimit = 60; // Max pixels hands can travel from base | |
| const reachFactor = 0.4; // Sensitivity | |
| const targetX = Math.max(Math.min(cursorPos.x * reachFactor, reachLimit), -reachLimit); | |
| const targetY = Math.max(Math.min(cursorPos.y * reachFactor, reachLimit), -reachLimit); | |
| const baseHugX = 25; | |
| const baseHugY = -25; | |
| const leftX = isClicking ? baseHugX + targetX : 0; | |
| const leftY = isClicking ? baseHugY + targetY : 0; | |
| const rightX = isClicking ? -baseHugX + targetX : 0; | |
| const rightY = isClicking ? baseHugY + targetY : 0; | |
| const rotBase = 15; | |
| const rotDynamic = targetY * 0.3; | |
| const leftRot = isClicking ? -rotBase + rotDynamic : 0; | |
| const rightRot = isClicking ? rotBase - rotDynamic : 0; | |
| return ( | |
| <g> | |
| {/* Left Hand Container */} | |
| <g className="animate-hand-idle-left" style={{ transformOrigin: '35px 150px' }}> | |
| <g | |
| style={{ | |
| transform: `translate(${leftX}px, ${leftY}px) rotate(${leftRot}deg)`, | |
| transition: 'transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275)' | |
| }} | |
| > | |
| <g transform="translate(10, 135) rotate(-15)"> | |
| <path d={handPath} fill="black" opacity="0.1" transform="translate(4, 4)" /> | |
| <path d={handPath} fill="url(#handGradient)" stroke="#D4A000" strokeWidth="2.5" /> | |
| <ellipse cx="25" cy="25" rx="12" ry="15" fill="white" opacity="0.2" transform="rotate(-10)" /> | |
| </g> | |
| </g> | |
| </g> | |
| {/* Right Hand Container */} | |
| <g className="animate-hand-idle-right" style={{ transformOrigin: '165px 150px' }}> | |
| <g | |
| style={{ | |
| transform: `translate(${rightX}px, ${rightY}px) rotate(${rightRot}deg)`, | |
| transition: 'transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275)' | |
| }} | |
| > | |
| <g transform="translate(190, 135) rotate(15) scale(-1, 1)"> | |
| <path d={handPath} fill="black" opacity="0.1" transform="translate(4, 4)" /> | |
| <path d={handPath} fill="url(#handGradient)" stroke="#D4A000" strokeWidth="2.5" /> | |
| <ellipse cx="25" cy="25" rx="12" ry="15" fill="white" opacity="0.2" transform="rotate(-10)" /> | |
| </g> | |
| </g> | |
| </g> | |
| </g> | |
| ); | |
| }; | |
| return ( | |
| <div className={containerClasses}> | |
| <svg | |
| ref={svgRef} | |
| viewBox="0 0 200 200" | |
| className="w-full h-full drop-shadow-2xl" | |
| style={{ filter: 'drop-shadow(0px 25px 30px rgba(0,0,0,0.3))' }} | |
| > | |
| <defs> | |
| <radialGradient id="faceGradient" cx="40%" cy="30%" r="90%" fx="30%" fy="30%"> | |
| <stop offset="0%" stopColor="#FFEA60" /> | |
| <stop offset="60%" stopColor="#FFD23F" /> | |
| <stop offset="100%" stopColor="#F5C22B" /> | |
| </radialGradient> | |
| <radialGradient id="handGradient" cx="30%" cy="30%" r="80%"> | |
| <stop offset="0%" stopColor="#FFEA60" /> | |
| <stop offset="100%" stopColor="#FFC800" /> | |
| </radialGradient> | |
| <filter id="softGlow" x="-20%" y="-20%" width="140%" height="140%"> | |
| <feGaussianBlur in="SourceAlpha" stdDeviation="3" /> | |
| <feOffset dx="0" dy="2" result="offsetblur" /> | |
| <feComponentTransfer> | |
| <feFuncA type="linear" slope="0.3" /> | |
| </feComponentTransfer> | |
| <feMerge> | |
| <feMergeNode /> | |
| <feMergeNode in="SourceGraphic" /> | |
| </feMerge> | |
| </filter> | |
| </defs> | |
| {/* Face Shape */} | |
| <circle cx="100" cy="100" r="90" fill="url(#faceGradient)" stroke="#D4A000" strokeWidth="3" /> | |
| {/* Snout */} | |
| <ellipse cx="100" cy="125" rx="35" ry="25" fill="#FFFFFF" opacity="0.15" /> | |
| {/* Highlights */} | |
| <path d="M 50 40 Q 80 20 110 40" stroke="white" strokeWidth="8" strokeLinecap="round" opacity="0.1" fill="none" transform="rotate(-15 80 30)" /> | |
| <circle cx="140" cy="60" r="8" fill="white" opacity="0.15" /> | |
| {/* Cheeks */} | |
| <g> | |
| <ellipse cx="45" cy="115" rx="14" ry="9" fill="#FF5E5E" opacity="0.5" filter="url(#softGlow)" /> | |
| <ellipse cx="155" cy="115" rx="14" ry="9" fill="#FF5E5E" opacity="0.5" filter="url(#softGlow)" /> | |
| <circle cx="48" cy="112" r="2" fill="white" opacity="0.4" /> | |
| <circle cx="158" cy="112" r="2" fill="white" opacity="0.4" /> | |
| </g> | |
| {renderEyes()} | |
| {renderMouth()} | |
| {renderHands()} | |
| </svg> | |
| </div> | |
| ); | |
| }; | |
| // 2. Controls Component | |
| const Controls = ({ mood, setMood, config, setConfig }) => { | |
| const toggleConfig = (key) => { | |
| setConfig(prev => ({ ...prev, [key]: !prev[key] })); | |
| }; | |
| return ( | |
| <div className="bg-slate-800/80 backdrop-blur-md p-6 rounded-3xl border border-slate-700 shadow-xl w-full max-w-sm space-y-6"> | |
| {/* Mood Selector */} | |
| <div className="space-y-3"> | |
| <label className="text-xs font-bold text-slate-400 uppercase tracking-wider">Character Mood</label> | |
| <div className="grid grid-cols-2 gap-2"> | |
| {Object.values(Mood).map((m) => ( | |
| <button | |
| key={m} | |
| onClick={() => setMood(m)} | |
| className={`px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${ | |
| mood === m | |
| ? 'bg-yellow-500 text-slate-900 shadow-lg scale-105' | |
| : 'bg-slate-700 text-slate-300 hover:bg-slate-600' | |
| }`} | |
| > | |
| {m.charAt(0) + m.slice(1).toLowerCase()} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="h-px bg-slate-700 w-full" /> | |
| {/* Animation Toggles */} | |
| <div className="space-y-3"> | |
| <label className="text-xs font-bold text-slate-400 uppercase tracking-wider">Behaviors</label> | |
| <div className="space-y-2"> | |
| <button | |
| onClick={() => toggleConfig('followMouse')} | |
| className={`w-full flex items-center justify-between p-3 rounded-xl transition-all border ${ | |
| config.followMouse ? 'bg-indigo-600/20 border-indigo-500' : 'bg-slate-700/50 border-transparent hover:bg-slate-700' | |
| }`} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <Eye className={`w-5 h-5 ${config.followMouse ? 'text-indigo-400' : 'text-slate-500'}`} /> | |
| <span className="text-sm font-medium">Eye Tracking</span> | |
| </div> | |
| <div className={`w-10 h-5 rounded-full relative transition-colors ${config.followMouse ? 'bg-indigo-500' : 'bg-slate-600'}`}> | |
| <div className={`absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${config.followMouse ? 'left-6' : 'left-1'}`} /> | |
| </div> | |
| </button> | |
| <button | |
| onClick={() => toggleConfig('floating')} | |
| className={`w-full flex items-center justify-between p-3 rounded-xl transition-all border ${ | |
| config.floating ? 'bg-blue-600/20 border-blue-500' : 'bg-slate-700/50 border-transparent hover:bg-slate-700' | |
| }`} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <Activity className={`w-5 h-5 ${config.floating ? 'text-blue-400' : 'text-slate-500'}`} /> | |
| <span className="text-sm font-medium">Float Gravity</span> | |
| </div> | |
| <div className={`w-10 h-5 rounded-full relative transition-colors ${config.floating ? 'bg-blue-500' : 'bg-slate-600'}`}> | |
| <div className={`absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${config.floating ? 'left-6' : 'left-1'}`} /> | |
| </div> | |
| </button> | |
| <button | |
| onClick={() => toggleConfig('breathing')} | |
| className={`w-full flex items-center justify-between p-3 rounded-xl transition-all border ${ | |
| config.breathing ? 'bg-emerald-600/20 border-emerald-500' : 'bg-slate-700/50 border-transparent hover:bg-slate-700' | |
| }`} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <Wind className={`w-5 h-5 ${config.breathing ? 'text-emerald-400' : 'text-slate-500'}`} /> | |
| <span className="text-sm font-medium">Life Breathing</span> | |
| </div> | |
| <div className={`w-10 h-5 rounded-full relative transition-colors ${config.breathing ? 'bg-emerald-500' : 'bg-slate-600'}`}> | |
| <div className={`absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${config.breathing ? 'left-6' : 'left-1'}`} /> | |
| </div> | |
| </button> | |
| <button | |
| onClick={() => toggleConfig('blink')} | |
| className={`w-full flex items-center justify-between p-3 rounded-xl transition-all border ${ | |
| config.blink ? 'bg-amber-600/20 border-amber-500' : 'bg-slate-700/50 border-transparent hover:bg-slate-700' | |
| }`} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <Zap className={`w-5 h-5 ${config.blink ? 'text-amber-400' : 'text-slate-500'}`} /> | |
| <span className="text-sm font-medium">Auto Blink</span> | |
| </div> | |
| <div className={`w-10 h-5 rounded-full relative transition-colors ${config.blink ? 'bg-amber-500' : 'bg-slate-600'}`}> | |
| <div className={`absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${config.blink ? 'left-6' : 'left-1'}`} /> | |
| </div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 3. App Component | |
| const App = () => { | |
| const [mood, setMood] = useState(Mood.HAPPY); | |
| const [config, setConfig] = useState({ | |
| followMouse: true, | |
| floating: true, | |
| breathing: true, | |
| blink: true | |
| }); | |
| const [isClicking, setIsClicking] = useState(false); | |
| const handleMouseDown = () => setIsClicking(true); | |
| const handleMouseUp = () => setIsClicking(false); | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-indigo-950 flex flex-col items-center justify-center p-4 relative overflow-hidden"> | |
| {/* Background decoration */} | |
| <div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none opacity-20"> | |
| <div className="absolute top-20 left-20 w-72 h-72 bg-yellow-500 rounded-full blur-[100px]" /> | |
| <div className="absolute bottom-20 right-20 w-96 h-96 bg-indigo-500 rounded-full blur-[120px]" /> | |
| </div> | |
| <header className="absolute top-6 left-6 z-10 flex items-center gap-3"> | |
| <h1 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2"> | |
| Hugging Face <span className="text-yellow-400">Alive</span> | |
| </h1> | |
| </header> | |
| <main className="flex flex-col lg:flex-row items-center justify-center gap-12 z-10 w-full max-w-6xl"> | |
| {/* Character Stage */} | |
| <div | |
| className="relative group cursor-pointer" | |
| onMouseDown={handleMouseDown} | |
| onMouseUp={handleMouseUp} | |
| onTouchStart={handleMouseDown} | |
| onTouchEnd={handleMouseUp} | |
| > | |
| <div className="absolute inset-0 bg-yellow-400/10 rounded-full blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> | |
| <HuggingFaceLogo | |
| mood={mood} | |
| config={config} | |
| isClicking={isClicking} | |
| /> | |
| {/* Instruction hint */} | |
| <div className="absolute -bottom-12 left-1/2 -translate-x-1/2 text-slate-400 text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap"> | |
| Click to hug! | |
| </div> | |
| </div> | |
| {/* Controls Panel */} | |
| <div className="w-full max-w-sm"> | |
| <Controls | |
| mood={mood} | |
| setMood={setMood} | |
| config={config} | |
| setConfig={setConfig} | |
| /> | |
| </div> | |
| </main> | |
| <footer className="absolute bottom-6 text-slate-500 text-xs flex items-center gap-4"> | |
| <span className="flex items-center gap-1"> | |
| Made with <Heart className="w-3 h-3 text-red-500 fill-current" /> by a React Engineer | |
| </span> | |
| <a href="#" className="hover:text-white transition-colors flex items-center gap-1"> | |
| <Github className="w-3 h-3" /> View Source | |
| </a> | |
| </footer> | |
| </div> | |
| ); | |
| }; | |
| // --- Mount --- | |
| const root = createRoot(document.getElementById('root')); | |
| root.render(<App />); | |
| </script> | |
| </body> | |
| </html> |