Flash Calculator

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flash Meter — Guide Number Calculator</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
:root{
--bg:#121212;
--panel:#1a1a1a;
--panel-alt:#212121;
--border:#3a3a3a;
--border-soft:#292929;
--accent:#f2f0ec;
--accent-dim:#7a7a7a;
--text:#e8e8e8;
--text-muted:#999999;
--text-faint:#6b6b6b;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
body{
background:
radial-gradient(ellipse 900px 500px at 50% -10%, rgba(255,255,255,0.05), transparent 60%),
var(--bg);
color:var(--text);
font-family:'Space Grotesk',sans-serif;
min-height:100vh;
padding:32px 16px 64px;
display:flex;
justify-content:center;
}
.device{
width:100%;
max-width:520px;
}
.header{
display:flex;
align-items:baseline;
justify-content:space-between;
padding-bottom:18px;
border-bottom:1px solid var(--border-soft);
margin-bottom:22px;
}
.header .mark{
font-family:'JetBrains Mono',monospace;
font-size:11px;
letter-spacing:0.18em;
color:var(--accent);
text-transform:uppercase;
}
.header h1{
font-size:22px;
font-weight:700;
letter-spacing:-0.01em;
margin:2px 0 0;
}
.header .sub{
font-size:12px;
color:var(--text-muted);
text-align:right;
line-height:1.5;
font-family:'JetBrains Mono',monospace;
}
.panel{
background:var(--panel);
border:1px solid var(--border);
border-radius:10px;
padding:18px 18px 20px;
margin-bottom:16px;
position:relative;
}
.panel-label{
font-family:'JetBrains Mono',monospace;
font-size:10px;
letter-spacing:0.14em;
text-transform:uppercase;
color:var(--text-faint);
margin-bottom:14px;
display:flex;
justify-content:space-between;
align-items:center;
}
.row{
display:grid;
grid-template-columns:1fr 1fr;
gap:12px;
margin-bottom:12px;
}
.row:last-child{margin-bottom:0;}
.field{display:flex;flex-direction:column;gap:6px;}
.field label{
font-size:11px;
color:var(--text-muted);
font-weight:500;
}
.field .unit-wrap{
display:flex;
border:1px solid var(--border);
border-radius:6px;
background:var(--panel-alt);
overflow:hidden;
}
.field input[type=number]{
flex:1;
min-width:0;
background:transparent;
border:none;
color:var(--text);
font-family:'JetBrains Mono',monospace;
font-size:15px;
font-weight:500;
padding:9px 10px;
outline:none;
}
.field input[type=number]:disabled{color:var(--text-faint);}
.field select{
background:var(--panel-alt);
border:none;
color:var(--text-muted);
font-family:'JetBrains Mono',monospace;
font-size:12px;
padding:0 8px;
border-left:1px solid var(--border);
outline:none;
}
.field .unit-wrap:focus-within{border-color:var(--accent-dim);}
.field select.full{
padding:9px 8px;
border-left:none;
font-size:13px;
width:100%;
}
.toggle-group{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:8px;
margin-bottom:16px;
}
.toggle-group.two{grid-template-columns:repeat(2,1fr);}
.toggle-btn{
background:var(--panel-alt);
border:1px solid var(--border);
color:var(--text-muted);
font-family:'Space Grotesk',sans-serif;
font-size:12px;
font-weight:600;
padding:9px 6px;
border-radius:6px;
cursor:pointer;
transition:all .15s ease;
text-align:center;
}
.toggle-btn:hover{border-color:var(--accent-dim);}
.toggle-btn.active{
background:rgba(255,255,255,0.10);
border-color:var(--accent);
color:#111;
}
.readout{
background:linear-gradient(180deg,#0f0c09,#131009);
border:1px solid var(--border);
border-radius:8px;
padding:16px 18px;
margin-bottom:16px;
text-align:center;
}
.readout .rlabel{
font-family:'JetBrains Mono',monospace;
font-size:10px;
letter-spacing:0.14em;
text-transform:uppercase;
color:var(--text-faint);
margin-bottom:6px;
}
.readout .rvalue{
font-family:'JetBrains Mono',monospace;
font-size:34px;
font-weight:700;
color:var(--accent);
text-shadow:0 0 14px rgba(255,255,255,0.18);
letter-spacing:-0.01em;
}
.readout .rnote{
font-size:11px;
color:var(--text-muted);
margin-top:6px;
font-family:'JetBrains Mono',monospace;
}
.readout.warn{background:var(--text);border-color:var(--text);}
.readout.warn .rlabel{color:#3a3a3a;}
.readout.warn .rvalue{color:#111;text-shadow:none;}
.readout.warn .rnote{color:#3a3a3a;}
.readout.small .rvalue{font-size:24px;}
.readout.cool .rvalue{color:var(--accent);}
.dial-wrap{margin-bottom:4px;}
.dial-title{
display:flex;
justify-content:space-between;
align-items:baseline;
margin-bottom:9px;
}
.dial-title label{font-size:11px;color:var(--text-muted);font-weight:500;}
.dial-title .stops{
font-family:'JetBrains Mono',monospace;
font-size:11px;
color:var(--text-faint);
}
.dial{
display:flex;
border:1px solid var(--border);
border-radius:6px;
overflow:hidden;
background:var(--panel-alt);
}
.dial button{
flex:1;
background:transparent;
border:none;
border-right:1px solid var(--border-soft);
color:var(--text-muted);
font-family:'JetBrains Mono',monospace;
font-size:11px;
padding:9px 2px;
cursor:pointer;
transition:all .15s ease;
}
.dial button:last-child{border-right:none;}
.dial button:hover{background:rgba(232,163,61,0.08);}
.dial button.active{
background:var(--accent);
color:#141414;
font-weight:700;
}
.dial button:disabled{cursor:default;}
.dial.disabled button{opacity:0.45;pointer-events:none;}
.check-toggle{
display:flex;
align-items:center;
gap:8px;
cursor:pointer;
user-select:none;
}
.check-toggle input{accent-color:var(--accent);width:14px;height:14px;}
.check-toggle span{font-size:12px;color:var(--text-muted);font-weight:500;}
.body-collapse{
margin-top:14px;
padding-top:14px;
border-top:1px solid var(--border-soft);
display:none;
}
.body-collapse.open{display:block;}
.context-line{
font-family:'JetBrains Mono',monospace;
font-size:11px;
color:var(--text-faint);
margin:-2px 0 12px;
}
.context-line b{color:var(--text-muted);font-weight:600;}
.quick-row{
display:flex;
gap:6px;
margin-top:8px;
flex-wrap:wrap;
}
.quick-btn{
background:var(--panel-alt);
border:1px solid var(--border);
color:var(--text-muted);
font-family:'JetBrains Mono',monospace;
font-size:10.5px;
padding:6px 9px;
border-radius:5px;
cursor:pointer;
}
.quick-btn:hover{border-color:var(--accent-dim);color:var(--accent);}
.sync-banner{
margin-top:12px;
border-radius:6px;
padding:10px 12px;
font-size:12px;
font-family:'JetBrains Mono',monospace;
line-height:1.5;
border:1px solid var(--border);
background:var(--panel-alt);
color:var(--text-muted);
}
.sync-banner.bad{
border-color:var(--text);
background:var(--text);
color:#111;
font-weight:600;
}
.sync-banner.ok{
border-color:var(--border);
background:var(--panel-alt);
color:var(--text-muted);
}
.hss-row{
display:flex;
align-items:center;
gap:8px;
margin-top:10px;
}
.hss-row label{font-size:11px;color:var(--text-muted);}
.hss-row input{accent-color:var(--accent);width:14px;height:14px;}
.divider-label{
font-family:'JetBrains Mono',monospace;
font-size:10px;
letter-spacing:0.12em;
text-transform:uppercase;
color:var(--text-faint);
margin:16px 0 10px;
}
.footnote{
font-size:11px;
color:var(--text-faint);
line-height:1.6;
padding:0 4px;
font-family:'JetBrains Mono',monospace;
}
@media (max-width:480px){
body{padding:18px 10px 40px;}
.device{max-width:100%;}
.header{margin-bottom:16px;padding-bottom:14px;}
.header h1{font-size:19px;}
.header .sub{font-size:10.5px;}
.panel{padding:14px 12px 16px;margin-bottom:12px;border-radius:9px;}
.panel-label{margin-bottom:11px;}
.row{gap:9px;margin-bottom:10px;}
.field label{font-size:10.5px;}
.field input[type=number]{font-size:14px;padding:8px 8px;}
.toggle-group{gap:6px;margin-bottom:13px;}
.toggle-btn{font-size:11px;padding:8px 4px;}
.rvalue{font-size:26px !important;}
.readout{padding:14px 12px;margin-bottom:12px;}
.readout.small .rvalue{font-size:19px !important;}
.dial button{font-size:9px;padding:8px 1px;}
.quick-btn{font-size:10px;padding:6px 7px;}
.footnote{font-size:10.5px;padding:0 2px;}
}
@media (max-width:360px){
.row{grid-template-columns:1fr;}
.dial{flex-wrap:wrap;}
.dial button{flex:1 1 25%;border-bottom:1px solid var(--border-soft);}
}
</style>
</head>
<body>
<div class="device">
<div class="header">
<div>
<div class="mark">GN ÷ D = f</div>
<h1>Flash Meter</h1>
</div>
<div class="sub">guide number<br>calculator</div>
</div>
<div class="panel">
<div class="panel-label">Fixed settings</div>
<div class="row">
<div class="field">
<label for="iso">ISO</label>
<div class="unit-wrap">
<input type="number" id="iso" value="200" min="1" step="1">
</div>
</div>
<div class="field">
<label for="gn">Guide number (@ ISO 100)</label>
<div class="unit-wrap">
<input type="number" id="gn" value="15" min="0.1" step="0.5">
<select id="gnUnit">
<option value="m">m</option>
<option value="ft">ft</option>
</select>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-label">Solve for</div>
<div class="toggle-group" id="solveGroup">
<button class="toggle-btn active" data-solve="power">Power</button>
<button class="toggle-btn" data-solve="distance">Distance</button>
<button class="toggle-btn" data-solve="aperture">Aperture</button>
</div>
<div class="row">
<div class="field">
<label for="aperture">Aperture (f-number)</label>
<div class="unit-wrap">
<span style="padding:9px 0 9px 10px;color:var(--text-faint);font-family:'JetBrains Mono',monospace;font-size:15px;">f/</span>
<input type="number" id="aperture" value="8" min="0.5" step="0.1">
</div>
</div>
<div class="field">
<label for="distance">Distance to subject</label>
<div class="unit-wrap">
<input type="number" id="distance" value="6" min="0.01" step="0.1">
<select id="distUnit">
<option value="m">m</option>
<option value="ft" selected>ft</option>
</select>
</div>
</div>
</div>
<div class="dial-wrap">
<div class="dial-title">
<label>Flash power</label>
<span class="stops" id="stopsLabel">0 stops from full</span>
</div>
<div class="dial" id="dial">
<button data-stop="0">1/1</button>
<button data-stop="1">1/2</button>
<button data-stop="2">1/4</button>
<button data-stop="3">1/8</button>
<button data-stop="4">1/16</button>
<button data-stop="5">1/32</button>
<button data-stop="6">1/64</button>
<button data-stop="7">1/128</button>
</div>
</div>
</div>
<div class="readout" id="readout">
<div class="rlabel" id="readoutLabel">Required flash power</div>
<div class="rvalue" id="readoutValue">1/2</div>
<div class="rnote" id="readoutNote">1.0 stops down from full</div>
</div>
<div class="panel">
<label class="check-toggle">
<input type="checkbox" id="ambientEnable">
<span>Balance flash with ambient light (optional)</span>
</label>
<div class="body-collapse" id="ambientBody">
<div class="context-line">Flash exposure is locked at <b><span id="ctxAperture">f/5.6</span></b>, ISO <b><span id="ctxIso">100</span></b> — background exposure is set by shutter speed alone.</div>
<div class="toggle-group two" id="ambientMethodGroup">
<button class="toggle-btn active" data-method="preset">Lighting preset</button>
<button class="toggle-btn" data-method="meter">Meter reading</button>
</div>
<div id="presetRow" class="row" style="grid-template-columns:1fr;">
<div class="field">
<label for="presetSelect">Ambient light condition</label>
<div class="unit-wrap">
<select id="presetSelect" class="full">
<option value="15">Bright direct sun</option>
<option value="14">Hazy sun / slight overcast</option>
<option value="13">Cloudy bright, no shadows</option>
<option value="12" selected>Overcast / open shade</option>
<option value="9">Sunset / golden hour</option>
<option value="7">Bright indoor room</option>
<option value="5">Average indoor room</option>
<option value="3">Twilight / dusk</option>
<option value="0">City street at night</option>
<option value="-3">Dark interior, minimal light</option>
</select>
</div>
</div>
</div>
<div id="meterRow" class="row" style="grid-template-columns:1fr;display:none;">
<div class="field">
<label for="ambientMeter">Ambient-only meter reading (no flash), at the aperture &amp; ISO above</label>
<div class="unit-wrap">
<span style="padding:9px 0 9px 10px;color:var(--text-faint);font-family:'JetBrains Mono',monospace;font-size:15px;">1/</span>
<input type="number" id="ambientMeter" value="60" min="1" step="1">
<span style="padding:9px 10px;color:var(--text-faint);font-family:'JetBrains Mono',monospace;font-size:12px;">s</span>
</div>
</div>
</div>
<div class="divider-label">Your shutter speed</div>
<div class="row" style="grid-template-columns:1fr;">
<div class="field">
<div class="unit-wrap">
<span style="padding:9px 0 9px 10px;color:var(--text-faint);font-family:'JetBrains Mono',monospace;font-size:15px;">1/</span>
<input type="number" id="shutterSpeed" value="125" min="1" step="1">
<span style="padding:9px 10px;color:var(--text-faint);font-family:'JetBrains Mono',monospace;font-size:12px;">s</span>
</div>
</div>
</div>
<div class="quick-row">
<button class="quick-btn" data-set="match">match ambient (0)</button>
<button class="quick-btn" data-set="-1">−1 stop</button>
<button class="quick-btn" data-set="-2">−2 stops</button>
<button class="quick-btn" data-set="1">+1 stop</button>
</div>
<div class="readout small cool" id="balanceReadout" style="margin-top:16px;">
<div class="rlabel">Background vs. subject</div>
<div class="rvalue" id="balanceValue">±0 stops</div>
<div class="rnote" id="balanceNote">balanced — background reads as a natural exposure</div>
</div>
<div class="divider-label">Sync speed check</div>
<div class="row">
<div class="field">
<label for="syncSpeed">Camera sync speed (1/x s)</label>
<div class="unit-wrap">
<span style="padding:9px 0 9px 10px;color:var(--text-faint);font-family:'JetBrains Mono',monospace;font-size:15px;">1/</span>
<input type="number" id="syncSpeed" value="200" min="1" step="1">
</div>
</div>
<div class="field">
<label>&nbsp;</label>
<div class="hss-row" style="margin-top:9px;">
<input type="checkbox" id="hssEnable">
<label for="hssEnable">HSS / FP enabled</label>
</div>
</div>
</div>
<div class="sync-banner" id="syncBanner"></div>
</div>
</div>
<div class="footnote">
Guide number method: aperture = (GN × √power) ÷ distance, where GN scales with √(ISO⁄100) and power is expressed as a fraction of full output. Below sync speed, shutter speed doesn't change the flash-lit exposure — it only raises or lowers the ambient background relative to it, which is what the balance section models.
</div>
</div>
<script>
(function(){
const iso = document.getElementById('iso');
const gn = document.getElementById('gn');
const gnUnit = document.getElementById('gnUnit');
const aperture = document.getElementById('aperture');
const distance = document.getElementById('distance');
const distUnit = document.getElementById('distUnit');
const dial = document.getElementById('dial');
const dialButtons = [...dial.querySelectorAll('button')];
const stopsLabel = document.getElementById('stopsLabel');
const readout = document.getElementById('readout');
const readoutLabel = document.getElementById('readoutLabel');
const readoutValue = document.getElementById('readoutValue');
const readoutNote = document.getElementById('readoutNote');
const solveGroup = document.getElementById('solveGroup');
let solveFor = 'power';
let stops = 1;
function toMeters(val, unit){ return unit === 'ft' ? val * 0.3048 : val; }
function fromMeters(val, unit){ return unit === 'ft' ? val / 0.3048 : val; }
function fmt(n, d=2){
if(!isFinite(n)) return '—';
return n.toFixed(d).replace(/\.00$/,'').replace(/(\.\d)0$/,'$1');
}
function powerFractionLabel(stopsVal){
const denom = Math.pow(2, stopsVal);
if(Math.abs(stopsVal - Math.round(stopsVal)) < 0.05){
return '1/' + Math.round(denom);
}
return '1/' + fmt(denom, 1);
}
function updateFieldStates(){
aperture.disabled = (solveFor === 'aperture');
distance.disabled = (solveFor === 'distance');
dial.classList.toggle('disabled', solveFor === 'power');
}
function updateDialUI(){
let nearest = Math.round(stops);
nearest = Math.max(0, Math.min(7, nearest));
dialButtons.forEach(b => {
b.classList.toggle('active', parseInt(b.dataset.stop) === nearest && Math.abs(stops - nearest) < 0.05);
});
stopsLabel.textContent = (solveFor === 'power')
? 'computed'
: fmt(stops,1) + ' stop' + (Math.abs(stops-1)<0.001?'':'s') + ' from full';
}
let currentApertureVal = 5.6;
let currentDistanceMeters = 3;
function compute(){
const isoVal = parseFloat(iso.value) || 100;
const gnVal = parseFloat(gn.value) || 0;
const gnMeters = toMeters(gnVal, gnUnit.value);
const gnIso = gnMeters * Math.sqrt(isoVal / 100);
let apVal = parseFloat(aperture.value) || 0;
let distValM = toMeters(parseFloat(distance.value) || 0, distUnit.value);
updateFieldStates();
if(solveFor === 'power'){
currentApertureVal = apVal;
currentDistanceMeters = distValM;
if(apVal <= 0 || distValM <= 0 || gnIso <= 0){
setReadout('Required flash power', '—', 'enter valid values', false);
updateAmbient();
return;
}
const ratio = (apVal * distValM) / gnIso;
const fraction = ratio * ratio;
const neededStops = -Math.log2(fraction);
stops = neededStops;
updateDialUI();
if(fraction > 1){
const overStops = Math.log2(fraction);
setReadout('Required flash power', 'over 1/1', 'needs +' + fmt(overStops,1) + ' stops beyond full power', true);
} else if(fraction <= 0){
setReadout('Required flash power', '—', 'enter valid values', false);
} else {
setReadout('Required flash power', powerFractionLabel(neededStops), fmt(neededStops,1) + ' stops down from full', false);
}
updateAmbient();
return;
}
if(solveFor === 'distance'){
currentApertureVal = apVal;
if(apVal <= 0 || gnIso <= 0){
setReadout('Maximum distance', '—', 'enter valid values', false);
updateAmbient();
return;
}
const gnAtPower = gnIso * Math.sqrt(Math.pow(2, -stops));
const distM = gnAtPower / apVal;
currentDistanceMeters = distM;
const distOut = fromMeters(distM, distUnit.value);
distance.value = fmt(distOut, 2);
updateDialUI();
setReadout('Maximum distance', fmt(distOut,2) + ' ' + distUnit.value, 'at f/' + fmt(apVal,1) + ', ' + powerFractionLabel(stops) + ' power', false);
updateAmbient();
return;
}
if(solveFor === 'aperture'){
currentDistanceMeters = distValM;
if(distValM <= 0 || gnIso <= 0){
setReadout('Required aperture', '—', 'enter valid values', false);
updateAmbient();
return;
}
const gnAtPower = gnIso * Math.sqrt(Math.pow(2, -stops));
const apOut = gnAtPower / distValM;
currentApertureVal = apOut;
aperture.value = fmt(apOut, 1);
updateDialUI();
setReadout('Required aperture', 'f/' + fmt(apOut,1), 'at ' + fmt(fromMeters(distValM, distUnit.value),2) + ' ' + distUnit.value + ', ' + powerFractionLabel(stops) + ' power', false);
updateAmbient();
return;
}
}
function setReadout(label, value, note, warn){
readoutLabel.textContent = label;
readoutValue.textContent = value;
readoutNote.textContent = note;
readout.classList.toggle('warn', !!warn);
}
solveGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.toggle-btn');
if(!btn) return;
solveGroup.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
solveFor = btn.dataset.solve;
compute();
});
dial.addEventListener('click', (e) => {
const btn = e.target.closest('button');
if(!btn || solveFor === 'power') return;
stops = parseInt(btn.dataset.stop);
compute();
});
[iso, gn, aperture, distance].forEach(el => el.addEventListener('input', compute));
[gnUnit, distUnit].forEach(el => el.addEventListener('change', compute));
const ambientEnable = document.getElementById('ambientEnable');
const ambientBody = document.getElementById('ambientBody');
const ctxAperture = document.getElementById('ctxAperture');
const ctxIso = document.getElementById('ctxIso');
const ambientMethodGroup = document.getElementById('ambientMethodGroup');
const presetRow = document.getElementById('presetRow');
const meterRow = document.getElementById('meterRow');
const presetSelect = document.getElementById('presetSelect');
const ambientMeter = document.getElementById('ambientMeter');
const shutterSpeed = document.getElementById('shutterSpeed');
const balanceValue = document.getElementById('balanceValue');
const balanceNote = document.getElementById('balanceNote');
const balanceReadout = document.getElementById('balanceReadout');
const syncSpeed = document.getElementById('syncSpeed');
const hssEnable = document.getElementById('hssEnable');
const syncBanner = document.getElementById('syncBanner');
const quickRow = document.querySelector('.quick-row');
let ambientMethod = 'preset';
ambientEnable.addEventListener('change', () => {
ambientBody.classList.toggle('open', ambientEnable.checked);
updateAmbient();
});
ambientMethodGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.toggle-btn');
if(!btn) return;
ambientMethodGroup.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
ambientMethod = btn.dataset.method;
presetRow.style.display = (ambientMethod === 'preset') ? '' : 'none';
meterRow.style.display = (ambientMethod === 'meter') ? '' : 'none';
updateAmbient();
});
function getAmbientX0(){
const isoVal = parseFloat(iso.value) || 100;
const apVal = currentApertureVal || 0;
if(ambientMethod === 'meter'){
return parseFloat(ambientMeter.value) || 0;
} else {
const ev100 = parseFloat(presetSelect.value);
if(apVal <= 0) return 0;
return (Math.pow(2, ev100) * (isoVal / 100)) / (apVal * apVal);
}
}
function updateAmbient(){
ctxAperture.textContent = 'f/' + fmt(currentApertureVal, 1);
ctxIso.textContent = fmt(parseFloat(iso.value) || 100, 0);
if(!ambientEnable.checked) return;
const x0 = getAmbientX0();
const x = parseFloat(shutterSpeed.value) || 0;
if(x0 <= 0 || x <= 0){
balanceValue.textContent = '—';
balanceNote.textContent = 'enter valid values';
balanceReadout.classList.remove('warn');
return;
}
const stopsDiff = Math.log2(x0 / x);
const rounded = Math.round(stopsDiff * 10) / 10;
const sign = rounded > 0.05 ? '+' : (rounded < -0.05 ? '' : '±');
balanceValue.textContent = sign + fmt(rounded, 1) + ' stop' + (Math.abs(rounded - 1) < 0.001 ? '' : 's');
if(rounded > 1.5){
balanceNote.textContent = 'background well brighter than subject — flash reads as fill against a bright scene';
balanceReadout.classList.add('warn');
balanceReadout.classList.remove('cool');
} else if(rounded > 0.3){
balanceNote.textContent = 'background brighter than subject — soft, open, backlit feel';
balanceReadout.classList.remove('warn'); balanceReadout.classList.add('cool');
} else if(rounded >= -0.3){
balanceNote.textContent = 'balanced — background reads as a natural exposure';
balanceReadout.classList.remove('warn'); balanceReadout.classList.add('cool');
} else if(rounded >= -2){
balanceNote.textContent = 'background falls off — subject pops from a moodier backdrop';
balanceReadout.classList.remove('warn'); balanceReadout.classList.add('cool');
} else {
balanceNote.textContent = 'background reads near black — dramatic, subject isolated on darkness';
balanceReadout.classList.remove('warn'); balanceReadout.classList.add('cool');
}
checkSync();
}
quickRow.addEventListener('click', (e) => {
const btn = e.target.closest('.quick-btn');
if(!btn) return;
const x0 = getAmbientX0();
if(x0 <= 0) return;
const target = btn.dataset.set;
let d = 0;
if(target === 'match') d = 0;
else d = parseFloat(target);
const newX = x0 / Math.pow(2, d);
shutterSpeed.value = Math.max(1, Math.round(newX));
updateAmbient();
});
[presetSelect, ambientMeter, shutterSpeed].forEach(el => el.addEventListener('input', updateAmbient));
presetSelect.addEventListener('change', updateAmbient);
function checkSync(){
if(!ambientEnable.checked){
syncBanner.textContent = '—';
syncBanner.className = 'sync-banner';
return;
}
const sVal = parseFloat(shutterSpeed.value) || 0;
const syncVal = parseFloat(syncSpeed.value) || 0;
if(hssEnable.checked){
syncBanner.className = 'sync-banner ok';
syncBanner.textContent = 'HSS enabled — flash pulses through the whole exposure, so 1/' + fmt(sVal,0) + 's is fine, but max flash power drops the faster you go.';
return;
}
if(sVal > syncVal){
syncBanner.className = 'sync-banner bad';
syncBanner.textContent = '1/' + fmt(sVal,0) + 's is faster than your 1/' + fmt(syncVal,0) + 's sync speed. Part of the frame will be blocked by the shutter curtain unless HSS is enabled.';
} else {
syncBanner.className = 'sync-banner ok';
syncBanner.textContent = '1/' + fmt(sVal,0) + 's is at or below sync speed — full flash power reaches the whole frame.';
}
}
[shutterSpeed, syncSpeed].forEach(el => el.addEventListener('input', checkSync));
hssEnable.addEventListener('change', checkSync);
updateFieldStates();
updateDialUI();
compute();
checkSync();
})();
</script>
</body>
</html>