Initial Release
This commit is contained in:
181
app.js
Normal file
181
app.js
Normal file
@@ -0,0 +1,181 @@
|
||||
const isoAEl = document.getElementById("isoA");
|
||||
const fAEl = document.getElementById("fA");
|
||||
const tAEl = document.getElementById("tA");
|
||||
|
||||
const isoBEl = document.getElementById("isoB");
|
||||
const fBEl = document.getElementById("fB");
|
||||
const tBEl = document.getElementById("tB");
|
||||
|
||||
const resultEl = document.getElementById("result");
|
||||
|
||||
document.getElementById("calcBtn").addEventListener("click", calculate);
|
||||
document.getElementById("resetBtn").addEventListener("click", reset);
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") calculate();
|
||||
});
|
||||
|
||||
function parseShutter(value) {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return NaN;
|
||||
|
||||
if (raw.includes("/")) {
|
||||
const parts = raw.split("/");
|
||||
if (parts.length !== 2) return NaN;
|
||||
const num = parseFloat(parts[0]);
|
||||
const den = parseFloat(parts[1]);
|
||||
if (!isFinite(num) || !isFinite(den) || den === 0) return NaN;
|
||||
return num / den;
|
||||
}
|
||||
|
||||
const v = parseFloat(raw);
|
||||
if (!isFinite(v)) return NaN;
|
||||
return v;
|
||||
}
|
||||
|
||||
function parsePositiveOrEmpty(value, parser = parseFloat) {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (raw === "") return { empty: true, value: null };
|
||||
const parsed = parser(raw);
|
||||
if (!isFinite(parsed) || parsed <= 0) return { empty: false, value: NaN };
|
||||
return { empty: false, value: parsed };
|
||||
}
|
||||
|
||||
function prettyShutter(seconds) {
|
||||
if (!(seconds > 0) || !isFinite(seconds)) return "Invalid";
|
||||
|
||||
// exact decimal seconds (up to 8 decimals, trimmed)
|
||||
const exactSeconds = seconds.toFixed(8).replace(/0+$/, "").replace(/\.$/, "");
|
||||
|
||||
if (seconds < 1) {
|
||||
// exact reciprocal (not snapped to common shutter values)
|
||||
const reciprocal = 1 / seconds;
|
||||
const reciprocalStr = reciprocal
|
||||
.toFixed(6)
|
||||
.replace(/0+$/, "")
|
||||
.replace(/\.$/, "");
|
||||
|
||||
return `1/${reciprocalStr} s (${exactSeconds} s)`;
|
||||
}
|
||||
|
||||
return `${exactSeconds} s`;
|
||||
}
|
||||
|
||||
function formatAperture(n) {
|
||||
if (!(n > 0) || !isFinite(n)) return "Invalid";
|
||||
return `f/${n.toFixed(2).replace(/0+$/, "").replace(/\.$/, "")}`;
|
||||
}
|
||||
|
||||
function formatISO(iso) {
|
||||
if (!(iso > 0) || !isFinite(iso)) return "Invalid";
|
||||
return `ISO ${Math.round(iso)}`;
|
||||
}
|
||||
|
||||
function calculate() {
|
||||
// Reference (must all be present)
|
||||
const isoA = parseFloat(isoAEl.value);
|
||||
const fA = parseFloat(fAEl.value);
|
||||
const tA = parseShutter(tAEl.value);
|
||||
|
||||
if (
|
||||
!(isoA > 0) ||
|
||||
!(fA > 0) ||
|
||||
!(tA > 0) ||
|
||||
!isFinite(isoA) ||
|
||||
!isFinite(fA) ||
|
||||
!isFinite(tA)
|
||||
) {
|
||||
resultEl.innerHTML = `<span class="error">Camera A must have valid positive ISO, f-stop, and shutter speed.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Camera B: exactly one missing
|
||||
const isoBParsed = parsePositiveOrEmpty(isoBEl.value, parseFloat);
|
||||
const fBParsed = parsePositiveOrEmpty(fBEl.value, parseFloat);
|
||||
const tBParsed = parsePositiveOrEmpty(tBEl.value, parseShutter);
|
||||
|
||||
const fields = [
|
||||
{ key: "iso", ...isoBParsed },
|
||||
{ key: "f", ...fBParsed },
|
||||
{ key: "t", ...tBParsed },
|
||||
];
|
||||
|
||||
const emptyFields = fields.filter((f) => f.empty);
|
||||
if (emptyFields.length !== 1) {
|
||||
resultEl.innerHTML = `<span class="error">For Camera B, leave exactly one field empty (the one to calculate).</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (fields.some((f) => !f.empty && (!isFinite(f.value) || f.value <= 0))) {
|
||||
resultEl.innerHTML = `<span class="error">Camera B has invalid values. Use positive numbers, and shutter like 1/250 or 0.004.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const missing = emptyFields[0].key;
|
||||
let isoB = isoBParsed.value;
|
||||
let fB = fBParsed.value;
|
||||
let tB = tBParsed.value;
|
||||
|
||||
// From:
|
||||
// tB = tA * (fB^2 / fA^2) * (isoA / isoB)
|
||||
// Rearranged for each missing variable:
|
||||
if (missing === "t") {
|
||||
tB = tA * ((fB * fB) / (fA * fA)) * (isoA / isoB);
|
||||
if (!(tB > 0) || !isFinite(tB)) {
|
||||
resultEl.innerHTML = `<span class="error">Could not compute shutter speed from given values.</span>`;
|
||||
return;
|
||||
}
|
||||
tBEl.value = tB.toPrecision(6);
|
||||
|
||||
resultEl.innerHTML = `
|
||||
<div class="ok">Calculated shutter speed for Camera B: <span class="mono">${prettyShutter(tB)}</span></div>
|
||||
<div style="margin-top:8px;">B now: <span class="mono">${formatISO(isoB)}, ${formatAperture(fB)}, ${prettyShutter(tB)}</span></div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (missing === "iso") {
|
||||
// isoB = isoA * tA * fB^2 / ( tB * fA^2 )
|
||||
isoB = (isoA * tA * (fB * fB)) / (tB * (fA * fA));
|
||||
if (!(isoB > 0) || !isFinite(isoB)) {
|
||||
resultEl.innerHTML = `<span class="error">Could not compute ISO from given values.</span>`;
|
||||
return;
|
||||
}
|
||||
isoBEl.value = Math.round(isoB).toString();
|
||||
|
||||
resultEl.innerHTML = `
|
||||
<div class="ok">Calculated ISO for Camera B: <span class="mono">${formatISO(isoB)}</span></div>
|
||||
<div style="margin-top:8px;">B now: <span class="mono">${formatISO(isoB)}, ${formatAperture(fB)}, ${prettyShutter(tB)}</span></div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (missing === "f") {
|
||||
// fB^2 = (tB * fA^2 * isoB) / (tA * isoA)
|
||||
// fB = sqrt(...)
|
||||
const inside = (tB * (fA * fA) * isoB) / (tA * isoA);
|
||||
fB = Math.sqrt(inside);
|
||||
if (!(fB > 0) || !isFinite(fB)) {
|
||||
resultEl.innerHTML = `<span class="error">Could not compute aperture from given values.</span>`;
|
||||
return;
|
||||
}
|
||||
fBEl.value = fB.toFixed(2);
|
||||
|
||||
resultEl.innerHTML = `
|
||||
<div class="ok">Calculated aperture for Camera B: <span class="mono">${formatAperture(fB)}</span></div>
|
||||
<div style="margin-top:8px;">B now: <span class="mono">${formatISO(isoB)}, ${formatAperture(fB)}, ${prettyShutter(tB)}</span></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
isoAEl.value = "100";
|
||||
fAEl.value = "2.0";
|
||||
tAEl.value = "1/1000";
|
||||
|
||||
isoBEl.value = "";
|
||||
fBEl.value = "";
|
||||
tBEl.value = "";
|
||||
|
||||
resultEl.textContent = "Result will appear here.";
|
||||
}
|
||||
Reference in New Issue
Block a user