Tina Tarighian
commited on
Commit
·
bb6831d
1
Parent(s):
0a001bd
button
Browse files- components/CameraSetup.js +3 -20
- components/MainContent.js +2 -7
- components/ThoughtBubble.js +3 -4
- hooks/useDeviceAndCanvas.js +1 -8
- pages/index.js +4 -4
- styles/globals.css +0 -47
components/CameraSetup.js
CHANGED
@@ -35,13 +35,12 @@ const CameraSetup = ({
|
|
35 |
tracks.forEach(track => track.stop());
|
36 |
}
|
37 |
|
38 |
-
// Get camera stream
|
39 |
const stream = await navigator.mediaDevices.getUserMedia({
|
40 |
video: {
|
41 |
facingMode: facingMode,
|
42 |
-
width: { ideal:
|
43 |
-
height: { ideal:
|
44 |
-
aspectRatio: { ideal: 4/3 } // Add ideal aspect ratio
|
45 |
},
|
46 |
audio: false
|
47 |
});
|
@@ -142,22 +141,6 @@ const CameraSetup = ({
|
|
142 |
|
143 |
try {
|
144 |
await videoRef.current.play();
|
145 |
-
|
146 |
-
// Explicitly handle metadata loading after camera switch
|
147 |
-
videoRef.current.onloadedmetadata = () => {
|
148 |
-
if (!isMounted.current) return;
|
149 |
-
|
150 |
-
const videoWidth = videoRef.current.videoWidth;
|
151 |
-
const videoHeight = videoRef.current.videoHeight;
|
152 |
-
const aspectRatio = videoWidth / videoHeight;
|
153 |
-
|
154 |
-
// Update aspect ratio and canvas size
|
155 |
-
setVideoAspectRatio(aspectRatio);
|
156 |
-
updateCanvasSize(aspectRatio);
|
157 |
-
|
158 |
-
console.log(`Camera switched with aspect ratio: ${aspectRatio}`);
|
159 |
-
};
|
160 |
-
|
161 |
} catch (playError) {
|
162 |
console.log("Play interrupted during camera switch:", playError);
|
163 |
// Don't treat play interruptions as fatal errors
|
|
|
35 |
tracks.forEach(track => track.stop());
|
36 |
}
|
37 |
|
38 |
+
// Get camera stream
|
39 |
const stream = await navigator.mediaDevices.getUserMedia({
|
40 |
video: {
|
41 |
facingMode: facingMode,
|
42 |
+
width: { ideal: 1920 },
|
43 |
+
height: { ideal: 1080 }
|
|
|
44 |
},
|
45 |
audio: false
|
46 |
});
|
|
|
141 |
|
142 |
try {
|
143 |
await videoRef.current.play();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
} catch (playError) {
|
145 |
console.log("Play interrupted during camera switch:", playError);
|
146 |
// Don't treat play interruptions as fatal errors
|
components/MainContent.js
CHANGED
@@ -45,7 +45,7 @@ const MainContent = ({
|
|
45 |
/>
|
46 |
)}
|
47 |
|
48 |
-
<div ref={containerRef} className=
|
49 |
{/* Camera Setup */}
|
50 |
<CameraSetup
|
51 |
videoRef={videoRef}
|
@@ -63,12 +63,7 @@ const MainContent = ({
|
|
63 |
<canvas
|
64 |
ref={canvasRef}
|
65 |
className="rounded-lg shadow-lg w-full"
|
66 |
-
style={{
|
67 |
-
height: `${canvasHeight}px`,
|
68 |
-
maxHeight: isMobile ? '70vh' : 'none',
|
69 |
-
objectFit: 'cover',
|
70 |
-
objectPosition: 'center'
|
71 |
-
}}
|
72 |
width={canvasWidth}
|
73 |
height={canvasHeight}
|
74 |
/>
|
|
|
45 |
/>
|
46 |
)}
|
47 |
|
48 |
+
<div ref={containerRef} className="relative w-full max-w-4xl canvas-container">
|
49 |
{/* Camera Setup */}
|
50 |
<CameraSetup
|
51 |
videoRef={videoRef}
|
|
|
63 |
<canvas
|
64 |
ref={canvasRef}
|
65 |
className="rounded-lg shadow-lg w-full"
|
66 |
+
style={{ height: `${canvasHeight}px` }}
|
|
|
|
|
|
|
|
|
|
|
67 |
width={canvasWidth}
|
68 |
height={canvasHeight}
|
69 |
/>
|
components/ThoughtBubble.js
CHANGED
@@ -127,14 +127,13 @@ const ThoughtBubble = ({
|
|
127 |
};
|
128 |
}
|
129 |
|
130 |
-
|
131 |
-
const offset = isMobile ? 8 : 12; // Reduced from 12/20
|
132 |
|
133 |
if (isLeftHand) {
|
134 |
// For left hand, position to the right of thumb
|
135 |
return {
|
136 |
position: 'absolute',
|
137 |
-
top: `${thumbPosition.y - (isMobile ?
|
138 |
left: `${thumbPosition.x + offset}px`,
|
139 |
width: getBubbleWidth(),
|
140 |
maxWidth: isThinking ? 'none' : `${canvasWidth - thumbPosition.x - (offset * 2)}px` // Prevent overflow
|
@@ -143,7 +142,7 @@ const ThoughtBubble = ({
|
|
143 |
// For right hand, position to the left of thumb
|
144 |
return {
|
145 |
position: 'absolute',
|
146 |
-
top: `${thumbPosition.y - (isMobile ?
|
147 |
right: `${canvasWidth - thumbPosition.x + offset}px`,
|
148 |
width: getBubbleWidth(),
|
149 |
maxWidth: isThinking ? 'none' : `${thumbPosition.x - (offset * 2)}px` // Prevent overflow
|
|
|
127 |
};
|
128 |
}
|
129 |
|
130 |
+
const offset = isMobile ? 12 : 20; // Space between thumb and bubble
|
|
|
131 |
|
132 |
if (isLeftHand) {
|
133 |
// For left hand, position to the right of thumb
|
134 |
return {
|
135 |
position: 'absolute',
|
136 |
+
top: `${thumbPosition.y - (isMobile ? 20 : 30)}px`,
|
137 |
left: `${thumbPosition.x + offset}px`,
|
138 |
width: getBubbleWidth(),
|
139 |
maxWidth: isThinking ? 'none' : `${canvasWidth - thumbPosition.x - (offset * 2)}px` // Prevent overflow
|
|
|
142 |
// For right hand, position to the left of thumb
|
143 |
return {
|
144 |
position: 'absolute',
|
145 |
+
top: `${thumbPosition.y - (isMobile ? 20 : 30)}px`,
|
146 |
right: `${canvasWidth - thumbPosition.x + offset}px`,
|
147 |
width: getBubbleWidth(),
|
148 |
maxWidth: isThinking ? 'none' : `${thumbPosition.x - (offset * 2)}px` // Prevent overflow
|
hooks/useDeviceAndCanvas.js
CHANGED
@@ -45,14 +45,7 @@ const useDeviceAndCanvas = () => {
|
|
45 |
const containerWidth = document.querySelector('.canvas-container')?.clientWidth || window.innerWidth;
|
46 |
// Set maximum width for the canvas - increased for desktop
|
47 |
const maxWidth = Math.min(containerWidth, isMobile ? 640 : 960);
|
48 |
-
|
49 |
-
// Calculate height based on aspect ratio, but reduce height on mobile
|
50 |
-
let height = maxWidth / aspectRatio;
|
51 |
-
|
52 |
-
// For mobile, reduce the height by 25% to make it more compact
|
53 |
-
if (isMobile) {
|
54 |
-
height = height * 0.75;
|
55 |
-
}
|
56 |
|
57 |
setCanvasWidth(maxWidth);
|
58 |
setCanvasHeight(height);
|
|
|
45 |
const containerWidth = document.querySelector('.canvas-container')?.clientWidth || window.innerWidth;
|
46 |
// Set maximum width for the canvas - increased for desktop
|
47 |
const maxWidth = Math.min(containerWidth, isMobile ? 640 : 960);
|
48 |
+
const height = maxWidth / aspectRatio;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
setCanvasWidth(maxWidth);
|
51 |
setCanvasHeight(height);
|
pages/index.js
CHANGED
@@ -11,9 +11,9 @@ const inter = Inter({ subsets: ['latin'] });
|
|
11 |
|
12 |
const Header = () => {
|
13 |
return (
|
14 |
-
<div className="
|
15 |
<div className="w-full flex justify-between items-center text-base max-w-7xl mx-auto">
|
16 |
-
<div className="text-gray-500
|
17 |
<span className="text-black font-bold text-lg mr-2">HandSpew</span>
|
18 |
Built with <a
|
19 |
href="https://ai.google.dev"
|
@@ -42,12 +42,12 @@ export default function Home() {
|
|
42 |
<Head>
|
43 |
<title>HandSpew</title>
|
44 |
<meta name="description" content="Generate thoughts based on hand gestures using MediaPipe and Gemini" />
|
45 |
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
46 |
<link rel="icon" href="/favicon.ico" />
|
47 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap" />
|
48 |
</Head>
|
49 |
<Header />
|
50 |
-
<main className="flex flex-col items-center
|
51 |
<HandDetector />
|
52 |
</main>
|
53 |
</>
|
|
|
11 |
|
12 |
const Header = () => {
|
13 |
return (
|
14 |
+
<div className="fixed top-0 left-0 right-0 w-full bg-white p-4 z-50 shadow-sm">
|
15 |
<div className="w-full flex justify-between items-center text-base max-w-7xl mx-auto">
|
16 |
+
<div className="text-gray-500">
|
17 |
<span className="text-black font-bold text-lg mr-2">HandSpew</span>
|
18 |
Built with <a
|
19 |
href="https://ai.google.dev"
|
|
|
42 |
<Head>
|
43 |
<title>HandSpew</title>
|
44 |
<meta name="description" content="Generate thoughts based on hand gestures using MediaPipe and Gemini" />
|
45 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
46 |
<link rel="icon" href="/favicon.ico" />
|
47 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap" />
|
48 |
</Head>
|
49 |
<Header />
|
50 |
+
<main className="flex min-h-screen flex-col items-center justify-center p-4 bg-white font-['Google_Sans',sans-serif] pt-20">
|
51 |
<HandDetector />
|
52 |
</main>
|
53 |
</>
|
styles/globals.css
CHANGED
@@ -16,24 +16,10 @@
|
|
16 |
}
|
17 |
}
|
18 |
|
19 |
-
html, body {
|
20 |
-
height: 100%;
|
21 |
-
overflow-x: hidden;
|
22 |
-
position: relative;
|
23 |
-
}
|
24 |
-
|
25 |
body {
|
26 |
color: var(--foreground);
|
27 |
background: var(--background);
|
28 |
font-family: 'Google Sans', Arial, Helvetica, sans-serif;
|
29 |
-
-webkit-overflow-scrolling: touch; /* Enables momentum scrolling on iOS */
|
30 |
-
}
|
31 |
-
|
32 |
-
/* Fix for iOS Safari 100vh issue */
|
33 |
-
@supports (-webkit-touch-callout: none) {
|
34 |
-
.min-h-screen {
|
35 |
-
height: -webkit-fill-available;
|
36 |
-
}
|
37 |
}
|
38 |
|
39 |
/* Minimal thought bubble styling */
|
@@ -70,23 +56,6 @@ body {
|
|
70 |
}
|
71 |
}
|
72 |
|
73 |
-
/* Canvas container styling */
|
74 |
-
.canvas-container {
|
75 |
-
position: relative;
|
76 |
-
width: 100%;
|
77 |
-
max-width: 100%;
|
78 |
-
overflow: hidden;
|
79 |
-
border-radius: 12px; /* Add rounded corners */
|
80 |
-
}
|
81 |
-
|
82 |
-
.canvas-container canvas {
|
83 |
-
display: block;
|
84 |
-
width: 100%;
|
85 |
-
height: auto;
|
86 |
-
object-fit: cover; /* Changed from contain to cover */
|
87 |
-
border-radius: 12px; /* Match container */
|
88 |
-
}
|
89 |
-
|
90 |
/* Mobile-specific styles */
|
91 |
@media (max-width: 767px) {
|
92 |
.thought-bubble {
|
@@ -97,20 +66,4 @@ body {
|
|
97 |
font-size: 12px;
|
98 |
line-height: 1.3;
|
99 |
}
|
100 |
-
|
101 |
-
/* Compact layout for mobile */
|
102 |
-
.canvas-container {
|
103 |
-
margin-top: 0;
|
104 |
-
}
|
105 |
-
|
106 |
-
/* Reduce spacing around elements on mobile */
|
107 |
-
.w-full {
|
108 |
-
margin-bottom: 0.5rem;
|
109 |
-
}
|
110 |
-
|
111 |
-
/* Make header more compact on mobile */
|
112 |
-
.sticky {
|
113 |
-
padding-top: 0.5rem;
|
114 |
-
padding-bottom: 0.5rem;
|
115 |
-
}
|
116 |
}
|
|
|
16 |
}
|
17 |
}
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
body {
|
20 |
color: var(--foreground);
|
21 |
background: var(--background);
|
22 |
font-family: 'Google Sans', Arial, Helvetica, sans-serif;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
}
|
24 |
|
25 |
/* Minimal thought bubble styling */
|
|
|
56 |
}
|
57 |
}
|
58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
/* Mobile-specific styles */
|
60 |
@media (max-width: 767px) {
|
61 |
.thought-bubble {
|
|
|
66 |
font-size: 12px;
|
67 |
line-height: 1.3;
|
68 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
}
|