add: stylesheet

This commit is contained in:
Tom Boullay
2026-04-28 09:07:56 +02:00
parent 06e59a972f
commit 7dea0f99a8
6 changed files with 63 additions and 5 deletions
Binary file not shown.
+13 -1
View File
@@ -1,4 +1,16 @@
import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot"; import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot";
import type { HandTrackingStatus } from "@/types/handTracking";
const STATUS_LABELS: Record<HandTrackingStatus, string> = {
idle: "Idle",
requesting_camera: "Requesting camera",
starting_camera: "Starting camera",
connecting_server: "Connecting server",
connecting: "Connecting",
connected: "Connected",
disconnected: "Disconnected",
error: "Error",
};
export function HandTrackingOverlay(): React.JSX.Element | null { export function HandTrackingOverlay(): React.JSX.Element | null {
const { hands, status, serverStatus, error } = useHandTrackingSnapshot(); const { hands, status, serverStatus, error } = useHandTrackingSnapshot();
@@ -12,7 +24,7 @@ export function HandTrackingOverlay(): React.JSX.Element | null {
return ( return (
<aside className="hand-tracking-overlay" aria-label="Hand tracking status"> <aside className="hand-tracking-overlay" aria-label="Hand tracking status">
<strong>Hand tracking</strong> <strong>Hand tracking</strong>
<span>Status: {status}</span> <span>Status: {STATUS_LABELS[status]}</span>
{serverStatus ? <span>Server: {serverStatus}</span> : null} {serverStatus ? <span>Server: {serverStatus}</span> : null}
<span>Hands: {hands.length}</span> <span>Hands: {hands.length}</span>
<span>Pinch: {pinching ? "yes" : "no"}</span> <span>Pinch: {pinching ? "yes" : "no"}</span>
+1
View File
@@ -5,6 +5,7 @@ export const HAND_TRACKING_FRAME_WIDTH = 320;
export const HAND_TRACKING_FRAME_HEIGHT = 240; export const HAND_TRACKING_FRAME_HEIGHT = 240;
export const HAND_TRACKING_TARGET_FPS = 10; export const HAND_TRACKING_TARGET_FPS = 10;
export const HAND_TRACKING_JPEG_QUALITY = 0.55; export const HAND_TRACKING_JPEG_QUALITY = 0.55;
export const HAND_TRACKING_CAMERA_TIMEOUT_MS = 8_000;
export const HAND_TRACKING_RESPONSE_TIMEOUT_MS = 1_500; export const HAND_TRACKING_RESPONSE_TIMEOUT_MS = 1_500;
export function getHandTrackingWsUrl(): string { export function getHandTrackingWsUrl(): string {
+44 -2
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { import {
HAND_TRACKING_CAMERA_TIMEOUT_MS,
HAND_TRACKING_FRAME_HEIGHT, HAND_TRACKING_FRAME_HEIGHT,
HAND_TRACKING_FRAME_WIDTH, HAND_TRACKING_FRAME_WIDTH,
HAND_TRACKING_JPEG_QUALITY, HAND_TRACKING_JPEG_QUALITY,
@@ -29,6 +30,32 @@ function getBase64Payload(dataUrl: string): string {
return dataUrl.slice(dataUrl.indexOf(",") + 1); return dataUrl.slice(dataUrl.indexOf(",") + 1);
} }
function getCameraStreamWithTimeout(
constraints: MediaStreamConstraints,
): Promise<MediaStream> {
let didTimeout = false;
const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
const timeoutPromise = new Promise<never>((_, reject) => {
window.setTimeout(() => {
didTimeout = true;
reject(
new Error(
"Camera request timed out. Restart Arc or check camera permissions for localhost:5173.",
),
);
}, HAND_TRACKING_CAMERA_TIMEOUT_MS);
});
streamPromise.then((stream) => {
if (didTimeout) {
stream.getTracks().forEach((track) => track.stop());
}
});
return Promise.race([streamPromise, timeoutPromise]);
}
export function useRemoteHandTracking({ export function useRemoteHandTracking({
enabled, enabled,
websocketUrl = getHandTrackingWsUrl(), websocketUrl = getHandTrackingWsUrl(),
@@ -116,13 +143,13 @@ export function useRemoteHandTracking({
setSnapshot({ setSnapshot({
hands: [], hands: [],
status: "connecting", status: "requesting_camera",
serverStatus: null, serverStatus: null,
error: null, error: null,
}); });
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await getCameraStreamWithTimeout({
video: { video: {
width: HAND_TRACKING_FRAME_WIDTH, width: HAND_TRACKING_FRAME_WIDTH,
height: HAND_TRACKING_FRAME_HEIGHT, height: HAND_TRACKING_FRAME_HEIGHT,
@@ -136,12 +163,27 @@ export function useRemoteHandTracking({
return; return;
} }
setSnapshot((current) => ({
...current,
status: "starting_camera",
}));
const video = document.createElement("video"); const video = document.createElement("video");
video.muted = true; video.muted = true;
video.playsInline = true; video.playsInline = true;
video.srcObject = stream; video.srcObject = stream;
await video.play(); await video.play();
if (cancelled) {
stream.getTracks().forEach((track) => track.stop());
return;
}
setSnapshot((current) => ({
...current,
status: "connecting_server",
}));
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = HAND_TRACKING_FRAME_WIDTH; canvas.width = HAND_TRACKING_FRAME_WIDTH;
canvas.height = HAND_TRACKING_FRAME_HEIGHT; canvas.height = HAND_TRACKING_FRAME_HEIGHT;
+2 -2
View File
@@ -1,6 +1,6 @@
:root { :root {
color-scheme: dark; color-scheme: dark;
font-family: Inter; font-family: Helvetica, Arial, sans-serif;
} }
html, html,
@@ -83,7 +83,7 @@ canvas {
.hand-tracking-overlay { .hand-tracking-overlay {
position: fixed; position: fixed;
right: 16px; right: 16px;
bottom: 16px; bottom: 132px;
z-index: 20; z-index: 20;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+3
View File
@@ -10,6 +10,9 @@ export interface HandTrackingHand {
export type HandTrackingStatus = export type HandTrackingStatus =
| "idle" | "idle"
| "requesting_camera"
| "starting_camera"
| "connecting_server"
| "connecting" | "connecting"
| "connected" | "connected"
| "disconnected" | "disconnected"