Skip to content

Commit 3c0bb81

Browse files
jack-arturoclaude
andcommitted
feat(graph-viewer): puffy white Mario-style gloves with inverted depth
Visual improvements: - Inverted depth scaling: hand closer to camera = smaller (farther in 3D) - Puffy white glove appearance like Mario/cartoon hands - Semi-transparent with soft edges and volumetric feel - Rounded fingertips with highlight dots - Knuckle bumps and wrist cuff details - Layered strokes create puffy tube effect on fingers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 97fc7c1 commit 3c0bb81

2 files changed

Lines changed: 190 additions & 160 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState, useCallback } from 'react'
22

33
// Build version - update this when making significant changes
4-
const BUILD_VERSION = '2024-12-11-gesture-v5'
4+
const BUILD_VERSION = '2024-12-11-glove-v6'
55
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'
66
import { useGraphSnapshot } from './hooks/useGraphData'
77
import { useAuth } from './hooks/useAuth'

src/components/Hand2DOverlay.tsx

Lines changed: 189 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,6 @@
1212
import { useState, useEffect, useRef } from 'react'
1313
import 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
3516
const 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

Comments
 (0)