@@ -45,6 +45,7 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
4545 const navigate = useNavigate ( ) ;
4646 const videoRef = useRef < HTMLVideoElement > ( null ) ;
4747 const preProcessCanvasRef = useRef < HTMLCanvasElement | null > ( null ) ;
48+ const overlayCanvasRef = useRef < HTMLCanvasElement | null > ( null ) ;
4849 const modelRef = useRef < tf . GraphModel | null > ( null ) ;
4950 const detectorRef = useRef < handPoseDetection . HandDetector | null > ( null ) ;
5051 const labelsRef = useRef < string [ ] > ( [ ] ) ;
@@ -57,6 +58,7 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
5758 const [ handsDetected , setHandsDetected ] = useState ( false ) ;
5859 const [ showTranslator , setShowTranslator ] = useState ( false ) ;
5960 const [ showHelp , setShowHelp ] = useState ( false ) ;
61+ const [ showHandTracking , setShowHandTracking ] = useState ( true ) ;
6062 const { toast } = useToast ( ) ;
6163
6264 // Sentence building state
@@ -275,6 +277,74 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
275277 return { label, score : bestScore } ;
276278 } , [ mode ] ) ;
277279
280+ // Draw hand tracking landmarks on overlay canvas
281+ const drawHandLandmarks = useCallback ( ( hands : handPoseDetection . Hand [ ] ) => {
282+ if ( ! overlayCanvasRef . current || ! videoRef . current || ! showHandTracking ) {
283+ return ;
284+ }
285+
286+ const canvas = overlayCanvasRef . current ;
287+ const ctx = canvas . getContext ( '2d' ) ;
288+ if ( ! ctx ) return ;
289+
290+ // Clear previous drawings
291+ ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
292+
293+ if ( ! hands . length ) return ;
294+
295+ // Draw each hand
296+ hands . forEach ( ( hand ) => {
297+ const keypoints = hand . keypoints ;
298+ if ( ! keypoints || keypoints . length !== 21 ) return ;
299+
300+ // Draw connections between landmarks
301+ const connections = [
302+ // Thumb
303+ [ 0 , 1 ] , [ 1 , 2 ] , [ 2 , 3 ] , [ 3 , 4 ] ,
304+ // Index finger
305+ [ 0 , 5 ] , [ 5 , 6 ] , [ 6 , 7 ] , [ 7 , 8 ] ,
306+ // Middle finger
307+ [ 0 , 9 ] , [ 9 , 10 ] , [ 10 , 11 ] , [ 11 , 12 ] ,
308+ // Ring finger
309+ [ 0 , 13 ] , [ 13 , 14 ] , [ 14 , 15 ] , [ 15 , 16 ] ,
310+ // Pinky
311+ [ 0 , 17 ] , [ 17 , 18 ] , [ 18 , 19 ] , [ 19 , 20 ] ,
312+ // Palm
313+ [ 5 , 9 ] , [ 9 , 13 ] , [ 13 , 17 ]
314+ ] ;
315+
316+ // Draw lines
317+ ctx . strokeStyle = '#00FF00' ;
318+ ctx . lineWidth = 2 ;
319+ connections . forEach ( ( [ start , end ] ) => {
320+ const startPoint = keypoints [ start ] ;
321+ const endPoint = keypoints [ end ] ;
322+ if ( startPoint && endPoint ) {
323+ ctx . beginPath ( ) ;
324+ ctx . moveTo ( startPoint . x , startPoint . y ) ;
325+ ctx . lineTo ( endPoint . x , endPoint . y ) ;
326+ ctx . stroke ( ) ;
327+ }
328+ } ) ;
329+
330+ // Draw keypoints
331+ keypoints . forEach ( ( keypoint , index ) => {
332+ // Wrist is larger and different color
333+ if ( index === 0 ) {
334+ ctx . fillStyle = '#FF0000' ;
335+ ctx . beginPath ( ) ;
336+ ctx . arc ( keypoint . x , keypoint . y , 6 , 0 , 2 * Math . PI ) ;
337+ ctx . fill ( ) ;
338+ } else {
339+ ctx . fillStyle = '#00FF00' ;
340+ ctx . beginPath ( ) ;
341+ ctx . arc ( keypoint . x , keypoint . y , 4 , 0 , 2 * Math . PI ) ;
342+ ctx . fill ( ) ;
343+ }
344+ } ) ;
345+ } ) ;
346+ } , [ showHandTracking ] ) ;
347+
278348 const computeCropBox = useCallback ( ( hand : handPoseDetection . Hand , frameWidth : number , frameHeight : number ) : CropBox => {
279349 const points = hand . keypoints ?? [ ] ;
280350 if ( ! points . length ) {
@@ -341,6 +411,9 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
341411 flipHorizontal : false ,
342412 } ) ;
343413
414+ // Draw hand tracking overlay
415+ drawHandLandmarks ( hands ) ;
416+
344417 if ( ! hands . length ) {
345418 setPrediction ( "—" ) ;
346419 setConfidence ( 0 ) ;
@@ -418,7 +491,7 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
418491 } ;
419492
420493 detectFrame ( ) ;
421- } , [ computeCropBox , getTopPrediction , preprocessFrame , preprocessLandmarks , useLandmarks ] ) ;
494+ } , [ computeCropBox , getTopPrediction , preprocessFrame , preprocessLandmarks , useLandmarks , drawHandLandmarks ] ) ;
422495
423496 // Sentence building functions - track how long a letter is held
424497 const updateLetterHold = useCallback ( ( letter : string ) => {
@@ -533,6 +606,31 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
533606 } ;
534607 } , [ mode , isModeEnabled , inputWidth , inputHeight , initCamera , loadModelAssets , loadHandDetector , stopCamera , stopDetection ] ) ;
535608
609+ // Setup overlay canvas dimensions when video is ready
610+ useEffect ( ( ) => {
611+ if ( videoRef . current && overlayCanvasRef . current && isCameraReady ) {
612+ const video = videoRef . current ;
613+ const canvas = overlayCanvasRef . current ;
614+
615+ const updateCanvasSize = ( ) => {
616+ canvas . width = video . videoWidth ;
617+ canvas . height = video . videoHeight ;
618+ } ;
619+
620+ // Set initial size
621+ if ( video . videoWidth > 0 ) {
622+ updateCanvasSize ( ) ;
623+ }
624+
625+ // Update on video metadata loaded
626+ video . addEventListener ( 'loadedmetadata' , updateCanvasSize ) ;
627+
628+ return ( ) => {
629+ video . removeEventListener ( 'loadedmetadata' , updateCanvasSize ) ;
630+ } ;
631+ }
632+ } , [ isCameraReady ] ) ;
633+
536634 useEffect ( ( ) => {
537635 if ( ! isModeEnabled ) {
538636 return ;
@@ -636,6 +734,12 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
636734 style = { { transform : "scaleX(-1)" } }
637735 muted
638736 />
737+ { /* Hand tracking overlay canvas */ }
738+ < canvas
739+ ref = { overlayCanvasRef }
740+ className = "absolute top-0 left-0 w-full h-full pointer-events-none scale-x-[-1]"
741+ style = { { transform : "scaleX(-1)" } }
742+ />
639743 { ! isCameraReady && (
640744 < div className = "absolute inset-0 flex items-center justify-center bg-muted/80 z-10" >
641745 < p className = "text-lg text-muted-foreground" >
@@ -654,11 +758,22 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
654758 </ div >
655759 ) }
656760 { isCameraReady && isModelReady && isDetectorReady && (
657- < div className = { `absolute top-4 right-4 px-3 py-2 rounded-lg text-sm z-10 transition-colors ${
761+ < >
762+ < div className = { `absolute top-4 right-4 px-3 py-2 rounded-lg text-sm z-10 transition-colors ${
658763 handsDetected ? 'bg-pastel-green text-gray-800' : 'bg-muted text-muted-foreground'
659764 } `} >
660765 { handsDetected ? '✋ Hand Detected' : '👋 Show your hand' }
661766 </ div >
767+ { /* Hand tracking toggle */ }
768+ < Button
769+ variant = "outline"
770+ size = "sm"
771+ onClick = { ( ) => setShowHandTracking ( ! showHandTracking ) }
772+ className = "absolute top-4 left-4 z-10 bg-background/80 backdrop-blur-sm"
773+ >
774+ { showHandTracking ? '👁️ Hide Tracking' : '👁️ Show Tracking' }
775+ </ Button >
776+ </ >
662777 ) }
663778 { ! isModeEnabled && (
664779 < div className = "absolute inset-0 flex items-center justify-center bg-muted/80 z-10" >
@@ -795,6 +910,8 @@ const LiveDemo = ({ mode, onBack, onOpenDataRecorder }: LiveDemoProps) => {
795910 < li > • Keep your hand clearly visible in the camera frame</ li >
796911 < li > • Position yourself 1-2 feet from the camera</ li >
797912 < li > • Good lighting helps improve detection accuracy</ li >
913+ < li > • < strong > Green dots and lines show hand tracking in real-time</ strong > </ li >
914+ < li > • Use the "Show/Hide Tracking" button to toggle hand landmarks</ li >
798915 < li > • < strong > Hold each gesture for 3 seconds</ strong > to capture the letter</ li >
799916 < li > • Watch the progress bar fill up as you hold the gesture</ li >
800917 < li > • To capture the same letter twice (e.g., "HELLO"), briefly remove your hand between captures</ li >
0 commit comments