11  Lie algebra sl(2)

This chapter is heavily inspired by two excellent videos:

11.1 the group SL(2)

The group SL(2) is the group of 2x2 matrices with determinant 1. It is a fundamental example in the study of Lie groups and Lie algebras. The determinant of a matrix measures volume scaling. If you apply a linear transformation with matrix A to a region of space, the volume gets multiplied by \det(A). So \det=1 means volume preserving transformations. This is a very natural class of symmetries — transformations that don’t shrink or expand space, just reshape it.

A general element of this group can be written as:

g = \begin{pmatrix} a & b \\ c & d \end{pmatrix}

where a, b, c, d \in \mathbb{C} and the determinant is 1, i.e., ad - bc = 1.

Change of point of view. Each of the elements a,b,c,d is complex-valued, and we can imagine that every matrix in SL(2,\mathbb{C}) is a point in an 4-dimensional complex space \mathbb{C}^4 . However, the constraint that the determinant equals 1 reduces the degrees of freedom by 1, meaning that the group SL(2,\mathbb{C}) is a 3-dimensional surface embedded in a larger \mathbb{C}^4. If you prefer to think of 1 complex dimension as equivalent to 2 real dimensions, then just multiply all the dimensions by 2. This surface is curved, because of the nonlinear constraint ad - bc = 1. In mathematical parlance, we call this surface a manifold.

11.2 tangent space

Let’s parametrize each element of the 2x2 matrix in SL(2) with the variable t:

g(t) = \begin{pmatrix} a(t) & b(t) \\ c(t) & d(t) \end{pmatrix}.

We can imagine that this parametrization defines a general curve in the manifold of SL(2). We will require that this curve passes through the identity element of SL(2) at t = 0:

g(t)\Big|_{t=0} = \begin{pmatrix} a(t) & b(t) \\ c(t) & d(t) \end{pmatrix}_{t=0} = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}.

What we will do now is to take the derivative of this curve with respect to the parameter t, which will give us a vector in the tangent space of SL(2) at each point:

\dot{g}(t) = \begin{pmatrix} \dot{a}(t) & \dot{b}(t) \\ \dot{c}(t) & \dot{d}(t) \end{pmatrix}, where the dot means “time derivative”.

Let’s now compute the derivative of the condition ad - bc = 1 with respect to t, and evaluate it at t = 0:

\frac{d}{dt}(ad - bc)\Big|_{t=0} = \frac{da}{dt}d\Big|_{t=0} + a\frac{dd}{dt}\Big|_{t=0} - \cancel{\frac{db}{dt}c\Big|_{t=0}} - \cancel{b\frac{dc}{dt}\Big|_{t=0}} = 0.

The last two terms cancel out because at t = 0, we have b(0) = c(0) = 0. Also, at t = 0, we have a(0) = d(0) = 1, so we get:

\dot{a}(0) + \dot{d}(0) = 0.

This means that the derivative of the matrix g(t) at t = 0 is

X = \dot{g}(0) = \begin{pmatrix} \dot{a}(0) & \dot{b}(0) \\ \dot{c}(0) & \dot{d}(0) \end{pmatrix}, \text{ where } \dot{a}(0) + \dot{d}(0) = 0.

For convenience, we will call the tangent vector of g(t) at the identity X.

Let’s take stock of what happened. From Calculus (and Physics), we are used to the idea that taking the time-derivative of the position curve gives us a vector for the velocity. The velocity is tangent to the displacement curve at each point. That’s exactly what we did here, but we did it for all possible curves in the manifold that pass through the identity element of SL(2). By taking the derivative of the curves A(t) at t = 0 (at the identity element), we obtained vectors in the tangent space of SL(2) at that point. These new vectors have exactly the same dimension as the original (in this case 3 complex dimensions), but they live in a flat (Euclidean) space rather than in the curved manifold of SL(2).

The curved gray surface represents the manifold of SL(2), and the plane below it represents the tangent space at the identity element. An element in the manifold gets mapped by the derivative to a point in the tangent space.

11.3 back and forth between the group and the tangent space

We’ve seen that, starting from a curve g(t), we can get to an element of the tangent space X by taking its derivative at the identity:

g(t) \xrightarrow[]{\frac{d}{dt}} X.

How do we go back? Starting from X in the tangent space, how to get the curve g in the manifold? The answer is: exponentiation!

X \xrightarrow[]{\exp} g(t).

Let’s see how this works. X is a 2\times2 matrix, and its (parametric)exponential is

