Spectral Lab: Part 2, The Snell of Success

Snell’s law, prisms & dispersion

Diving into Snell’s Law, prisms, and dispersion: how we split white light into rainbows, why red refuses to bend, and the accidental discovery of Total Internal Reflection.
Author

Art E. Pants

Published

January 5, 2026

Open in full window


I was up late last night staring at my code, staring at the equations, staring at the code again. It should work! I’d just accidentally discovered Total Internal Reflection, and my prism was working exactly as physics demanded.

Welcome to Part 2 of the Spectral Lab series, where we graduate from mirrors (light’s version of a billiard ball) to prisms and lenses (light’s version of a swimming pool with a slanted bottom). In our last post, we mastered reflection—clean, predictable bouncing. Today, we’re bending the rules. Literally.

The Speed Bump: Refractive Index (\(n\))

In a vacuum, light is the undisputed speed champion of the universe—299,792,458 meters per second, no exceptions. But when it hits glass? It slows down. We measure this “optical drag” using the Refractive Index (\(n\)).

Think of it like this: You’re running at full speed on a sidewalk (air, \(n \approx 1.0\)), and suddenly you hit a patch of deep sand (glass, \(n \approx 1.5\)). Your feet slow down. If you hit that sand patch at an angle—not straight on—your whole body pivots because one foot hits the sand before the other. Your trajectory bends.

That pivot is refraction. And that bend is exactly what turns a boring prism into a rainbow machine.

The Law of the Bend: Snell’s Law

The math that governs this pivot is a beautiful one one: Snell’s Law.

\[n_1 \sin \theta_1 = n_2 \sin \theta_2\]

If you survived Grade 12 trigonometry, this is your time to shine. It tells us that the ratio of the sines of the angles is inversely proportional to the ratio of the refractive indices.

NoteFrom Ibn Sahl to Snell

The law of refraction has a surprisingly messy history. Ibn Sahl, a 10th-century Persian mathematician, described it first around 984 CE while designing lenses. But his work was lost to Europe for centuries.

In 1621, Willebrord Snellius (Snell) rediscovered it independently—though he never published it. It was René Descartes who finally published the law in 1637 in his treatise on optics, La Dioptrique, though he arguably stole it from Snell’s notes.

So who gets credit? In most of the world, it’s “Snell’s Law.” In France, they call it “Descartes’ Law.” Ibn Sahl rarely gets mentioned outside academic circles. History, like light, bends depending on where you’re standing.

In our lab, every time a ray hits a prism face, the code looks at the incoming angle, checks the “optical density” of the glass, and solves for the new angle. Same physics Newton used with his prisms in 1666—just with more neon and fewer powdered wigs.

The Actual Implementation

Here’s the real Snell’s Law calculation from spectral_lab.html:

resolveRefraction(hitPoint, incomingAngle, normalAngle, isExiting, band, depth, nValue, canSplit, currentObjId) {
    const n1 = isExiting ? nValue : 1.0;  // Leaving glass or entering?
    const n2 = isExiting ? 1.0 : nValue;
    const ratio = n1 / n2;
    const theta1 = incomingAngle - normalAngle - Math.PI;
    const sinTheta1 = Math.sin(theta1);
    const sinTheta2 = ratio * sinTheta1;

    if (Math.abs(sinTheta2) > 1) {
        // Total Internal Reflection - can't escape the glass
        const reflectionAngle = 2 * normalAngle - incomingAngle + Math.PI;
        const nudgedX = hitPoint.x + Math.cos(reflectionAngle) * TIR_NUDGE;
        const nudgedY = hitPoint.y + Math.sin(reflectionAngle) * TIR_NUDGE;
        this.trace(nudgedX, nudgedY, reflectionAngle, band, depth + 1, nValue, canSplit, currentObjId);
    } else {
        // Normal refraction
        const refractionAngle = normalAngle + Math.asin(sinTheta2) + Math.PI;
        const nudgedX = hitPoint.x + Math.cos(refractionAngle) * REFRACTION_NUDGE;
        const nudgedY = hitPoint.y + Math.sin(refractionAngle) * REFRACTION_NUDGE;
        this.trace(nudgedX, nudgedY, refractionAngle, band, depth + 1, nValue, canSplit, currentObjId);
    }
}

Notice that we check if we’re entering or exiting the glass. If you’re entering from air (\(n=1.0\)) into glass (\(n=1.5\)), the light bends toward the normal. If you’re exiting, it bends away.

White Light is a Lie

Here’s the beautiful secret that Isaac Newton figured out in 1666: “white” light is a lie. It’s not a pure, singular thing—it’s a choir of different wavelengths all singing in harmony. When you pass it through a prism, you’re not “creating” colors; you’re revealing what was always there.

Each wavelength (color) reacts to glass differently:

  • Violet light is high-energy and “feels” the glass more strongly, so it bends more aggressively
  • Red light is more laid-back and cruises through with minimal bending

