Skip to content

Commit a779592

Browse files
committed
feat: Add sensor comparison feature
1 parent 6988330 commit a779592

File tree

3 files changed

+302
-3
lines changed

3 files changed

+302
-3
lines changed

src/App.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@ import FocalLength from './components/FocalLength'
33
import ImageGallery from './components/ImageGallery'
44
import LanguageToggle from './components/LanguageToggle'
55
import PixelComparison from './components/PixelComparison'
6+
import SensorComparison from './components/SensorComparison'
67
import { useLanguage } from './i18n/languageContext'
78

89
const App = () => {
910
const { t, language } = useLanguage();
1011
const [selectedPixels, setSelectedPixels] = useState<number[]>([10, 40]) // Default to 10MP and 40MP
11-
const [activeTab, setActiveTab] = useState<'size' | 'gallery' | 'focal'>('size')
12+
const [selectedSensors, setSelectedSensors] = useState<string[]>(['full-frame', 'aps-c']) // Default to FF and APS-C
13+
const [activeTab, setActiveTab] = useState<'size' | 'gallery' | 'focal' | 'sensor'>('size')
1214

1315
const megapixelOptions = [10, 20, 30, 40, 60, 100]
16+
const sensorOptions = [
17+
'medium-format',
18+
'full-frame',
19+
'aps-c',
20+
'm43',
21+
'1-inch',
22+
'2-3-inch',
23+
'1-1.7-inch',
24+
'1-2.3-inch',
25+
'1-3-inch'
26+
]
1427

1528
const handleTogglePixel = (pixel: number) => {
1629
setSelectedPixels(prev =>
@@ -20,6 +33,14 @@ const App = () => {
2033
)
2134
}
2235

36+
const handleToggleSensor = (sensor: string) => {
37+
setSelectedSensors(prev =>
38+
prev.includes(sensor)
39+
? prev.filter(s => s !== sensor)
40+
: [...prev, sensor]
41+
)
42+
}
43+
2344
// Format megapixel display for Chinese language
2445
const formatMegapixels = (pixel: number) => {
2546
if (language === 'zh') {
@@ -33,6 +54,25 @@ const App = () => {
3354
}
3455
};
3556

57+
// Get sensor name for display
58+
const getSensorName = (sensorKey: string) => {
59+
if (language === 'zh') {
60+
return t(`sensor.${sensorKey}`);
61+
}
62+
const names: Record<string, string> = {
63+
'medium-format': 'Medium Format',
64+
'full-frame': 'Full Frame (35mm)',
65+
'aps-c': 'APS-C',
66+
'm43': 'M43',
67+
'1-inch': '1 inch',
68+
'2-3-inch': '2/3 inch',
69+
'1-1.7-inch': '1/1.7 inch',
70+
'1-2.3-inch': '1/2.3 inch',
71+
'1-3-inch': '1/3 inch'
72+
};
73+
return names[sensorKey];
74+
};
75+
3676
return (
3777
<div className="container mx-auto px-4 py-10 max-w-6xl">
3878
<div className="flex justify-end mb-4">
@@ -60,6 +100,16 @@ const App = () => {
60100
onClick={() => setActiveTab('size')}
61101
>
62102
{t('tabs.size')}
103+
</button>
104+
<button
105+
className={`px-6 py-3 text-md font-medium ${
106+
activeTab === 'sensor'
107+
? 'border-b-2 border-blue-500 text-blue-600'
108+
: 'text-gray-500 hover:text-gray-700'
109+
}`}
110+
onClick={() => setActiveTab('sensor')}
111+
>
112+
{t('tabs.sensor')}
63113
</button>
64114
<button
65115
className={`px-6 py-3 text-md font-medium ${
@@ -104,6 +154,27 @@ const App = () => {
104154
</div>
105155
)}
106156

157+
{/* Sensor Controls */}
158+
{activeTab === 'sensor' && (
159+
<div className="p-6 border-b border-gray-200">
160+
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('controls.sensors')}</h2>
161+
162+
<div className="flex flex-wrap gap-3">
163+
{sensorOptions.map(sensor => (
164+
<label key={sensor} className="inline-flex items-center cursor-pointer">
165+
<input
166+
type="checkbox"
167+
className="form-checkbox rounded text-blue-500 h-5 w-5"
168+
checked={selectedSensors.includes(sensor)}
169+
onChange={() => handleToggleSensor(sensor)}
170+
/>
171+
<span className="ml-2 text-gray-700">{getSensorName(sensor)}</span>
172+
</label>
173+
))}
174+
</div>
175+
</div>
176+
)}
177+
107178
{/* Tab Content */}
108179
{activeTab === 'size' && selectedPixels.length > 0 && (
109180
<PixelComparison selectedPixels={selectedPixels} />
@@ -116,12 +187,22 @@ const App = () => {
116187
{activeTab === 'focal' && (
117188
<FocalLength />
118189
)}
190+
191+
{activeTab === 'sensor' && selectedSensors.length > 0 && (
192+
<SensorComparison selectedSensors={selectedSensors} />
193+
)}
119194

120-
{(activeTab !== 'focal' && selectedPixels.length === 0) && (
195+
{(activeTab !== 'focal' && activeTab === 'size' && selectedPixels.length === 0) && (
121196
<div className="p-6 text-center text-gray-500">
122197
{t('controls.empty')}
123198
</div>
124199
)}
200+
201+
{(activeTab === 'sensor' && selectedSensors.length === 0) && (
202+
<div className="p-6 text-center text-gray-500">
203+
{t('controls.empty.sensors')}
204+
</div>
205+
)}
125206
</div>
126207

127208
<footer className="mt-12 text-center text-gray-500 text-sm">
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { useLanguage } from '../i18n/languageContext';
3+
4+
interface SensorComparisonProps {
5+
selectedSensors: string[];
6+
}
7+
8+
// Sensor dimensions data
9+
const sensorDimensions: Record<string, { width: number; height: number; name: string }> = {
10+
'medium-format': { width: 53, height: 40, name: 'Medium Format' },
11+
'full-frame': { width: 36, height: 24, name: 'Full Frame (35mm)' },
12+
'aps-c': { width: 23.6, height: 15.6, name: 'APS-C' },
13+
'm43': { width: 17.3, height: 13, name: 'M43' },
14+
'1-inch': { width: 13.2, height: 8.8, name: '1 inch' },
15+
'2-3-inch': { width: 8.8, height: 6.6, name: '2/3 inch' },
16+
'1-1.7-inch': { width: 7.6, height: 5.7, name: '1/1.7 inch' },
17+
'1-2.3-inch': { width: 6.3, height: 4.7, name: '1/2.3 inch' },
18+
'1-3-inch': { width: 4.8, height: 3.6, name: '1/3 inch' }
19+
};
20+
21+
const SensorComparison: React.FC<SensorComparisonProps> = ({ selectedSensors }) => {
22+
const { t, language } = useLanguage();
23+
const [isStacked, setIsStacked] = useState(false);
24+
const [containerWidth, setContainerWidth] = useState(0);
25+
const containerRef = useRef<HTMLDivElement>(null);
26+
27+
// Update container width on mount and resize
28+
useEffect(() => {
29+
const updateWidth = () => {
30+
if (containerRef.current) {
31+
setContainerWidth(containerRef.current.offsetWidth - 48); // Subtract padding
32+
}
33+
};
34+
35+
updateWidth();
36+
window.addEventListener('resize', updateWidth);
37+
return () => window.removeEventListener('resize', updateWidth);
38+
}, []);
39+
40+
// Function to calculate scaled dimensions for display
41+
const calculateDimensions = (sensorKey: string): { width: number; height: number } => {
42+
const dimensions = sensorDimensions[sensorKey];
43+
const maxSensorWidth = sensorDimensions['medium-format'].width; // Width of medium format sensor
44+
45+
// Calculate scale factor based on container width
46+
// If container is smaller than medium format sensor, use container width as reference
47+
const baseWidth = Math.min(containerWidth, 600); // Limit max width to 600px
48+
const scaleFactor = baseWidth / maxSensorWidth;
49+
50+
// Apply the scale factor to maintain relative sizes
51+
const width = dimensions.width * scaleFactor;
52+
const height = dimensions.height * scaleFactor;
53+
54+
return { width, height };
55+
};
56+
57+
// Always sort sensors by size for consistent ordering
58+
const sortedSensors = [...selectedSensors].sort((a, b) =>
59+
(sensorDimensions[b].width * sensorDimensions[b].height) -
60+
(sensorDimensions[a].width * sensorDimensions[a].height)
61+
);
62+
63+
// Find dimensions of largest selected sensor for container sizing
64+
let maxWidth = 0;
65+
let maxHeight = 0;
66+
67+
selectedSensors.forEach(sensor => {
68+
const { width, height } = calculateDimensions(sensor);
69+
maxWidth = Math.max(maxWidth, width);
70+
maxHeight = Math.max(maxHeight, height);
71+
});
72+
73+
return (
74+
<div className="p-6" ref={containerRef}>
75+
<div className="flex justify-between items-center mb-6">
76+
<h2 className="text-xl font-semibold text-gray-800">{t('sensor.title')}</h2>
77+
<div className="flex items-center">
78+
<span className="mr-2 text-sm text-gray-600">{t('sensor.stack')}</span>
79+
<label className="inline-flex items-center cursor-pointer">
80+
<input
81+
type="checkbox"
82+
className="sr-only peer"
83+
checked={isStacked}
84+
onChange={() => setIsStacked(!isStacked)}
85+
/>
86+
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-100 dark:peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:translate-x-[-100%] peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500"></div>
87+
</label>
88+
</div>
89+
</div>
90+
91+
{isStacked ? (
92+
// Stacked view - sensors overlay each other with centers aligned
93+
<div className="relative mx-auto my-10 overflow-hidden">
94+
<div
95+
className="relative"
96+
style={{
97+
width: `${maxWidth}px`,
98+
height: `${maxHeight}px`,
99+
minHeight: '200px',
100+
maxWidth: '100%',
101+
margin: '0 auto'
102+
}}
103+
>
104+
{sortedSensors.map((sensor, index) => {
105+
const { width, height } = calculateDimensions(sensor);
106+
const actualDimensions = sensorDimensions[sensor];
107+
108+
// Calculate positioning to center this block relative to container
109+
const leftOffset = (maxWidth - width) / 2;
110+
const topOffset = (maxHeight - height) / 2;
111+
const zIndex = index + 1;
112+
113+
return (
114+
<div
115+
key={sensor}
116+
className="absolute transform-gpu"
117+
style={{
118+
left: `${leftOffset}px`,
119+
top: `${topOffset}px`,
120+
zIndex,
121+
}}
122+
>
123+
<div
124+
className="sensor-block bg-blue-50 hover:bg-blue-100 relative border-2 border-gray-400"
125+
style={{
126+
width: `${width}px`,
127+
height: `${height}px`,
128+
minWidth: '40px',
129+
minHeight: '30px',
130+
}}
131+
>
132+
<div className="absolute bottom-0 left-0 right-0 bg-white bg-opacity-75 text-gray-700 text-xs py-1 px-2">
133+
{actualDimensions.name} ({actualDimensions.width}×{actualDimensions.height}mm)
134+
</div>
135+
</div>
136+
</div>
137+
);
138+
})}
139+
</div>
140+
</div>
141+
) : (
142+
// Regular view - sensors are stacked vertically with smaller on top
143+
<div className="flex flex-col items-center justify-center space-y-8 py-4">
144+
{[...sortedSensors].reverse().map(sensor => {
145+
const { width, height } = calculateDimensions(sensor);
146+
const actualDimensions = sensorDimensions[sensor];
147+
148+
return (
149+
<div key={sensor} className="flex flex-col items-center mb-8 w-full">
150+
<div
151+
className="sensor-block bg-blue-50 hover:bg-blue-100 mb-2 relative"
152+
style={{
153+
width: `${width}px`,
154+
height: `${height}px`,
155+
minWidth: '40px',
156+
minHeight: '30px',
157+
maxWidth: '100%'
158+
}}
159+
>
160+
<div className="text-xs md:text-sm font-medium absolute top-2 left-2 bg-white bg-opacity-75 px-2 py-1 rounded">
161+
{actualDimensions.name}
162+
</div>
163+
</div>
164+
<div className="text-sm text-gray-500">
165+
{actualDimensions.width}×{actualDimensions.height}mm
166+
</div>
167+
<div className="text-xs text-gray-400 mt-1">
168+
({(actualDimensions.width * actualDimensions.height).toFixed(1)} mm²)
169+
</div>
170+
</div>
171+
);
172+
})}
173+
</div>
174+
)}
175+
176+
<div className="mt-8 p-4 bg-gray-50 rounded-lg">
177+
<h3 className="text-md font-medium text-gray-700 mb-2">{t('sensor.about.title')}</h3>
178+
<p className="text-sm text-gray-600">
179+
{t('sensor.about.description')}
180+
</p>
181+
</div>
182+
</div>
183+
);
184+
};
185+
186+
export default SensorComparison;

0 commit comments

Comments
 (0)