1212import { useState , useEffect , useRef } from 'react'
1313import type { GestureState , PinchRay } from '../hooks/useHandGestures'
1414
15- // Hand skeleton connections (pairs of landmark indices)
16- const HAND_CONNECTIONS = [
17- // Thumb
18- [ 0 , 1 ] , [ 1 , 2 ] , [ 2 , 3 ] , [ 3 , 4 ] ,
19- // Index finger
20- [ 0 , 5 ] , [ 5 , 6 ] , [ 6 , 7 ] , [ 7 , 8 ] ,
21- // Middle finger
22- [ 0 , 9 ] , [ 9 , 10 ] , [ 10 , 11 ] , [ 11 , 12 ] ,
23- // Ring finger
24- [ 0 , 13 ] , [ 13 , 14 ] , [ 14 , 15 ] , [ 15 , 16 ] ,
25- // Pinky
26- [ 0 , 17 ] , [ 17 , 18 ] , [ 18 , 19 ] , [ 19 , 20 ] ,
27- // Palm connections
28- [ 5 , 9 ] , [ 9 , 13 ] , [ 13 , 17 ] , [ 0 , 17 ] ,
29- ]
30-
31- // Finger groups for palm fill
32- const PALM_OUTLINE = [ 0 , 5 , 9 , 13 , 17 , 0 ] // Wrist and base of each finger
33-
3415// Fingertip indices
3516const FINGERTIPS = [ 4 , 8 , 12 , 16 , 20 ]
3617
@@ -278,11 +259,17 @@ interface GhostHandProps {
278259 isGhost ?: boolean
279260}
280261
281- function GhostHand ( { landmarks, color, gradientId, isGhost = false } : GhostHandProps ) {
282- // Calculate scale based on hand depth (z of wrist)
262+ /**
263+ * PuffyGlove - Mario-style white glove with puffy, rounded appearance
264+ * Semi-transparent with soft edges and volumetric feel
265+ */
266+ function GhostHand ( { landmarks, color : _color , gradientId : _gradientId , isGhost = false } : GhostHandProps ) {
267+ // INVERTED depth scaling: hand closer to camera (negative Z) = smaller (farther in 3D space)
268+ // Hand pulled back (positive Z) = bigger (closer to viewer)
283269 const wristZ = landmarks [ 0 ] . z || 0
284- const depthScale = 1 + wristZ * 5
285- const clampedScale = Math . max ( 0.3 , Math . min ( 1.5 , depthScale ) )
270+ // Invert: positive Z (hand far from camera) = bigger, negative Z (close to camera) = smaller
271+ const depthScale = 1 - wristZ * 4 // Inverted from before
272+ const clampedScale = Math . max ( 0.4 , Math . min ( 1.8 , depthScale ) )
286273
287274 // Un-mirror the X coordinate (webcam is mirrored, so flip it back)
288275 const toSvg = ( lm : { x : number ; y : number } ) => ( {
@@ -292,153 +279,196 @@ function GhostHand({ landmarks, color, gradientId, isGhost = false }: GhostHandP
292279
293280 const points = landmarks . map ( toSvg )
294281
295- // Base properties scaled with depth
296- const strokeWidth = 0.25 * clampedScale
297- const jointRadius = 0.35 * clampedScale
298- const fingertipRadius = 0.5 * clampedScale
299- const palmOpacity = isGhost ? 0.15 : 0.25
300- const lineOpacity = isGhost ? 0.3 : 0.5
301-
302- // Create palm fill path
303- const palmPath = PALM_OUTLINE . map ( ( idx , i ) => {
304- const p = points [ idx ]
305- return i === 0 ? `M ${ p . x } ${ p . y } ` : `L ${ p . x } ${ p . y } `
306- } ) . join ( ' ' ) + ' Z'
307-
308- // Create finger fill paths for 3D effect
309- const createFingerPath = ( base : number , _tip : number ) => {
310- const indices = [ base , base + 1 , base + 2 , base + 3 ]
311- const fingerPoints = indices . map ( i => points [ i ] )
312- // Create a rounded path along the finger
313- return `M ${ fingerPoints [ 0 ] . x } ${ fingerPoints [ 0 ] . y } ` +
314- `Q ${ fingerPoints [ 1 ] . x } ${ fingerPoints [ 1 ] . y } ${ fingerPoints [ 2 ] . x } ${ fingerPoints [ 2 ] . y } ` +
315- `Q ${ fingerPoints [ 2 ] . x } ${ fingerPoints [ 2 ] . y } ${ fingerPoints [ 3 ] . x } ${ fingerPoints [ 3 ] . y } `
282+ // Puffy glove sizing - everything is rounder and bigger
283+ const baseFingerWidth = 1.2 * clampedScale
284+ const fingertipRadius = 0.9 * clampedScale
285+ const knuckleRadius = 0.7 * clampedScale
286+ const gloveOpacity = isGhost ? 0.4 : 0.6
287+
288+ // Finger segment indices: [base, mid1, mid2, tip]
289+ const FINGERS = [
290+ [ 1 , 2 , 3 , 4 ] , // Thumb
291+ [ 5 , 6 , 7 , 8 ] , // Index
292+ [ 9 , 10 , 11 , 12 ] , // Middle
293+ [ 13 , 14 , 15 , 16 ] , // Ring
294+ [ 17 , 18 , 19 , 20 ] , // Pinky
295+ ]
296+
297+ // Create puffy finger path with rounded capsule segments
298+ const createPuffyFingerPath = ( fingerIndices : number [ ] ) => {
299+ const fingerPoints = fingerIndices . map ( i => points [ i ] )
300+ // Create a thick rounded path
301+ let path = `M ${ fingerPoints [ 0 ] . x } ${ fingerPoints [ 0 ] . y } `
302+ for ( let i = 1 ; i < fingerPoints . length ; i ++ ) {
303+ path += ` L ${ fingerPoints [ i ] . x } ${ fingerPoints [ i ] . y } `
304+ }
305+ return path
306+ }
307+
308+ // Palm center for radial gradient
309+ const palmCenter = {
310+ x : ( points [ 0 ] . x + points [ 5 ] . x + points [ 9 ] . x + points [ 13 ] . x + points [ 17 ] . x ) / 5 ,
311+ y : ( points [ 0 ] . y + points [ 5 ] . y + points [ 9 ] . y + points [ 13 ] . y + points [ 17 ] . y ) / 5 ,
316312 }
317313
314+ // Create smooth palm outline
315+ const palmPath = `
316+ M ${ points [ 0 ] . x } ${ points [ 0 ] . y }
317+ Q ${ points [ 1 ] . x } ${ points [ 1 ] . y } ${ points [ 2 ] . x } ${ points [ 2 ] . y }
318+ L ${ points [ 5 ] . x } ${ points [ 5 ] . y }
319+ Q ${ ( points [ 5 ] . x + points [ 9 ] . x ) / 2 } ${ Math . min ( points [ 5 ] . y , points [ 9 ] . y ) - 1 }
320+ ${ points [ 9 ] . x } ${ points [ 9 ] . y }
321+ Q ${ ( points [ 9 ] . x + points [ 13 ] . x ) / 2 } ${ Math . min ( points [ 9 ] . y , points [ 13 ] . y ) - 0.5 }
322+ ${ points [ 13 ] . x } ${ points [ 13 ] . y }
323+ Q ${ ( points [ 13 ] . x + points [ 17 ] . x ) / 2 } ${ Math . min ( points [ 13 ] . y , points [ 17 ] . y ) - 0.5 }
324+ ${ points [ 17 ] . x } ${ points [ 17 ] . y }
325+ L ${ points [ 0 ] . x } ${ points [ 0 ] . y }
326+ Z
327+ `
328+
318329 return (
319330 < g >
320- { /* Palm fill - gives 3D depth appearance */ }
321- < path
322- d = { palmPath }
323- fill = { `url(#${ gradientId } )` }
324- opacity = { palmOpacity }
325- />
326-
327- { /* Finger strokes with gradient for 3D effect */ }
328- { [ [ 1 , 4 ] , [ 5 , 8 ] , [ 9 , 12 ] , [ 13 , 16 ] , [ 17 , 20 ] ] . map ( ( [ base ] , idx ) => (
331+ { /* Definitions for this hand */ }
332+ < defs >
333+ { /* Puffy white glove gradient */ }
334+ < radialGradient id = { `glove-gradient-${ palmCenter . x . toFixed ( 0 ) } ` } cx = "30%" cy = "30%" r = "70%" >
335+ < stop offset = "0%" stopColor = "#ffffff" stopOpacity = "0.9" />
336+ < stop offset = "50%" stopColor = "#f0f0f5" stopOpacity = "0.7" />
337+ < stop offset = "100%" stopColor = "#d0d0e0" stopOpacity = "0.5" />
338+ </ radialGradient >
339+
340+ { /* Soft shadow/depth filter */ }
341+ < filter id = { `glove-shadow-${ palmCenter . x . toFixed ( 0 ) } ` } x = "-50%" y = "-50%" width = "200%" height = "200%" >
342+ < feGaussianBlur in = "SourceAlpha" stdDeviation = "0.8" result = "blur" />
343+ < feOffset in = "blur" dx = "0.3" dy = "0.5" result = "shadow" />
344+ < feComposite in = "SourceGraphic" in2 = "shadow" operator = "over" />
345+ </ filter >
346+
347+ { /* Soft glow for volumetric effect */ }
348+ < filter id = { `glove-glow-${ palmCenter . x . toFixed ( 0 ) } ` } x = "-100%" y = "-100%" width = "300%" height = "300%" >
349+ < feGaussianBlur stdDeviation = "1" result = "glow" />
350+ < feMerge >
351+ < feMergeNode in = "glow" />
352+ < feMergeNode in = "SourceGraphic" />
353+ </ feMerge >
354+ </ filter >
355+ </ defs >
356+
357+ { /* Main glove group with shadow */ }
358+ < g filter = { `url(#glove-shadow-${ palmCenter . x . toFixed ( 0 ) } )` } opacity = { gloveOpacity } >
359+
360+ { /* Palm - puffy rounded shape */ }
329361 < path
330- key = { `finger-fill-${ idx } ` }
331- d = { createFingerPath ( base === 1 ? 1 : base , base === 1 ? 4 : base + 3 ) }
332- fill = "none"
333- stroke = { color }
334- strokeWidth = { strokeWidth * 3 }
335- strokeLinecap = "round"
336- opacity = { 0.15 }
362+ d = { palmPath }
363+ fill = { `url(#glove-gradient-${ palmCenter . x . toFixed ( 0 ) } )` }
364+ stroke = "#ffffff"
365+ strokeWidth = { 0.3 * clampedScale }
366+ strokeOpacity = { 0.5 }
337367 />
338- ) ) }
339368
340- { /* Skeleton lines - thin glowing */ }
341- { HAND_CONNECTIONS . map ( ( [ i , j ] , idx ) => {
342- const p1 = points [ i ]
343- const p2 = points [ j ]
344- return (
345- < line
346- key = { `line-${ idx } ` }
347- x1 = { p1 . x }
348- y1 = { p1 . y }
349- x2 = { p2 . x }
350- y2 = { p2 . y }
351- stroke = { color }
352- strokeWidth = { strokeWidth }
353- strokeLinecap = "round"
354- opacity = { lineOpacity }
355- />
356- )
357- } ) }
358-
359- { /* Knuckle highlights - larger for 3D effect */ }
360- { KNUCKLES . map ( ( idx ) => {
361- const p = points [ idx ]
362- return (
363- < g key = { `knuckle-${ idx } ` } >
364- < circle
365- cx = { p . x }
366- cy = { p . y }
367- r = { jointRadius * 1.5 }
368- fill = { color }
369- opacity = { 0.2 }
369+ { /* Fingers - thick puffy tubes */ }
370+ { FINGERS . map ( ( fingerIndices , idx ) => (
371+ < g key = { `finger-${ idx } ` } >
372+ { /* Finger tube - thick white stroke for puffy look */ }
373+ < path
374+ d = { createPuffyFingerPath ( fingerIndices ) }
375+ fill = "none"
376+ stroke = "#ffffff"
377+ strokeWidth = { baseFingerWidth * 2.5 }
378+ strokeLinecap = "round"
379+ strokeLinejoin = "round"
380+ strokeOpacity = { 0.5 }
370381 />
371- < circle
372- cx = { p . x }
373- cy = { p . y }
374- r = { jointRadius }
375- fill = { color }
376- opacity = { 0.4 }
382+ { /* Inner highlight */ }
383+ < path
384+ d = { createPuffyFingerPath ( fingerIndices ) }
385+ fill = "none"
386+ stroke = "#f8f8ff"
387+ strokeWidth = { baseFingerWidth * 1.5 }
388+ strokeLinecap = "round"
389+ strokeLinejoin = "round"
390+ strokeOpacity = { 0.7 }
377391 />
378- </ g >
379- )
380- } ) }
381-
382- { /* Joint dots */ }
383- { points . map ( ( p , idx ) => {
384- const isFingertip = FINGERTIPS . includes ( idx )
385- const isKnuckle = KNUCKLES . includes ( idx )
386- if ( isKnuckle ) return null // Already rendered
387-
388- const radius = isFingertip ? fingertipRadius : jointRadius
389- return (
390- < circle
391- key = { `joint-${ idx } ` }
392- cx = { p . x }
393- cy = { p . y }
394- r = { radius }
395- fill = { color }
396- opacity = { isFingertip ? 0.7 : 0.4 }
397- />
398- )
399- } ) }
400-
401- { /* Fingertip glow - the main "ghost" effect */ }
402- { FINGERTIPS . map ( ( idx ) => {
403- const p = points [ idx ]
404- return (
405- < g key = { `fingertip-glow-${ idx } ` } >
406- { /* Outer glow */ }
407- < circle
408- cx = { p . x }
409- cy = { p . y }
410- r = { fingertipRadius * 3 }
411- fill = { color }
412- opacity = { 0.1 }
413- />
414- { /* Inner glow */ }
415- < circle
416- cx = { p . x }
417- cy = { p . y }
418- r = { fingertipRadius * 1.8 }
419- fill = { color }
420- opacity = { 0.2 }
421- />
422- { /* Core */ }
423- < circle
424- cx = { p . x }
425- cy = { p . y }
426- r = { fingertipRadius }
427- fill = "#ffffff"
428- opacity = { 0.5 }
392+ { /* Core line for definition */ }
393+ < path
394+ d = { createPuffyFingerPath ( fingerIndices ) }
395+ fill = "none"
396+ stroke = "#ffffff"
397+ strokeWidth = { baseFingerWidth * 0.8 }
398+ strokeLinecap = "round"
399+ strokeLinejoin = "round"
400+ strokeOpacity = { 0.9 }
429401 />
430402 </ g >
431- )
432- } ) }
433-
434- { /* Wrist indicator */ }
435- < circle
436- cx = { points [ 0 ] . x }
437- cy = { points [ 0 ] . y }
438- r = { jointRadius * 2 }
439- fill = { color }
440- opacity = { 0.3 }
441- />
403+ ) ) }
404+
405+ { /* Fingertip puffs - round ball ends like Mario gloves */ }
406+ { FINGERTIPS . map ( ( idx ) => {
407+ const p = points [ idx ]
408+ return (
409+ < g key = { `tip-${ idx } ` } >
410+ { /* Outer soft glow */ }
411+ < circle
412+ cx = { p . x }
413+ cy = { p . y }
414+ r = { fingertipRadius * 2 }
415+ fill = "#ffffff"
416+ opacity = { 0.3 }
417+ />
418+ { /* Main puff */ }
419+ < circle
420+ cx = { p . x }
421+ cy = { p . y }
422+ r = { fingertipRadius * 1.4 }
423+ fill = "#f8f8ff"
424+ opacity = { 0.7 }
425+ />
426+ { /* Highlight dot */ }
427+ < circle
428+ cx = { p . x - fingertipRadius * 0.3 }
429+ cy = { p . y - fingertipRadius * 0.3 }
430+ r = { fingertipRadius * 0.5 }
431+ fill = "#ffffff"
432+ opacity = { 0.9 }
433+ />
434+ </ g >
435+ )
436+ } ) }
437+
438+ { /* Knuckle bumps - subtle rounded protrusions */ }
439+ { KNUCKLES . map ( ( idx ) => {
440+ const p = points [ idx ]
441+ return (
442+ < g key = { `knuckle-${ idx } ` } >
443+ < circle
444+ cx = { p . x }
445+ cy = { p . y }
446+ r = { knuckleRadius * 1.8 }
447+ fill = "#f0f0f5"
448+ opacity = { 0.5 }
449+ />
450+ < circle
451+ cx = { p . x }
452+ cy = { p . y }
453+ r = { knuckleRadius }
454+ fill = "#ffffff"
455+ opacity = { 0.7 }
456+ />
457+ </ g >
458+ )
459+ } ) }
460+
461+ { /* Wrist cuff - puffy ring */ }
462+ < circle
463+ cx = { points [ 0 ] . x }
464+ cy = { points [ 0 ] . y }
465+ r = { knuckleRadius * 2.5 }
466+ fill = "none"
467+ stroke = "#ffffff"
468+ strokeWidth = { knuckleRadius * 1.5 }
469+ opacity = { 0.4 }
470+ />
471+ </ g >
442472 </ g >
443473 )
444474}
0 commit comments