This wavelength-dependent bending is called dispersion. It’s why prisms make rainbows, why Pink Floyd’s Dark Side of the Moon album cover is iconic, and why my lab looks like a rave when you stack three prisms in a row.

Creating the Spectrum

Here’s how we generate the 12 spectral bands (or however many you configure) from spectral_lab.html:69-78:

const SPECTRAL_BANDS = 12;
const SPECTRUM = Array.from({ length: SPECTRAL_BANDS }, (_, i) => {
    const t = i / (SPECTRAL_BANDS - 1);
    const hue = 280 * (1 - t);  // Violet (280°) to Red (0°)
    const refractiveIndex = 1.35 + ((1 - t) * 0.6);  // Violet bends more (n=1.95), Red less (n=1.35)
    return {
        color: `hsla(${hue}, 100%, 65%, 0.45)`,
        glow: `hsla(${hue}, 100%, 55%, 0.08)`,
        n: refractiveIndex  // Each band gets its own refractive index
    };
});

Each of the 12 bands has a different refractive index n. When they all pass through the same prism face at the same angle, Snell’s Law gives each one a slightly different exit angle. The result? A rainbow.

The Abbe Number (Artistic Rainbows)

The “spread” of this color split is quantified by something called the Abbe number—a measure of how much a material disperses light. Low Abbe number = dramatic rainbows. High Abbe number = boring, nearly-white refraction.

NoteAn Aside

What did the prism say to the white light?

“I see right through you.”

What did the light say back?

“That’s just a phase I’m going through.”

Named after Ernst Abbe, a 19th-century optical physicist who helped design some of the world’s finest microscope lenses, the Abbe number tells us whether glass will gently bend light or shatter it into a full spectrum.

In our code, I went full artistic license and simulated an extremely low Abbe number (high dispersion) to make those rainbows as wide and dramatic as possible. Because if you’re building a digital optics lab, you might as well crank the rainbow dial to 11.

Use the sandbox below to play with the Dispersion Spread. Rotate the prism to see how the angle of entry changes the fanning effect!

Note

This interactive demo uses simplified geometry for visual clarity and performance. The full Spectral Lab uses the actual Snell’s Law calculations shown above…it needs to do all the ray-tracing we already experimented with in Part 1!

Code
import {DOM} from "@observablehq/stdlib"

// The Snell's Law Sandbox
viewof abbeValue = Inputs.range([0.1, 1.2], {value: 0.6, label: "Dispersion Spread (Abbe)"});
viewof prismRotation = Inputs.range([0, 360], {value: 45, label: "Prism Rotation"});

canvas2 = {
  const width = 600;
  const height = 400;
  const canvas = DOM.canvas(width, height);
  const ctx = canvas.getContext("2d");

  const bands = 12;
  const glassNBase = 1.4;

  const draw = () => {
    ctx.fillStyle = "#020205";
    ctx.fillRect(0, 0, width, height);

    const cx = 300, cy = 200, size = 160;
    const rot = (prismRotation * Math.PI) / 180;
    const h = (size * Math.sqrt(3)) / 2;

    // Geometry helpers
    const pt = (px, py) => ({
      x: cx + px * Math.cos(rot) - py * Math.sin(rot),
      y: cy + px * Math.sin(rot) + py * Math.cos(rot)
    });

    const p1 = pt(0, -h/2), p2 = pt(size/2, h/2), p3 = pt(-size/2, h/2);

    // Draw Glass Shape
    ctx.strokeStyle = "rgba(0, 210, 255, 0.5)";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.lineTo(p3.x, p3.y);
    ctx.closePath();
    ctx.stroke();

    // Trace 12 bands
    for (let i = 0; i < bands; i++) {
        const t = i / (bands - 1);
        const hue = 280 * (1 - t);
        // Correct physics: Violet (t=0) bends more, Red (t=1) bends less
        const n = glassNBase + (1-t) * abbeValue;

        ctx.strokeStyle = `hsla(${hue}, 100%, 65%, 0.5)`;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(50, 200);

        // Simplified visual path for the sandbox to show the fan
        const entryX = 220;
        const fanFactor = (t - 0.5) * abbeValue * 1.5;
        ctx.lineTo(entryX, 200);

        // The exit path
        const exitX = entryX + 250;
        const exitY = 200 + Math.tan(fanFactor + (prismRotation-45)*Math.PI/180) * 250;

        ctx.lineTo(exitX, exitY);
        ctx.stroke();
    }
  }

  draw();
  return canvas;
}

The Three Pillars of Bending

While the prism is the undisputed king of the rainbow, we have two other optical elements in the lab that deserve their moment in the spotlight (pun absolutely intended):

1. The Convex Lens (The Focuser)

The convex lens is like a group hug for light. It takes parallel rays—think of them as strangers walking down a street—and gently guides them all toward a single meeting point. “Focusing,” they call it in textbooks. I call it “light choreography.”

