Tina Tarighian commited on
Commit
987ec04
·
1 Parent(s): 7fecabc
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/CameraSetup.js CHANGED
@@ -35,15 +35,19 @@ 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: 1920 },
43
- height: { ideal: 1080 }
 
44
  },
45
  audio: false
46
- });
 
 
 
47
 
48
  // Check if component is still mounted before continuing
49
  if (!isMounted.current) {
@@ -71,6 +75,9 @@ const CameraSetup = ({
71
  const videoWidth = videoRef.current.videoWidth;
72
  const videoHeight = videoRef.current.videoHeight;
73
  const aspectRatio = videoWidth / videoHeight;
 
 
 
74
  setVideoAspectRatio(aspectRatio);
75
 
76
  // Update canvas size with the correct aspect ratio
@@ -103,7 +110,7 @@ const CameraSetup = ({
103
  tracks.forEach(track => track.stop());
104
  }
105
  };
106
- }, [videoRef, canvasRef, containerRef, facingMode, setCameraError, setVideoAspectRatio, updateCanvasSize]);
107
 
108
  // Function to switch camera
109
  const switchCamera = async () => {
@@ -126,8 +133,9 @@ const CameraSetup = ({
126
  const stream = await navigator.mediaDevices.getUserMedia({
127
  video: {
128
  facingMode: newFacingMode,
129
- width: { ideal: 1920 },
130
- height: { ideal: 1080 }
 
131
  },
132
  audio: false
133
  });
@@ -183,7 +191,9 @@ const CameraSetup = ({
183
  aria-label={`Switch to ${facingMode === 'user' ? 'back' : 'front'} camera`}
184
  >
185
  <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
186
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
 
 
187
  </svg>
188
  </button>
189
  )}
 
35
  tracks.forEach(track => track.stop());
36
  }
37
 
38
+ // Set camera constraints based on device type
39
+ const constraints = {
40
+ video: {
41
  facingMode: facingMode,
42
+ width: { ideal: isMobile ? 1280 : 1920 },
43
+ height: { ideal: isMobile ? 720 : 1080 },
44
+ aspectRatio: isMobile ? { ideal: 4/3 } : undefined // Prefer 4:3 on mobile
45
  },
46
  audio: false
47
+ };
48
+
49
+ // Get camera stream
50
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
51
 
52
  // Check if component is still mounted before continuing
53
  if (!isMounted.current) {
 
75
  const videoWidth = videoRef.current.videoWidth;
76
  const videoHeight = videoRef.current.videoHeight;
77
  const aspectRatio = videoWidth / videoHeight;
78
+
79
+ console.log(`Camera dimensions: ${videoWidth}x${videoHeight}, aspect ratio: ${aspectRatio}`);
80
+
81
  setVideoAspectRatio(aspectRatio);
82
 
83
  // Update canvas size with the correct aspect ratio
 
110
  tracks.forEach(track => track.stop());
111
  }
112
  };
113
+ }, [videoRef, canvasRef, containerRef, facingMode, setCameraError, setVideoAspectRatio, updateCanvasSize, isMobile]);
114
 
115
  // Function to switch camera
116
  const switchCamera = async () => {
 
133
  const stream = await navigator.mediaDevices.getUserMedia({
134
  video: {
135
  facingMode: newFacingMode,
136
+ width: { ideal: isMobile ? 1280 : 1920 },
137
+ height: { ideal: isMobile ? 720 : 1080 },
138
+ aspectRatio: isMobile ? { ideal: 4/3 } : undefined
139
  },
140
  audio: false
141
  });
 
191
  aria-label={`Switch to ${facingMode === 'user' ? 'back' : 'front'} camera`}
192
  >
193
  <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
194
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
195
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
196
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 9l-6 6" />
197
  </svg>
198
  </button>
199
  )}
