Add medium-format lens updates and preview controls
This commit is contained in:
+204
-12
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||||
import Plot from 'react-plotly.js';
|
import Plot from 'react-plotly.js';
|
||||||
import { findSensorById } from './data/sensors.js';
|
import { defaultSensors, findSensorById } from './data/sensors.js';
|
||||||
import { defaultLenses } from './data/lenses.js';
|
import { defaultLenses } from './data/lenses.js';
|
||||||
import {
|
import {
|
||||||
FramingModes,
|
FramingModes,
|
||||||
@@ -12,8 +12,58 @@ import {
|
|||||||
} from './utils/optics.js';
|
} from './utils/optics.js';
|
||||||
|
|
||||||
const COLOR_PALETTE = ['#38bdf8', '#a855f7', '#f97316', '#22c55e', '#e11d48', '#6366f1', '#14b8a6', '#f59e0b'];
|
const COLOR_PALETTE = ['#38bdf8', '#a855f7', '#f97316', '#22c55e', '#e11d48', '#6366f1', '#14b8a6', '#f59e0b'];
|
||||||
|
const PREVIEW_DISTANCE_MIN = 0.5;
|
||||||
|
const PREVIEW_DISTANCE_MAX = 50;
|
||||||
|
const NORMAL_RATIO_MIN = 0.7;
|
||||||
|
const NORMAL_RATIO_MAX = 1.5;
|
||||||
|
|
||||||
const INITIAL_SELECTED_LENS_IDS = defaultLenses.slice(0, 3).map((lens) => lens.id);
|
const computeInitialLensIds = () => {
|
||||||
|
const lensesBySensor = defaultLenses.reduce((acc, lens) => {
|
||||||
|
if (!acc[lens.sensorId]) {
|
||||||
|
acc[lens.sensorId] = [];
|
||||||
|
}
|
||||||
|
acc[lens.sensorId].push(lens);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const selectedIds = defaultSensors
|
||||||
|
.map((sensor) => {
|
||||||
|
const lenses = lensesBySensor[sensor.id];
|
||||||
|
if (!lenses || lenses.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scored = lenses.map((lens) => {
|
||||||
|
const aperture = Number.isFinite(lens.maxAperture) ? lens.maxAperture : Infinity;
|
||||||
|
const delta = Math.abs(lens.focalLength - sensor.diagonal);
|
||||||
|
const ratio = sensor.diagonal > 0 ? lens.focalLength / sensor.diagonal : Infinity;
|
||||||
|
const withinNormalBand = ratio >= NORMAL_RATIO_MIN && ratio <= NORMAL_RATIO_MAX;
|
||||||
|
return { lens, aperture, delta, withinNormalBand };
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalCandidates = scored.filter((entry) => entry.withinNormalBand);
|
||||||
|
const candidates = normalCandidates.length > 0 ? normalCandidates : scored;
|
||||||
|
|
||||||
|
candidates.sort((a, b) => {
|
||||||
|
if (a.aperture !== b.aperture) {
|
||||||
|
return a.aperture - b.aperture;
|
||||||
|
}
|
||||||
|
if (a.delta !== b.delta) {
|
||||||
|
return a.delta - b.delta;
|
||||||
|
}
|
||||||
|
return a.lens.focalLength - b.lens.focalLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
return candidates[0]?.lens.id ?? null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return selectedIds.length > 0
|
||||||
|
? Array.from(new Set(selectedIds))
|
||||||
|
: defaultLenses.slice(0, 3).map((lens) => lens.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_SELECTED_LENS_IDS = computeInitialLensIds();
|
||||||
|
|
||||||
const createInitialColorAssignments = (lensIds) => {
|
const createInitialColorAssignments = (lensIds) => {
|
||||||
const assignments = {};
|
const assignments = {};
|
||||||
@@ -133,10 +183,31 @@ const filterDatasetForPlotType = (dataset, plotType) => {
|
|||||||
return dataset;
|
return dataset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PreviewPanel = ({ lensCurves }) => {
|
const PreviewPanel = ({ lensCurves, previewDistanceMeters }) => {
|
||||||
const previewItems = lensCurves.map((curve) => {
|
const previewItems = lensCurves.map((curve) => {
|
||||||
const infinityStat = curve.checkpointStats.find((stat) => !Number.isFinite(stat.separation));
|
const targetDistance = previewDistanceMeters;
|
||||||
const blurMm = infinityStat?.blurMm ?? 0;
|
const safeDistance = Number.isFinite(targetDistance) ? Math.max(targetDistance, PREVIEW_DISTANCE_MIN) : targetDistance;
|
||||||
|
const targetSeparation = Number.isFinite(safeDistance) ? safeDistance - curve.subjectDistance : Infinity;
|
||||||
|
|
||||||
|
let stat = curve.checkpointStats.find((entry) => {
|
||||||
|
if (!Number.isFinite(targetSeparation)) {
|
||||||
|
return !Number.isFinite(entry.separation);
|
||||||
|
}
|
||||||
|
return Number.isFinite(entry.separation) && Math.abs(entry.separation - targetSeparation) < 1e-6;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stat) {
|
||||||
|
const derivedStats = buildStatsAtCheckpoints({
|
||||||
|
lens: curve.lens,
|
||||||
|
sensor: curve.sensor,
|
||||||
|
aperture: curve.apertureUsed,
|
||||||
|
subjectDistance: curve.subjectDistance,
|
||||||
|
checkpoints: [targetSeparation],
|
||||||
|
});
|
||||||
|
stat = derivedStats[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const blurMm = stat?.blurMm ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lens: curve.lens,
|
lens: curve.lens,
|
||||||
@@ -148,12 +219,18 @@ const PreviewPanel = ({ lensCurves }) => {
|
|||||||
|
|
||||||
const maxBlurMm = previewItems.reduce((maxValue, item) => Math.max(maxValue, item.blurMm), 0);
|
const maxBlurMm = previewItems.reduce((maxValue, item) => Math.max(maxValue, item.blurMm), 0);
|
||||||
const denominator = maxBlurMm > 0 ? maxBlurMm : 1;
|
const denominator = maxBlurMm > 0 ? maxBlurMm : 1;
|
||||||
|
const distanceLabel = Number.isFinite(previewDistanceMeters)
|
||||||
|
? `${formatNumber(previewDistanceMeters, previewDistanceMeters >= 10 ? 0 : 1)} m`
|
||||||
|
: '∞';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
|
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
|
||||||
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-400">Bokeh preview</h2>
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Bokeh preview</h2>
|
||||||
|
<span className="text-[10px] uppercase tracking-wide text-slate-500">Scene distance: {distanceLabel}</span>
|
||||||
|
</div>
|
||||||
{previewItems.length === 0 ? (
|
{previewItems.length === 0 ? (
|
||||||
<div className="text-sm text-slate-500">Select at least one lens to preview its blur at infinity.</div>
|
<div className="text-sm text-slate-500">Select at least one lens to preview its blur.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap justify-center gap-4">
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
{previewItems.map((item) => {
|
{previewItems.map((item) => {
|
||||||
@@ -178,7 +255,7 @@ const PreviewPanel = ({ lensCurves }) => {
|
|||||||
<div className="text-xs text-slate-200">
|
<div className="text-xs text-slate-200">
|
||||||
<div className="font-medium text-slate-100">{item.lens.name}</div>
|
<div className="font-medium text-slate-100">{item.lens.name}</div>
|
||||||
<div className="text-[10px] uppercase tracking-wide text-slate-500">
|
<div className="text-[10px] uppercase tracking-wide text-slate-500">
|
||||||
∞ blur {formatNumber(item.blurMm, 2)} mm
|
Blur {formatNumber(item.blurMm, 2)} mm
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +267,31 @@ const PreviewPanel = ({ lensCurves }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlotSection = ({ lensCurves, plotType, setPlotType }) => {
|
const PreviewDistanceControl = ({ value, onChange, max }) => {
|
||||||
|
const isInfinity = value >= max;
|
||||||
|
const displayValue = isInfinity ? '∞' : `${formatNumber(value, value >= 10 ? 0 : 1)} m`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
|
||||||
|
<label className="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
|
||||||
|
<span>Preview distance</span>
|
||||||
|
<span className="text-slate-200">{displayValue}</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={PREVIEW_DISTANCE_MIN}
|
||||||
|
max={max}
|
||||||
|
step="0.5"
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(Number.parseFloat(event.target.value))}
|
||||||
|
className="mt-3 w-full"
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-[11px] text-slate-500">Drag to compare blur at absolute scene distances. Set to max for infinity.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlotSection = ({ lensCurves, plotType, setPlotType, previewDistanceMeters }) => {
|
||||||
const axisConfig = getAxisConfig(plotType);
|
const axisConfig = getAxisConfig(plotType);
|
||||||
|
|
||||||
const plotData = lensCurves.map((curve, index) => {
|
const plotData = lensCurves.map((curve, index) => {
|
||||||
@@ -227,6 +328,28 @@ const PlotSection = ({ lensCurves, plotType, setPlotType }) => {
|
|||||||
...lensCurves.flatMap((curve) => curve.dataset.map((point) => point.blurPercentOfDiagonal))
|
...lensCurves.flatMap((curve) => curve.dataset.map((point) => point.blurPercentOfDiagonal))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shouldShowPreviewLine =
|
||||||
|
Number.isFinite(previewDistanceMeters) && (plotType === PlotTypes.DISTANCE || plotType === PlotTypes.ZOOMED);
|
||||||
|
const previewLineShape = shouldShowPreviewLine
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
xref: 'x',
|
||||||
|
yref: 'paper',
|
||||||
|
x0: previewDistanceMeters,
|
||||||
|
x1: previewDistanceMeters,
|
||||||
|
y0: 0,
|
||||||
|
y1: 1,
|
||||||
|
line: {
|
||||||
|
color: 'rgba(148, 163, 184, 0.35)',
|
||||||
|
width: 1,
|
||||||
|
dash: 'dot',
|
||||||
|
},
|
||||||
|
layer: 'below',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 p-4">
|
<div className="flex flex-col gap-4 p-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
@@ -273,6 +396,7 @@ const PlotSection = ({ lensCurves, plotType, setPlotType }) => {
|
|||||||
range: [0, maxY * 1.1],
|
range: [0, maxY * 1.1],
|
||||||
ticksuffix: ' %',
|
ticksuffix: ' %',
|
||||||
},
|
},
|
||||||
|
shapes: previewLineShape,
|
||||||
}}
|
}}
|
||||||
useResizeHandler
|
useResizeHandler
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
@@ -291,14 +415,56 @@ const ControlsPanel = ({
|
|||||||
toggleLensSelection,
|
toggleLensSelection,
|
||||||
activeLensId,
|
activeLensId,
|
||||||
setActiveLensId,
|
setActiveLensId,
|
||||||
|
lensSensorFilter,
|
||||||
|
setLensSensorFilter,
|
||||||
|
sensorFilterOptions,
|
||||||
}) => {
|
}) => {
|
||||||
|
const lensesToRender = useMemo(() => {
|
||||||
|
if (lensSensorFilter === 'all') {
|
||||||
|
return defaultLenses;
|
||||||
|
}
|
||||||
|
return defaultLenses.filter((lens) => lens.sensorId === lensSensorFilter);
|
||||||
|
}, [lensSensorFilter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 p-4">
|
<div className="space-y-6 p-4">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-slate-400 mb-2">Lenses & sensors</h2>
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-slate-400 mb-2">Lenses & sensors</h2>
|
||||||
<p className="mb-3 text-xs text-slate-500">Each lens automatically pairs with its native sensor format for accurate comparisons.</p>
|
<p className="mb-3 text-xs text-slate-500">Each lens automatically pairs with its native sensor format for accurate comparisons.</p>
|
||||||
|
<div className="mb-3 flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLensSensorFilter('all')}
|
||||||
|
className={`rounded border px-2 py-1 text-xs uppercase tracking-wide transition ${
|
||||||
|
lensSensorFilter === 'all'
|
||||||
|
? 'border-accent/60 bg-slate-900/80 text-slate-100 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]'
|
||||||
|
: 'border-slate-800 bg-slate-900/50 text-slate-400 hover:border-accent/40 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{sensorFilterOptions.map((sensor) => {
|
||||||
|
const isActive = lensSensorFilter === sensor.id;
|
||||||
|
const label = sensor.shortLabel ?? sensor.name;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={sensor.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLensSensorFilter(sensor.id)}
|
||||||
|
className={`rounded border px-2 py-1 text-xs uppercase tracking-wide transition ${
|
||||||
|
isActive
|
||||||
|
? 'border-accent/60 bg-slate-900/80 text-slate-100 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]'
|
||||||
|
: 'border-slate-800 bg-slate-900/50 text-slate-400 hover:border-accent/40 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
title={sensor.name}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{defaultLenses.map((lens) => {
|
{lensesToRender.map((lens) => {
|
||||||
const isSelected = selectedLensIds.includes(lens.id);
|
const isSelected = selectedLensIds.includes(lens.id);
|
||||||
const isActive = activeLensId === lens.id;
|
const isActive = activeLensId === lens.id;
|
||||||
const sensor = findSensorById(lens.sensorId);
|
const sensor = findSensorById(lens.sensorId);
|
||||||
@@ -573,6 +739,7 @@ const OpticsPanel = ({
|
|||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [selectedLensIds, setSelectedLensIds] = useState(INITIAL_SELECTED_LENS_IDS);
|
const [selectedLensIds, setSelectedLensIds] = useState(INITIAL_SELECTED_LENS_IDS);
|
||||||
|
const [lensSensorFilter, setLensSensorFilter] = useState('all');
|
||||||
const [framingMode, setFramingMode] = useState(FramingModes.MATCH_HEIGHT);
|
const [framingMode, setFramingMode] = useState(FramingModes.MATCH_HEIGHT);
|
||||||
const [subjectHeight, setSubjectHeight] = useState(1.7);
|
const [subjectHeight, setSubjectHeight] = useState(1.7);
|
||||||
const [frameFill, setFrameFill] = useState(0.75);
|
const [frameFill, setFrameFill] = useState(0.75);
|
||||||
@@ -587,6 +754,17 @@ const App = () => {
|
|||||||
const [checkpointsInput, setCheckpointsInput] = useState('3,6,10');
|
const [checkpointsInput, setCheckpointsInput] = useState('3,6,10');
|
||||||
const [activeLensId, setActiveLensId] = useState(null);
|
const [activeLensId, setActiveLensId] = useState(null);
|
||||||
const [lensColors, setLensColors] = useState(() => createInitialColorAssignments(INITIAL_SELECTED_LENS_IDS));
|
const [lensColors, setLensColors] = useState(() => createInitialColorAssignments(INITIAL_SELECTED_LENS_IDS));
|
||||||
|
const [previewDistance, setPreviewDistance] = useState(PREVIEW_DISTANCE_MAX);
|
||||||
|
|
||||||
|
const sensorFilterOptions = useMemo(() => {
|
||||||
|
const lensSensorIds = new Set(defaultLenses.map((lens) => lens.sensorId));
|
||||||
|
return defaultSensors.filter((sensor) => lensSensorIds.has(sensor.id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const previewDistanceMeters = useMemo(
|
||||||
|
() => (previewDistance >= PREVIEW_DISTANCE_MAX ? Infinity : previewDistance),
|
||||||
|
[previewDistance]
|
||||||
|
);
|
||||||
|
|
||||||
const checkpoints = useMemo(() => {
|
const checkpoints = useMemo(() => {
|
||||||
const userValues = parseCheckpointInput(checkpointsInput);
|
const userValues = parseCheckpointInput(checkpointsInput);
|
||||||
@@ -750,6 +928,9 @@ const App = () => {
|
|||||||
toggleLensSelection={toggleLensSelection}
|
toggleLensSelection={toggleLensSelection}
|
||||||
activeLensId={activeLensId}
|
activeLensId={activeLensId}
|
||||||
setActiveLensId={setActiveLensId}
|
setActiveLensId={setActiveLensId}
|
||||||
|
lensSensorFilter={lensSensorFilter}
|
||||||
|
setLensSensorFilter={setLensSensorFilter}
|
||||||
|
sensorFilterOptions={sensorFilterOptions}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -758,10 +939,16 @@ const App = () => {
|
|||||||
lensCurves={lensCurves}
|
lensCurves={lensCurves}
|
||||||
plotType={plotType}
|
plotType={plotType}
|
||||||
setPlotType={setPlotType}
|
setPlotType={setPlotType}
|
||||||
|
previewDistanceMeters={previewDistanceMeters}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-6 px-4 pb-6 lg:hidden">
|
<div className="space-y-6 px-4 pb-6 lg:hidden">
|
||||||
<PreviewPanel lensCurves={lensCurves} />
|
<PreviewPanel lensCurves={lensCurves} previewDistanceMeters={previewDistanceMeters} />
|
||||||
|
<PreviewDistanceControl
|
||||||
|
value={previewDistance}
|
||||||
|
onChange={setPreviewDistance}
|
||||||
|
max={PREVIEW_DISTANCE_MAX}
|
||||||
|
/>
|
||||||
<FramingPanel
|
<FramingPanel
|
||||||
framingMode={framingMode}
|
framingMode={framingMode}
|
||||||
setFramingMode={setFramingMode}
|
setFramingMode={setFramingMode}
|
||||||
@@ -799,7 +986,12 @@ const App = () => {
|
|||||||
|
|
||||||
<aside className="hidden w-full max-w-xs border-l border-slate-900/60 bg-slate-950/60 lg:block">
|
<aside className="hidden w-full max-w-xs border-l border-slate-900/60 bg-slate-950/60 lg:block">
|
||||||
<div className="space-y-6 px-4 py-6">
|
<div className="space-y-6 px-4 py-6">
|
||||||
<PreviewPanel lensCurves={lensCurves} />
|
<PreviewPanel lensCurves={lensCurves} previewDistanceMeters={previewDistanceMeters} />
|
||||||
|
<PreviewDistanceControl
|
||||||
|
value={previewDistance}
|
||||||
|
onChange={setPreviewDistance}
|
||||||
|
max={PREVIEW_DISTANCE_MAX}
|
||||||
|
/>
|
||||||
<FramingPanel
|
<FramingPanel
|
||||||
framingMode={framingMode}
|
framingMode={framingMode}
|
||||||
setFramingMode={setFramingMode}
|
setFramingMode={setFramingMode}
|
||||||
|
|||||||
+17
-1
@@ -34,12 +34,28 @@ export const defaultLenses = [
|
|||||||
{ id: 'fujifilm_500_56', brand: 'Fujifilm', name: 'XF 500mm f/5.6', focalLength: 500, maxAperture: 5.6, sensorId: 'aps_c_fuji' },
|
{ id: 'fujifilm_500_56', brand: 'Fujifilm', name: 'XF 500mm f/5.6', focalLength: 500, maxAperture: 5.6, sensorId: 'aps_c_fuji' },
|
||||||
{ id: 'voigtlander_35_09', brand: 'Voigtländer', name: 'Nokton 35mm f/0.9 (APS-C)', focalLength: 35, maxAperture: 0.9, sensorId: 'aps_c_fuji' },
|
{ id: 'voigtlander_35_09', brand: 'Voigtländer', name: 'Nokton 35mm f/0.9 (APS-C)', focalLength: 35, maxAperture: 0.9, sensorId: 'aps_c_fuji' },
|
||||||
|
|
||||||
// GFX (44×33) primes
|
// 44×33 medium format primes
|
||||||
{ id: 'fuji_gf_23_4', brand: 'Fujifilm', name: 'GF 23mm f/4 R LM WR', focalLength: 23, maxAperture: 4, sensorId: 'gfx_44x33' },
|
{ id: 'fuji_gf_23_4', brand: 'Fujifilm', name: 'GF 23mm f/4 R LM WR', focalLength: 23, maxAperture: 4, sensorId: 'gfx_44x33' },
|
||||||
{ id: 'fuji_gf_55_17', brand: 'Fujifilm', name: 'GF 55mm f/1.7 R WR', focalLength: 55, maxAperture: 1.7, sensorId: 'gfx_44x33' },
|
{ id: 'fuji_gf_55_17', brand: 'Fujifilm', name: 'GF 55mm f/1.7 R WR', focalLength: 55, maxAperture: 1.7, sensorId: 'gfx_44x33' },
|
||||||
{ id: 'fuji_gf_63_28', brand: 'Fujifilm', name: 'GF 63mm f/2.8 R WR', focalLength: 63, maxAperture: 2.8, sensorId: 'gfx_44x33' },
|
{ id: 'fuji_gf_63_28', brand: 'Fujifilm', name: 'GF 63mm f/2.8 R WR', focalLength: 63, maxAperture: 2.8, sensorId: 'gfx_44x33' },
|
||||||
{ id: 'fuji_gf_80_17', brand: 'Fujifilm', name: 'GF 80mm f/1.7 R WR', focalLength: 80, maxAperture: 1.7, sensorId: 'gfx_44x33' },
|
{ id: 'fuji_gf_80_17', brand: 'Fujifilm', name: 'GF 80mm f/1.7 R WR', focalLength: 80, maxAperture: 1.7, sensorId: 'gfx_44x33' },
|
||||||
{ id: 'fuji_gf_110_2', brand: 'Fujifilm', name: 'GF 110mm f/2 R LM WR', focalLength: 110, maxAperture: 2, sensorId: 'gfx_44x33' },
|
{ id: 'fuji_gf_110_2', brand: 'Fujifilm', name: 'GF 110mm f/2 R LM WR', focalLength: 110, maxAperture: 2, sensorId: 'gfx_44x33' },
|
||||||
|
{ id: 'mitakon_65_14', brand: 'Mitakon', name: 'Mitakon Speedmaster 65mm f/1.4', focalLength: 65, maxAperture: 1.4, sensorId: 'gfx_44x33' },
|
||||||
|
{ id: 'mitakon_80_16', brand: 'Mitakon', name: 'Mitakon Speedmaster 80mm f/1.6', focalLength: 80, maxAperture: 1.6, sensorId: 'gfx_44x33' },
|
||||||
|
|
||||||
|
// Hasselblad XCD primes
|
||||||
|
{ id: 'hasselblad_xcd_25_25', brand: 'Hasselblad', name: 'XCD 25mm f/2.5', focalLength: 25, maxAperture: 2.5, sensorId: 'gfx_44x33' },
|
||||||
|
{ id: 'hasselblad_xcd_38_25', brand: 'Hasselblad', name: 'XCD 38mm f/2.5', focalLength: 38, maxAperture: 2.5, sensorId: 'gfx_44x33' },
|
||||||
|
{ id: 'hasselblad_xcd_55_25', brand: 'Hasselblad', name: 'XCD 55mm f/2.5', focalLength: 55, maxAperture: 2.5, sensorId: 'gfx_44x33' },
|
||||||
|
{ id: 'hasselblad_xcd_80_19', brand: 'Hasselblad', name: 'XCD 80mm f/1.9', focalLength: 80, maxAperture: 1.9, sensorId: 'gfx_44x33' },
|
||||||
|
{ id: 'hasselblad_xcd_90_25', brand: 'Hasselblad', name: 'XCD 90mm f/2.5', focalLength: 90, maxAperture: 2.5, sensorId: 'gfx_44x33' },
|
||||||
|
|
||||||
|
// Phase One (XF) primes
|
||||||
|
{ id: 'phaseone_sk_80_28_ls', brand: 'Schneider Kreuznach', name: 'Schneider Kreuznach 80mm LS f/2.8', focalLength: 80, maxAperture: 2.8, sensorId: 'phase_one_645' },
|
||||||
|
{ id: 'phaseone_sk_110_28_ls', brand: 'Schneider Kreuznach', name: 'Schneider Kreuznach 110mm LS f/2.8', focalLength: 110, maxAperture: 2.8, sensorId: 'phase_one_645' },
|
||||||
|
{ id: 'phaseone_sk_120_4_ls_macro', brand: 'Schneider Kreuznach', name: 'Schneider Kreuznach 120mm LS Macro f/4', focalLength: 120, maxAperture: 4, sensorId: 'phase_one_645' },
|
||||||
|
{ id: 'phaseone_sk_150_28_ls_if', brand: 'Schneider Kreuznach', name: 'Schneider Kreuznach 150mm LS f/2.8 IF', focalLength: 150, maxAperture: 2.8, sensorId: 'phase_one_645' },
|
||||||
|
{ id: 'phaseone_sk_240_45_ls_if', brand: 'Schneider Kreuznach', name: 'Schneider Kreuznach 240mm LS f/4.5 IF', focalLength: 240, maxAperture: 4.5, sensorId: 'phase_one_645' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const findLensById = (lensId, lenses = defaultLenses) =>
|
export const findLensById = (lensId, lenses = defaultLenses) =>
|
||||||
|
|||||||
+12
-1
@@ -7,6 +7,7 @@ const baseSensors = [
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 24,
|
height: 24,
|
||||||
aspectRatio: '3:2',
|
aspectRatio: '3:2',
|
||||||
|
shortLabel: 'FF',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'aps_c_fuji',
|
id: 'aps_c_fuji',
|
||||||
@@ -14,13 +15,23 @@ const baseSensors = [
|
|||||||
width: 23.5,
|
width: 23.5,
|
||||||
height: 15.6,
|
height: 15.6,
|
||||||
aspectRatio: '3:2',
|
aspectRatio: '3:2',
|
||||||
|
shortLabel: 'APS-C',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gfx_44x33',
|
id: 'gfx_44x33',
|
||||||
name: 'GFX Medium Format 44×33',
|
name: 'Medium Format 44×33',
|
||||||
width: 44,
|
width: 44,
|
||||||
height: 33,
|
height: 33,
|
||||||
aspectRatio: '4:3',
|
aspectRatio: '4:3',
|
||||||
|
shortLabel: '44×33',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'phase_one_645',
|
||||||
|
name: 'Phase One 645 53.7×40.4',
|
||||||
|
width: 53.7,
|
||||||
|
height: 40.4,
|
||||||
|
aspectRatio: '4:3',
|
||||||
|
shortLabel: 'P1',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user