\exp(tX) = t\left(I + X + \frac{X^2}{2!} + \frac{X^3}{3!} + \ldots\right)

This is exactly the same formula for the exponent of scalars, but applied to matrices. Note that the identity matrix I takes the role of the number 1. The matrix X raised to any power is still a 2\times2 matrix. Therefore, the series above also gives as a result a 2\times2 matrix. Now, how can we know if this new matrix “lands” in the manifold, where the group elements live? We just need to check if the determinant of \exp(tX) is 1, if it is, we’re golden.

We’ve proved elsewhere that

\det(\exp(X)) = \exp(\text{trace}(X)).

We know that the trace of tX (like that of X) is zero, so

\det(\exp(tX)) = \exp(\text{trace}(tX)) = \exp(0) = 1.

And this shows that exponentiating an element of the tangent space brings us to a curve g(t) in the manifold.

The proof is water tight, but somewhat not satisfying. I guessed that the solution was the exponential, and verified that it does the job. Why should the exponential appear in the first place?

The previous analogy between position and velocity will be useful. If a particle has position x(t) and constant velocity v, then for a very small time step \Delta t

v \approx \frac{x(t+\Delta t)-x(t)}{\Delta t}.

Rearranging,

x(t+\Delta t) \approx x(t) + \Delta t \cdot v.

So in ordinary Euclidean space, constant velocity means that each small time step updates the position by

x(t)\longmapsto x(t)+\Delta t\cdot v

The same idea works for a Lie group, except that the update operation is no longer addition. In a group, motions are composed by multiplication. Therefore, the analogue of adding a small displacement is multiplying by a small group element near the identity.

If g(t) is a curve in the manifold for which g(0)=I and \dot{g}(t)=X, we can rewrite its derivative by using the definition

\frac{d}{dt}g(t)\Big|_{t=0} \approx \frac{g(\Delta t)-g(0)}{\Delta t} = \frac{g(\Delta t)-I}{\Delta t}.

Rearranging,

g(\Delta t) \approx I + \Delta t \cdot \frac{d}{dt}g(t)\Big|_{t=0} = I + \Delta t \cdot X.

From the above, and the fact that steps in the manifold are composed by multiplications, we get the updating rule

g(t)\longmapsto g(t) \left(I+\Delta t\cdot X\right)

Let’s start at the identity,

g_0=I,

and we’ll take the tiny step once, giving

g_1 = I \left(I + \Delta t \cdot X \right).

Applying the tiny step once more, we get

g_2 = \left(I + \Delta t \cdot X \right)^2.

Assume we have to take n steps to reach time t, therefore \Delta t = t/n. After n tiny steps:

g(t) = g_n = \left(I + \frac{tX}{n} \right)^n.

Of course, you know where this is going. In the limit where we slice t in infinitely many n slices:

g(t) = \lim_{n\to\infty}\left(I + \frac{tX}{n} \right)^n = \exp(tX).

We’ve reached the same result as before, but this time the exponential appears naturally from the derivation.

To sum up: taking the derivative of a curve g(t) in the manifold gives us the element X in the tangent space. Exponentiating X brings us back to g(t).

11.4 wait, what?

Is this correct? The derivative and the exponential are lanes travelling in opposite directions in the same road? Usually, we have the following pairs: (\exp,\log) and (\text{derivative},\text{integral}). Did we not just mix elements of these two pairs?

I will show now that, in a sense, taking the derivative or the log of a group element is the same. Conversely, exponentiating or integrating an element in the tangent space give the same result.

11.4.1 derivative = log

Let’s take a group element g close to the identity. We can write it as

g = I + \Delta t X + O(\Delta t^2),

where X is some matrix and \Delta t is small. Taking the log:

\log(g) = \log(I + \Delta t X + O(\Delta t^2)).

We now use the Taylor series for the log:

\log(g) = \log(I+\Delta t X) = \Delta t X - \frac{(\Delta t X)^2}{2} + \frac{(\Delta t X)^3}{3} - \ldots = \Delta t X + O(\Delta t^2).

To leading order in \Delta t, the log simply gives back \Delta t X, which is exactly the tangent vector X, up to the small parameter \Delta t.

Both the derivative and the log do the same job, they extract the linear part of the deviation from the identity. Differentiation gives the exact geometric definition for what the tangent space is, while the logarithm is a computational shortcut, it extracts the tangent vector from a specific point near the identity, without needing a whole curve in the manifold.

11.4.2 integral = exp

We’ve seen before that the rule for taking small steps along a curve g(t) is

