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 = `Camera A must have valid positive ISO, f-stop, and shutter speed.`; 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 = `For Camera B, leave exactly one field empty (the one to calculate).`; return; } if (fields.some((f) => !f.empty && (!isFinite(f.value) || f.value <= 0))) { resultEl.innerHTML = `Camera B has invalid values. Use positive numbers, and shutter like 1/250 or 0.004.`; 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 = `Could not compute shutter speed from given values.`; return; } tBEl.value = tB.toPrecision(6); resultEl.innerHTML = `