From c4cad629c945cd716d373e6dba4accc5093b7de0 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Wed, 3 Jun 2026 00:42:05 +0200 Subject: [PATCH] feat(handtracking): redesign SVG hand as primary visualization Rewrite the live hand visualizer as a light-blue silhouette with a crisp dark-blue outline, suitable as the primary hand UI (replacing the buggy 3D glove for the default flow): - Palm polygon (landmarks 0,1,5,9,13,17) and five finger tubes merged via an SVG feMorphology filter, so the outline is a single continuous ring with no internal seams. - Q curves bow out to two synthetic wrist corners (perpendicular to the palm centerline) for a rounded heel of palm. - Straight L edges between MCPs along the top - the filter dilation rounds the corners visually, no creux. - Each finger path starts half a stroke inside the palm so the round base cap is hidden under the palm fill. - Whole silhouette shrunk to 65% of the tracked hand size around the centroid, with 0.8 group opacity, and a faint MediaPipe skeleton overlay (lines + dots) on top. Update the static fallback silhouettes (HandTrackingFallback) to a matching curved-path look in a 100x120 viewBox. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/ui/HandTrackingFallback.tsx | 77 +++++-- src/components/ui/HandTrackingVisualizer.tsx | 217 +++++++++++++++---- src/index.css | 8 +- 3 files changed, 235 insertions(+), 67 deletions(-) diff --git a/src/components/ui/HandTrackingFallback.tsx b/src/components/ui/HandTrackingFallback.tsx index b5d2d53..e874992 100644 --- a/src/components/ui/HandTrackingFallback.tsx +++ b/src/components/ui/HandTrackingFallback.tsx @@ -4,29 +4,70 @@ import { type HandTrackingGloveHandedness, } from "@/hooks/handTracking/useHandTrackingGloveStatus"; -// Simple schematic silhouettes used as a last-resort fallback when the -// rigged glove model has failed to load. Both icons share the same -// 48x48 viewBox and the same stroke/fill rules from the .css. +// Hand silhouettes used as a last-resort fallback when the rigged glove +// model has failed to load. Both icons share a 100x120 viewBox so finger +// lengths and the thumb angle stay anatomically readable. const OpenHandShape = (): React.JSX.Element => ( - <> - - - - - - - + ); const FistShape = (): React.JSX.Element => ( <> - - - - - - + + + + + + ); @@ -66,7 +107,7 @@ export function HandTrackingFallback(): React.JSX.Element | null { = [ +// MediaPipe indexes the 21 hand landmarks predictably: +// 0 wrist, 1-4 thumb (base→tip), 5-8 index, 9-12 middle, 13-16 ring, 17-20 pinky. +const FINGER_LANDMARKS: Array = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + [17, 18, 19, 20], +]; +const SKELETON_BONES: Array<[number, number]> = [ [0, 1], [1, 2], [2, 3], @@ -26,70 +34,187 @@ const HAND_CONNECTIONS: Array<[number, number]> = [ [0, 17], ]; -const LANDMARK_FILL = "#67e8f9"; // cyan-300, opaque interior -const LANDMARK_STROKE = "#0c4a6e"; // sky-900, dark blue outline -const LANDMARK_STROKE_FIST = "#1e3a8a"; // blue-900, thicker accent when fist -const CONNECTION_STROKE = "#ffffff"; // white bones -const INDEX_TIP_LANDMARK = 8; +const HAND_FILL = "#bfdbfe"; // blue-200, light interior +const HAND_OUTLINE_COLOR = "#1e3a8a"; // blue-900, crisp dark outline +const HAND_OUTLINE_RADIUS = 2; // px +// Shrink the rendered hand around its centroid. Grab/physics keep using raw +// landmarks elsewhere, so the silhouette is just visually smaller. +const RENDER_SCALE = 0.65; +const FINGER_THICKNESS_FACTOR = 0.08; // fraction of (scaled) hand length +const WRIST_HALF_WIDTH = 0.28; +const SKELETON_STROKE = "rgba(30, 58, 138, 0.22)"; +const SKELETON_DOT_FILL = "rgba(30, 58, 138, 0.35)"; +const FILTER_ID = "hand-tracking-outline"; export function HandTrackingVisualizer(): React.JSX.Element | null { const { hands, status } = useHandTrackingSnapshot(); - const showHandTrackingSvg = useDebugStore((debug) => - debug.getShowHandTrackingSvg(), - ); - const gloves = useHandTrackingGloveStatus((state) => state.gloves); - const hasLoadedGlove = Object.values(gloves).some( - (gloveStatus) => gloveStatus === "loaded", + const showHandTrackingModel = useDebugStore((debug) => + debug.getShowHandTrackingModel(), ); - if ( - status === "idle" || - hands.length === 0 || - (hasLoadedGlove && !showHandTrackingSvg) - ) { + if (status === "idle" || hands.length === 0 || showHandTrackingModel) { return null; } + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + return (