g(t)\longmapsto g(t) \left(I+\Delta t\cdot X\right).

This means that if we know where the curve is at time t, taking one step further gives

g(t+\Delta t)\approx g(t)\left( I+ \Delta t \cdot X \right) = g(t) + \Delta t \cdot g(t)X.

Rearranging, and taking the limit \Delta t\to 0,

\lim_{\Delta t\to 0}\frac{g(t+\Delta t)-g(t)}{\Delta t} = \dot{g}(t) = g(t)X.

This last equality is really important:

\dot{g}(t) = g(t)X, \qquad g(0)=I

It says that g(t) is not any curve on the manifold that passes through the identity. This is the one curve whose velocity at any point is proportional to itself!

Of course, we can solve this first-order differential equation by integrating it, giving what we found before: g(t)=\exp(tX). Thus we have shown that, in a sense, integration and exponentiation give the same thing.

11.5 the tangent space is a vector space

A vector space has the following property: if A and B are two vectors in the vector space, their linear combination \mu_1 A + \mu_2 B is also a vector belonging to this vector space. The one condition we know about the tangent space is that its elements are 2\times2 matrices with trace zero. So let’s use that to prove that our tangent space is a vector space. Using the fact that the trace operation is linear:

\text{trace}(\mu_1 A + \mu_2 B) = \mu_1 \underbrace{\text{trace}(A)}_{=0} + \mu_2 \underbrace{\text{trace}(B)}_{=0} = 0.

If you want to see this in extra detail, let’s write

A=\begin{pmatrix}a_{11} & a_{12}\\a_{21} & -a_{11}\end{pmatrix}, \qquad B=\begin{pmatrix}b_{11} & b_{12}\\b_{21} & -b_{11}\end{pmatrix}.

Then

\begin{align*} \mu_1 A + \mu_2B &= \mu_1\begin{pmatrix}a_{11} & a_{12}\\a_{21} & -a_{11}\end{pmatrix} + \mu_2 \begin{pmatrix}b_{11} & b_{12}\\b_{21} & -b_{11}\end{pmatrix} \\ &= \begin{pmatrix}\mu_1a_{11}+\mu_2b_{11} & \mu_1a_{12}+\mu_2b_{12}\\ \mu_1a_{21}+\mu_2b_{21} & -\mu_1a_{11}-\mu_2b_{11}\end{pmatrix}\\ \text{trace}(\mu_1 A + \mu_2B) &= \mu_1a_{11}+\mu_2b_{11}-\mu_1a_{11}-\mu_2b_{11} \\ &= \mu_1 \underbrace{(a_{11}-a_{11})}_{\text{trace}(A)=0} + \mu_2 \underbrace{(b_{11}-b_{11})}_{\text{trace}(B)=0} =0 \end{align*}

And that concludes the proof. \blacksquare

11.6 the tangent space is an “algebra”

An algebra is a vector space equipped with a special operation. The natural operation of vector spaces is the addition: you can add two vectors and get another vector. That’s not what I’m talking about. The extra operation also takes two vectors and return a third vector. What should this extra operation be?

11.7 The Lie Algebra sl(2)

What we just found is a new vector space related to the manifold. The elements in this flat space are given by

sl(2) = \left\{ \begin{pmatrix} a & b \\ c & d \end{pmatrix} \Big| a + d = 0 \right\}

This is a regular vector space of (complex) dimension 3. If we sum two elements of this space, we get another element of the same space. What promotes this vector space to an algebra is a way to multiply two elements together and stay inside the set. Remember the group commutator from before? Refresher: XYX^{-1}Y^{-1}. This measures “how much X and Y fail to commute” as group elements.

We want to push the group commutator XYX^{-1}Y^{-1} down to the tangent space, to find the natural product on sl(2).

To do this, we take X and Y to be group elements close to the identity — in other words, we take small steps away from I in two directions A and B in the tangent space:

X = I + \Delta t \, A, \quad Y = I + \Delta t \, B

where A, B \in sl(2) are tangent vectors (trace-zero matrices), and \Delta t is a small parameter.

We need X^{-1} and Y^{-1}. We use the matrix version of the geometric series expansion \frac{1}{1+x} \approx 1 - x + x^2 - \ldots:

(I + \Delta t \, A)^{-1} \approx I - \Delta t \, A + \Delta t^2 A^2 + O(\Delta t^3)

We can verify this is correct by multiplying both sides by (I + \Delta t \, A) and checking that everything cancels to give I, up to order \Delta t^2:

