Zoom lens 2

by PhilQ

HTML

<svg id="diagram" xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="-200 0 800 100" fill="none">
<!-- <rect y="50" x="50" width="50" height="50" fill="#ff0000"/> -->
<line x1="-100%" x2="200%" y1="50%" y2="50%" stroke="#ccc" />
<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

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 N_GLASS = 1.458;
const N_AIR   = 1.000273;
const SVG_NS = 'http://www.w3.org/2000/svg';
const DEG2RAD = Math.PI / 180;
const RAD2DEG = 180 / Math.PI;

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

class Lens {
    static defaults = {
        cx: 0,
        cy: 0,

        height: 80,
        min_thickness: 0,

        r1: 100,
        r2: 100,

        n: 1.458,
    };

    /**
     * Source properties
     */

    #cx;
    #cy;

    #height;
    #min_thickness;

    #r1;
    #r2;

    #n;

    /**
     * Derived properties
     */

    #shape;
    #p1;
    #p2;

    #sag1;
    #sag2;

    #width;
    #edge_thickness;
    #center_thickness;

    #x1l;
    #x1r;
    #x2l;
    #x2r;
    #c1x;
    #c2x;

    #focal_length;
    #focal_offset1;
    #focal_offset2;
    #power;

    constructor(params = {}) {
        const p = { ...Lens.defaults, ...params, };

        this.#validateSetRecalc(p);
    }

    #validateSetRecalc(params) {
        if ('r1' in params && params.r1 === 0) params.r1 = Infinity;
        if ('r2' in params && params.r2 === 0) params.r2 =...