DEV Community

Madlen J.
Madlen J.

Posted on

Building a Contact Lens Vertex Calculator: The Formula That Looks Simple Until It Isn't

I needed a contact lens vertex calculator for a tool site I'm building. Seemed straightforward — there's a known formula, plug in the numbers, done.

It wasn't done.

Here's what I actually ran into.


What Vertex Distance Is (Quick Background)

When you wear glasses, the lens sits about 12mm in front of your eye. A contact lens sits directly on the cornea — effectively zero distance.

This matters because optical power isn't linear. The same lens power behaves differently depending on how far it is from the eye. A -6.00D spectacle lens doesn't deliver -6.00D to the corneal plane. It delivers slightly less.

The formula that corrects for this is called the vertex conversion formula:

CL Power = F / (1 - d × F)
Enter fullscreen mode Exit fullscreen mode

Where:

  • F = spectacle lens power in diopters
  • d = vertex distance in meters (so 12mm = 0.012)

Plug in -6.00 at 12mm:

CL = -6.00 / (1 - 0.012 × -6.00)
CL = -6.00 / (1 + 0.072)
CL = -6.00 / 1.072
CL ≈ -5.597
Enter fullscreen mode Exit fullscreen mode

Rounded to the nearest 0.25D (how contact lenses are actually manufactured): -5.75D.

That half diopter difference is clinically significant. At low prescriptions it barely matters. Above ±4.00D it matters a lot.

So far so good. One formula, clean output. Where did it get complicated?


Problem 1: The Cylinder

Most people with astigmatism have both a sphere and a cylinder in their prescription. My first instinct was to convert them independently.

Wrong.

The sphere and cylinder in a spectacle prescription aren't independent values — the cylinder is written as a modification on top of the sphere. To convert correctly, you need to:

  1. Convert the sphere power through vertex formula → get CL sphere
  2. Add sphere + cylinder to get the combined meridian power → convert that through vertex formula
  3. Subtract the converted sphere from the converted combined → that's your CL cylinder

In code:

function vertexConvert(power, vertexMm) {
  const d = vertexMm / 1000;
  return power / (1 - d * power);
}

function calculate(sph, cyl, vertexMm) {
  const clSph = vertexConvert(sph, vertexMm);

  // Only calculate cylinder if it exists
  if (cyl === 0 || isNaN(cyl)) {
    return { sphere: clSph, cylinder: 0 };
  }

  const combined = vertexConvert(sph + cyl, vertexMm);
  const clCyl = combined - clSph;

  return { sphere: clSph, cylinder: clCyl };
}
Enter fullscreen mode Exit fullscreen mode

If you skip step 2 and just run the cylinder through the formula independently, you get a wrong answer. Not catastrophically wrong, but wrong enough that someone ordering contacts based on your calculator gets the wrong prescription.


Problem 2: Rounding

Contact lenses come in 0.25D increments. So every output needs to round to the nearest 0.25.

function roundTo025(val) {
  return Math.round(val * 4) / 4;
}
Enter fullscreen mode Exit fullscreen mode

Simple. But here's the thing — should you round before or after the cylinder calculation?

If you round the sphere first, then use the rounded sphere value in the cylinder subtraction, you introduce rounding error into the cylinder. The correct approach is to do all the raw math first, then round each output independently at the very end.

function calculate(sph, cyl, vertexMm) {
  const clSphRaw = vertexConvert(sph, vertexMm);

  let clCylRaw = 0;
  if (cyl !== 0 && !isNaN(cyl)) {
    const combinedRaw = vertexConvert(sph + cyl, vertexMm);
    clCylRaw = combinedRaw - clSphRaw;
  }

  // Round at the end, not during
  return {
    sphere: roundTo025(clSphRaw),
    cylinder: roundTo025(clCylRaw)
  };
}
Enter fullscreen mode Exit fullscreen mode

This is the kind of thing that doesn't show up in the formula documentation anywhere. You figure it out when you start testing edge cases and notice the outputs are slightly off.


Problem 3: The Add Power

Presbyopic patients have an "add power" in their prescription — extra magnification for near vision. It needs vertex conversion too.

But add power is always positive and always refers to the near addition over the distance prescription. The vertex conversion for add is independent of sphere and cylinder — you just run it through the same formula on its own.

function calculate(sph, cyl, add, vertexMm) {
  const clSphRaw = vertexConvert(sph, vertexMm);

  let clCylRaw = 0;
  if (cyl !== 0 && !isNaN(cyl)) {
    clCylRaw = vertexConvert(sph + cyl, vertexMm) - clSphRaw;
  }

  let clAddRaw = null;
  if (add && !isNaN(add)) {
    clAddRaw = vertexConvert(add, vertexMm);
  }

  return {
    sphere: roundTo025(clSphRaw),
    cylinder: roundTo025(clCylRaw),
    add: clAddRaw !== null ? roundTo025(clAddRaw) : null
  };
}
Enter fullscreen mode Exit fullscreen mode

Problem 4: The Edge Case Nobody Mentions

What happens when the denominator approaches zero?

CL = F / (1 - d × F)
Enter fullscreen mode Exit fullscreen mode

If d × F = 1, you're dividing by zero. At 12mm vertex distance, that happens when F = 83.33D — a prescription that doesn't exist in reality. So in practice you're safe.

But what if someone enters a vertex distance of 100mm (0.1 meters) and a power of 10D?

1 - 0.1 × 10 = 1 - 1 = 0
Enter fullscreen mode Exit fullscreen mode

Division by zero. The calculator breaks silently or returns Infinity.

Add a guard:

function vertexConvert(power, vertexMm) {
  const d = vertexMm / 1000;
  const denominator = 1 - d * power;

  if (Math.abs(denominator) < 0.001) {
    return null; // Invalid combination
  }

  return power / denominator;
}
Enter fullscreen mode Exit fullscreen mode

Then handle null in your UI. A vertex distance of 100mm isn't realistic for glasses, but someone will enter it. Someone always does.


What I Learned

The formula is one line. The implementation is about fifty, once you handle cylinder correctly, round at the right stage, manage the add power independently, and guard against degenerate inputs.

This is a pattern I keep hitting with "simple" calculators — the math is usually not the hard part. The hard part is understanding the domain well enough to know what edge cases exist, what the inputs actually mean, and where naive implementations silently produce wrong answers.

For vertex conversion specifically: the cylinder interaction is the thing most online implementations get wrong. If you're building something similar, test it against known values from an optical textbook, not just against other online calculators — because a lot of them have the same bug.


The working version is live at contact lens calculator if you want to see the output behavior. The full JS is straightforward once the above is sorted — happy to share more of it in the comments if useful.

Top comments (0)