\begin{align*} & (I + \Delta t \, A)(I - \Delta t \, A + \Delta t^2 A^2) = \\ &=I - \Delta t \, A + \Delta t^2 A^2 + \Delta t \, A - \Delta t^2 A^2 + \Delta t^3 A^3 \\ &= I + O(\Delta t^3) \end{align*}

Similarly:

(I + \Delta t \, B)^{-1} \approx I - \Delta t \, B + \Delta t^2 B^2 + O(\Delta t^3)

Now we are ready to compute the group commutator.

Step 1 — compute XY:

XY = (I + \Delta t \, A)(I + \Delta t \, B) = I + \Delta t(A + B) + \Delta t^2 AB + O(\Delta t^3)

Step 2 — compute X^{-1}Y^{-1}:

X^{-1}Y^{-1} = (I - \Delta t \, A + \Delta t^2 A^2)(I - \Delta t \, B + \Delta t^2 B^2) = I - \Delta t(A+B) + \Delta t^2(A^2 + AB + B^2) + O(\Delta t^3)

Step 3 — multiply them together, keeping only terms up to \Delta t^2:

XYX^{-1}Y^{-1} = \bigl[I + \Delta t(A+B) + \Delta t^2 AB\bigr]\bigl[I - \Delta t(A+B) + \Delta t^2(A^2+AB+B^2)\bigr]

Expanding and collecting terms order by order:

  • Order \Delta t^0: I
  • Order \Delta t^1: +(A+B) - (A+B) = 0 — the first order terms cancel completely
  • Order \Delta t^2: +(A^2+AB+B^2) - (A+B)^2 + AB

Let’s expand (A+B)^2 = A^2 + AB + BA + B^2 and substitute:

A^2 + AB + B^2 - A^2 - AB - BA - B^2 + AB = AB - BA

So we arrive at:

XYX^{-1}Y^{-1} = I + \Delta t^2(AB - BA) + O(\Delta t^3)

We now have a group element close to the identity, but we need to properly extract the tangent vector from it — just like we extract a tangent vector from a curve by dividing by \Delta t and taking the limit.

Recall how we computed the tangent vector of a single curve A(t):

A'(0) = \lim_{\Delta t \to 0} \frac{A(\Delta t) - I}{\Delta t}

We divided by \Delta t because a single group element moving away from the identity produces a leading correction of order \Delta t.

But here the leading correction is \Delta t^2, not \Delta t. Why? Because the group commutator involves two independent small steps — one of size \Delta t in the direction of A, another of size \Delta t in the direction of B. The commutator measures the interaction between these two steps.

Interactions between two small quantities are always second order — just like the cross term xy in a Taylor expansion in two variables. The first order terms only see each step individually, and they cancel completely, because going forward and coming back cancels to first order. What survives is the second order interaction between the two directions.

So the correct thing to do is to divide by \Delta t^2:

\frac{XYX^{-1}Y^{-1} - I}{\Delta t^2} = AB - BA + O(\Delta t)

And taking the limit:

\lim_{\Delta t \to 0} \frac{XYX^{-1}Y^{-1} - I}{\Delta t^2} = AB - BA

This is the Lie bracket, also called the commutator:

[A, B] = AB - BA

We need to verify that [A,B] is itself a trace-zero matrix — that is, it stays inside sl(2). Using the fact that \text{trace}(AB) = \text{trace}(BA) always holds (regardless of whether AB = BA):

\text{trace}([A,B]) = \text{trace}(AB - BA) = \text{trace}(AB) - \text{trace}(BA) = 0

The Lie bracket is not invented — it is inherited from the group commutator. It is the leading order remnant of asking “how much do A and B fail to commute?”, pushed down from the curved group to the flat tangent space via a second order perturbation expansion.

This promotes sl(2) from a mere vector space to a Lie algebra: a vector space equipped with the bracket product [A,B] = AB - BA.

11.8 commutator

Putting one’s socks on, and then the shoes, is not the same as putting the shoes on and then the socks. The order of operations often matters. If I have two elements X,Y of my SL(2) group (i.e. matrices) will the order of operations matter?

XY \stackrel{?}{=} YX

In order to find the difference between the two sides of the equation, ideally I would subtract the same quantity from both sides. Unfortunately, groups only have multiplication and the inverse, not addition/subtraction. To measure the effect of doing operations in different orders, we right-multiply both sides of the equation by X^{-1} on then right-multiply again by Y^{-1}. We get:

XYX^{-1}Y^{-1} \stackrel{?}{=} I.

