Initialize project without preview renderer
This commit is contained in:
@@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user