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 |
}
|