Zoom lens diagram - Copyright 2026 vHW

by PhilQ

HTML

<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="-200 0 800 100" fill="none" id="svg">
<!-- <rect y="50" x="50" width="50" height="50" fill="#ff0000"/> -->
<g id="rays"></g>
</svg>
<div class="controls">
  <div class="input">
    <label>Zoom</label>
    <input type="range" id="zoom" value="44" min="18" max="70" step="1">
  </div>
</div>

SCSS

body {
  background-color: #eee;
}

.controls {
  display: flex;
  width: 100%;
  max-width: 1024px;
  margin: 2rem auto 0;
  flex-direction: row;
  gap: 1rem;
  background: #ddd;
  padding: 1rem;
}

.input {
  width: 25%;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  justify-content: flex-start;
  align-items: stretch;
}

JavaScript

const degToRad = (deg) => {
  return (deg * Math.PI) / 180.0;
}

const radToDeg = (rad) => {
  return (rad * 180.0) / Math.PI;
}

function easeOutSine(x) {
  return Math.sin((x * Math.PI) / 2);
}

function easeInSine(x) {
  return 1 - Math.cos((x * Math.PI) / 2);
}

function easeInOutSine(x) {
  return -(Math.cos(Math.PI * x) - 1) / 2;
}

const createTag = (tag, props = {}, content = '') => {
  const ns = 'http://www.w3.org/2000/svg';
  const el = document.createElementNS(ns, tag);

  if (false && 'svg' === tag) {
    el.setAttributeNS(
      'http://www.w3.org/2000/xmlns/',
      'xmlns:xlink',
      'http://www.w3.org/1999/xlink'
    );
  }

  for (const prop in props) {
    el.setAttribute(prop, props[prop]);
  }
  
  if (content?.length) {
    el.textContent = content;
  }

  return el;
}

const refractive_index_air = 1.000273; // Air
const refractive_index_glass = 1.458; // Quartz

/**
 * Intersect a left-going ray with a circular arc and compute:
 * - intersection point
 * - incidence angle (relative to the surface normal)
 * - refraction angle using Snell's law
 *
 * Coordinate system:
 * - +x = right
 * - +y = down/up does not matter as long as consistent
 * - angles in degrees
 * - 180° = straight left
 * - 135° = top-left
 * - 225° = bottom-left
 *
 * Arc assumptions:
 * - The arc is part of a circle
 * - It is symmetric around the x-axis through center.y
 * - "convex"  => using the RIGHT half of the circle
 * - "concave" => using the LEFT half of the circle
 * - height is the total visible arc height
 *
 * Returns:
 * {
 *   point: { x, y },
 *   incidenceAngleDeg,
 *   refractionAngleDeg,
 *   normal: { x, y },
 *   refractedDirection: { x, y }
 * }
 *
 * or false if no valid intersection exists.
 */
function rayArcIntersection({
  rayX,
  rayY,
  rayAngleDeg,

  centerX,
  centerY,
  radius,

  sag, // optional, only used for extra clipping
  convex, // true =...