|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const drawLandmarks = (ctx, landmarks, canvas, isMobile) => { |
|
|
|
const connections = [ |
|
|
|
[0, 1], [1, 2], [2, 3], [3, 4], |
|
|
|
[0, 5], [5, 6], [6, 7], [7, 8], |
|
|
|
[9, 10], [10, 11], [11, 12], [5, 9], |
|
|
|
[9, 13], [13, 14], [14, 15], [15, 16], |
|
|
|
[13, 17], [17, 18], [18, 19], [19, 20], |
|
|
|
[0, 17] |
|
]; |
|
|
|
|
|
ctx.lineWidth = isMobile ? 2 : 3; |
|
ctx.strokeStyle = '#217BFE'; |
|
|
|
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(); |
|
} |
|
|
|
|
|
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(); |
|
}); |
|
|
|
|
|
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(); |
|
}); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const analyzeHandGesture = (landmarks) => { |
|
const thumbTip = landmarks[4]; |
|
const indexTip = landmarks[8]; |
|
const wrist = landmarks[0]; |
|
|
|
|
|
|
|
const isLeftHand = !(thumbTip.x < wrist.x); |
|
|
|
|
|
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) |
|
); |
|
|
|
|
|
const isOpen = distance > 0.1; |
|
|
|
return { |
|
isOpen, |
|
isLeftHand, |
|
thumbPosition: { |
|
x: thumbTip.x, |
|
y: thumbTip.y |
|
} |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
} |
|
`; |
|
}; |