components/HandDetector.js CHANGED
@@ -12,8 +12,8 @@ const HandDetector = () => {
12
  const canvasRef = useRef(null);
13
  const containerRef = useRef(null);
14
 
15
- // Camera state
16
- const [facingMode, setFacingMode] = useState('user'); // 'user' for front camera, 'environment' for back camera
17
  const [cameraError, setCameraError] = useState(false);
18
 
19
  // Prompt state
@@ -32,13 +32,6 @@ const HandDetector = () => {
32
  updateCanvasSize
33
  } = useDeviceAndCanvas();
34
 
35
- // Set default camera to back camera on mobile devices
36
- useEffect(() => {
37
- if (isMobile) {
38
- setFacingMode('environment');
39
- }
40
- }, [isMobile]);
41
-
42
  const {
43
  handDetected,
44
  isMouthOpen,
 
12
  const canvasRef = useRef(null);
13
  const containerRef = useRef(null);
14
 
15
+ // Camera state - Set default to 'environment' (back camera) for mobile
16
+ const [facingMode, setFacingMode] = useState('environment');
17
  const [cameraError, setCameraError] = useState(false);
18
 
19
  // Prompt state
 
32
  updateCanvasSize
33
  } = useDeviceAndCanvas();
34
 
 
 
 
 
 
 
 
35
  const {
36
  handDetected,
37
  isMouthOpen,
components/MainContent.js CHANGED
@@ -34,15 +34,13 @@ const MainContent = ({
34
  createPopParticles
35
  }) => {
36
  return (
37
- <div className="w-full flex flex-col items-center space-y-2">
38
- {/* Conditionally render Prompt Editor based on device */}
39
- {!isMobile && (
40
- <PromptEditor
41
- customPrompt={customPrompt}
42
- setCustomPrompt={setCustomPrompt}
43
- isMac={isMac}
44
- />
45
- )}
46
 
47
  <div ref={containerRef} className="relative w-full max-w-4xl canvas-container">
48
  {/* Camera Setup */}
@@ -61,8 +59,12 @@ const MainContent = ({
61
  {/* Canvas for hand detection */}
62
  <canvas
63
  ref={canvasRef}
64
- className="rounded-lg shadow-lg w-full"
65
- style={{ height: `${canvasHeight}px` }}
 
 
 
 
66
  width={canvasWidth}
67
  height={canvasHeight}
68
  />
@@ -92,17 +94,7 @@ const MainContent = ({
92
  createPopParticles={createPopParticles}
93
  />
94
  </div>
95
-
96
- {/* Render Prompt Editor below camera on mobile */}
97
- {isMobile && (
98
- <PromptEditor
99
- customPrompt={customPrompt}
100
- setCustomPrompt={setCustomPrompt}
101
- isMac={isMac}
102
- isMobile={isMobile}
103
- />
104
- )}
105
- </div>
106
  );
107
  };
108
 
 
34
  createPopParticles
35
  }) => {
36
  return (
37
+ <>
38
+ {/* Prompt Editor */}
39
+ <PromptEditor
40
+ customPrompt={customPrompt}
41
+ setCustomPrompt={setCustomPrompt}
42
+ isMac={isMac}
43
+ />
 
 
44
 
45
  <div ref={containerRef} className="relative w-full max-w-4xl canvas-container">
46
  {/* Camera Setup */}
 
59
  {/* Canvas for hand detection */}
60
  <canvas
61
  ref={canvasRef}
62
+ className="rounded-lg shadow-lg w-full object-contain"
63
+ style={{
64
+ height: `${canvasHeight}px`,
65
+ maxHeight: isMobile ? '70vh' : 'none',
66
+ objectFit: 'contain'
67
+ }}
68
  width={canvasWidth}
69
  height={canvasHeight}
70
  />
 
94
  createPopParticles={createPopParticles}
95
  />
96
  </div>
97
+ </>
 
 
 
 
 
 
 
 
 
 
98
  );
99
  };
100
 
components/PromptEditor.js CHANGED
@@ -1,29 +1,24 @@
1
  import { useState } from 'react';
2
 
3
- const PromptEditor = ({ customPrompt, setCustomPrompt, isMac, isMobile }) => {
4
  const [isEditingPrompt, setIsEditingPrompt] = useState(false);
5
- const [isCollapsed, setIsCollapsed] = useState(isMobile); // Collapsed by default on mobile
6
  const [tempPrompt, setTempPrompt] = useState("");
7
 
8
  return (
9
- <div className={`w-full max-w-4xl ${isMobile ? 'mt-2 mb-4' : 'mb-4'}`}>
10
  <div className="flex justify-between items-center mb-1">
11
- <label
12
- htmlFor="system-prompt"
13
- className={`block ${isMobile ? 'text-xs' : 'text-sm'} font-medium text-gray-700 cursor-pointer`}
14
- onClick={() => isMobile && setIsCollapsed(!isCollapsed)}
15
- >
16
- System Prompt {isMobile && (isCollapsed ? '▼' : '▲')}
17
  </label>
18
- {!isEditingPrompt && !isCollapsed ? (
19
  <button
20
  onClick={() => {
21
  setTempPrompt(customPrompt);
22
  setIsEditingPrompt(true);
23
  }}
24
- className={`${isMobile ? 'text-xs' : 'text-sm'} text-blue-600 hover:text-blue-800 flex items-center`}
25
  >
26
- <svg xmlns="http://www.w3.org/2000/svg" className={`${isMobile ? 'h-3 w-3' : 'h-4 w-4'} mr-1`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
27
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
28
  </svg>
29
  Edit
@@ -31,53 +26,49 @@ const PromptEditor = ({ customPrompt, setCustomPrompt, isMac, isMobile }) => {
31
  ) : null}
32
  </div>
33
 
34
- {!isCollapsed && (
35
- <>
36
- {isEditingPrompt ? (
37
- <div>
38
- <textarea
39
- id="system-prompt"
40
- value={tempPrompt}
41
- onChange={(e) => setTempPrompt(e.target.value)}
42
- onKeyDown={(e) => {
43
- // Save on Ctrl+Enter or Cmd+Enter
44
- if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
45
- e.preventDefault();
46
- setCustomPrompt(tempPrompt);
47
- setIsEditingPrompt(false);
48
- }
49
- }}
50
- className={`w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 ${isMobile ? 'text-xs' : 'text-sm'}`}
51
- rows={isMobile ? 3 : 4}
52
- placeholder="Enter your custom prompt for Gemini..."
53
- />
54
- <div className="flex justify-end mt-2 space-x-2">
55
- <span className={`${isMobile ? 'text-xxs' : 'text-xs'} text-gray-500 self-center mr-auto`}>
56
- Tip: Press {isMac ? '⌘' : 'Ctrl'}+Enter to save
57
- </span>
58
- <button
59
- onClick={() => setIsEditingPrompt(false)}
60
- className={`px-3 py-1 ${isMobile ? 'text-xs' : 'text-sm'} text-gray-600 border border-gray-300 rounded-md hover:bg-gray-100`}
61
- >
62
- Cancel
63
- </button>
64
- <button
65
- onClick={() => {
66
- setCustomPrompt(tempPrompt);
67
- setIsEditingPrompt(false);
68
- }}
69
- className={`px-3 py-1 ${isMobile ? 'text-xs' : 'text-sm'} text-white bg-blue-600 rounded-md hover:bg-blue-700`}
70
- >
71
- Save
72
- </button>
73
- </div>
74
- </div>
75
- ) : (
76
- <div className={`p-2 bg-gray-50 border border-gray-200 rounded-lg ${isMobile ? 'text-xs' : 'text-sm'} text-gray-800 whitespace-pre-wrap ${isMobile ? 'max-h-20' : 'max-h-32'} overflow-y-auto`}>
77
- {customPrompt}
78
- </div>
79
- )}
80
- </>
81
  )}
82
  </div>
83
  );
 
1
  import { useState } from 'react';
2
 
3
+ const PromptEditor = ({ customPrompt, setCustomPrompt, isMac }) => {
4
  const [isEditingPrompt, setIsEditingPrompt] = useState(false);
 
5
  const [tempPrompt, setTempPrompt] = useState("");
6
 
7
  return (
8
+ <div className="w-full max-w-4xl mb-4">
9
  <div className="flex justify-between items-center mb-1">
10
+ <label htmlFor="system-prompt" className="block text-sm font-medium text-gray-700">
11
+ System Prompt
 
 
 
 
12
  </label>
13
+ {!isEditingPrompt ? (
14
  <button
15
  onClick={() => {
16
  setTempPrompt(customPrompt);
17
  setIsEditingPrompt(true);
18
  }}
19
+ className="text-sm text-blue-600 hover:text-blue-800 flex items-center"
20
  >
21
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
22
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
23
  </svg>
24
  Edit
 
26
  ) : null}
27
  </div>
28
 
29
+ {isEditingPrompt ? (
30
+ <div>
31
+ <textarea
32
+ id="system-prompt"
33
+ value={tempPrompt}
34
+ onChange={(e) => setTempPrompt(e.target.value)}
35
+ onKeyDown={(e) => {
36
+ // Save on Ctrl+Enter or Cmd+Enter
37
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
38
+ e.preventDefault();
39
+ setCustomPrompt(tempPrompt);
40
+ setIsEditingPrompt(false);
41
+ }
42
+ }}
43
+ className="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
44
+ rows={4}
45
+ placeholder="Enter your custom prompt for Gemini..."
46
+ />
47
+ <div className="flex justify-end mt-2 space-x-2">
48
+ <span className="text-xs text-gray-500 self-center mr-auto">
49
+ Tip: Press {isMac ? '⌘' : 'Ctrl'}+Enter to save
50
+ </span>
51
+ <button
52
+ onClick={() => setIsEditingPrompt(false)}
53
+ className="px-3 py-1 text-sm text-gray-600 border border-gray-300 rounded-md hover:bg-gray-100"
54
+ >
55
+ Cancel
56
+ </button>
57
+ <button
58
+ onClick={() => {
59
+ setCustomPrompt(tempPrompt);
60
+ setIsEditingPrompt(false);
61
+ }}
62
+ className="px-3 py-1 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700"
63
+ >
64
+ Save
65
+ </button>
66
+ </div>
67
+ </div>
68
+ ) : (
69
+ <div className="p-3 bg-gray-50 border border-gray-200 rounded-lg text-xs md:text-sm text-gray-800 whitespace-pre-wrap">
70
+ {customPrompt}
71
+ </div>
 
 
 
 
72
  )}
73
  </div>
74
  );
hooks/useDeviceAndCanvas.js CHANGED
@@ -43,21 +43,35 @@ const useDeviceAndCanvas = () => {
43
  if (!isComponentMounted.current) return;
44
 
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
- let height = maxWidth / aspectRatio;
49
 
50
- // Limit height on mobile to ensure system prompt is visible without scrolling
 
51
  if (isMobile) {
52
- const viewportHeight = window.innerHeight;
53
- // Use a fixed height on mobile that's appropriate for most devices
54
- // This ensures the camera and prompt are both visible without scrolling
55
- const maxMobileHeight = Math.min(viewportHeight * 0.5, 350); // 50% of viewport or 350px max
56
- height = Math.min(height, maxMobileHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
-
59
- setCanvasWidth(maxWidth);
60
- setCanvasHeight(height);
61
  }, [isMobile]);
62
 
63
  return {
 
43
  if (!isComponentMounted.current) return;
44
 
45
  const containerWidth = document.querySelector('.canvas-container')?.clientWidth || window.innerWidth;
 
 
 
46
 
47
+ // Scale down for mobile devices
48
+ let maxWidth;
49
  if (isMobile) {
50
+ // On mobile, use a smaller canvas size to prevent it from being too tall
51
+ maxWidth = Math.min(containerWidth, 400);
52
+
53
+ // Ensure we maintain the correct aspect ratio
54
+ // For mobile, we want to ensure the height isn't too tall
55
+ // Most phone cameras have aspect ratios around 4:3 or 16:9
56
+ // If the aspect ratio is very wide or very tall, we'll normalize it
57
+ let normalizedAspectRatio = aspectRatio;
58
+
59
+ // If aspect ratio is extreme, normalize it to prevent stretching
60
+ if (aspectRatio < 0.5) normalizedAspectRatio = 0.5; // Very tall
61
+ if (aspectRatio > 2) normalizedAspectRatio = 2; // Very wide
62
+
63
+ const height = maxWidth / normalizedAspectRatio;
64
+
65
+ setCanvasWidth(maxWidth);
66
+ setCanvasHeight(height);
67
+ } else {
68
+ // On desktop, use a larger canvas size
69
+ maxWidth = Math.min(containerWidth, 960);
70
+ const height = maxWidth / aspectRatio;
71
+
72
+ setCanvasWidth(maxWidth);
73
+ setCanvasHeight(height);
74
  }
 
 
 
75
  }, [isMobile]);
76
 
77
  return {
pages/index.js CHANGED
@@ -11,7 +11,7 @@ const inter = Inter({ subsets: ['latin'] });
11
 
12
  const Header = () => {
13
  return (
14
- <div className="sticky top-0 left-0 right-0 w-full bg-white p-3 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>
@@ -42,15 +42,13 @@ 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, maximum-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-start p-3 bg-white font-['Google_Sans',sans-serif] pt-16">
51
- <div className="w-full max-w-4xl flex flex-col items-center">
52
- <HandDetector />
53
- </div>
54
  </main>
55
  </>
56
  );
 
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>
 
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-start p-4 bg-white font-['Google_Sans',sans-serif] pt-20 overflow-y-auto">
51
+ <HandDetector />
 
 
52
  </main>
53
  </>
54
  );
styles/globals.css CHANGED
@@ -18,8 +18,8 @@
18
 
19
  html, body {
20
  height: 100%;
21
- width: 100%;
22
  overflow-x: hidden;
 
23
  -webkit-overflow-scrolling: touch;
24
  }
25
 
@@ -30,6 +30,27 @@ body {
30
  position: relative;
31
  }
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  /* Minimal thought bubble styling */
34
  .thought-bubble {
35
  position: absolute;
@@ -74,10 +95,4 @@ body {
74
  font-size: 12px;
75
  line-height: 1.3;
76
  }
77
-
78
- /* Add text-xxs class for very small text on mobile */
79
- .text-xxs {
80
- font-size: 0.65rem;
81
- line-height: 1rem;
82
- }
83
  }
 
18
 
19
  html, body {
20
  height: 100%;
 
21
  overflow-x: hidden;
22
+ overflow-y: auto;
23
  -webkit-overflow-scrolling: touch;
24
  }
25
 
 
30
  position: relative;
31
  }
32
 
33
+ /* Canvas container styling */
34
+ .canvas-container {
35
+ display: flex;
36
+ justify-content: center;
37
+ align-items: center;
38
+ }
39
+
40
+ .canvas-container canvas {
41
+ object-fit: contain;
42
+ max-width: 100%;
43
+ height: auto;
44
+ aspect-ratio: auto;
45
+ }
46
+
47
+ /* Prevent stretching on mobile */
48
+ @media (max-width: 767px) {
49
+ .canvas-container canvas {
50
+ max-height: 70vh;
51
+ }
52
+ }
53
+
54
  /* Minimal thought bubble styling */
55
  .thought-bubble {
56
  position: absolute;
 
95
  font-size: 12px;
96
  line-height: 1.3;
97
  }
 
 
 
 
 
 
98
  }