Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ee8a16fb | ||
|
|
eb8d699afe |
+129
-3
@@ -13,6 +13,66 @@ import {
|
|||||||
|
|
||||||
const COLOR_PALETTE = ['#38bdf8', '#a855f7', '#f97316', '#22c55e', '#e11d48', '#6366f1', '#14b8a6', '#f59e0b'];
|
const COLOR_PALETTE = ['#38bdf8', '#a855f7', '#f97316', '#22c55e', '#e11d48', '#6366f1', '#14b8a6', '#f59e0b'];
|
||||||
|
|
||||||
|
const INITIAL_SELECTED_LENS_IDS = defaultLenses.slice(0, 3).map((lens) => lens.id);
|
||||||
|
|
||||||
|
const createInitialColorAssignments = (lensIds) => {
|
||||||
|
const assignments = {};
|
||||||
|
lensIds.forEach((lensId, index) => {
|
||||||
|
assignments[lensId] = index % COLOR_PALETTE.length;
|
||||||
|
});
|
||||||
|
return assignments;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reconcileColorAssignments = (selectedLensIds, previousAssignments) => {
|
||||||
|
if (selectedLensIds.length === 0) {
|
||||||
|
return selectedLensIds.length === Object.keys(previousAssignments).length ? previousAssignments : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAssignments = {};
|
||||||
|
const usedIndices = new Set();
|
||||||
|
|
||||||
|
selectedLensIds.forEach((lensId) => {
|
||||||
|
const priorIndex = previousAssignments[lensId];
|
||||||
|
if (typeof priorIndex === 'number' && !usedIndices.has(priorIndex)) {
|
||||||
|
nextAssignments[lensId] = priorIndex;
|
||||||
|
usedIndices.add(priorIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableIndices = [];
|
||||||
|
for (let i = 0; i < COLOR_PALETTE.length; i += 1) {
|
||||||
|
if (!usedIndices.has(i)) {
|
||||||
|
availableIndices.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let overflowIndex = 0;
|
||||||
|
selectedLensIds.forEach((lensId) => {
|
||||||
|
if (typeof nextAssignments[lensId] === 'number') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let assignedIndex;
|
||||||
|
if (availableIndices.length > 0) {
|
||||||
|
assignedIndex = availableIndices.shift();
|
||||||
|
} else {
|
||||||
|
assignedIndex = overflowIndex % COLOR_PALETTE.length;
|
||||||
|
overflowIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAssignments[lensId] = assignedIndex;
|
||||||
|
usedIndices.add(assignedIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
const previousKeys = Object.keys(previousAssignments);
|
||||||
|
const nextKeys = Object.keys(nextAssignments);
|
||||||
|
const assignmentsChanged =
|
||||||
|
previousKeys.length !== nextKeys.length ||
|
||||||
|
selectedLensIds.some((lensId) => nextAssignments[lensId] !== previousAssignments[lensId]);
|
||||||
|
|
||||||
|
return assignmentsChanged ? nextAssignments : previousAssignments;
|
||||||
|
};
|
||||||
|
|
||||||
const plotTypeOptions = [
|
const plotTypeOptions = [
|
||||||
{ value: PlotTypes.DISTANCE, label: 'Absolute scene distance' },
|
{ value: PlotTypes.DISTANCE, label: 'Absolute scene distance' },
|
||||||
{ value: PlotTypes.NORMALIZED, label: 'Blur vs normalized distance' },
|
{ value: PlotTypes.NORMALIZED, label: 'Blur vs normalized distance' },
|
||||||
@@ -73,6 +133,62 @@ const filterDatasetForPlotType = (dataset, plotType) => {
|
|||||||
return dataset;
|
return dataset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PreviewPanel = ({ lensCurves }) => {
|
||||||
|
const previewItems = lensCurves.map((curve) => {
|
||||||
|
const infinityStat = curve.checkpointStats.find((stat) => !Number.isFinite(stat.separation));
|
||||||
|
const blurPercent = infinityStat?.blurPercentOfDiagonal ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
lens: curve.lens,
|
||||||
|
blurPercent,
|
||||||
|
colorIndex: curve.colorIndex,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxBlurPercent = previewItems.reduce((maxValue, item) => Math.max(maxValue, item.blurPercent), 0);
|
||||||
|
const denominator = maxBlurPercent > 0 ? maxBlurPercent : 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
{previewItems.length === 0 ? (
|
||||||
|
<div className="text-sm text-slate-500">Select at least one lens to preview its blur at infinity.</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
{previewItems.map((item) => {
|
||||||
|
const normalized = Math.min(item.blurPercent / denominator, 1);
|
||||||
|
const diameter = Math.max(18, 30 + normalized * 70);
|
||||||
|
const paletteColor = COLOR_PALETTE[item.colorIndex % COLOR_PALETTE.length];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.lens.id} className="flex w-32 flex-col items-center gap-2 text-center">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-full border"
|
||||||
|
style={{
|
||||||
|
width: `${diameter}px`,
|
||||||
|
height: `${diameter}px`,
|
||||||
|
borderColor: paletteColor,
|
||||||
|
background: `${paletteColor}1A`,
|
||||||
|
boxShadow: `0 0 12px ${paletteColor}33`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">{item.lens.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-200">
|
||||||
|
<div className="font-medium text-slate-100">{item.lens.name}</div>
|
||||||
|
<div className="text-[10px] uppercase tracking-wide text-slate-500">
|
||||||
|
∞ blur {formatNumber(item.blurPercent, 2)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const PlotSection = ({ lensCurves, plotType, setPlotType }) => {
|
const PlotSection = ({ lensCurves, plotType, setPlotType }) => {
|
||||||
const axisConfig = getAxisConfig(plotType);
|
const axisConfig = getAxisConfig(plotType);
|
||||||
|
|
||||||
@@ -80,6 +196,8 @@ const PlotSection = ({ lensCurves, plotType, setPlotType }) => {
|
|||||||
const filteredDataset = filterDatasetForPlotType(curve.dataset, plotType);
|
const filteredDataset = filterDatasetForPlotType(curve.dataset, plotType);
|
||||||
const xValues = filteredDataset.map(axisConfig.accessor);
|
const xValues = filteredDataset.map(axisConfig.accessor);
|
||||||
const yValues = filteredDataset.map((point) => point.blurPercentOfDiagonal);
|
const yValues = filteredDataset.map((point) => point.blurPercentOfDiagonal);
|
||||||
|
const paletteIndex =
|
||||||
|
typeof curve.colorIndex === 'number' ? curve.colorIndex % COLOR_PALETTE.length : index % COLOR_PALETTE.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: xValues,
|
x: xValues,
|
||||||
@@ -96,7 +214,7 @@ const PlotSection = ({ lensCurves, plotType, setPlotType }) => {
|
|||||||
name: `${curve.lens.name} (${formatAperture(curve.apertureUsed)})`,
|
name: `${curve.lens.name} (${formatAperture(curve.apertureUsed)})`,
|
||||||
line: {
|
line: {
|
||||||
width: 2,
|
width: 2,
|
||||||
color: COLOR_PALETTE[index % COLOR_PALETTE.length],
|
color: COLOR_PALETTE[paletteIndex],
|
||||||
},
|
},
|
||||||
hovertemplate: `<b>%{text}</b><br>${axisConfig.label}: %{x:.2f}<br>Blur: %{customdata[0]:.2f}% diag (%{customdata[1]:.2f} mm)<extra></extra>`,
|
hovertemplate: `<b>%{text}</b><br>${axisConfig.label}: %{x:.2f}<br>Blur: %{customdata[0]:.2f}% diag (%{customdata[1]:.2f} mm)<extra></extra>`,
|
||||||
text: Array(filteredDataset.length).fill(curve.lens.name),
|
text: Array(filteredDataset.length).fill(curve.lens.name),
|
||||||
@@ -453,7 +571,7 @@ const OpticsPanel = ({
|
|||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [selectedLensIds, setSelectedLensIds] = useState(defaultLenses.slice(0, 3).map((lens) => lens.id));
|
const [selectedLensIds, setSelectedLensIds] = useState(INITIAL_SELECTED_LENS_IDS);
|
||||||
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);
|
||||||
@@ -467,6 +585,7 @@ const App = () => {
|
|||||||
const [maxSeparationLimit, setMaxSeparationLimit] = useState(80);
|
const [maxSeparationLimit, setMaxSeparationLimit] = useState(80);
|
||||||
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 checkpoints = useMemo(() => {
|
const checkpoints = useMemo(() => {
|
||||||
const userValues = parseCheckpointInput(checkpointsInput);
|
const userValues = parseCheckpointInput(checkpointsInput);
|
||||||
@@ -492,6 +611,10 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLensColors((currentAssignments) => reconcileColorAssignments(selectedLensIds, currentAssignments));
|
||||||
|
}, [selectedLensIds]);
|
||||||
|
|
||||||
const lensCurves = useMemo(() => {
|
const lensCurves = useMemo(() => {
|
||||||
return selectedLenses
|
return selectedLenses
|
||||||
.map((lens) => {
|
.map((lens) => {
|
||||||
@@ -536,10 +659,11 @@ const App = () => {
|
|||||||
dataset,
|
dataset,
|
||||||
checkpointStats,
|
checkpointStats,
|
||||||
entrancePupil: checkpointStats[0]?.entrancePupil ?? 0,
|
entrancePupil: checkpointStats[0]?.entrancePupil ?? 0,
|
||||||
|
colorIndex: lensColors[lens.id] ?? 0,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}, [selectedLenses, framingMode, fixedSubjectDistance, subjectHeight, frameFill, customSubjectDistance, customDimension, separationSamples, checkpoints]);
|
}, [selectedLenses, framingMode, fixedSubjectDistance, subjectHeight, frameFill, customSubjectDistance, customDimension, separationSamples, checkpoints, lensColors]);
|
||||||
|
|
||||||
const statsPayload = lensCurves.map((curve) => ({
|
const statsPayload = lensCurves.map((curve) => ({
|
||||||
lens: curve.lens,
|
lens: curve.lens,
|
||||||
@@ -636,6 +760,7 @@ const App = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<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} />
|
||||||
<FramingPanel
|
<FramingPanel
|
||||||
framingMode={framingMode}
|
framingMode={framingMode}
|
||||||
setFramingMode={setFramingMode}
|
setFramingMode={setFramingMode}
|
||||||
@@ -673,6 +798,7 @@ 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} />
|
||||||
<FramingPanel
|
<FramingPanel
|
||||||
framingMode={framingMode}
|
framingMode={framingMode}
|
||||||
setFramingMode={setFramingMode}
|
setFramingMode={setFramingMode}
|
||||||
|
|||||||
Reference in New Issue
Block a user