{
  "schemaVersion": 1,
  "slug": "shutter-speed-calculator",
  "category": "photography",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "48d8e551efbe85e1827399a4ff4ffd2e09c37686e224f825f0b96ec0ecf12f96",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/shutter-speed-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_h = 1÷(F×c)÷2^s; T_m = b÷[(v÷3.6)×(F÷1000)÷D]; T = min(T_h, T_m)",
    "vars": [
      [
        "F",
        "Focal length (mm)"
      ],
      [
        "c",
        "Crop factor"
      ],
      [
        "s",
        "Stabilisation stops"
      ],
      [
        "v",
        "Subject speed (km/h)"
      ],
      [
        "D",
        "Subject distance (m)"
      ],
      [
        "b",
        "Acceptable blur threshold"
      ],
      [
        "T_h",
        "Handheld-safe shutter speed (s)"
      ],
      [
        "T_m",
        "Motion-freeze shutter speed (s)"
      ],
      [
        "T",
        "Recommended shutter speed (s)"
      ]
    ],
    "note": "Recommended speed is the smaller of handheld-safe and motion-freeze values; v÷3.6 converts km/h to m/s; with no moving subject the motion limit is unrestricted; output is shown as 1/n s."
  },
  "externalEvidence": null,
  "contract": {
    "compute": "const effective = focalLength*cropFactor; const rule = 1/effective; const stabStops = stabilisation === 'none' ? 0 : Number(stabilisation) || 0; const handheld = rule*Math.pow(2, stabStops); const speedMs = subjectSpeed/3.6; const imageSpeed = subjectDistance > 0 ? speedMs*(focalLength/1000)/subjectDistance : 0; const motion = imageSpeed > 0 ? (blurTolerance/1000)/imageSpeed : Infinity; const worst = Math.min(handheld, motion); const fmt = (t) => !Number.isFinite(t) ? 'No limit' : t >= 1 ? Math.round(t*100)/100 + ' s' : '1/' + Math.round(1/t); return { handheldSpeed: fmt(handheld), reciprocalValue: handheld, motionBlurSpeed: fmt(motion), recommended: fmt(worst), stopsGain: stabStops };",
    "inputs": [
      {
        "name": "focalLength",
        "type": "number",
        "default": 85,
        "min": 3,
        "max": 2000,
        "step": 1,
        "slider": true
      },
      {
        "name": "cropFactor",
        "type": "select",
        "default": 1,
        "options": [
          {
            "value": 1
          },
          {
            "value": 1.5
          },
          {
            "value": 1.6
          },
          {
            "value": 2
          }
        ]
      },
      {
        "name": "stabilisation",
        "type": "select",
        "default": "none",
        "options": [
          {
            "value": "none"
          },
          {
            "value": 2
          },
          {
            "value": 4
          },
          {
            "value": 6
          }
        ]
      },
      {
        "name": "subjectSpeed",
        "type": "number",
        "default": 0,
        "min": 0,
        "max": 1000,
        "step": 1,
        "slider": true
      },
      {
        "name": "subjectDistance",
        "type": "number",
        "default": 5,
        "min": 0.1,
        "max": 1000,
        "step": 0.1
      },
      {
        "name": "blurTolerance",
        "type": "number",
        "default": 0.03,
        "min": 0.001,
        "max": 1,
        "step": 0.001,
        "slider": true
      }
    ],
    "outputs": [
      {
        "name": "handheldSpeed",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "reciprocalValue",
        "precision": 6,
        "unit": "s"
      },
      {
        "name": "motionBlurSpeed",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "recommended",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "stopsGain",
        "precision": 0,
        "unit": "stops"
      }
    ],
    "interpret": null
  }
}