That is to say, is doing X followed by Y, then the inverse of X, then the inverse of Y the same as doing nothing?

introducing A and B

Show the code
topojson = require("topojson-client@3")
world = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
  .then(r => r.json())


{
  const W=185, H=265, R=78, cx=W/2, cy=135, ext=28, gap=14;
  const BG='#dddddd', OCEAN='#1a598a74', π=Math.PI;

  // ── explicit 3×3 rotation matrices in the fixed observer frame ────────────
  // x = viewing direction, y = screen horizontal, z = screen vertical
  //
  // A(t): ambient rotation around fixed screen-vertical axis
  // B(t): ambient tilt around fixed viewing axis

  function Rz(deg) {
    const c=Math.cos(deg*π/180), s=Math.sin(deg*π/180);
    return [[c,-s,0],[s,c,0],[0,0,1]];
  }

  function Rx(deg) {
    const c=Math.cos(deg*π/180), s=Math.sin(deg*π/180);
    return [[1,0,0],[0,c,-s],[0,s,c]];
  }

  function mul(A,B) {
    const C=[[0,0,0],[0,0,0],[0,0,0]];
    for(let i=0;i<3;i++) for(let j=0;j<3;j++) for(let k=0;k<3;k++)
      C[i][j]+=A[i][k]*B[k][j];
    return C;
  }

  function applyM(M, lon, lat) {
    const φ=lat*π/180, λ=lon*π/180;
    const x=Math.cos(φ)*Math.cos(λ);
    const y=Math.cos(φ)*Math.sin(λ);
    const z=Math.sin(φ);

    const rx=M[0][0]*x+M[0][1]*y+M[0][2]*z;
    const ry=M[1][0]*x+M[1][1]*y+M[1][2]*z;
    const rz=M[2][0]*x+M[2][1]*y+M[2][2]*z;

    return [
      Math.atan2(ry,rx)*180/π,
      Math.asin(Math.max(-1,Math.min(1,rz)))*180/π
    ];
  }

  const INIT = Rz(-75);

  function makeSlider(label, min, max, step, init) {
    const wrap=document.createElement('div');
    wrap.style.cssText='margin-bottom:5px;height:38px';

    const lbl=document.createElement('label');
    lbl.textContent=label;
    lbl.style.cssText='color:black;font-family:Arial,sans-serif;font-size:13px;'+
      'font-weight:bold;display:block;margin-bottom:1px';

    const inp=document.createElement('input');
    inp.type='range';
    inp.min=min;
    inp.max=max;
    inp.step=step;
    inp.value=init;
    inp.style.cssText='width:100%;display:block;cursor:pointer;margin:0';

    wrap.append(lbl,inp);
    return {wrap,inp};
  }

  function makeSpacer() {
    const div=document.createElement('div');
    div.style.cssText='height:38px;margin-bottom:5px';
    return div;
  }

  const {wrap:wA,inp:iA}=makeSlider('A(t)',0,75,1,0);
  const {wrap:wB,inp:iB}=makeSlider('B(t)',0,23.44,0.1,0);

  const container=document.createElement('div');
  container.style.cssText=`background:${BG};padding:8px;display:flex;`+
    `gap:${gap}px;width:${3*W+2*gap+16}px;box-sizing:border-box;align-items:flex-start`;

  const colL=document.createElement('div');
  colL.style.cssText=`width:${W}px;flex-shrink:0`;
  const svgL=d3.create('svg').attr('width',W).attr('height',H)
    .style('background',BG).node();
  colL.append(makeSpacer(),svgL);

  const colM=document.createElement('div');
  colM.style.cssText=`width:${W}px;flex-shrink:0`;
  const svgM=d3.create('svg').attr('width',W).attr('height',H)
    .style('background',BG).node();
  colM.append(wA,svgM);

  const colR=document.createElement('div');
  colR.style.cssText=`width:${W}px;flex-shrink:0`;
  const svgR=d3.create('svg').attr('width',W).attr('height',H)
    .style('background',BG).node();
  colR.append(wB,svgR);

  container.append(colL,colM,colR);

  function draw(svgNode, M) {
    const svg=d3.select(svgNode);
    svg.selectAll('*').remove();

    const fn=(lon,lat)=>applyM(M,lon,lat);

    const baseProj=d3.geoOrthographic()
      .scale(R)
      .translate([cx,cy])
      .rotate([0,0,0]);

    const customProj={
      stream(sink){
        const ps=baseProj.stream(sink);
        return {
          point(lon,lat){
            const [rl,rp]=fn(lon,lat);
            ps.point(rl,rp);
          },
          lineStart(){ ps.lineStart(); },
          lineEnd(){ ps.lineEnd(); },
          polygonStart(){ ps.polygonStart(); },
          polygonEnd(){ ps.polygonEnd(); },
          sphere(){ if(ps.sphere) ps.sphere(); }
        };
      }
    };

    const path=d3.geoPath(customProj);

    function project(lon,lat){
      const [rl,rp]=fn(lon,lat);
      return baseProj([rl,rp]);
    }

    function visible(lon,lat){
      const φ=lat*π/180, λ=lon*π/180;
      const x=Math.cos(φ)*Math.cos(λ);
      const y=Math.cos(φ)*Math.sin(λ);
      const z=Math.sin(φ);

      return M[0][0]*x+M[0][1]*y+M[0][2]*z > -0.01;
    }

    svg.append('circle')
      .attr('cx',cx)
      .attr('cy',cy)
      .attr('r',R)
      .attr('fill',OCEAN);

    svg.append('path')
      .datum(d3.geoGraticule().step([15,90])())
      .attr('fill','none')
      .attr('stroke','rgba(200,200,200,0.40)')
      .attr('stroke-width',0.7)
      .attr('d',path);

    svg.append('path')
      .datum({type:'LineString',coordinates:d3.range(-180,181,1).map(l=>[l,0])})
      .attr('fill','none')
      .attr('stroke','rgba(255,255,180,0.35)')
      .attr('stroke-width',1)
      .attr('d',path);

    svg.append('path')
      .datum(topojson.feature(world,world.objects.land))
      .attr('fill','#2d6a1f')
      .attr('d',path);

    svg.append('path')
      .datum(topojson.mesh(world,world.objects.countries))
      .attr('fill','none')
      .attr('stroke','rgba(255,255,255,0.35)')
      .attr('stroke-width',0.5)
      .attr('d',path);

    svg.append('circle')
      .attr('cx',cx)
      .attr('cy',cy)
      .attr('r',R)
      .attr('fill','none')
      .attr('stroke','rgba(0,0,0,0.20)')
      .attr('stroke-width',1);

    const npPx=project(0,90)??[cx,cy-R];
    const spPx=project(0,-90)??[cx,cy+R];

    const dx=npPx[0]-spPx[0];
    const dy=npPx[1]-spPx[1];
    const len=Math.sqrt(dx*dx+dy*dy)||1;
    const ux=dx/len;
    const uy=dy/len;

    svg.append('line')
      .attr('x1',npPx[0])
      .attr('y1',npPx[1])
      .attr('x2',npPx[0]+ux*ext)
      .attr('y2',npPx[1]+uy*ext)
      .attr('stroke','#e17701')
      .attr('stroke-width',2.5);

    svg.append('line')
      .attr('x1',spPx[0])
      .attr('y1',spPx[1])
      .attr('x2',spPx[0]-ux*ext)
      .attr('y2',spPx[1]-uy*ext)
      .attr('stroke','#e17701')
      .attr('stroke-width',2.5);

    for(const [tx,ty] of [
      [npPx[0]+ux*ext,npPx[1]+uy*ext],
      [spPx[0]-ux*ext,spPx[1]-uy*ext]
    ]){
      svg.append('circle')
        .attr('cx',tx)
        .attr('cy',ty)
        .attr('r',3.5)
        .attr('fill','#e17701');
    }

    if(visible(0,90)){
      const p=project(0,90);
      if(p){
        svg.append('circle')
          .attr('cx',p[0])
          .attr('cy',p[1])
          .attr('r',4)
          .attr('fill','red')
          .attr('stroke','white')
          .attr('stroke-width',1.2);
      }
    }
  }

  function update(){
    const A=+iA.value;
    const B=+iB.value;

    draw(svgL, INIT);
    draw(svgM, mul(Rz(A), INIT));
    draw(svgR, mul(Rx(B), INIT));
  }

  for(const inp of [iA,iB]) inp.addEventListener('input',update);
  update();

  return container;
}

