Spaces:
Sleeping
Sleeping
| /** | |
| * Utility functions for hand detection and drawing | |
| */ | |
| /** | |
| * Draw hand landmarks on canvas | |
| * @param {CanvasRenderingContext2D} ctx - Canvas context | |
| * @param {Array} landmarks - Hand landmarks from MediaPipe | |
| * @param {HTMLCanvasElement} canvas - Canvas element | |
| * @param {boolean} isMobile - Whether the device is mobile | |
| */ | |
| export const drawLandmarks = (ctx, landmarks, canvas, isMobile) => { | |
| // Draw connections | |
| const connections = [ | |
| // Thumb | |
| [0, 1], [1, 2], [2, 3], [3, 4], | |
| // Index finger | |
| [0, 5], [5, 6], [6, 7], [7, 8], | |
| // Middle finger | |
| [9, 10], [10, 11], [11, 12], [5, 9], | |
| // Ring finger | |
| [9, 13], [13, 14], [14, 15], [15, 16], | |
| // Pinky | |
| [13, 17], [17, 18], [18, 19], [19, 20], | |
| // Palm | |
| [0, 17] | |
| ]; | |
| // Use the requested colors | |
| ctx.lineWidth = isMobile ? 2 : 3; // Thinner lines on mobile | |
| ctx.strokeStyle = '#217BFE'; // Primary hand color | |
| for (const [start, end] of connections) { | |
| ctx.beginPath(); | |
| ctx.moveTo(landmarks[start].x * canvas.width, landmarks[start].y * canvas.height); | |
| ctx.lineTo(landmarks[end].x * canvas.width, landmarks[end].y * canvas.height); | |
| ctx.stroke(); | |
| } | |
| // Draw landmarks with secondary color | |
| ctx.fillStyle = '#AC87EB'; | |
| landmarks.forEach(landmark => { | |
| ctx.beginPath(); | |
| ctx.arc(landmark.x * canvas.width, landmark.y * canvas.height, isMobile ? 3 : 4, 0, 2 * Math.PI); | |
| ctx.fill(); | |
| }); | |
| // Highlight thumb and index finger | |
| const thumbTip = landmarks[4]; | |
| const indexTip = landmarks[8]; | |
| ctx.fillStyle = '#217BFE'; | |
| [thumbTip, indexTip].forEach(tip => { | |
| ctx.beginPath(); | |
| ctx.arc(tip.x * canvas.width, tip.y * canvas.height, isMobile ? 4 : 6, 0, 2 * Math.PI); | |
| ctx.fill(); | |
| }); | |
| }; | |
| /** | |
| * Check if hand is open (thumb away from index finger) | |
| * @param {Array} landmarks - Hand landmarks from MediaPipe | |
| * @returns {Object} - Object containing isOpen and other hand information | |
| */ | |
| export const analyzeHandGesture = (landmarks) => { | |
| const thumbTip = landmarks[4]; // Thumb tip | |
| const indexTip = landmarks[8]; // Index finger tip | |
| const wrist = landmarks[0]; // Wrist | |
| // Determine if it's a left or right hand | |
| // If thumb is to the left of the wrist, it's likely a right hand | |
| const isLeftHand = !(thumbTip.x < wrist.x); | |
| // Calculate distance between thumb and index finger | |
| const distance = Math.sqrt( | |
| Math.pow(thumbTip.x - indexTip.x, 2) + | |
| Math.pow(thumbTip.y - indexTip.y, 2) + | |
| Math.pow(thumbTip.z - indexTip.z, 2) | |
| ); | |
| // Hand is open when distance is greater than threshold | |
| const isOpen = distance > 0.1; | |
| return { | |
| isOpen, | |
| isLeftHand, | |
| thumbPosition: { | |
| x: thumbTip.x, | |
| y: thumbTip.y | |
| } | |
| }; | |
| }; | |
| /** | |
| * Create animation keyframes for hand detection animations | |
| * @returns {string} - CSS keyframes string | |
| */ | |
| export const getAnimationKeyframes = () => { | |
| return ` | |
| @keyframes thinking-blink { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.7; transform: scale(0.95); } | |
| } | |
| @keyframes spring-out { | |
| 0% { | |
| transform: scale(0) translateY(0); | |
| opacity: 0; | |
| } | |
| 20% { | |
| transform: scale(0.3) translateY(-5px); | |
| opacity: 0.7; | |
| } | |
| 40% { | |
| transform: scale(1.5) translateY(-30px); | |
| } | |
| 60% { | |
| transform: scale(0.8) translateY(15px); | |
| } | |
| 75% { | |
| transform: scale(1.2) translateY(-10px); | |
| } | |
| 90% { | |
| transform: scale(0.95) translateY(5px); | |
| } | |
| 100% { | |
| transform: scale(1) translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| @keyframes wiggle { | |
| 0% { transform: rotate(0deg); } | |
| 15% { transform: rotate(-15deg); } | |
| 30% { transform: rotate(12deg); } | |
| 45% { transform: rotate(-8deg); } | |
| 60% { transform: rotate(5deg); } | |
| 75% { transform: rotate(-2deg); } | |
| 100% { transform: rotate(0deg); } | |
| } | |
| /* Combined animation that handles both scale and rotation */ | |
| @keyframes spring-wiggle { | |
| 0% { | |
| transform: scale(0) rotate(0deg) translateY(0); | |
| opacity: 0; | |
| } | |
| 15% { | |
| transform: scale(0.2) rotate(-5deg) translateY(-5px); | |
| opacity: 0.5; | |
| } | |
| 30% { | |
| transform: scale(1.5) rotate(12deg) translateY(-30px); | |
| opacity: 1; | |
| } | |
| 45% { | |
| transform: scale(0.8) rotate(-8deg) translateY(15px); | |
| } | |
| 60% { | |
| transform: scale(1.2) rotate(5deg) translateY(-10px); | |
| } | |
| 75% { | |
| transform: scale(0.95) rotate(-2deg) translateY(5px); | |
| } | |
| 90% { | |
| transform: scale(1.05) rotate(1deg) translateY(-2px); | |
| } | |
| 100% { | |
| transform: scale(1) rotate(0deg) translateY(0); | |
| } | |
| } | |
| /* Add new animations for particles and popping */ | |
| @keyframes float-particle { | |
| 0% { | |
| transform: translate(0, 0) rotate(0deg); | |
| opacity: 1; | |
| } | |
| 100% { | |
| transform: translate(var(--tx), var(--ty)) rotate(var(--r)); | |
| opacity: 0; | |
| } | |
| } | |
| @keyframes pop-out { | |
| 0% { | |
| transform: scale(1); | |
| opacity: 1; | |
| } | |
| 50% { | |
| transform: scale(1.2); | |
| } | |
| 100% { | |
| transform: scale(0); | |
| opacity: 0; | |
| } | |
| } | |
| `; | |
| }; |