{
  "schemaVersion": 1,
  "slug": "exposure-calculator",
  "category": "photography",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "fa8eca37bed53de42d85a1ea8659f8332fd323dc9301954837c5996c83358a18",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/exposure-calculator.json",
    "src/engine.js"
  ],
  "runtimeSource": "/sources/engine.txt",
  "runtimeHash": "5c888d8898afbdee36267bb9977829e1e2ac568f6677c8ee87469c139864645c",
  "scope": "The compute function records implemented equations, constants, lookup tables and custom rules. Inputs record defaults and allowed values; outputs record precision and units. Defaults are examples, not recommendations. General background references are not evidence for this specific algorithm.",
  "publishedFormula": {
    "formula": "t = N² × 2^(-EV) × 100 ÷ ISO",
    "vars": [
      [
        "EV",
        "exposure value (scene brightness)"
      ],
      [
        "N",
        "aperture f-number"
      ],
      [
        "ISO",
        "ISO sensitivity"
      ],
      [
        "t",
        "shutter speed (s)"
      ]
    ],
    "note": "Equivalent exposure does not preserve depth of field, motion blur or noise. ND stops to lengthen an exposure to 0.5 s are estimated only when t > 0.5 s; brightness labels use approximate EV bands."
  },
  "externalEvidence": null,
  "contract": {
    "compute": "const isoFactor = iso/100; const t = Math.pow(2, -ev)*(aperture*aperture)/isoFactor; const fraction = t >= 1 ? t.toFixed(2) + ' s' : '1/' + Math.round(1/t); const bright = ev < 3 ? 'Very dark — night or candlelight' : ev < 8 ? 'Indoor lighting' : ev < 12 ? 'Open shade or overcast' : ev < 15 ? 'Bright overcast to hazy sun' : ev < 17 ? 'Sunny' : 'Very bright — snow or sand in full sun'; const tIso100 = Math.pow(2, -ev)*(aperture*aperture)/1; const nd = Math.log2(t/0.5); return { shutterSpeed: t, shutterFraction: fraction, exposureValue: ev, brightness: bright, ndNeeded: nd > 0 ? nd : 0, equivIso100: tIso100 >= 1 ? tIso100.toFixed(2) + ' s' : '1/' + Math.round(1/tIso100) };",
    "inputs": [
      {
        "name": "ev",
        "type": "number",
        "default": 12,
        "min": -6,
        "max": 20,
        "step": 0.33,
        "slider": true
      },
      {
        "name": "iso",
        "type": "select",
        "default": 100,
        "options": [
          {
            "value": 100
          },
          {
            "value": 200
          },
          {
            "value": 400
          },
          {
            "value": 800
          },
          {
            "value": 1600
          },
          {
            "value": 3200
          },
          {
            "value": 6400
          }
        ]
      },
      {
        "name": "aperture",
        "type": "number",
        "default": 8,
        "min": 0.7,
        "max": 64,
        "step": 0.1,
        "slider": true
      }
    ],
    "outputs": [
      {
        "name": "shutterSpeed",
        "precision": 5,
        "unit": "s"
      },
      {
        "name": "shutterFraction",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "exposureValue",
        "precision": 2,
        "unit": ""
      },
      {
        "name": "brightness",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "ndNeeded",
        "precision": 1,
        "unit": "stops"
      },
      {
        "name": "equivIso100",
        "precision": 0,
        "unit": "",
        "type": "text"
      }
    ],
    "interpret": null
  }
}