commutation test

Show the code
{
  const W=280, H=340, R=115, cx=W/2, cy=165, ext=40, gap=20;
  const BG='#dddddd', π=Math.PI;

  // ── explicit 3×3 rotation matrices in the fixed observer frame ────────────
  // screen/view convention:
  // x = viewing direction, y = screen horizontal, z = screen vertical
  //
  // A = Rz: eastward rotation around the fixed screen-vertical axis
  // B = Rx: tilt/roll around the fixed viewing axis, so the pole tilts in screen plane

  function Rz(deg) {
    const c=Math.cos(deg*π/180), s=Math.sin(deg*π/180);
    return [[c,-s,0],[s,c,0],[0,0,1]];
  }

  function Rx(deg) {
    const c=Math.cos(deg*π/180), s=Math.sin(deg*π/180);
    return [[1,0,0],[0,c,-s],[0,s,c]];
  }

  function mul(A,B) {
    const C=[[0,0,0],[0,0,0],[0,0,0]];
    for(let i=0;i<3;i++) for(let j=0;j<3;j++) for(let k=0;k<3;k++)
      C[i][j]+=A[i][k]*B[k][j];
    return C;
  }

  function applyM(M, lon, lat) {
    const φ=lat*π/180, λ=lon*π/180;
    const x=Math.cos(φ)*Math.cos(λ);
    const y=Math.cos(φ)*Math.sin(λ);
    const z=Math.sin(φ);

    const rx=M[0][0]*x+M[0][1]*y+M[0][2]*z;
    const ry=M[1][0]*x+M[1][1]*y+M[1][2]*z;
    const rz=M[2][0]*x+M[2][1]*y+M[2][2]*z;

    return [
      Math.atan2(ry,rx)*180/π,
      Math.asin(Math.max(-1,Math.min(1,rz)))*180/π
    ];
  }

  const INIT = Rz(-75);

  function makeSlider(label, min, max, step, init) {
    const wrap=document.createElement('div');
    wrap.style.cssText='margin-bottom:5px';

    const lbl=document.createElement('label');
    lbl.textContent=label;
    lbl.style.cssText='color:black;font-family:Arial,sans-serif;font-size:13px;'+
      'font-weight:bold;display:block;margin-bottom:1px';

    const inp=document.createElement('input');
    inp.type='range';
    inp.min=min;
    inp.max=max;
    inp.step=step;
    inp.value=init;
    inp.style.cssText='width:100%;display:block;cursor:pointer;margin:0';

    wrap.append(lbl,inp);
    return {wrap,inp};
  }

  const {wrap:wLA,inp:iLA}=makeSlider('A(t) — ambient eastward rotation',0,75,1,0);
  const {wrap:wLB,inp:iLB}=makeSlider('B(t) — ambient screen-plane tilt',0,23.44,0.1,0);

  const {wrap:wRB,inp:iRB}=makeSlider('B(t) — ambient screen-plane tilt',0,23.44,0.1,0);
  const {wrap:wRA,inp:iRA}=makeSlider('A(t) — ambient eastward rotation',0,75,1,0);

  const container=document.createElement('div');
  container.style.cssText=`background:${BG};padding:8px;display:flex;`+
    `gap:${gap}px;width:${2*W+gap+16}px;box-sizing:border-box;align-items:flex-start`;

  const colL=document.createElement('div');
  colL.style.cssText=`width:${W}px;flex-shrink:0`;
  const svgL=d3.create('svg').attr('width',W).attr('height',H)
    .style('background',BG).node();
  colL.append(wLA,wLB,svgL);

  const colR=document.createElement('div');
  colR.style.cssText=`width:${W}px;flex-shrink:0`;
  const svgR=d3.create('svg').attr('width',W).attr('height',H)
    .style('background',BG).node();
  colR.append(wRB,wRA,svgR);

  container.append(colL,colR);

  function draw(svgNode, M) {
    const svg=d3.select(svgNode);
    svg.selectAll('*').remove();

    const fn=(lon,lat)=>applyM(M,lon,lat);

    const baseProj=d3.geoOrthographic()
      .scale(R)
      .translate([cx,cy])
      .rotate([0,0,0]);

    const customProj={
      stream(sink){
        const ps=baseProj.stream(sink);
        return {
          point(lon,lat){
            const [rl,rp]=fn(lon,lat);
            ps.point(rl,rp);
          },
          lineStart(){ ps.lineStart(); },
          lineEnd(){ ps.lineEnd(); },
          polygonStart(){ ps.polygonStart(); },
          polygonEnd(){ ps.polygonEnd(); },
          sphere(){ if(ps.sphere) ps.sphere(); }
        };
      }
    };

    const path=d3.geoPath(customProj);

    function project(lon,lat){
      const [rl,rp]=fn(lon,lat);
      return baseProj([rl,rp]);
    }

    function visible(lon,lat){
      const φ=lat*π/180, λ=lon*π/180;
      const x=Math.cos(φ)*Math.cos(λ);
      const y=Math.cos(φ)*Math.sin(λ);
      const z=Math.sin(φ);

      return M[0][0]*x+M[0][1]*y+M[0][2]*z > -0.01;
    }

    svg.append('circle')
      .attr('cx',cx)
      .attr('cy',cy)
      .attr('r',R)
      .attr('fill','#1a598a74');

    svg.append('path')
      .datum(d3.geoGraticule().step([15,90])())
      .attr('fill','none')
      .attr('stroke','rgba(200,200,200,0.40)')
      .attr('stroke-width',0.7)
      .attr('d',path);

    svg.append('path')
      .datum({type:'LineString',coordinates:d3.range(-180,181,1).map(l=>[l,0])})
      .attr('fill','none')
      .attr('stroke','rgba(255,255,180,0.35)')
      .attr('stroke-width',1)
      .attr('d',path);

    svg.append('path')
      .datum(topojson.feature(world,world.objects.land))
      .attr('fill','#2d6a1f')
      .attr('d',path);

    svg.append('path')
      .datum(topojson.mesh(world,world.objects.countries))
      .attr('fill','none')
      .attr('stroke','rgba(255,255,255,0.35)')
      .attr('stroke-width',0.5)
      .attr('d',path);

    svg.append('circle')
      .attr('cx',cx)
      .attr('cy',cy)
      .attr('r',R)
      .attr('fill','none')
      .attr('stroke','rgba(0,0,0,0.20)')
      .attr('stroke-width',1);

    const npPx=project(0,90)??[cx,cy-R];
    const spPx=project(0,-90)??[cx,cy+R];

    const dx=npPx[0]-spPx[0];
    const dy=npPx[1]-spPx[1];
    const len=Math.sqrt(dx*dx+dy*dy)||1;
    const ux=dx/len;
    const uy=dy/len;

    svg.append('line')
      .attr('x1',npPx[0])
      .attr('y1',npPx[1])
      .attr('x2',npPx[0]+ux*ext)
      .attr('y2',npPx[1]+uy*ext)
      .attr('stroke','#e17701')
      .attr('stroke-width',2.5);

    svg.append('line')
      .attr('x1',spPx[0])
      .attr('y1',spPx[1])
      .attr('x2',spPx[0]-ux*ext)
      .attr('y2',spPx[1]-uy*ext)
      .attr('stroke','#e17701')
      .attr('stroke-width',2.5);

    for(const [tx,ty] of [
      [npPx[0]+ux*ext,npPx[1]+uy*ext],
      [spPx[0]-ux*ext,spPx[1]-uy*ext]
    ]){
      svg.append('circle')
        .attr('cx',tx)
        .attr('cy',ty)
        .attr('r',3.5)
        .attr('fill','#e17701');
    }

    if(visible(0,90)){
      const p=project(0,90);
      if(p){
        svg.append('circle')
          .attr('cx',p[0])
          .attr('cy',p[1])
          .attr('r',4)
          .attr('fill','red')
          .attr('stroke','white')
          .attr('stroke-width',1.2);
      }
    }
  }

  // ── stateful ambient-frame updates ────────────────────────────────────────
  //
  // important:
  // slider values are not recomputed into a fresh matrix.
  // instead, each slider movement applies the delta since the previous value.
  // this preserves the historical order in which the user moved the sliders.

  const stateL = {
    M: INIT,
    A: +iLA.value,
    B: +iLB.value
  };

  const stateR = {
    M: INIT,
    A: +iRA.value,
    B: +iRB.value
  };

  function redraw(){
    draw(svgL,stateL.M);
    draw(svgR,stateR.M);
  }

  iLA.addEventListener('input',() => {
    const next=+iLA.value;
    const delta=next-stateL.A;
    stateL.M=mul(Rz(delta),stateL.M);
    stateL.A=next;
    redraw();
  });

  iLB.addEventListener('input',() => {
    const next=+iLB.value;
    const delta=next-stateL.B;
    stateL.M=mul(Rx(delta),stateL.M);
    stateL.B=next;
    redraw();
  });

  iRA.addEventListener('input',() => {
    const next=+iRA.value;
    const delta=next-stateR.A;
    stateR.M=mul(Rz(delta),stateR.M);
    stateR.A=next;
    redraw();
  });

  iRB.addEventListener('input',() => {
    const next=+iRB.value;
    const delta=next-stateR.B;
    stateR.M=mul(Rx(delta),stateR.M);
    stateR.B=next;
    redraw();
  });

  redraw();

  return container;
}