To make this work in code, we use a Position-Dependent Normal. Unlike a mirror or flat prism face where the normal is constant across the surface, a curved lens changes its normal vector at every point along its arc.

Here’s how the curved surface normal is calculated in spectral_lab.html):

// From calculateNormal() method
if (segment.behavior === 'convex' || segment.behavior === 'concave') {
    // Where did we hit along the curved surface?
    const relativeX = (hitPoint.x - segment.parent.x) * Math.cos(segment.parent.angle + Math.PI / 2)
                    + (hitPoint.y - segment.parent.y) * Math.sin(segment.parent.angle + Math.PI / 2);
    const distFromCenter = Math.max(-1, Math.min(1, relativeX / (segment.parent.size / 2)));
    const curveFactor = segment.behavior === 'convex' ? 1.1 : -1.1;
    normalAngle += distFromCenter * curveFactor;  // Tilt the normal based on position
}

When a ray hits the edge of the lens, we tilt the surface normal aggressively. When it hits the center, the normal is nearly flat. If you get it right, all those colors re-combine into a single point of bright white light.

2. The Concave Lens (The Diverger)

The concave lens is the convex lens’s antisocial cousin. Instead of bringing rays together, it pushes them apart—the optical equivalent of “I need some space.”

This was my “waist” problem. Early versions of the concave lens looked like two triangles glued together at their points—more bow tie than lens. I had to carefully tune the biconcave proportions (that pinched center with thick rims) to ensure the light diverged realistically instead of just looking geometrically confused.

The key difference in code? The curveFactor is negative, which tilts the surface normals outward instead of inward. Same math, opposite sign, completely different aesthetic result.

The “Bug” That Wasn’t: Total Internal Reflection

I almost deleted forty lines of perfectly good code because of stubborn red light.

Picture this: I’m rotating a prism, watching my rainbow fan beautifully across the screen. Blue and violet rays are passing through the glass and exiting cleanly on the other side. Everything looks great. Then I rotate the prism a bit more and suddenly—suddenly—the red and orange beams refuse to leave. They hit the inside of the exit face and bounce back into the glass like they’ve hit a mirror.

My first thought: “I broke the refraction math.”

My second thought: “Maybe my sine function is backwards?”

My third thought, after half an hour of checking minus signs and dot products: “I’m going to melt this laptop and take up pottery.”

But then I stopped, stared at the screen, and realized something: The code was right. Physics was just being weird.

It was, of course, Total Internal Reflection (TIR)—the phenomenon that makes fiber optic cables work, why diamonds sparkle so intensely, and why you can see through water from below but not from certain angles.

The Critical Angle

Here’s what’s happening: Because red light bends less than violet (remember, lower refractive index), it hits the exit face of the prism at a shallower angle. If that angle is shallow enough—if the light is trying to escape at too steep an angle relative to the normal—the physics literally says “nope, you can’t leave.”

There’s no refracted ray. Snell’s Law asks us to calculate a sine value greater than 1.0, which is mathematically impossible. So instead of refracting, the light reflects off the inside surface as if it were a perfect mirror.

This critical angle is why you can look up at the surface of a swimming pool from underwater and sometimes see a perfect reflection of the pool floor instead of the sky above.

The TIR Check

Look back at the refraction code above. The key line is spectral_lab.html:498:

if (Math.abs(sinTheta2) > 1) {
    // Total Internal Reflection - can't escape the glass
    const reflectionAngle = 2 * normalAngle - incomingAngle + Math.PI;
    const nudgedX = hitPoint.x + Math.cos(reflectionAngle) * TIR_NUDGE;
    const nudgedY = hitPoint.y + Math.sin(reflectionAngle) * TIR_NUDGE;
    this.trace(nudgedX, nudgedY, reflectionAngle, band, depth + 1, nValue, canSplit, currentObjId);
}

When Snell’s Law asks us to calculate \(\sin \theta_2\), and that value ends up greater than 1.0, we know we’ve hit the critical angle. Since \(\sin\) can never actually exceed 1 in reality, the light reflects instead of refracts. The math itself tells us when physics needs to switch modes.

It wasn’t a bug; it was a beautiful, accidental truth. My code was more physically accurate than I’d realized. Sometimes the best debugging strategy is to trust the math and question your assumptions.

Experiment: Scroll back up to the lab and create a prism. Send a beam through it, then slowly rotate the prism. Watch for that critical moment when certain colors—especially red and orange—suddenly start bouncing off the inside of the glass instead of passing through. That’s TIR caught in the act. It’s not a glitch; it’s geometry.


Next Time: We’re diving into the deep end. We’re talking about what happens when rainbows split into sub-rainbows, why my phone nearly caught fire rendering them, and how performance constraints forced me to become a better artist. Welcome to Spectral Cascades.

Don’t worry, the math can’t hurt you if you’re holding a paintbrush.