Initialize project without preview renderer

This commit is contained in:
2025-09-28 23:30:02 +02:00
commit 2852c88a77
16 changed files with 11459 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
export const FramingModes = {
FIXED_DISTANCE: 'fixed-distance',
MATCH_DIAGONAL: 'match-diagonal',
MATCH_HEIGHT: 'match-height',
CUSTOM: 'custom-framing',
};
export const PlotTypes = {
DISTANCE: 'distance',
ZOOMED: 'zoomed',
NORMALIZED: 'normalized',
SEPARATION: 'separation',
DIOPTER: 'diopter',
};
const MM_PER_METER = 1000;
const DEFAULT_SUBJECT_WIDTH_RATIO = 0.35; // approximate shoulder-to-height ratio for diagonal framing
const coercePositiveNumber = (value, fallback) => {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback;
};
const metersToMillimeters = (valueMeters) => valueMeters * MM_PER_METER;
const millimetersToMeters = (valueMillimeters) => valueMillimeters / MM_PER_METER;
export const calculateSensorDiagonal = (sensor) =>
Math.sqrt(sensor.width ** 2 + sensor.height ** 2);
export const calculateEntrancePupil = (focalLengthMm, aperture) =>
focalLengthMm / coercePositiveNumber(aperture, 1.0);
const calculateMagnification = (imageSizeMm, objectSizeMm) => {
const safeImage = coercePositiveNumber(imageSizeMm, 1);
const safeObject = coercePositiveNumber(objectSizeMm, 1);
return safeImage / safeObject;
};
const distanceForMagnification = (focalLengthMm, magnification) => {
const safeMag = coercePositiveNumber(magnification, 1e-6);
return focalLengthMm * (1 + 1 / safeMag);
};
export const computeSubjectDistance = ({
framingMode,
lens,
sensor,
fixedSubjectDistance,
subjectHeight,
frameFill,
customSubjectDistance,
customDimension = 'height',
}) => {
const focalLength = lens.focalLength;
const safeFrameFill = Math.min(Math.max(frameFill ?? 0.8, 1e-3), 0.999);
const subjectHeightMeters = coercePositiveNumber(subjectHeight, 1.7);
const subjectHeightMm = metersToMillimeters(subjectHeightMeters);
if (framingMode === FramingModes.FIXED_DISTANCE) {
return coercePositiveNumber(fixedSubjectDistance, 2);
}
if (framingMode === FramingModes.CUSTOM && customSubjectDistance) {
return coercePositiveNumber(customSubjectDistance, 2);
}
const dimensionSelector = framingMode === FramingModes.MATCH_DIAGONAL || customDimension === 'diagonal'
? sensor.diagonal
: sensor.height;
const subjectDimensionMm = framingMode === FramingModes.MATCH_DIAGONAL || customDimension === 'diagonal'
? subjectHeightMm * Math.sqrt(1 + DEFAULT_SUBJECT_WIDTH_RATIO ** 2)
: subjectHeightMm;
const imageSizeMm = safeFrameFill * dimensionSelector;
const magnification = calculateMagnification(imageSizeMm, subjectDimensionMm);
const subjectDistanceMm = distanceForMagnification(focalLength, magnification);
return Math.max(millimetersToMeters(subjectDistanceMm), 0.2);
};
export const calculateBlurCircle = ({ focalLengthMm, aperture, subjectDistanceM, sceneDistanceM }) => {
const safeAperture = coercePositiveNumber(aperture, 1);
const f = focalLengthMm;
const u = metersToMillimeters(coercePositiveNumber(subjectDistanceM, 0.5));
const s = metersToMillimeters(coercePositiveNumber(sceneDistanceM, 0.51));
if (Math.abs(s - u) < 1e-6) {
return 0;
}
if (Math.abs(u - f) < 1e-6) {
return 0;
}
const numerator = f ** 2 * Math.abs(u - s);
const denominator = safeAperture * s * (u - f);
return numerator / Math.abs(denominator);
};
export const buildSeparationSamples = ({
foregroundRange = 0,
maxSeparation = 30,
steps = 160,
} = {}) => {
const clampedForeground = Math.max(foregroundRange, 0);
const clampedBackground = Math.max(maxSeparation, 0.1);
const sampleCount = Math.max(Math.floor(steps), 5);
const offsets = [];
const totalSpan = clampedForeground + clampedBackground;
const frontShare = totalSpan > 0 ? clampedForeground / totalSpan : 0;
let frontCount = clampedForeground > 0 ? Math.max(Math.floor(sampleCount * frontShare), 3) : 0;
frontCount = Math.min(frontCount, sampleCount - 3);
if (clampedForeground > 0 && frontCount > 0) {
for (let i = frontCount; i >= 1; i -= 1) {
const t = i / (frontCount + 1);
const eased = t ** 1.4; // denser near the subject plane
offsets.push(-clampedForeground * eased);
}
}
offsets.push(0);
const backCount = Math.max(sampleCount - offsets.length, 3);
for (let i = 1; i <= backCount; i += 1) {
const t = i / (backCount + 1);
const eased = t ** 1.2;
offsets.push(clampedBackground * eased);
}
return offsets;
};
export const buildBlurDataset = ({
lens,
sensor,
aperture,
subjectDistance,
separationSamples,
}) => {
const entrancePupil = calculateEntrancePupil(lens.focalLength, aperture);
const safeSubjectDistance = coercePositiveNumber(subjectDistance, 0.5);
return separationSamples.map((offset) => {
let sceneDistance = safeSubjectDistance + offset;
sceneDistance = Math.max(sceneDistance, 0.2);
const focusOffset = sceneDistance - safeSubjectDistance;
const blurMm = calculateBlurCircle({
focalLengthMm: lens.focalLength,
aperture,
subjectDistanceM: safeSubjectDistance,
sceneDistanceM: sceneDistance,
});
const normalizedDistance = sceneDistance / safeSubjectDistance;
const diopterDifference = (1 / safeSubjectDistance) - (1 / sceneDistance);
const percentOfDiagonal = (blurMm / sensor.diagonal) * 100;
return {
focusOffset,
sceneDistance,
normalizedDistance,
diopterDifference,
blurMm,
blurPercentOfDiagonal: percentOfDiagonal,
};
}).map((point) => ({
...point,
entrancePupil,
}));
};
export const buildStatsAtCheckpoints = ({
lens,
sensor,
aperture,
subjectDistance,
checkpoints,
}) => {
const entrancePupil = calculateEntrancePupil(lens.focalLength, aperture);
const safeSubjectDistance = coercePositiveNumber(subjectDistance, 0.5);
return checkpoints.map((separation) => {
let sceneDistance;
let blurMm;
if (!Number.isFinite(separation)) {
sceneDistance = Infinity;
blurMm = calculateBlurCircle({
focalLengthMm: lens.focalLength,
aperture,
subjectDistanceM: safeSubjectDistance,
sceneDistanceM: Infinity,
});
} else {
sceneDistance = safeSubjectDistance + separation;
blurMm = calculateBlurCircle({
focalLengthMm: lens.focalLength,
aperture,
subjectDistanceM: safeSubjectDistance,
sceneDistanceM: sceneDistance,
});
}
return {
lensId: lens.id,
lensName: lens.name,
separation,
blurMm,
blurPercentOfDiagonal: (blurMm / sensor.diagonal) * 100,
entrancePupil,
subjectDistance: safeSubjectDistance,
sceneDistance,
};
});
};