commit 780bf0fde9e02bc65ca24f5116a2d78e4039f870
parent b86298dddd9a1efe21d2501f5d7c198760d812cb
Author: ling0x <ling0x@users.noreply.github.com>
Date: Mon, 7 Sep 2026 12:29:13 +0100
calculus
Diffstat:
4 files changed, 184 insertions(+), 1475 deletions(-)
diff --git a/mathematics/calculus_derivatives.txt b/mathematics/calculus_derivatives.txt
@@ -0,0 +1,173 @@
+CALCULUS NOTES: DERIVATIVES, NOTATION, AND GRADIENTS
+======================================================
+
+
+1. WHAT IS A DERIVATIVE?
+-------------------------
+The derivative f'(x) describes the rate of change (the slope) of a
+function f(x) at every point. If f'(x) = 5, the function is changing
+by 5 units for every 1 unit that x changes.
+
+
+2. THE d/dx NOTATION
+---------------------
+d/dx is NOT multiplication. It is an operator -- an instruction
+waiting for a function to act on, the same way sin( ) is an
+instruction waiting for a number.
+
+ d/dx { f(x) } reads as: "take the derivative of f(x) with
+ respect to x"
+
+Other equivalent notations for the same thing:
+ f'(x) Df(x) df/dx f-dot (for derivatives w.r.t. time)
+
+d/dx(x) = 1 (plain x is x^1, so the power rule gives 1*x^0 = 1)
+
+
+3. RISE-OVER-RUN -> THE FORMAL DEFINITION OF A DERIVATIVE
+-----------------------------------------------------------
+Slope requires two points. Rise-over-run between two points on f(x):
+
+ m = (f(x + delta_x) - f(x)) / delta_x
+
+This line through two points on the curve is called a SECANT line.
+It is only an approximation of the true slope at a single point,
+because it averages the slope across the whole gap.
+
+To get the EXACT slope at one point, shrink the gap (delta_x) toward
+zero. The secant line rotates into the TANGENT line -- the line that
+touches the curve at exactly one point and matches its slope there.
+(Note: a tangent line is not "perpendicular" to anything in general --
+that property is specific to circles, where a tangent is perpendicular
+to the radius. For a general curve, tangent just means "matches the
+curve's direction at that single point.")
+
+This limiting process is the formal definition of the derivative:
+
+ f'(x) = lim (delta_x -> 0) of [f(x + delta_x) - f(x)] / delta_x
+
+Every derivative rule (power rule, trig derivatives, etc.) is just
+this limit, worked out once in advance for a given function so you
+never have to redo it by hand.
+
+
+4. THE POWER RULE (derived from rise-over-run)
+------------------------------------------------
+General rule:
+ d/dx (x^n) = n * x^(n-1)
+
+Worked example for f(x) = x^2:
+ f'(x) = lim [(x+dx)^2 - x^2] / dx
+ = lim [x^2 + 2x*dx + dx^2 - x^2] / dx
+ = lim [2x*dx + dx^2] / dx
+ = lim (2x + dx)
+ = 2x (as dx -> 0, the leftover dx vanishes)
+
+Same process for f(x) = x^3 gives f'(x) = 3x^2, matching the shortcut
+3*x^(3-1) = 3x^2.
+
+More examples:
+ d/dx(x^3) = 3x^2
+ d/dx(x^5) = 5x^4
+ d/dx(x^8) = 8x^7
+ d/dx(x^100) = 100x^99
+
+
+5. RULES FOR COMBINING FUNCTIONS
+-----------------------------------
+Sum rule:
+ d/dx(x^3 + x^2) = 3x^2 + 2x
+
+Product rule (f times g):
+ d/dx[f(x)g(x)] = f'(x)g(x) + f(x)g'(x)
+ Example: d/dx[x^2 sin(x)] = 2x sin(x) + x^2 cos(x)
+
+Quotient rule (f over g):
+ d/dx[f(x)/g(x)] = [f'(x)g(x) - f(x)g'(x)] / g(x)^2
+ Example: d/dx[x^2/(x+1)] = (x^2 + 2x) / (x+1)^2
+
+Chain rule (a function inside another function):
+ d/dx[f(g(x))] = f'(g(x)) * g'(x)
+ Example: d/dx[(x^2+1)^5] = 5(x^2+1)^4 * 2x = 10x(x^2+1)^4
+
+Combined example -- f(x) = x^2 * e^(3x) (product rule + chain rule):
+ d/dx(e^(3x)) = 3e^(3x) [chain rule on the inside]
+ d/dx[x^2 e^(3x)] = 2x*e^(3x) + x^2*3e^(3x)
+ = e^(3x)(2x + 3x^2) [product rule]
+
+
+6. KNOWN DERIVATIVES TO MEMORIZE
+------------------------------------
+ sin(x) -> cos(x)
+ cos(x) -> -sin(x)
+ e^x -> e^x (its own derivative)
+ ln(x) -> 1/x
+
+WHY d/dx[sin(x)] = cos(x):
+Using the angle-addition identity sin(x+dx) = sin(x)cos(dx) + cos(x)sin(dx),
+the rise-over-run limit splits into two well-known limits:
+ lim (cos(dx)-1)/dx = 0 and lim sin(dx)/dx = 1
+which leaves exactly cos(x).
+
+Intuition: cos(x) is not a picture of a tangent line sitting on top of
+sin(x) -- it is a completely separate curve. Its HEIGHT at each x
+records the SLOPE of sin(x) at that same x. Wherever sin(x) is flat
+(a peak/trough), cos(x) crosses zero. Wherever sin(x) is rising or
+falling fastest, cos(x) is at its max or min. Every point on cos(x)
+is the result of running the two-points-collapsing-into-a-tangent
+process (section 3) at that x -- the full curve is just that
+snapshot taken continuously across all x.
+
+
+7. PARTIAL DERIVATIVES
+--------------------------
+Once a function has more than one input, like f(x, y), "the slope"
+is ambiguous -- it depends on which direction you move. A partial
+derivative freezes every variable except one and differentiates
+normally with respect to that one:
+
+ df/dx -- slope moving along x only, y held constant
+ df/dy -- slope moving along y only, x held constant
+
+Example: f(x,y) = x^2 + y^2
+ df/dx = 2x
+ df/dy = 2y
+
+
+8. THE GRADIENT
+--------------------
+The gradient bundles ALL of a function's partial derivatives into a
+single vector:
+
+ grad f = ( df/dx , df/dy )
+
+It is not a new calculation -- it is a packaging step. Geometrically,
+the gradient vector points in the direction of steepest increase of
+the function, and its length tells you how steep that climb is.
+Visually, it is built by joining the df/dx component and the df/dy
+component tip-to-tail (vector addition).
+
+Partial derivatives = the ingredients (one number each).
+Gradient = the finished vector made from all of them together.
+
+
+9. NOTATION / PRONUNCIATION GUIDE
+--------------------------------------
+ d/dx "d, d x" -- ordinary derivative operator
+ partial (d/dx symbol looks like a rounded d)
+ "partial f, partial x" (read df/dx as "partial f partial x")
+ Symbol: the partial derivative sign. NOT a Greek letter --
+ it's a stylized cursive "d". Easy to confuse with delta
+ because of the rounded shape, but it's a distinct symbol.
+
+ delta (Δ / δ) "delta" -- "a change in" something (e.g. Δx)
+ lambda (λ) "lambda" -- unrelated shape (angled/forked),
+ doesn't resemble partial or delta
+
+ nabla (∇) "NAH-blah" (also called "del")
+ Named after an ancient harp of similar shape.
+ Operator form: nabla = (d/dx, d/dy) -- attach it to a
+ function to get the gradient: nabla f = grad f.
+ Same symbol also appears (later topics) as:
+ nabla . F (divergence, a scalar)
+ nabla x F (curl, a vector)
diff --git a/mathematics/subdivision-interactive.html b/mathematics/subdivision-interactive.html
@@ -1,1475 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Subdivision Surfaces — from the refinement equation</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:wght@600;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
-<style>
-:root{
- --ground:#10151a; --panel:#171e25; --panel2:#1d262e;
- --chalk:#e8e6df; --dim:#9aa5ad; --cage:#6b7c8c;
- --face:#f2b04a; --edge:#4fd6c4; --vert:#e86a92;
- --rule:rgba(107,124,140,.22);
- --mono:"IBM Plex Mono",ui-monospace,monospace;
- --sans:"IBM Plex Sans",system-ui,sans-serif;
- --cond:"IBM Plex Sans Condensed","IBM Plex Sans",system-ui,sans-serif;
-}
-*{box-sizing:border-box}
-html{-webkit-text-size-adjust:100%}
-body{margin:0;background:var(--ground);color:var(--chalk);font-family:var(--sans);
- font-size:16.5px;line-height:1.65;-webkit-font-smoothing:antialiased}
-canvas{display:block;max-width:100%}
-.wrap{max-width:1080px;margin:0 auto;padding:0 22px 110px}
-.col{max-width:66ch}
-h1,h2,h3{font-family:var(--cond);font-weight:700;letter-spacing:-.015em;line-height:1.08;margin:0}
-h2{font-size:clamp(1.5rem,4.4vw,2.15rem);margin:0 0 .5rem}
-h4{font-family:var(--mono);font-size:.72rem;font-weight:600;letter-spacing:.14em;
- text-transform:uppercase;color:var(--dim);margin:2rem 0 .6rem}
-p{margin:0 0 1rem} ul,ol{margin:0 0 1rem;padding-left:1.25rem} li{margin-bottom:.4rem}
-strong{font-weight:600;color:#fff} em{color:var(--chalk)}
-code{font-family:var(--mono);font-size:.87em;background:rgba(107,124,140,.16);
- padding:.12em .38em;border-radius:3px}
-.hero{padding:clamp(52px,9vw,92px) 0 12px}
-.eyebrow{font-family:var(--mono);font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
- color:var(--cage);margin-bottom:1.4rem}
-.hero h1{font-size:clamp(2.35rem,8vw,4.5rem);letter-spacing:-.03em;margin-bottom:1.1rem}
-.hero h1 .thin{display:block;font-weight:600;color:var(--dim);font-size:.44em;
- letter-spacing:-.01em;margin-top:.55rem}
-.lede{font-size:clamp(1.02rem,2.4vw,1.2rem);color:var(--dim);max-width:56ch}
-.lede b{color:var(--chalk);font-weight:500}
-.legend{position:sticky;top:0;z-index:50;background:rgba(16,21,26,.94);
- backdrop-filter:blur(10px);border-bottom:1px solid var(--rule);padding:9px 0;margin:52px 0 0}
-.legend-in{max-width:1080px;margin:0 auto;padding:0 22px;display:flex;gap:16px;
- flex-wrap:wrap;align-items:center;font-family:var(--mono);font-size:.7rem}
-.key{display:flex;align-items:center;gap:6px;color:var(--dim);white-space:nowrap}
-.dot{width:9px;height:9px;border-radius:50%;flex:none}
-.dot.f{background:var(--face)}.dot.e{background:var(--edge)}
-.dot.v{background:var(--vert)}.dot.c{background:var(--cage)}
-section{padding:62px 0 0}
-.snum{font-family:var(--mono);font-size:.7rem;letter-spacing:.18em;color:var(--cage);
- display:block;margin-bottom:.85rem}
-.snum b{color:var(--edge);font-weight:500}
-.deriv{border-left:2px solid var(--rule);padding:2px 0 2px 20px;margin:1.5rem 0}
-.deriv.f{border-color:rgba(242,176,74,.55)}
-.deriv.e{border-color:rgba(79,214,196,.55)}
-.deriv.v{border-color:rgba(232,106,146,.55)}
-.deriv h5{font-family:var(--mono);font-size:.7rem;letter-spacing:.13em;text-transform:uppercase;
- color:var(--dim);margin:0 0 .55rem;font-weight:500}
-.deriv p:last-child{margin-bottom:0}
-.note{background:var(--panel);border:1px solid var(--rule);border-radius:5px;
- padding:16px 18px;margin:1.5rem 0;font-size:.94rem}
-.note h5{font-family:var(--mono);font-size:.7rem;letter-spacing:.13em;text-transform:uppercase;
- color:var(--edge);margin:0 0 .5rem;font-weight:600}
-.note p:last-child{margin-bottom:0}
-figure{margin:2.2rem 0;background:var(--panel);border:1px solid var(--rule);
- border-radius:6px;overflow:hidden}
-.figbar{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:10px 14px;
- border-bottom:1px solid var(--rule);background:var(--panel2)}
-.ftitle{font-family:var(--mono);font-size:.7rem;letter-spacing:.11em;text-transform:uppercase;
- color:var(--dim);margin-right:auto}
-figure canvas{width:100%;height:auto;touch-action:none;cursor:grab;background:#0d1216}
-figure canvas:active{cursor:grabbing}
-figcaption{padding:12px 16px 14px;font-size:.85rem;color:var(--dim);
- border-top:1px solid var(--rule);line-height:1.6}
-figcaption b{color:var(--chalk);font-weight:500}
-.readout{font-family:var(--mono);font-size:.73rem;color:var(--dim);padding:9px 14px;
- border-top:1px solid var(--rule);min-height:34px;line-height:1.55}
-.readout b{color:var(--chalk);font-weight:500}
-.ctl{display:flex;align-items:center;gap:7px;font-family:var(--mono);font-size:.71rem;color:var(--dim)}
-input[type=range]{-webkit-appearance:none;appearance:none;background:transparent;
- width:104px;height:18px;margin:0;cursor:pointer}
-input[type=range]::-webkit-slider-runnable-track{height:2px;background:var(--rule)}
-input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;
- border-radius:50%;background:var(--chalk);margin-top:-5px;border:none}
-input[type=range]::-moz-range-track{height:2px;background:var(--rule)}
-input[type=range]::-moz-range-thumb{width:12px;height:12px;border-radius:50%;
- background:var(--chalk);border:none}
-input[type=range]:focus-visible{outline:2px solid var(--edge);outline-offset:3px}
-.seg{display:inline-flex;border:1px solid var(--rule);border-radius:4px;overflow:hidden}
-.seg button{font-family:var(--mono);font-size:.69rem;background:transparent;color:var(--dim);
- border:0;padding:5px 9px;cursor:pointer;border-right:1px solid var(--rule)}
-.seg button:last-child{border-right:0}
-.seg button[aria-pressed=true]{background:var(--chalk);color:#0d1216;font-weight:600}
-.seg button:focus-visible{outline:2px solid var(--edge);outline-offset:-2px}
-.btn{font-family:var(--mono);font-size:.69rem;background:transparent;color:var(--dim);
- border:1px solid var(--rule);border-radius:4px;padding:5px 10px;cursor:pointer}
-.btn:hover{color:var(--chalk);border-color:var(--cage)}
-.btn:focus-visible{outline:2px solid var(--edge);outline-offset:2px}
-.val{color:var(--chalk);min-width:1.6em;display:inline-block}
-label.chk{display:inline-flex;align-items:center;gap:5px;cursor:pointer}
-input[type=checkbox]{accent-color:var(--edge);width:13px;height:13px;margin:0}
-.stencils{display:flex;gap:22px;flex-wrap:wrap;margin:1.6rem 0}
-.sten{font-family:var(--mono);font-size:.72rem}
-.sten .lab{color:var(--dim);letter-spacing:.1em;text-transform:uppercase;
- font-size:.66rem;margin-bottom:7px}
-.sten .grid{display:inline-grid;gap:2px;background:var(--rule);padding:2px;border-radius:3px}
-.sten .cell{background:var(--panel2);width:38px;height:30px;display:flex;
- align-items:center;justify-content:center;color:var(--chalk)}
-.sten.fS .cell.hi{background:rgba(242,176,74,.2);color:var(--face)}
-.sten.eS .cell.hi{background:rgba(79,214,196,.2);color:var(--edge)}
-.sten.vS .cell.hi{background:rgba(232,106,146,.2);color:var(--vert)}
-.tbl{width:100%;overflow-x:auto;margin:1.7rem 0;-webkit-overflow-scrolling:touch}
-table{border-collapse:collapse;font-size:.86rem;min-width:520px;width:100%}
-th,td{text-align:left;padding:9px 13px 9px 0;border-bottom:1px solid var(--rule);
- vertical-align:top}
-th{font-family:var(--mono);font-size:.67rem;letter-spacing:.1em;text-transform:uppercase;
- color:var(--dim);font-weight:500;white-space:nowrap}
-td code{background:none;padding:0}
-pre{font-family:var(--mono);font-size:.79rem;background:var(--panel);
- border:1px solid var(--rule);border-radius:5px;padding:14px 16px;overflow-x:auto;
- line-height:1.7;color:var(--chalk)}
-hr{border:0;border-top:1px solid var(--rule);margin:58px 0 0}
-.end{font-family:var(--mono);font-size:.76rem;color:var(--cage);line-height:1.9;padding-top:46px}
-.katex{font-size:1.03em}
-.katex-display{margin:1.15rem 0;overflow-x:auto;overflow-y:hidden;padding:2px 0}
-@media (max-width:640px){
- body{font-size:16px}
- .wrap{padding:0 16px 80px}
- .legend-in{padding:0 16px;gap:11px;font-size:.66rem}
- .deriv{padding-left:14px}
- .sten .cell{width:33px;height:27px}
- .figbar{gap:10px;padding:9px 11px}
- input[type=range]{width:84px}
-}
-@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;transition-duration:.01ms!important}}
-</style>
-</head>
-<body>
-<div class="wrap">
-
-<header class="hero">
- <div class="eyebrow">Refinement operators on meshes</div>
- <h1>Subdivision<br>Surfaces
- <span class="thin">Every mask below is a B-spline in disguise.</span></h1>
- <p class="lede">Drag the white points. A <b>control polygon</b> is hit repeatedly by one
- linear operator; in the limit it becomes a curve you never wrote an equation for. Everything
- else here is that same idea applied to meshes.</p>
-</header>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 1 — refinement, live</span>
- <div class="seg" id="f1scheme">
- <button data-s="chaikin" aria-pressed="true">Chaikin · quadratic</button>
- <button data-s="cubic" aria-pressed="false">Cubic B-spline</button></div>
- <span class="ctl">level <input type="range" id="f1lvl" min="0" max="6" value="2">
- <b class="val" id="f1lvlv">2</b></span>
- <label class="chk ctl"><input type="checkbox" id="f1limit" checked> limit</label>
- </div>
- <canvas id="f1"></canvas>
- <div class="readout" id="f1out"></div>
- <figcaption>Drag the white points — one finger works on a phone. <b>Chaikin</b> discards the
- old vertices and puts two new points on every edge (a <em>dual</em> scheme). <b>Cubic</b>
- keeps them and relaxes them (<em>primal</em>). At level 6 the polygon has already become the
- curve.</figcaption>
-</figure>
-
-<div class="legend"><div class="legend-in">
- <span class="key" style="letter-spacing:.13em">COLOUR KEY</span>
- <span class="key"><i class="dot c"></i>control cage, level k</span>
- <span class="key"><i class="dot f"></i>face point</span>
- <span class="key"><i class="dot e"></i>edge point</span>
- <span class="key"><i class="dot v"></i>vertex point</span>
-</div></div>
-
-<section><div class="col">
-<span class="snum"><b>00</b> · VOCABULARY & THE COUNTING ARGUMENT</span>
-<h2>Why irregular vertices are unavoidable</h2>
-<p>A polygon mesh \(M=(V,E,F)\) carries two independent kinds of information, and subdivision
-treats them separately:</p>
-<ul>
-<li><strong>Topology</strong> — which vertices join to which. Purely combinatorial.</li>
-<li><strong>Geometry</strong> — the map \(V\to\mathbb{R}^3\) of positions. Purely numerical.</li>
-</ul>
-<p>Each step applies a <em>topological rule</em> (split faces) then a <em>geometric rule</em>
-(average positions). Keeping them apart is what lets the method work on arbitrary shapes.</p>
-<h4>Definitions</h4>
-<div class="tbl"><table>
-<tr><th>Term</th><th>Meaning</th></tr>
-<tr><td><strong>Valence</strong> \(n(v)\)</td><td>number of edges incident to \(v\)</td></tr>
-<tr><td><strong>\(k\)-ring</strong> of \(v\)</td><td>vertices at graph distance \(\le k\)</td></tr>
-<tr><td><strong>Manifold</strong></td><td>every edge has 1 or 2 faces; every vertex's faces form one fan</td></tr>
-<tr><td><strong>Regular vertex</strong></td><td>quad mesh: \(n=4\). Triangle mesh: \(n=6\)</td></tr>
-<tr><td><strong>Extraordinary vertex</strong> (EV)</td><td>any vertex that is not regular</td></tr>
-<tr><td><strong>Regular region</strong></td><td>submesh with no EV — locally a \(\mathbb{Z}^2\) grid</td></tr>
-</table></div>
-<div class="deriv">
-<h5>Derivation — the Euler obstruction</h5>
-<p>For a closed orientable mesh, \(V-E+F=2-2g\). Suppose a closed quad mesh had <em>every</em>
-vertex of valence 4. Each face has 4 edges, each edge shared by 2 faces, so \(4F=2E\Rightarrow
-F=E/2\). Each vertex has 4 edge-ends, each edge has 2 ends, so \(4V=2E\Rightarrow V=E/2\).
-Then</p>
-\[\frac{E}{2}-E+\frac{E}{2}=0=2-2g\;\Longrightarrow\;g=1\]
-<p>So <strong>only a torus admits an all-regular closed quad mesh.</strong> A sphere, a car
-body, a wing fairing — every one <em>must</em> contain extraordinary vertices.</p>
-</div>
-<p>This drives everything below. Tensor-product B-splines need a global rectangular domain and
-therefore cannot cover a closed surface. Subdivision throws away the global domain and keeps
-only the <em>local</em> averaging rule — after which the whole literature is one question:
-<em>what happens at the EVs?</em></p>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 2 — valence, and where the EVs sit</span>
- <div class="seg" id="f2shape">
- <button data-s="cube" aria-pressed="true">Cube</button>
- <button data-s="torus" aria-pressed="false">Torus</button>
- <button data-s="lshape" aria-pressed="false">L-block</button></div>
- </div>
- <canvas id="f2"></canvas>
- <div class="readout" id="f2out"></div>
- <figcaption>Drag to rotate. Vertices coloured by valence:
- <b style="color:#6b7c8c">grey = 4, regular</b>;
- <b style="color:#e86a92">rose = extraordinary</b>. The cube's 8 corners are all valence 3 —
- every one an EV. The torus is the single shape that gets away with none.</figcaption>
-</figure>
-</section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>01</b> · WHERE THE MASKS COME FROM</span>
-<h2>The refinement equation</h2>
-<p>Subdivision is read <em>backwards</em> out of B-spline theory, so we need that first —
-briefly, and only the part that matters.</p>
-<h4>Bases</h4>
-<p>Bernstein: \(B_i^n(t)=\binom{n}{i}t^i(1-t)^{n-i}\), giving
-\(\mathbf{C}(t)=\sum_i\mathbf{P}_iB_i^n(t)\). Since \(B_i^n\ge0\) and \(\sum_iB_i^n\equiv1\) —
-a <strong>partition of unity</strong> — affine invariance and the convex-hull property come
-free. Every mask below satisfies that pair.</p>
-<p>B-splines replace Bernstein with Cox–de Boor on a knot vector:</p>
-\[N_{i,p}(t)=\frac{t-t_i}{t_{i+p}-t_i}N_{i,p-1}(t)+\frac{t_{i+p+1}-t}{t_{i+p+1}-t_{i+1}}N_{i+1,p-1}(t)\]
-<p><strong>Uniform</strong> means \(t_i=i\); then every basis function is an integer translate of
-one <strong>cardinal B-spline</strong>, a repeated box convolution:</p>
-\[N_p=\underbrace{\chi_{[0,1]}*\cdots*\chi_{[0,1]}}_{p+1\ \text{factors}},\qquad
-\operatorname{supp}N_p=[0,p+1],\qquad N_p\in C^{p-1}\]
-<div class="deriv e">
-<h5>Derivation — the two-scale relation</h5>
-<p>Cardinal B-splines are <strong>refinable</strong>. In Fourier,
-\(\widehat{N_p}(\omega)=\big(\tfrac{1-e^{-i\omega}}{i\omega}\big)^{p+1}\), so halving the
-frequency gives \(\widehat{N_p}(\omega)=a(e^{-i\omega/2})\widehat{N_p}(\omega/2)\) with
-\(a(z)=(1+z)^{p+1}/2^{p}\). Transforming back:</p>
-\[\boxed{\;N_p(t)=\frac{1}{2^{p}}\sum_{k=0}^{p+1}\binom{p+1}{k}N_p(2t-k)\;}\]
-<p>Call \(a_k=2^{-p}\binom{p+1}{k}\) the <strong>mask</strong> and \(a(z)=\sum_ka_kz^k\) the
-<strong>symbol</strong>. This is the whole trick: <em>the same curve, on a knot grid twice as
-fine, has different but computable control points.</em> Refinement replaces evaluation.</p>
-</div>
-<div class="deriv">
-<h5>Derivation — mask to operator</h5>
-\[\boxed{\;\mathbf{p}^{k+1}_i=\sum_j a_{\,i-2j}\,\mathbf{p}^k_j\;}\]
-<p>The \(-2j\) encodes the scale change. Splitting by the parity of \(i\) gives two
-independent <strong>stencils</strong>:</p>
-\[\text{even (vertex):}\ \mathbf{p}^{k+1}_{2i}=\sum_j a_{2j}\mathbf{p}^k_{i-j}
-\qquad \text{odd (edge):}\ \mathbf{p}^{k+1}_{2i+1}=\sum_j a_{2j+1}\mathbf{p}^k_{i-j}\]
-<p><strong>Convergence:</strong> \(a(1)=2\) and \(a(-1)=0\) — equivalently the even
-coefficients sum to 1 <em>and</em> the odd ones sum to 1. Each stencil is separately a
-partition of unity, so affine invariance and convex-hull containment hold at <em>every</em>
-level, not just in the limit.</p>
-</div>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 3 — symbol, mask, stencils</span>
- <span class="ctl">degree p <input type="range" id="f3p" min="1" max="5" value="3">
- <b class="val" id="f3pv">3</b></span>
- </div>
- <canvas id="f3"></canvas>
- <div class="readout" id="f3out"></div>
- <figcaption>Left: \(N_p\) in white, drawn as the sum of its own half-scale copies weighted by
- the mask (<b style="color:#e86a92">even k</b>, <b style="color:#4fd6c4">odd k</b>) — that is
- the two-scale relation, plotted. Right: the mask split by parity. Degree 2 is Chaikin;
- degree 3 is split-and-smooth, which becomes Catmull–Clark.</figcaption>
-</figure>
-
-<div class="col">
-<div class="deriv e">
-<h5>Degree 2 — Chaikin, 1974</h5>
-<p>\(a(z)=\tfrac14(1+z)^3\), mask \(\tfrac14[1,3,3,1]\). Parity split:</p>
-\[\mathbf{q}_i=\tfrac34\mathbf{p}_i+\tfrac14\mathbf{p}_{i+1},\qquad
-\mathbf{r}_i=\tfrac14\mathbf{p}_i+\tfrac34\mathbf{p}_{i+1}\]
-<p>Two new points per edge at \(\tfrac14,\tfrac34\); old vertices <em>discarded</em>. Chaikin
-invented it as pure <strong>corner cutting</strong>, no spline in mind — Riesenfeld later
-proved the limit is the uniform quadratic B-spline. The idea traces to de Rham in the
-1940s.</p>
-</div>
-<div class="deriv v">
-<h5>Degree 3 — split and smooth</h5>
-<p>\(a(z)=\tfrac18(1+z)^4\), mask \(\tfrac18[1,4,6,4,1]\). Parity split:</p>
-\[\text{odd}=\tfrac18[4,4]=[\tfrac12,\tfrac12],\qquad
-\text{even}=\tfrac18[1,6,1]=[\tfrac18,\tfrac34,\tfrac18]\]
-<p>As an algorithm: <strong>insert every edge midpoint, then relax every old vertex by
-\(\tfrac18,\tfrac34,\tfrac18\)</strong>. That relaxation weight is the direct ancestor of every
-surface vertex rule on this page.</p>
-</div>
-<div class="note"><h5>Lane–Riesenfeld, 1980</h5>
-<p>The general degree-\(p\) scheme is one midpoint-doubling pass then \(p\) passes of pairwise
-averaging — convolving with \(\tfrac12[1,1]\) raises the degree by one each time. \(O(p)\) per
-point and trivially vectorised.</p></div>
-<h4>Primal vs dual — a distinction that persists</h4>
-<div class="tbl"><table>
-<tr><th></th><th>Old vertices</th><th>New mesh sits</th><th>Curve</th><th>Surface</th></tr>
-<tr><td><strong>Dual</strong></td><td>discarded</td><td>in the <em>faces</em></td><td>Chaikin</td><td>Doo–Sabin</td></tr>
-<tr><td><strong>Primal</strong></td><td>retained, moved</td><td>on <em>vertices + edges</em></td><td>cubic</td><td>Catmull–Clark, Loop</td></tr>
-</table></div>
-<div class="deriv">
-<h5>Derivation — smoothness of the limit</h5>
-<p>Given \(a(1)=2,a(-1)=0\) we factor \(a(z)=\tfrac{1+z}{2}a_{[1]}(z)\); then \(a_{[1]}\) is the
-symbol acting on forward differences \(\Delta\mathbf{p}_i=\mathbf{p}_{i+1}-\mathbf{p}_i\).
-<strong>Dyn–Gregory–Levin:</strong> if the difference scheme is contractive,
-\(\|S_{a_{[1]}}^{L}\|_\infty<1\) for some \(L\), then \(S_a\) converges to a \(C^0\) limit;
-recursing on divided differences gives \(C^1,C^2,\dots\) For \(a(z)=(1+z)^{p+1}/2^p\) this
-returns <strong>\(C^{p-1}\)</strong>. Cubic \(\Rightarrow C^2\). Hold on to that number.</p>
-</div>
-<h4>Tensor product, and exactly where it breaks</h4>
-\[\mathbf{S}(u,v)=\sum_{i=0}^{3}\sum_{j=0}^{3}\mathbf{P}_{ij}N_3(u-i)N_3(v-j)\]
-<p>Masks tensor too — the bicubic surface masks are outer products of \(\tfrac18[1,4,6,4,1]\)
-with itself:</p>
-<div class="stencils">
- <div class="sten vS"><div class="lab">vertex 1/64</div><div class="grid">
- <div class="cell">1</div><div class="cell">6</div><div class="cell">1</div>
- <div class="cell">6</div><div class="cell hi">36</div><div class="cell">6</div>
- <div class="cell">1</div><div class="cell">6</div><div class="cell">1</div></div></div>
- <div class="sten eS"><div class="lab">edge 1/16</div><div class="grid">
- <div class="cell">1</div><div class="cell">1</div>
- <div class="cell hi">6</div><div class="cell hi">6</div>
- <div class="cell">1</div><div class="cell">1</div></div></div>
- <div class="sten fS"><div class="lab">face 1/4</div><div class="grid">
- <div class="cell hi">1</div><div class="cell hi">1</div>
- <div class="cell hi">1</div><div class="cell hi">1</div></div></div>
-</div>
-<p>Here is the problem in one sentence. <strong>Tensor product requires a rectangular parameter
-domain</strong> — the mesh must be combinatorially \(\mathbb{Z}^2\). By §00 no closed surface
-except a torus admits that. Trimming, \(G^1\) patch stitching, singular polar caps: all the
-familiar NURBS pain.</p>
-<p>But look at the masks again — they are <em>local</em>, reading only a 1-ring. So stop asking
-for a global grid. Generalise the mask to arbitrary valence and apply it everywhere. That is
-the content of the next three sections.</p>
-</div></section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>02</b> · LIFTING CHAIKIN — DOO–SABIN, 1978</span>
-<h2>The dual scheme on surfaces</h2>
-<div class="deriv f"><h5>Topological rule</h5>
-<p>One new point per face-corner. Then three families of new faces: an <strong>F-face</strong>
-per old face, an <strong>E-face</strong> per old edge (4 points), and a <strong>V-face</strong>
-per old vertex \(v\) — an \(n(v)\)-gon. Old vertices vanish.</p></div>
-<div class="deriv"><h5>Geometric rule</h5>
-<p>For an \(n\)-gon, the new point at corner \(i\) is
-\(\mathbf{p}'_i=\sum_j\alpha_{ij}\mathbf{p}_j\) with</p>
-\[\alpha_{ii}=\frac{n+5}{4n},\qquad
-\alpha_{ij}=\frac{3+2\cos\!\big(\tfrac{2\pi(i-j)}{n}\big)}{4n}\quad(i\ne j)\]
-<p><strong>Check \(n=4\):</strong> \(\alpha_{ii}=9/16\); adjacent \(=3/16\); opposite
-\(=1/16\) — exactly \(\tfrac14[3,1]\otimes\tfrac14[3,1]=\tfrac1{16}[9,3,3,1]\), the
-biquadratic mask. ✓</p></div>
-<p>All faces become quads; the limit is biquadratic B-spline on regular regions, \(C^1\) at
-EVs. The \(\cos(2\pi k/n)\) is not decoration — it is the discrete Fourier basis on the
-\(n\)-cycle, the first hint that EV analysis will be spectral. §05 makes that explicit.</p>
-</div></section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>03</b> · LIFTING CUBIC — CATMULL–CLARK, 1978</span>
-<h2>Three passes, and one very useful accident</h2>
-<p>Catmull and Clark were graduate students at Utah. Doo and Sabin, in Europe, pointed out that
-the arbitrary-topology extension applied to the cubic case too; Catmull and Clark wrote it in
-the form everyone now uses. It is the industry standard — Pixar, OpenSubdiv, Maya, Blender.</p>
-<div class="deriv f"><h5>Pass 1 — face points</h5>
-\[\boxed{\;\mathbf{f}=\frac{1}{m}\sum_{i=1}^{m}\mathbf{v}_i\;}\]</div>
-<div class="deriv e"><h5>Pass 2 — edge points</h5>
-<p>Endpoints \(\mathbf{v}_1,\mathbf{v}_2\) and the two <em>new</em> face points:</p>
-\[\boxed{\;\mathbf{e}=\tfrac14(\mathbf{v}_1+\mathbf{v}_2+\mathbf{f}_1+\mathbf{f}_2)\;}\]</div>
-<div class="deriv v"><h5>Pass 3 — vertex points</h5>
-<p>\(Q\) = average of incident new face points, \(R\) = average of incident edge
-<em>midpoints</em>:</p>
-\[\boxed{\;\mathbf{v}'=\frac{Q+2R+(n-3)\mathbf{v}}{n}\;}\]
-<p>Equivalently, and cheaper:</p>
-\[\mathbf{v}'=\frac{n-2}{n}\mathbf{v}+\frac{1}{n^2}\sum_i\mathbf{e}_i+\frac{1}{n^2}\sum_i\mathbf{f}_i\]</div>
-<p>Then reconnect: every new face is
-<span style="color:#f2b04a">face point</span> → <span style="color:#4fd6c4">edge point</span>
-→ <span style="color:#e86a92">vertex point</span> → <span style="color:#4fd6c4">edge
-point</span>. All output faces are quads. The chain \(\mathbf{f}\to\mathbf{e}\to\mathbf{v}'\) is
-why it is three passes and not one.</p>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 4 — the three passes, with masks</span>
- <div class="seg" id="f4pass">
- <button data-p="0" aria-pressed="true">cage</button>
- <button data-p="1" aria-pressed="false">+ face</button>
- <button data-p="2" aria-pressed="false">+ edge</button>
- <button data-p="3" aria-pressed="false">+ vertex</button>
- <button data-p="4" aria-pressed="false">reconnect</button></div>
- <button class="btn" id="f4reset">reset cage</button>
- </div>
- <canvas id="f4"></canvas>
- <div class="readout" id="f4out">Step through the passes, then tap or hover any coloured point to see the stencil that produced it.</div>
- <figcaption>A planar cage with a <b style="color:#e86a92">valence-5 vertex</b> at the centre.
- <b>Hover or tap a coloured point</b> — the contributing cage points light up with their
- weights and dashed lines to the result. White points are draggable.</figcaption>
-</figure>
-
-<div class="col">
-<div class="deriv">
-<h5>Derivation — why this <em>is</em> bicubic, not an approximation of it</h5>
-<p>Set \(n=4\) in a regular grid. Each incident face point is
-\(\mathbf{f}_i=\tfrac14(\mathbf{v}+\mathbf{e}_a+\mathbf{e}_b+\mathbf{d})\); summing over four
-faces (each edge-neighbour appears twice, each diagonal once):</p>
-\[Q=\tfrac1{16}\Big(4\mathbf{v}+2\textstyle\sum\mathbf{e}+\sum\mathbf{d}\Big),\qquad
-R=\tfrac12\mathbf{v}+\tfrac18\textstyle\sum\mathbf{e}\]
-<p>Substituting into \(\mathbf{v}'=\tfrac14[Q+2R+\mathbf{v}]\):</p>
-\[\mathbf{v}'=\tfrac{9}{16}\mathbf{v}+\tfrac{3}{32}\textstyle\sum\mathbf{e}+\tfrac{1}{64}\sum\mathbf{d}\]
-<p>Against the bicubic mask \(\tfrac1{64}[1,6,1;6,36,6;1,6,1]\): centre \(36/64=9/16\) ✓,
-edge-neighbour \(6/64=3/32\) ✓, diagonal \(1/64\) ✓. So <strong>on regular regions
-Catmull–Clark <em>is</em> the bicubic uniform B-spline, exactly.</strong> The generalisation
-only ever acts near an EV.</p>
-</div>
-<h4>The valence bookkeeping — the structurally important part</h4>
-<div class="tbl"><table>
-<tr><th>New vertex</th><th>Valence</th></tr>
-<tr><td><span style="color:#f2b04a">face point</span> of an \(m\)-gon</td><td>\(m\)</td></tr>
-<tr><td><span style="color:#4fd6c4">edge point</span></td><td><strong>always 4</strong></td></tr>
-<tr><td><span style="color:#e86a92">vertex point</span> from \(\mathbf{v}\)</td><td>\(n(\mathbf{v})\) — <strong>preserved</strong></td></tr>
-</table></div>
-<ol>
-<li>After one step <strong>all faces are quads</strong>, so from step 2 on all face points have valence 4.</li>
-<li>The EV set is <strong>frozen after step 1</strong>. No new EVs are ever created.</li>
-<li>Each step halves edge lengths, so <strong>EVs become progressively isolated</strong>.</li>
-</ol>
-<p>Point 3 is the payoff. Once EVs are more than two rings apart, every remaining region has a
-\(4\times4\) net in rectangular topology — a genuine bicubic patch.</p>
-<div class="note"><h5>The limit surface</h5>
-<p>Catmull–Clark converges to a surface that is <strong>piecewise bicubic and \(C^2\)
-everywhere, except at finitely many isolated points where it is \(C^1\)</strong>. Those points
-are exactly the EVs of the original cage, and there are never more of them than you started
-with.</p></div>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 5 — Catmull–Clark in 3D</span>
- <div class="seg" id="f5shape">
- <button data-s="cube" aria-pressed="true">Cube</button>
- <button data-s="lshape" aria-pressed="false">L-block</button>
- <button data-s="pent" aria-pressed="false">Pentagon prism</button>
- <button data-s="tet" aria-pressed="false">Tetrahedron</button></div>
- <span class="ctl">level <input type="range" id="f5lvl" min="0" max="4" value="0">
- <b class="val" id="f5lvlv">0</b></span>
- <label class="chk ctl"><input type="checkbox" id="f5ev" checked> mark EVs</label>
- <label class="chk ctl"><input type="checkbox" id="f5cage"> show cage</label>
- </div>
- <canvas id="f5"></canvas>
- <div class="readout" id="f5out"></div>
- <figcaption>Drag to rotate. Watch the EV count: it jumps once at level 1 and then
- <b>never changes again</b>, exactly as the bookkeeping predicts. The surface also pulls
- inward — see the shrinkage note in §04.</figcaption>
-</figure>
-</section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>04</b> · TRIANGLES — LOOP, 1987</span>
-<h2>Where 5/8 and 3/8 come from</h2>
-<p>Charles Loop's Utah master's thesis. Triangles only, regular valence 6, topologically the
-1-to-4 split.</p>
-<div class="deriv e"><h5>Edge (odd) rule</h5>
-<p>\(\mathbf{v}_3,\mathbf{v}_4\) are the opposite vertices of the two adjacent triangles:</p>
-\[\boxed{\;\mathbf{e}=\tfrac38(\mathbf{v}_1+\mathbf{v}_2)+\tfrac18(\mathbf{v}_3+\mathbf{v}_4)\;}\]</div>
-<div class="deriv v"><h5>Vertex (even) rule</h5>
-\[\boxed{\;\mathbf{v}'=(1-n\beta)\mathbf{v}+\beta\sum_{i=1}^{n}\mathbf{v}_i\;}\qquad
-\beta(n)=\frac{1}{n}\left[\frac58-\left(\frac38+\frac14\cos\frac{2\pi}{n}\right)^{\!2}\right]\]
-<p>Warren's substitute: \(\beta=\tfrac{3}{8n}\) for \(n>3\), \(\beta=\tfrac{3}{16}\) for
-\(n=3\). <strong>Checks.</strong> At \(n=6\):
-\(\tfrac16[\tfrac58-(\tfrac12)^2]=\tfrac1{16}\), and \(\tfrac{3}{48}=\tfrac1{16}\) ✓. At
-\(n=3\): \(\tfrac13[\tfrac58-\tfrac1{16}]=\tfrac{3}{16}\) ✓. They agree at \(n=3,6\) and differ
-elsewhere — at \(n=4\), \(31/256\) versus \(3/32\).</p></div>
-<p>The limit is the <strong>quartic box spline</strong> on regular regions — \(C^2\) there,
-\(C^1\) at EVs.</p>
-<h4>Approximating vs interpolating — "why does it shrink?"</h4>
-<p>Every mask above is non-negative and sums to 1, so each new point lies in the convex hull of
-its stencil, and the limit surface lies in the convex hull of the cage. It pulls
-<em>inward</em>, most visibly at sharp corners and in concave regions. These are
-<strong>approximating</strong> schemes: control points are not on the surface.</p>
-<p><strong>Interpolating</strong> schemes — the 4-point rule, Butterfly — fix the old points and
-use negative weights:</p>
-\[\mathbf{p}^{k+1}_{2i+1}=-\tfrac1{16}\mathbf{p}_{i-1}+\tfrac9{16}\mathbf{p}_i
-+\tfrac9{16}\mathbf{p}_{i+1}-\tfrac1{16}\mathbf{p}_{i+2}\]
-<p>The negative weights buy interpolation but lose the convex-hull property, with worse fairness
-and less stable behaviour. Production accepts the shrinkage.</p>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 6 — Loop, and the shrinkage</span>
- <div class="seg" id="f6shape">
- <button data-s="tet" aria-pressed="true">Tetrahedron</button>
- <button data-s="icosa" aria-pressed="false">Icosahedron</button>
- <button data-s="octa" aria-pressed="false">Octahedron</button></div>
- <span class="ctl">level <input type="range" id="f6lvl" min="0" max="4" value="0">
- <b class="val" id="f6lvlv">0</b></span>
- <label class="chk ctl"><input type="checkbox" id="f6cage" checked> show cage</label>
- </div>
- <canvas id="f6"></canvas>
- <div class="readout" id="f6out"></div>
- <figcaption>The dashed grey wireframe is the level-0 cage. The shaded surface sits strictly
- <em>inside</em> it — the convex-hull property made visible, and the reason approximating
- schemes never interpolate their control points.</figcaption>
-</figure>
-
-<div class="col">
-<h4>The three schemes side by side</h4>
-<div class="tbl"><table>
-<tr><th></th><th>Doo–Sabin</th><th>Catmull–Clark</th><th>Loop</th></tr>
-<tr><td>Primal / dual</td><td>dual</td><td>primal</td><td>primal</td></tr>
-<tr><td>Input faces</td><td>any</td><td>any</td><td>triangles only</td></tr>
-<tr><td>Output faces</td><td>quads</td><td>quads</td><td>triangles</td></tr>
-<tr><td>Regular limit</td><td>biquadratic</td><td><strong>bicubic B-spline</strong></td><td>quartic box spline</td></tr>
-<tr><td>Continuity</td><td>\(C^1\)</td><td>\(C^2\), \(C^1\) at EV</td><td>\(C^2\), \(C^1\) at EV</td></tr>
-<tr><td>Regular valence</td><td>4</td><td>4</td><td>6</td></tr>
-</table></div>
-<p>Catmull–Clark won industrially for the \(C^2\) and, more importantly, for the clean
-bicubic-patch correspondence that makes §06 possible.</p>
-</div></section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>05</b> · WHAT HAPPENS AT AN EV</span>
-<h2>The local subdivision matrix</h2>
-<p>This is the part the lecture defers to "next quarter". It is the mathematical core, and it is
-entirely linear algebra.</p>
-<div class="deriv"><h5>Setup</h5>
-<p>Near an EV of valence \(n\), subdivision is a linear map on a finite neighbourhood. Collect
-the 1-ring control points — centre, \(n\) edge-neighbours, \(n\) face-diagonals — into
-\(\mathbf{P}^k\in\mathbb{R}^{2n+1}\) per coordinate:</p>
-\[\boxed{\;\mathbf{P}^{k+1}=A\mathbf{P}^{k}\;}\qquad\Longrightarrow\qquad\mathbf{P}^{k}=A^{k}\mathbf{P}^{0}\]
-<p>\(A\) is the <strong>local subdivision matrix</strong>. Everything about the surface at the EV
-is in its spectrum.</p></div>
-<div class="deriv"><h5>(a) \(\lambda_0=1\) always</h5>
-<p>Every row of \(A\) sums to 1 — partition of unity again — so \(A\mathbf{1}=\mathbf{1}\). That
-is affine invariance. Convergence forces \(|\lambda_1|<1\).</p></div>
-<div class="deriv"><h5>(b) Limit position — jump straight to the surface</h5>
-<p>With right eigenvectors \(\mathbf{x}_i\) and left eigenvectors \(\boldsymbol{\ell}_i\),
-\(A^k\to\mathbf{x}_0\boldsymbol{\ell}_0^{\top}\), so
-\(\mathbf{v}^{\infty}=\boldsymbol{\ell}_0^{\top}\mathbf{P}^0\). For Catmull–Clark
-(Halstead–Kass–DeRose 1993) this is the <strong>limit-point mask</strong></p>
-\[\boxed{\;\mathbf{v}^{\infty}=\frac{n^{2}\mathbf{v}+4\sum_i\mathbf{e}_i+\sum_i\mathbf{f}_i}{n(n+5)}\;}\]
-<p><strong>Check \(n=4\):</strong> \(\tfrac1{36}(16\mathbf{v}+4\sum\mathbf{e}+\sum\mathbf{f})\),
-matching \(\tfrac16[1,4,1]\otimes\tfrac16[1,4,1]\) ✓. One dot product and you are on the
-surface — no iteration.</p></div>
-<div class="deriv"><h5>(c) Tangent plane from the subdominant pair</h5>
-<p>Expanding \(\mathbf{P}^0=\sum_ic_i\mathbf{x}_i\),</p>
-\[\mathbf{P}^k=c_0\mathbf{x}_0+\lambda_1^kc_1\mathbf{x}_1+\lambda_2^kc_2\mathbf{x}_2+O(|\lambda_3|^k)\]
-<p>so the <strong>tangent plane is spanned by \(\boldsymbol{\ell}_1^{\top}\mathbf{P}^0\) and
-\(\boldsymbol{\ell}_2^{\top}\mathbf{P}^0\)</strong> — the two tangent masks; the normal is their
-cross product. Exact normals at an EV, no differencing.</p></div>
-<div class="deriv"><h5>(d) The \(C^1\) conditions</h5>
-<p><em>Necessary:</em> \(1>\lambda_1=\lambda_2>|\lambda_3|\), \(\lambda_1\) real with a
-two-dimensional eigenspace. <em>Sufficient</em> (Reif 1995): the <strong>characteristic
-map</strong> \(\Phi:\mathbb{R}^2\to\mathbb{R}^2\) built from
-\(\boldsymbol{\ell}_1,\boldsymbol{\ell}_2\) must be regular and injective — a finite check per
-valence, tabulated numerically once. For Catmull–Clark, with \(c=\cos(2\pi/n)\):</p>
-\[\lambda(n)=\tfrac{1}{16}\left(c+5+\sqrt{(c+9)(c+1)}\right)\]
-<p>At \(n=4\): \(\tfrac1{16}(5+3)=\tfrac12\) ✓ — the bicubic contraction rate. At \(n=3\):
-\(\approx0.4101\).</p></div>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 7 — subdominant eigenvalue vs valence</span>
- <span class="ctl">valence n <input type="range" id="f7n" min="3" max="12" value="4">
- <b class="val" id="f7nv">4</b></span>
- </div>
- <canvas id="f7"></canvas>
- <div class="readout" id="f7out"></div>
- <figcaption>\(\lambda(n)\) drifts away from the regular value \(\tfrac12\) in both directions.
- Since \(\lambda\) sets the contraction rate, and \(C^2\) needs a specific relation between
- \(\lambda^2\) and the next eigenvalue group, <b>high-valence vertices genuinely degrade the
- surface</b>. "Keep valences near 4" is a statement about eigenvalues, not taste.</figcaption>
-</figure>
-
-<div class="col">
-<div class="note"><h5>Curvature — where Catmull–Clark actually fails</h5>
-<p>\(C^2\) at an EV needs conditions relating \(\lambda_1^2\) to the next eigenvalue group, and
-Catmull–Clark generically <strong>fails</strong> them: curvature there is unbounded or zero.
-Tuned schemes (Sabin; Prautzsch–Umlauf; Karčiauskas–Peters) fix it at some other cost. For aero
-surfaces this is not academic — curvature-discontinuous points show up in reflection lines and
-pressure-gradient artefacts.</p></div>
-<h4>Exact evaluation — Stam, 1998</h4>
-<ol>
-<li>Map \((u,v)\) into a dyadic annulus — find the \(k\) after which it lies in a regular
-sub-patch.</li>
-<li>Apply \(A^k=X\Lambda^kX^{-1}\), so \(\Lambda^k\) is just scalar powers. \(O(1)\) in
-\(k\).</li>
-<li>Evaluate the resulting bicubic patch directly.</li>
-</ol>
-<p><strong>Exact positions and exact first and second derivatives at arbitrary \((u,v)\), in
-closed form.</strong> This is what turns a subdivision surface from a refinement process into
-an honest parametric surface.</p>
-</div></section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>06</b> · BACK TO PATCHES</span>
-<h2>B-spline to Bézier, and the 2008 shortcut</h2>
-<div class="deriv"><h5>Uniform bicubic B-spline → Bézier</h5>
-\[M=\frac16\begin{bmatrix}1&4&1&0\\0&4&2&0\\0&2&4&0\\0&1&4&1\end{bmatrix},\qquad B=MPM^{\!\top}\]
-<p>\(B\) is the Bézier net of the identical patch. Every regular Catmull–Clark region converts
-to a bicubic Bézier patch <em>exactly</em> — which is how subdivision surfaces enter GPU
-tessellation and interoperate with NURBS CAD.</p></div>
-<div class="note"><h5>Loop & Schaefer, TOG 2008</h5>
-<p>For EV-adjacent patches, drop exact evaluation: one bicubic patch for position plus two
-separately-constructed tangent fields for normals. \(C^0\) in position with small controlled
-error, but \(G^1\) in the normal field — so it <em>looks</em> right. Uniform patch
-representation, no eigen-machinery, no branching. This is what OpenSubdiv's feature-adaptive GPU
-path is built on, and the construction to reach for if you want closed-form
-\(\partial\mathbf{S}/\partial u\) across a whole surface.</p></div>
-</div></section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>07</b> · THE DATA STRUCTURE FROM HELL</span>
-<h2>Half-edges, and two permutations</h2>
-<p>The three passes need these in \(O(1)\) or \(O(\text{valence})\):</p>
-<div class="tbl"><table>
-<tr><th>Query</th><th>Needed by</th></tr>
-<tr><td>face → its vertices, in order</td><td><span style="color:#f2b04a">face points</span></td></tr>
-<tr><td><strong>edge → its two incident faces</strong></td><td><span style="color:#4fd6c4">edge points</span></td></tr>
-<tr><td><strong>vertex → its ordered 1-ring</strong></td><td><span style="color:#e86a92">vertex points</span></td></tr>
-</table></div>
-<p>An indexed face list gives you the first and neither of the others without a full scan. Hence
-the nickname.</p>
-<p>The "split edge" is what is now called the <strong>half-edge</strong> (also <em>dart</em>, or
-<em>directed edge</em>). Split every undirected edge into two opposite directed halves:</p>
-<pre>struct HalfEdge {
- origin: VertexId, <span style="color:#9aa5ad">// vertex it emanates from</span>
- twin: HalfEdgeId, <span style="color:#9aa5ad">// the opposite half-edge</span>
- next: HalfEdgeId, <span style="color:#9aa5ad">// next half-edge round this face (CCW)</span>
- face: FaceId, <span style="color:#9aa5ad">// the face on its left</span>
-}</pre>
-<p>Each vertex stores one outgoing half-edge; each face stores one of its half-edges.</p>
-<div class="deriv"><h5>The algebraic view — a combinatorial map</h5>
-<p>Two permutations on the set \(H\) of half-edges, \(|H|=2E\): the twin map \(\alpha\), an
-<strong>involution</strong> with \(\alpha^2=\mathrm{id}\) and no fixed points; and the next map
-\(\nu\). The entire topology is their orbits:</p>
-\[\text{faces}=\text{orbits of }\nu,\qquad\text{vertices}=\text{orbits of }\nu\circ\alpha,
-\qquad\text{edges}=\text{orbits of }\alpha\]
-<p>and \(V-E+F=2-2g\) recovers the genus. Two permutations; that is the whole structure.</p></div>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 8 — traversal</span>
- <div class="seg" id="f8mode">
- <button data-m="face" aria-pressed="true">face loop · ν</button>
- <button data-m="ring" aria-pressed="false">1-ring · ν∘α</button>
- <button data-m="twin" aria-pressed="false">twins · α</button></div>
- <button class="btn" id="f8step">step</button>
- <button class="btn" id="f8play">auto</button>
- </div>
- <canvas id="f8"></canvas>
- <div class="readout" id="f8out"></div>
- <figcaption>Each undirected edge is two opposed arrows. <b>Face loop</b> follows
- <code>next</code> until it closes. <b>1-ring</b> alternates <code>twin</code> then
- <code>next</code> — the "alternate twin and next" move, at \(O(\text{valence})\).</figcaption>
-</figure>
-
-<div class="col">
-<h4>Invariants</h4>
-\[\alpha(\alpha(h))=h,\quad \nu\text{-orbits}=\text{faces},\quad
-\mathrm{face}(\nu(h))=\mathrm{face}(h),\quad \mathrm{origin}(\alpha(h))=\mathrm{origin}(\nu(h))\]
-<p>Every traversal assumes all four. During construction they are <em>transiently violated</em> —
-which is exactly the segfault complaint. Not a soft warning: a half-built half-edge mesh either
-loops forever or dereferences garbage.</p>
-<p><strong>Restrictions.</strong> Manifold only — an edge with three faces has no well-defined
-twin. Boundaries need a null-face sentinel or a virtual boundary loop; the latter keeps the
-invariants total, and is what I would use.</p>
-<div class="note"><h5>Rust, specifically</h5>
-<p>Do not model this with references. The pointer graph is cyclic, so <code>&</code> /
-<code>&mut</code> is unrepresentable without <code>Rc<RefCell<_>></code> — which
-costs runtime borrow panics, cache locality, and <code>Send</code>.</p>
-<p><strong>Use an arena with integer handles:</strong> <code>Vec<HalfEdge></code> plus
-<code>u32</code> indices. Contiguous, cache-friendly, trivially <code>Send + Sync</code>. Then
-refinement is a pure <code>Mesh -> Mesh</code>, the hierarchy is a
-<code>Vec<Mesh></code>, and each level parallelises directly with rayon since all three
-passes are read-only on level \(k\) and write-only to level \(k{+}1\).</p>
-<p>Two refinements worth taking: store half-edges in pairs so <code>twin(h) = h ^ 1</code>
-(saves a field and an indirection), and use a slotmap with generational indices if you need
-handles stable under edge collapse. Alternatives: <em>directed edges</em>
-(Campagna–Kobbelt–Seidel 1998) for triangle-only meshes at roughly half the memory;
-<em>quad-edge</em> if you want the dual for free.</p></div>
-</div></section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>08</b> · A DIFFERENT ANIMAL</span>
-<h2>The geodesic sphere</h2>
-<p>Do not conflate this with the above. The problem is: how do you tessellate a sphere? Not with
-patches — a UV sphere degenerates at the poles and pinches textures; rational patches
-distort.</p>
-<ol>
-<li>Start with a regular <strong>icosahedron</strong>: \(V{=}12,E{=}30,F{=}20\), every vertex
-valence 5. Euler: \(12-30+20=2\) ✓. The closed triangle mesh with the most uniform possible
-vertex distribution.</li>
-<li><strong>1-to-4 split</strong> each triangle — the same topological operation as Loop.</li>
-<li><strong>Project</strong> radially: \(\mathbf{p}\mapsto r\mathbf{p}/\|\mathbf{p}\|\).</li>
-<li>Repeat. After \(k\) steps, \(F=20\cdot4^k\) and \(V=10\cdot4^k+2\).</li>
-</ol>
-<div class="note"><h5>The distinction that matters</h5>
-<p>Radial projection is <strong>interpolating</strong> and exact against the analytic sphere at
-every level. Running <strong>Loop</strong> on the same icosahedron is
-<strong>approximating</strong>: it shrinks inward and converges to a smooth blob that is
-<em>not</em> a sphere. Same combinatorial split, completely different geometry. Toggle below and
-watch the radius readout.</p></div>
-</div>
-
-<figure>
- <div class="figbar">
- <span class="ftitle">Fig 9 — project vs subdivide</span>
- <div class="seg" id="f9mode">
- <button data-m="project" aria-pressed="true">radial project</button>
- <button data-m="loop" aria-pressed="false">Loop subdivide</button></div>
- <span class="ctl">level <input type="range" id="f9lvl" min="0" max="4" value="2">
- <b class="val" id="f9lvlv">2</b></span>
- </div>
- <canvas id="f9"></canvas>
- <div class="readout" id="f9out"></div>
- <figcaption>Same 1-to-4 split in both cases. <b>Radial project</b> holds
- \(\|\mathbf{p}\|=1\) exactly. <b>Loop</b> lets the radius collapse — the min/max readout shows
- it drifting well below 1 and staying non-constant.</figcaption>
-</figure>
-</section>
-
-<hr>
-
-<section><div class="col">
-<span class="snum"><b>09</b> · DIFFERENTIATING THROUGH IT</span>
-<h2>What is smooth in the parameters, and what is not</h2>
-<div class="deriv"><h5>Positions are linear in the control points</h5>
-\[\mathbf{S}^{\infty}=L\,A^{k}\,\mathbf{P}^{0}\qquad\Longrightarrow\qquad
-\frac{\partial\mathbf{S}^{\infty}}{\partial\mathbf{P}^{0}}=L\,A^{k}\]
-<p>with \(L\) the limit/evaluation operator. This Jacobian is <strong>exact, sparse and
-constant</strong> — no approximation, no finite differencing. Autodiff through \(k\) rounds is
-\(k\) sparse matmuls with a known transpose; reverse mode is cheap, and you can regenerate the
-sparsity pattern rather than store it.</p></div>
-<p><strong>Surface derivatives</strong> come from §05 (Stam, exact) or §06 (Loop–Schaefer,
-closed-form per patch). Either gives differentiable normals — what you need for pressure
-integration or any normal-dependent objective.</p>
-<div class="note"><h5>The non-differentiable boundary is topology</h5>
-<p>Valence, connectivity and the EV set are discrete. You can freely optimise vertex positions
-with gradients; you cannot differentiate through an edge collapse, a valence change or a
-remesh. Same wall as differentiable iso-surface extraction: the geometry is smooth in the
-parameters, the combinatorics is not. Standard practice — fix the cage topology, optimise
-positions only, treat remeshing as a discrete outer step.</p></div>
-<p><strong>Sharp features.</strong> Production Catmull–Clark adds a per-edge crease sharpness
-\(\sigma\in[0,\infty)\), blending between the smooth rule and the piecewise-linear rule.
-Semi-sharp creases are \(C^0\) along the crease, and \(\sigma\) is a <em>continuous</em>
-parameter — so it is differentiable, useful if you ever want to optimise feature-line strength
-directly.</p>
-</div></section>
-
-<div class="col end">
-<div style="color:#9aa5ad;letter-spacing:.14em">READING ORDER</div>
-<div style="margin-top:12px">
-1 Joy, <em>On-Line Geometric Modeling Notes</em> — Subdivision Curves → Surfaces → Doo-Sabin → Catmull-Clark → Loop<br>
-2 Zorin & Schröder, <em>Subdivision for Modeling and Animation</em>, SIGGRAPH '99 — Ch. 4 for schemes, analysis chapter for §05<br>
-3 Stam 1998, <em>Exact Evaluation of Catmull-Clark Subdivision Surfaces at Arbitrary Parameter Values</em><br>
-4 Loop & Schaefer 2008, TOG 27(1) — the one for your purposes<br>
-5 Reif 1995 — the \(C^1\) sufficiency proof<br>
-6 Peters & Reif, <em>Subdivision Surfaces</em>, Springer 2008 — the monograph
-</div>
-</div>
-
-</div>
-
-<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
-<script>
-const C={ground:'#0d1216',chalk:'#e8e6df',dim:'#9aa5ad',cage:'#6b7c8c',
- face:'#f2b04a',edge:'#4fd6c4',vert:'#e86a92'};
-const REDUCED=matchMedia('(prefers-reduced-motion: reduce)').matches;
-
-function setup(cv,ar){
- cv._ar=ar; const ctx=cv.getContext('2d');
- function fit(){
- const r=cv.getBoundingClientRect(), dpr=Math.min(devicePixelRatio||1,2);
- const w=Math.max(1,r.width), h=w*ar;
- cv.width=Math.round(w*dpr); cv.height=Math.round(h*dpr);
- cv.style.height=h+'px'; ctx.setTransform(dpr,0,0,dpr,0,0);
- return {w,h};
- }
- return {ctx,fit};
-}
-function seg(id,cb){
- const el=document.getElementById(id);
- el.addEventListener('click',e=>{
- const b=e.target.closest('button'); if(!b)return;
- [...el.querySelectorAll('button')].forEach(x=>x.setAttribute('aria-pressed',x===b));
- cb(b.dataset);
- });
-}
-function slider(id,vid,cb){
- const s=document.getElementById(id), v=document.getElementById(vid);
- s.addEventListener('input',()=>{v.textContent=s.value; cb(+s.value);});
-}
-const V={
- add:(a,b)=>[a[0]+b[0],a[1]+b[1],(a[2]||0)+(b[2]||0)],
- sub:(a,b)=>[a[0]-b[0],a[1]-b[1],(a[2]||0)-(b[2]||0)],
- mul:(a,s)=>[a[0]*s,a[1]*s,(a[2]||0)*s],
- cross:(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]],
- dot:(a,b)=>a[0]*b[0]+a[1]*b[1]+a[2]*b[2],
- len:a=>Math.hypot(a[0],a[1],a[2]||0),
- norm:a=>{const l=V.len(a)||1;return [a[0]/l,a[1]/l,(a[2]||0)/l];},
- avg:ps=>V.mul(ps.reduce((a,p)=>V.add(a,p),[0,0,0]),1/ps.length)
-};
-function edgeTable(faces){
- const m=new Map();
- faces.forEach((f,fi)=>{for(let i=0;i<f.length;i++){
- const a=f[i],b=f[(i+1)%f.length],lo=Math.min(a,b),hi=Math.max(a,b),k=lo+','+hi;
- if(!m.has(k))m.set(k,{a:lo,b:hi,faces:[]});
- m.get(k).faces.push(fi);}});
- return m;
-}
-function valences(verts,faces){
- const val=new Array(verts.length).fill(0);
- edgeTable(faces).forEach(e=>{val[e.a]++;val[e.b]++;});
- return val;
-}
-
-/* ---------- FIG 1 ---------- */
-(function(){
- const cv=document.getElementById('f1'),{ctx,fit}=setup(cv,.49);
- const out=document.getElementById('f1out');
- let scheme='chaikin',lvl=2,limit=true,drag=-1,W=900,H=440;
- let P=[[.10,.72],[.22,.20],[.44,.80],[.60,.22],[.78,.70],[.92,.32]];
- function refine(p){
- const n=p.length,o=[];
- if(scheme==='chaikin'){
- for(let i=0;i<n-1;i++){const a=p[i],b=p[i+1];
- o.push([.75*a[0]+.25*b[0],.75*a[1]+.25*b[1]]);
- o.push([.25*a[0]+.75*b[0],.25*a[1]+.75*b[1]]);}
- }else{
- o.push(p[0]);
- for(let i=0;i<n-1;i++){const a=p[i],b=p[i+1];
- o.push([(a[0]+b[0])/2,(a[1]+b[1])/2]);
- if(i<n-2){const c=p[i+2];
- o.push([(a[0]+6*b[0]+c[0])/8,(a[1]+6*b[1]+c[1])/8]);}}
- o.push(p[n-1]);
- }
- return o;
- }
- const chain=k=>{let p=P;for(let i=0;i<k;i++)p=refine(p);return p;};
- const X=p=>p[0]*W,Y=p=>p[1]*H;
- function poly(pts,col,lw,dash){
- ctx.save();ctx.strokeStyle=col;ctx.lineWidth=lw;ctx.setLineDash(dash||[]);
- ctx.lineJoin='round';ctx.beginPath();
- pts.forEach((p,i)=>i?ctx.lineTo(X(p),Y(p)):ctx.moveTo(X(p),Y(p)));
- ctx.stroke();ctx.restore();
- }
- function draw(){
- const d=fit();W=d.w;H=d.h;ctx.clearRect(0,0,W,H);
- ctx.strokeStyle='rgba(107,124,140,.08)';ctx.lineWidth=1;
- for(let i=1;i<10;i++){ctx.beginPath();ctx.moveTo(W*i/10,0);ctx.lineTo(W*i/10,H);ctx.stroke();}
- for(let i=1;i<5;i++){ctx.beginPath();ctx.moveTo(0,H*i/5);ctx.lineTo(W,H*i/5);ctx.stroke();}
- if(limit)poly(chain(7),'rgba(232,230,223,.28)',3.4);
- poly(P,C.cage,1.4,[4,4]);
- const cur=chain(lvl),col=scheme==='chaikin'?C.edge:C.vert;
- poly(cur,col,2.2);
- if(lvl<=4){ctx.fillStyle=col;cur.forEach(p=>{ctx.beginPath();ctx.arc(X(p),Y(p),2.7,0,7);ctx.fill();});}
- P.forEach((p,i)=>{ctx.beginPath();ctx.arc(X(p),Y(p),i===drag?8:6.4,0,7);
- ctx.fillStyle=C.chalk;ctx.fill();ctx.lineWidth=2;ctx.strokeStyle=C.ground;ctx.stroke();});
- out.innerHTML='mask <b>'+(scheme==='chaikin'?'¼[1,3,3,1]':'⅛[1,4,6,4,1]')+'</b> · '
- +(scheme==='chaikin'?'dual — old vertices discarded':'primal — old vertices retained')
- +' · level <b>'+lvl+'</b>: '+P.length+' → <b>'+cur.length+'</b> points · limit is the uniform <b>'
- +(scheme==='chaikin'?'quadratic':'cubic')+'</b> B-spline, <b>C'
- +(scheme==='chaikin'?'¹':'²')+'</b>';
- }
- const pos=e=>{const r=cv.getBoundingClientRect();
- return [(e.clientX-r.left)/r.width,(e.clientY-r.top)/(r.width*.49)];};
- cv.addEventListener('pointerdown',e=>{const m=pos(e);let b=-1,bd=9;
- P.forEach((p,i)=>{const d=Math.hypot(p[0]-m[0],(p[1]-m[1])*.5);if(d<bd){bd=d;b=i;}});
- if(bd<.05){drag=b;cv.setPointerCapture(e.pointerId);draw();}});
- cv.addEventListener('pointermove',e=>{if(drag<0)return;const m=pos(e);
- P[drag]=[Math.max(.03,Math.min(.97,m[0])),Math.max(.05,Math.min(.95,m[1]))];draw();});
- cv.addEventListener('pointerup',()=>{drag=-1;draw();});
- seg('f1scheme',d=>{scheme=d.s;draw();});
- slider('f1lvl','f1lvlv',v=>{lvl=v;draw();});
- document.getElementById('f1limit').onchange=e=>{limit=e.target.checked;draw();};
- new ResizeObserver(draw).observe(cv);draw();
-})();
-
-/* ---------- 3D engine ---------- */
-function make3D(id,ar){
- const cv=document.getElementById(id),{ctx,fit}=setup(cv,ar);
- let yaw=.6,pitch=-.42,dn=false,lx=0,ly=0,spin=!REDUCED,W=900,H=400,model=null,o={};
- function rot(p){let[x,y,z]=p;
- let c=Math.cos(yaw),s=Math.sin(yaw);[x,z]=[x*c-z*s,x*s+z*c];
- c=Math.cos(pitch);s=Math.sin(pitch);[y,z]=[y*c-z*s,y*s+z*c];return[x,y,z];}
- function pj(p,sc,cx,cy){const d=4.4,k=d*sc/(d-p[2]);return[cx+p[0]*k,cy-p[1]*k,p[2]];}
- function render(){
- const d=fit();W=d.w;H=d.h;ctx.clearRect(0,0,W,H);if(!model)return;
- const cx=W/2,cy=H/2,sc=Math.min(W,H)*.40;
- const RV=model.verts.map(rot),PV=RV.map(p=>pj(p,sc,cx,cy));
- const L=V.norm([.4,.75,1]),tint=o.tint||[112,158,172];
- model.faces.map((f,fi)=>({f,z:f.reduce((a,i)=>a+RV[i][2],0)/f.length}))
- .sort((a,b)=>a.z-b.z).forEach(t=>{
- const f=t.f,a=RV[f[0]],b=RV[f[1]],c=RV[f[2]];
- const nr=V.norm(V.cross(V.sub(b,a),V.sub(c,a)));
- if(nr[2]<-.02)return;
- const sh=.11+.66*Math.pow(Math.max(0,V.dot(nr,L)),.85);
- ctx.beginPath();
- f.forEach((i,k)=>{const p=PV[i];k?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]);});
- ctx.closePath();
- ctx.fillStyle='rgb('+Math.round(tint[0]*sh)+','+Math.round(tint[1]*sh)+','+Math.round(tint[2]*sh)+')';
- ctx.fill();ctx.strokeStyle='rgba(232,230,223,.13)';ctx.lineWidth=.8;ctx.stroke();});
- if(o.cage){
- const cg=o.cage.verts.map(p=>pj(rot(p),sc,cx,cy));
- ctx.save();ctx.strokeStyle='rgba(107,124,140,.6)';ctx.lineWidth=1.1;ctx.setLineDash([3,3]);
- o.cage.faces.forEach(f=>{ctx.beginPath();
- f.forEach((i,k)=>{const p=cg[i];k?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]);});
- ctx.closePath();ctx.stroke();});
- ctx.restore();
- }
- if(o.marks)o.marks.forEach(m=>{const p=PV[m.i];
- if(!p||RV[m.i][2]<-.55)return;
- ctx.beginPath();ctx.arc(p[0],p[1],m.r||4.2,0,7);ctx.fillStyle=m.c;ctx.fill();});
- }
- cv.addEventListener('pointerdown',e=>{dn=true;spin=false;lx=e.clientX;ly=e.clientY;
- cv.setPointerCapture(e.pointerId);});
- cv.addEventListener('pointermove',e=>{if(!dn)return;
- yaw+=(e.clientX-lx)*.008;pitch+=(e.clientY-ly)*.008;
- pitch=Math.max(-1.35,Math.min(1.35,pitch));lx=e.clientX;ly=e.clientY;render();});
- cv.addEventListener('pointerup',()=>{dn=false;});
- new ResizeObserver(render).observe(cv);
- if(!REDUCED)(function l(){if(spin){yaw+=.0032;render();}requestAnimationFrame(l);})();
- return {set:(m,op)=>{model=m;o=op||{};render();}};
-}
-
-/* ---------- base meshes ---------- */
-const cubeMesh=()=>({verts:[[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]]
- .map(p=>V.mul(p,.72)),faces:[[0,3,2,1],[4,5,6,7],[0,1,5,4],[2,3,7,6],[1,2,6,5],[0,4,7,3]]});
-function lshapeMesh(){
- const s=.52,v=[[0,0,0],[2,0,0],[2,1,0],[1,1,0],[1,2,0],[0,2,0],
- [0,0,1],[2,0,1],[2,1,1],[1,1,1],[1,2,1],[0,2,1]]
- .map(p=>V.mul([p[0]-1,p[1]-1,p[2]-.5],s));
- return {verts:v,faces:[[0,5,4,3],[3,2,1,0],[6,7,8,9],[9,10,11,6],
- [0,1,7,6],[1,2,8,7],[2,3,9,8],[3,4,10,9],[4,5,11,10],[5,0,6,11]]};
-}
-function pentPrism(){
- const v=[],f=[],n=5,r=.72,h=.6;
- for(let i=0;i<n;i++){const a=i/n*Math.PI*2;
- v.push([r*Math.cos(a),-h,r*Math.sin(a)]);v.push([r*Math.cos(a),h,r*Math.sin(a)]);}
- const bot=[],top=[];
- for(let i=0;i<n;i++){bot.push(2*i);top.push(2*(n-1-i)+1);}
- f.push(bot.slice().reverse());f.push(top.slice().reverse());
- for(let i=0;i<n;i++){const j=(i+1)%n;f.push([2*i,2*j,2*j+1,2*i+1]);}
- return {verts:v,faces:f};
-}
-const tetraMesh=()=>({verts:[[1,1,1],[-1,-1,1],[-1,1,-1],[1,-1,-1]].map(p=>V.mul(V.norm(p),.92)),
- faces:[[0,1,2],[0,3,1],[0,2,3],[1,3,2]]});
-const octaMesh=()=>({verts:[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]].map(p=>V.mul(p,.88)),
- faces:[[0,2,4],[2,1,4],[1,3,4],[3,0,4],[2,0,5],[1,2,5],[3,1,5],[0,3,5]]});
-function icosaMesh(r){
- r=r||.86;const t=(1+Math.sqrt(5))/2,v=[];
- [[-1,t,0],[1,t,0],[-1,-t,0],[1,-t,0],[0,-1,t],[0,1,t],[0,-1,-t],[0,1,-t],
- [t,0,-1],[t,0,1],[-t,0,-1],[-t,0,1]].forEach(p=>v.push(V.mul(V.norm(p),r)));
- return {verts:v,faces:[[0,11,5],[0,5,1],[0,1,7],[0,7,10],[0,10,11],[1,5,9],[5,11,4],
- [11,10,2],[10,7,6],[7,1,8],[3,9,4],[3,4,2],[3,2,6],[3,6,8],[3,8,9],
- [4,9,5],[2,4,11],[6,2,10],[8,6,7],[9,8,1]]};
-}
-function torusMesh(){
- const R=.66,r=.27,nu=12,nv=8,v=[],f=[];
- for(let i=0;i<nu;i++)for(let j=0;j<nv;j++){
- const a=i/nu*Math.PI*2,b=j/nv*Math.PI*2;
- v.push([(R+r*Math.cos(b))*Math.cos(a),r*Math.sin(b),(R+r*Math.cos(b))*Math.sin(a)]);}
- const id=(i,j)=>(((i%nu)+nu)%nu)*nv+(((j%nv)+nv)%nv);
- for(let i=0;i<nu;i++)for(let j=0;j<nv;j++)
- f.push([id(i,j),id(i+1,j),id(i+1,j+1),id(i,j+1)]);
- return {verts:v,faces:f};
-}
-
-/* ---------- Catmull–Clark ---------- */
-function catmullClark(mesh){
- const {verts,faces}=mesh;
- const fp=faces.map(f=>V.avg(f.map(i=>verts[i])));
- const et=edgeTable(faces),keys=[...et.keys()];
- const eI=new Map(keys.map((k,i)=>[k,i]));
- const ep=keys.map(k=>{const e=et.get(k);
- return e.faces.length===2
- ? V.mul(V.add(V.add(verts[e.a],verts[e.b]),V.add(fp[e.faces[0]],fp[e.faces[1]])),.25)
- : V.mul(V.add(verts[e.a],verts[e.b]),.5);});
- const vf=verts.map(()=>[]),ve=verts.map(()=>[]);
- faces.forEach((f,fi)=>f.forEach(i=>vf[i].push(fi)));
- keys.forEach((k,i)=>{const e=et.get(k);ve[e.a].push(i);ve[e.b].push(i);});
- const nv=verts.map((v,i)=>{
- const n=ve[i].length;if(!n||!vf[i].length)return v;
- const F=V.avg(vf[i].map(fi=>fp[fi]));
- const R=V.avg(ve[i].map(ei=>{const e=et.get(keys[ei]);
- return V.mul(V.add(verts[e.a],verts[e.b]),.5);}));
- return V.add(V.add(V.mul(F,1/n),V.mul(R,2/n)),V.mul(v,(n-3)/n));});
- const oF=verts.length,oE=verts.length+faces.length;
- const outV=[...nv,...fp,...ep],outF=[];
- faces.forEach((f,fi)=>{const m=f.length;
- for(let i=0;i<m;i++){
- const vi=f[i],pv=f[(i-1+m)%m],nx=f[(i+1)%m];
- const e1=eI.get(Math.min(vi,nx)+','+Math.max(vi,nx));
- const e0=eI.get(Math.min(pv,vi)+','+Math.max(pv,vi));
- outF.push([vi,oE+e1,oF+fi,oE+e0]);}});
- return {verts:outV,faces:outF};
-}
-
-/* ---------- Loop ---------- */
-function loopSub(mesh){
- const {verts,faces}=mesh;
- const et=edgeTable(faces),keys=[...et.keys()];
- const eI=new Map(keys.map((k,i)=>[k,i]));
- const opp=keys.map(()=>[]);
- faces.forEach(f=>{for(let i=0;i<3;i++){
- const a=f[i],b=f[(i+1)%3],c=f[(i+2)%3];
- opp[eI.get(Math.min(a,b)+','+Math.max(a,b))].push(c);}});
- const ep=keys.map((k,i)=>{const e=et.get(k),o=opp[i];
- return o.length===2
- ? V.add(V.mul(V.add(verts[e.a],verts[e.b]),3/8),V.mul(V.add(verts[o[0]],verts[o[1]]),1/8))
- : V.mul(V.add(verts[e.a],verts[e.b]),.5);});
- const ring=verts.map(()=>new Set());
- et.forEach(e=>{ring[e.a].add(e.b);ring[e.b].add(e.a);});
- const nv=verts.map((v,i)=>{
- const nb=[...ring[i]],n=nb.length;if(!n)return v;
- const c=Math.cos(2*Math.PI/n);
- const beta=(5/8-Math.pow(3/8+c/4,2))/n;
- const s=nb.reduce((a,j)=>V.add(a,verts[j]),[0,0,0]);
- return V.add(V.mul(v,1-n*beta),V.mul(s,beta));});
- const oE=verts.length,outV=[...nv,...ep],outF=[];
- faces.forEach(f=>{const[a,b,c]=f;
- const ab=oE+eI.get(Math.min(a,b)+','+Math.max(a,b));
- const bc=oE+eI.get(Math.min(b,c)+','+Math.max(b,c));
- const ca=oE+eI.get(Math.min(c,a)+','+Math.max(c,a));
- outF.push([a,ab,ca],[b,bc,ab],[c,ca,bc],[ab,bc,ca]);});
- return {verts:outV,faces:outF};
-}
-function splitProject(mesh,r){
- const {verts,faces}=mesh;
- const et=edgeTable(faces),keys=[...et.keys()];
- const eI=new Map(keys.map((k,i)=>[k,i]));
- const ep=keys.map(k=>{const e=et.get(k);
- return V.mul(V.norm(V.mul(V.add(verts[e.a],verts[e.b]),.5)),r);});
- const oE=verts.length,outV=[...verts,...ep],outF=[];
- faces.forEach(f=>{const[a,b,c]=f;
- const ab=oE+eI.get(Math.min(a,b)+','+Math.max(a,b));
- const bc=oE+eI.get(Math.min(b,c)+','+Math.max(b,c));
- const ca=oE+eI.get(Math.min(c,a)+','+Math.max(c,a));
- outF.push([a,ab,ca],[b,bc,ab],[c,ca,bc],[ab,bc,ca]);});
- return {verts:outV,faces:outF};
-}
-
-/* ---------- FIG 2 ---------- */
-(function(){
- const R=make3D('f2',.44),out=document.getElementById('f2out');
- const build={cube:cubeMesh,torus:torusMesh,lshape:lshapeMesh};
- let shape='cube';
- function go(){
- const m=build[shape](),val=valences(m.verts,m.faces);
- const marks=val.map((n,i)=>({i,c:n===4?C.cage:C.vert,r:n===4?3.4:5.2}));
- const ev=val.filter(n=>n!==4).length;
- const E=[...edgeTable(m.faces).keys()].length;
- const chi=m.verts.length-E+m.faces.length;
- R.set(m,{marks,tint:[104,150,164]});
- out.innerHTML='V=<b>'+m.verts.length+'</b> E=<b>'+E+'</b> F=<b>'+m.faces.length
- +'</b> · χ = V−E+F = <b>'+chi+'</b> → genus <b>'+(2-chi)/2
- +'</b> · extraordinary vertices: <b style="color:'+(ev?C.vert:C.edge)+'">'+ev+'</b>'
- +(ev===0?' — only the torus gets away with this':'');
- }
- seg('f2shape',d=>{shape=d.s;go();});go();
-})();
-
-/* ---------- FIG 3 ---------- */
-(function(){
- const cv=document.getElementById('f3'),{ctx,fit}=setup(cv,.333);
- const out=document.getElementById('f3out');let p=3,W=900,H=300;
- function N(i,k,x){
- if(k===0)return (x>=i&&x<i+1)?1:0;
- return (x-i)/k*N(i,k-1,x)+(i+k+1-x)/k*N(i+1,k-1,x);
- }
- const bs=(d,t)=>N(0,d,t);
- function binom(n,k){let r=1;for(let i=0;i<k;i++)r=r*(n-i)/(i+1);return r;}
- function draw(){
- const d=fit();W=d.w;H=d.h;ctx.clearRect(0,0,W,H);
- const mask=[];for(let k=0;k<=p+1;k++)mask.push(binom(p+1,k)/Math.pow(2,p));
- const pad=30,lw=W*.55,lh=H-2*pad-14,x0=pad,y0=pad;
- const sup=p+1,span=sup*1.35;
- const px=t=>x0+t/span*lw, maxN=bs(p,sup/2)||1, py=v=>y0+lh-v/(maxN*1.14)*lh;
- ctx.strokeStyle='rgba(107,124,140,.25)';ctx.lineWidth=1;
- ctx.beginPath();ctx.moveTo(x0,y0+lh);ctx.lineTo(x0+lw,y0+lh);ctx.stroke();
- mask.forEach((a,k)=>{ctx.beginPath();let st=true;
- for(let t=0;t<=span;t+=.01){const v=a*bs(p,2*t-k);
- if(v<=0&&st)continue;const Xc=px(t),Yc=py(v);
- st?(ctx.moveTo(Xc,Yc),st=false):ctx.lineTo(Xc,Yc);}
- ctx.strokeStyle=k%2===0?'rgba(232,106,146,.6)':'rgba(79,214,196,.6)';
- ctx.lineWidth=1.4;ctx.stroke();});
- ctx.beginPath();
- for(let t=0;t<=span;t+=.006){const v=bs(p,t);
- t===0?ctx.moveTo(px(t),py(v)):ctx.lineTo(px(t),py(v));}
- ctx.strokeStyle=C.chalk;ctx.lineWidth=2.5;ctx.stroke();
- ctx.font='500 11px "IBM Plex Mono",monospace';
- ctx.fillStyle=C.chalk;ctx.fillText('N'+p+' = Σ of the copies below',x0+6,y0+12);
- ctx.fillStyle='rgba(232,106,146,.95)';ctx.fillText('even k',x0+6,y0+29);
- ctx.fillStyle='rgba(79,214,196,.95)';ctx.fillText('odd k',x0+62,y0+29);
- const rx=W*.63,rw=W-rx-pad,mx=Math.max(...mask);
- const bw=Math.min(28,rw/(mask.length+.8));
- mask.forEach((a,k)=>{
- const h=a/mx*(lh-24),Xc=rx+k*(bw+4),Yc=y0+lh-h;
- ctx.fillStyle=k%2===0?'rgba(232,106,146,.8)':'rgba(79,214,196,.8)';
- ctx.fillRect(Xc,Yc,bw,h);
- ctx.fillStyle=C.dim;ctx.font='500 9px "IBM Plex Mono",monospace';
- ctx.fillText(Math.round(a*Math.pow(2,p)),Xc+bw/2-3,Yc-5);
- ctx.fillText('a'+k,Xc+bw/2-6,y0+lh+13);});
- ctx.strokeStyle='rgba(107,124,140,.25)';
- ctx.beginPath();ctx.moveTo(rx-4,y0+lh);ctx.lineTo(rx+rw,y0+lh);ctx.stroke();
- const ev=mask.filter((_,k)=>k%2===0).reduce((a,b)=>a+b,0);
- const od=mask.filter((_,k)=>k%2===1).reduce((a,b)=>a+b,0);
- const fr=mask.map(a=>Math.round(a*Math.pow(2,p)));
- out.innerHTML='a(z) = (1+z)<sup>'+(p+1)+'</sup>/2<sup>'+p+'</sup> · mask <b>1/'
- +Math.pow(2,p)+'·['+fr.join(',')+']</b> · Σeven = <b>'+ev.toFixed(3)
- +'</b>, Σodd = <b>'+od.toFixed(3)+'</b> (both must be 1) · limit <b>C'
- +(p-1)+'</b>'+(p===2?' ← Chaikin':p===3?' ← split & smooth → Catmull–Clark':'');
- }
- slider('f3p','f3pv',v=>{p=v;draw();});
- new ResizeObserver(draw).observe(cv);draw();
-})();
-
-/* ---------- FIG 4 ---------- */
-(function(){
- const cv=document.getElementById('f4'),{ctx,fit}=setup(cv,.60);
- const out=document.getElementById('f4out');
- let pass=0,W=900,H=540,drag=-1,hov=null;
- function baseCage(){
- const cx=.5,cy=.5,R=.135,verts=[[cx,cy]];
- for(let i=0;i<5;i++){const a=-Math.PI/2+i*2*Math.PI/5;
- verts.push([cx+R*1.45*Math.cos(a),cy+R*Math.sin(a)]);}
- for(let i=0;i<5;i++){const a=-Math.PI/2+(i+.5)*2*Math.PI/5;
- verts.push([cx+R*2.5*1.45*Math.cos(a),cy+R*2.5*Math.sin(a)]);}
- const faces=[];
- for(let i=0;i<5;i++){const j=(i+1)%5;faces.push([0,1+i,6+i,1+j]);}
- return {verts,faces};
- }
- let cage=baseCage();
- function compute(){
- const {verts,faces}=cage;
- const fp=faces.map(f=>{const s=f.reduce((a,i)=>[a[0]+verts[i][0],a[1]+verts[i][1]],[0,0]);
- return [s[0]/f.length,s[1]/f.length];});
- const et=edgeTable(faces),keys=[...et.keys()];
- const ep=keys.map(k=>{const e=et.get(k);
- if(e.faces.length===2)return [(verts[e.a][0]+verts[e.b][0]+fp[e.faces[0]][0]+fp[e.faces[1]][0])/4,
- (verts[e.a][1]+verts[e.b][1]+fp[e.faces[0]][1]+fp[e.faces[1]][1])/4];
- return [(verts[e.a][0]+verts[e.b][0])/2,(verts[e.a][1]+verts[e.b][1])/2];});
- const vf=verts.map(()=>[]),ve=verts.map(()=>[]);
- faces.forEach((f,fi)=>f.forEach(i=>vf[i].push(fi)));
- keys.forEach((k,i)=>{const e=et.get(k);ve[e.a].push(i);ve[e.b].push(i);});
- const vp=verts.map((v,i)=>{
- const n=ve[i].length;if(!n||!vf[i].length)return v.slice();
- const F=vf[i].reduce((a,fi)=>[a[0]+fp[fi][0],a[1]+fp[fi][1]],[0,0]).map(x=>x/vf[i].length);
- const Rr=ve[i].reduce((a,ei)=>{const e=et.get(keys[ei]);
- return [a[0]+(verts[e.a][0]+verts[e.b][0])/2,a[1]+(verts[e.a][1]+verts[e.b][1])/2];},[0,0])
- .map(x=>x/ve[i].length);
- return [(F[0]+2*Rr[0]+(n-3)*v[0])/n,(F[1]+2*Rr[1]+(n-3)*v[1])/n];});
- return {fp,ep,vp,keys,et,vf,ve};
- }
- const X=p=>p[0]*W,Y=p=>p[1]*H;
- function draw(){
- const d=fit();W=d.w;H=d.h;ctx.clearRect(0,0,W,H);
- const {fp,ep,vp,keys,et,vf,ve}=compute(),{verts,faces}=cage;
- ctx.strokeStyle='rgba(107,124,140,.5)';ctx.lineWidth=1.3;
- et.forEach(e=>{ctx.beginPath();ctx.moveTo(X(verts[e.a]),Y(verts[e.a]));
- ctx.lineTo(X(verts[e.b]),Y(verts[e.b]));ctx.stroke();});
- if(pass>=4){
- faces.forEach((f,fi)=>{const m=f.length;
- for(let i=0;i<m;i++){
- const vi=f[i],pv=f[(i-1+m)%m],nx=f[(i+1)%m];
- const e1=keys.indexOf(Math.min(vi,nx)+','+Math.max(vi,nx));
- const e0=keys.indexOf(Math.min(pv,vi)+','+Math.max(pv,vi));
- const q=[vp[vi],ep[e1],fp[fi],ep[e0]];
- ctx.beginPath();q.forEach((p,k)=>k?ctx.lineTo(X(p),Y(p)):ctx.moveTo(X(p),Y(p)));
- ctx.closePath();ctx.fillStyle='rgba(232,230,223,.05)';ctx.fill();
- ctx.strokeStyle='rgba(232,230,223,.45)';ctx.lineWidth=1.1;ctx.stroke();}});
- }
- if(hov){
- let cs=[],tgt,col;
- if(hov.t==='f'){col=C.face;tgt=fp[hov.i];
- cs=faces[hov.i].map(i=>({p:verts[i],w:'1/'+faces[hov.i].length}));}
- if(hov.t==='e'){col=C.edge;tgt=ep[hov.i];const e=et.get(keys[hov.i]);
- cs=[{p:verts[e.a],w:'¼'},{p:verts[e.b],w:'¼'}];
- e.faces.forEach(fi=>cs.push({p:fp[fi],w:'¼'}));}
- if(hov.t==='v'){col=C.vert;tgt=vp[hov.i];
- cs=[{p:verts[hov.i],w:'(n−2)/n'}];
- vf[hov.i].forEach(fi=>cs.push({p:fp[fi],w:'1/n²'}));
- ve[hov.i].forEach(ei=>{const e=et.get(keys[ei]);
- cs.push({p:verts[e.a===hov.i?e.b:e.a],w:'1/n²'});});}
- ctx.save();ctx.font='600 10px "IBM Plex Mono",monospace';
- cs.forEach(c=>{
- ctx.globalAlpha=.45;ctx.strokeStyle=col;ctx.lineWidth=1.4;ctx.setLineDash([3,3]);
- ctx.beginPath();ctx.moveTo(X(c.p),Y(c.p));ctx.lineTo(X(tgt),Y(tgt));ctx.stroke();
- ctx.setLineDash([]);ctx.globalAlpha=1;
- ctx.beginPath();ctx.arc(X(c.p),Y(c.p),9,0,7);ctx.lineWidth=2;ctx.stroke();
- ctx.fillStyle=col;ctx.fillText(c.w,X(c.p)+12,Y(c.p)-9);});
- ctx.restore();
- }
- const dot=(p,c,r)=>{ctx.beginPath();ctx.arc(X(p),Y(p),r,0,7);ctx.fillStyle=c;ctx.fill();};
- if(pass>=1)fp.forEach((p,i)=>dot(p,C.face,hov&&hov.t==='f'&&hov.i===i?7:4.8));
- if(pass>=2)ep.forEach((p,i)=>dot(p,C.edge,hov&&hov.t==='e'&&hov.i===i?7:4.3));
- if(pass>=3)vp.forEach((p,i)=>{if(vf[i].length)dot(p,C.vert,hov&&hov.t==='v'&&hov.i===i?7:4.8);});
- verts.forEach((p,i)=>{ctx.beginPath();ctx.arc(X(p),Y(p),i===drag?6.6:5.2,0,7);
- ctx.fillStyle='rgba(232,230,223,.92)';ctx.fill();
- ctx.lineWidth=1.6;ctx.strokeStyle=C.ground;ctx.stroke();});
- if(!hov){
- const L=['cage only — 11 points, 5 quads, one valence-5 centre',
- 'face points: <b style="color:'+C.face+'">f = (1/m)·Σvᵢ</b> — one per face',
- 'edge points: <b style="color:'+C.edge+'">e = ¼(v₁+v₂+f₁+f₂)</b> — needs pass 1, so this is pass 2',
- 'vertex points: <b style="color:'+C.vert+'">v′ = (Q+2R+(n−3)v)/n</b> — needs both, so pass 3',
- 'reconnect: each quad is <b style="color:'+C.vert+'">v′</b> → <b style="color:'+C.edge+'">e</b> → <b style="color:'+C.face+'">f</b> → <b style="color:'+C.edge+'">e</b>'];
- out.innerHTML=L[pass]+(pass>0?' · hover or tap a coloured point for its stencil':'');
- }
- }
- const pos=e=>{const r=cv.getBoundingClientRect();
- return [(e.clientX-r.left)/r.width,(e.clientY-r.top)/(r.width*.60)];};
- function pick(m){
- const {fp,ep,vp,vf}=compute(),c=[];
- if(pass>=1)fp.forEach((p,i)=>c.push({t:'f',i,p}));
- if(pass>=2)ep.forEach((p,i)=>c.push({t:'e',i,p}));
- if(pass>=3)vp.forEach((p,i)=>{if(vf[i].length)c.push({t:'v',i,p});});
- let b=null,bd=9;
- c.forEach(x=>{const d=Math.hypot(x.p[0]-m[0],(x.p[1]-m[1])*.6);if(d<bd){bd=d;b=x;}});
- return bd<.024?b:null;
- }
- function mask(h){
- const {ve,et,keys}=compute();
- if(h.t==='f'){const m=cage.faces[h.i].length;
- out.innerHTML='<b style="color:'+C.face+'">face point</b> of a '+m+'-gon · f = (1/'+m
- +')·Σvᵢ · new valence <b>'+m+'</b>'
- +(m!==4?' → <b style="color:'+C.vert+'">becomes an EV</b>':' → regular');}
- if(h.t==='e'){const e=et.get(keys[h.i]);
- out.innerHTML='<b style="color:'+C.edge+'">edge point</b> · '
- +(e.faces.length<2?'boundary → ½(v₁+v₂)':'e = ¼(v₁ + v₂ + f₁ + f₂)')
- +' · new valence <b>4 always</b> → never an EV';}
- if(h.t==='v'){const n=ve[h.i].length;
- out.innerHTML='<b style="color:'+C.vert+'">vertex point</b>, n = <b>'+n
- +'</b> · v′ = (n−2)/n·v + (1/n²)Σeᵢ + (1/n²)Σfᵢ · centre weight <b>'
- +((n-2)/n).toFixed(3)+'</b> · valence <b>preserved</b>'
- +(n!==4?' → <b style="color:'+C.vert+'">stays an EV forever</b>':'');}
- }
- cv.addEventListener('pointerdown',e=>{
- const m=pos(e),h=pick(m);
- if(h){hov=h;drag=-1;mask(h);draw();return;}
- let b=-1,bd=9;
- cage.verts.forEach((p,i)=>{const d=Math.hypot(p[0]-m[0],(p[1]-m[1])*.6);if(d<bd){bd=d;b=i;}});
- if(bd<.03){drag=b;hov=null;cv.setPointerCapture(e.pointerId);draw();}});
- cv.addEventListener('pointermove',e=>{
- const m=pos(e);
- if(drag>=0){cage.verts[drag]=[Math.max(.03,Math.min(.97,m[0])),
- Math.max(.04,Math.min(.96,m[1]))];draw();return;}
- if(e.pointerType==='mouse'){const h=pick(m);
- const ch=(h&&!hov)||(!h&&hov)||(h&&hov&&(h.t!==hov.t||h.i!==hov.i));
- if(ch){hov=h;if(h)mask(h);draw();}}});
- cv.addEventListener('pointerup',()=>{drag=-1;});
- cv.addEventListener('pointerleave',()=>{if(hov){hov=null;draw();}});
- seg('f4pass',d=>{pass=+d.p;hov=null;draw();});
- document.getElementById('f4reset').onclick=()=>{cage=baseCage();hov=null;draw();};
- new ResizeObserver(draw).observe(cv);draw();
-})();
-
-/* ---------- FIG 5 ---------- */
-(function(){
- const R=make3D('f5',.522),out=document.getElementById('f5out');
- const build={cube:cubeMesh,lshape:lshapeMesh,pent:pentPrism,tet:tetraMesh};
- let shape='cube',lvl=0,mk=true,cg=false;
- function go(){
- const base=build[shape]();let m=base;
- for(let i=0;i<lvl;i++)m=catmullClark(m);
- const val=valences(m.verts,m.faces);
- const ev=val.map((n,i)=>n!==4?i:-1).filter(i=>i>=0);
- R.set(m,{marks:mk?ev.map(i=>({i,c:C.vert,r:4.4})):null,cage:cg?base:null,tint:[112,158,172]});
- const E=[...edgeTable(m.faces).keys()].length;
- out.innerHTML='level <b>'+lvl+'</b> · V=<b>'+m.verts.length+'</b> E=<b>'+E
- +'</b> F=<b>'+m.faces.length+'</b> · extraordinary vertices <b style="color:'+C.vert+'">'
- +ev.length+'</b>'+(lvl>=1?' — frozen from level 1 on':' — about to freeze')
- +' · all quads: <b>'+(m.faces.every(f=>f.length===4)?'yes':'no')+'</b>';
- }
- seg('f5shape',d=>{shape=d.s;go();});
- slider('f5lvl','f5lvlv',v=>{lvl=v;go();});
- document.getElementById('f5ev').onchange=e=>{mk=e.target.checked;go();};
- document.getElementById('f5cage').onchange=e=>{cg=e.target.checked;go();};
- go();
-})();
-
-/* ---------- FIG 6 ---------- */
-(function(){
- const R=make3D('f6',.50),out=document.getElementById('f6out');
- const build={tet:tetraMesh,icosa:()=>icosaMesh(.86),octa:octaMesh};
- let shape='tet',lvl=0,cg=true;
- function go(){
- const base=build[shape]();let m=base;
- for(let i=0;i<lvl;i++)m=loopSub(m);
- const val=valences(m.verts,m.faces),ev=val.filter(n=>n!==6).length;
- const rB=Math.max(...base.verts.map(V.len)),rN=Math.max(...m.verts.map(V.len));
- R.set(m,{cage:cg?base:null,tint:[150,140,175]});
- out.innerHTML='level <b>'+lvl+'</b> · V=<b>'+m.verts.length+'</b> F=<b>'+m.faces.length
- +'</b> · irregular (n≠6) <b style="color:'+C.vert+'">'+ev+'</b> · max radius <b>'
- +rN.toFixed(3)+'</b> vs cage <b>'+rB.toFixed(3)+'</b> → shrunk to <b>'
- +(100*rN/rB).toFixed(1)+'%</b> (convex hull property)';
- }
- seg('f6shape',d=>{shape=d.s;go();});
- slider('f6lvl','f6lvlv',v=>{lvl=v;go();});
- document.getElementById('f6cage').onchange=e=>{cg=e.target.checked;go();};
- go();
-})();
-
-/* ---------- FIG 7 ---------- */
-(function(){
- const cv=document.getElementById('f7'),{ctx,fit}=setup(cv,.366);
- const out=document.getElementById('f7out');let n=4,W=900,H=330;
- const lam=k=>{const c=Math.cos(2*Math.PI/k);return (c+5+Math.sqrt((c+9)*(c+1)))/16;};
- function draw(){
- const d=fit();W=d.w;H=d.h;ctx.clearRect(0,0,W,H);
- const pl=52,pr=22,pt=24,pb=38,gw=W-pl-pr,gh=H-pt-pb;
- const n0=3,n1=12,y0=.35,y1=.62;
- const px=k=>pl+(k-n0)/(n1-n0)*gw, py=v=>pt+gh-(v-y0)/(y1-y0)*gh;
- ctx.font='500 10px "IBM Plex Mono",monospace';
- for(let v=.35;v<=.621;v+=.05){
- ctx.strokeStyle='rgba(107,124,140,.13)';ctx.lineWidth=1;
- ctx.beginPath();ctx.moveTo(pl,py(v));ctx.lineTo(pl+gw,py(v));ctx.stroke();
- ctx.fillStyle=C.dim;ctx.fillText(v.toFixed(2),12,py(v)+3);}
- ctx.strokeStyle='rgba(79,214,196,.55)';ctx.lineWidth=1.4;ctx.setLineDash([5,4]);
- ctx.beginPath();ctx.moveTo(pl,py(.5));ctx.lineTo(pl+gw,py(.5));ctx.stroke();
- ctx.setLineDash([]);ctx.fillStyle=C.edge;
- ctx.fillText('λ = ½ regular, n = 4',pl+8,py(.5)-7);
- ctx.beginPath();
- for(let k=n0;k<=n1;k+=.02){k===n0?ctx.moveTo(px(k),py(lam(k))):ctx.lineTo(px(k),py(lam(k)));}
- ctx.strokeStyle=C.chalk;ctx.lineWidth=2.2;ctx.stroke();
- for(let k=n0;k<=n1;k++){
- ctx.beginPath();ctx.arc(px(k),py(lam(k)),k===n?7:3.6,0,7);
- ctx.fillStyle=k===n?C.vert:(k===4?C.edge:'rgba(232,230,223,.55)');ctx.fill();
- ctx.fillStyle=C.dim;ctx.fillText(k,px(k)-3,pt+gh+16);}
- ctx.fillText('valence n',pl+gw/2-22,pt+gh+31);
- const l=lam(n);
- out.innerHTML='n = <b>'+n+'</b> · λ = <b>'+l.toFixed(5)+'</b> · deviation from ½ <b style="color:'
- +(Math.abs(l-.5)<1e-9?C.edge:C.vert)+'">'+(l>=.5?'+':'')+(l-.5).toFixed(5)
- +'</b> · contraction per level ≈ <b>'+(l*100).toFixed(1)+'%</b>'
- +(n===4?' — exactly the bicubic rate':n===3?' — fastest contraction, flattest limit'
- :' — slower contraction, curvature degrades');
- }
- slider('f7n','f7nv',v=>{n=v;draw();});
- new ResizeObserver(draw).observe(cv);draw();
-})();
-
-/* ---------- FIG 8 ---------- */
-(function(){
- const cv=document.getElementById('f8'),{ctx,fit}=setup(cv,.467);
- const out=document.getElementById('f8out');
- let W=900,H=420,mode='face',k=0,timer=null;
- const P=[[.24,.22],[.50,.17],[.76,.24],[.20,.50],[.50,.50],[.80,.50],
- [.24,.80],[.50,.85],[.76,.78]];
- const F=[[0,3,4,1],[1,4,5,2],[3,6,7,4],[4,7,8,5]];
- const HE=[],hm=new Map();
- F.forEach((f,fi)=>{const s=HE.length;
- f.forEach((v,i)=>{HE.push({o:v,d:f[(i+1)%f.length],face:fi,next:s+((i+1)%f.length),twin:-1});
- hm.set(v+'>'+f[(i+1)%f.length],HE.length-1);});});
- HE.forEach(h=>{const t=hm.get(h.d+'>'+h.o);h.twin=t===undefined?-1:t;});
- const vOut=new Map();HE.forEach((h,i)=>{if(!vOut.has(h.o))vOut.set(h.o,i);});
- const X=p=>p[0]*W,Y=p=>p[1]*H;
- const sFace=()=>{const s=0,o=[s];let h=HE[s].next;while(h!==s){o.push(h);h=HE[h].next;}return o;};
- const sRing=()=>{const s=vOut.get(4),o=[s];let h=s;
- for(let i=0;i<10;i++){const t=HE[h].twin;if(t<0)break;h=HE[t].next;if(h===s)break;o.push(h);}
- return o;};
- const sTwin=()=>{const o=[];HE.forEach((h,i)=>{if(h.twin>i)o.push(i);});return o;};
- function arrow(a,b,col,lw){
- const dx=b[0]-a[0],dy=b[1]-a[1],L=Math.hypot(dx,dy),ux=dx/L,uy=dy/L;
- const ox=-uy*.012,oy=ux*.012,s=.19;
- const A=[a[0]+ux*L*s+ox,a[1]+uy*L*s+oy],B=[b[0]-ux*L*s+ox,b[1]-uy*L*s+oy];
- ctx.strokeStyle=col;ctx.lineWidth=lw;ctx.lineCap='round';
- ctx.beginPath();ctx.moveTo(X(A),Y(A));ctx.lineTo(X(B),Y(B));ctx.stroke();
- const hx=X(B),hy=Y(B),an=Math.atan2(Y(B)-Y(A),X(B)-X(A)),s2=8;
- ctx.beginPath();ctx.moveTo(hx,hy);
- ctx.lineTo(hx-s2*Math.cos(an-.42),hy-s2*Math.sin(an-.42));
- ctx.lineTo(hx-s2*Math.cos(an+.42),hy-s2*Math.sin(an+.42));
- ctx.closePath();ctx.fillStyle=col;ctx.fill();
- }
- function draw(){
- const d=fit();W=d.w;H=d.h;ctx.clearRect(0,0,W,H);
- F.forEach(f=>{ctx.beginPath();
- f.forEach((i,j)=>{const p=P[i];j?ctx.lineTo(X(p),Y(p)):ctx.moveTo(X(p),Y(p));});
- ctx.closePath();ctx.fillStyle='rgba(107,124,140,.07)';ctx.fill();});
- const sq=mode==='face'?sFace():mode==='ring'?sRing():sTwin();
- const cur=sq[k%sq.length];
- const col=mode==='face'?C.face:mode==='ring'?C.vert:C.edge;
- HE.forEach(h=>arrow(P[h.o],P[h.d],'rgba(107,124,140,.32)',1.2));
- sq.slice(0,(k%sq.length)+1).forEach(i=>arrow(P[HE[i].o],P[HE[i].d],col,2.4));
- const h=HE[cur];arrow(P[h.o],P[h.d],'#fff',3.2);
- if(mode==='twin'&&h.twin>=0){const t=HE[h.twin];arrow(P[t.o],P[t.d],C.edge,3.2);}
- P.forEach((p,i)=>{const c=mode==='ring'&&i===4;
- ctx.beginPath();ctx.arc(X(p),Y(p),c?7:5.4,0,7);
- ctx.fillStyle=c?C.vert:'rgba(232,230,223,.9)';ctx.fill();
- ctx.lineWidth=1.6;ctx.strokeStyle=C.ground;ctx.stroke();
- ctx.font='500 10px "IBM Plex Mono",monospace';ctx.fillStyle=C.dim;
- ctx.fillText('v'+i,X(p)+9,Y(p)-8);});
- out.innerHTML=(mode==='face'
- ?'<b>ν orbit</b> = face '+h.face+' · h = '+cur+', next = '+h.next
- :mode==='ring'
- ?'<b>ν∘α orbit</b> = 1-ring of v4 · h = '+cur+', twin = '+h.twin+', then next'
- :'<b>α involution</b> · h = '+cur+' ⟷ twin = '+h.twin)
- +' · step <b>'+((k%sq.length)+1)+'/'+sq.length+'</b> · |H| = 2E = <b>'+HE.length+'</b>';
- }
- seg('f8mode',d=>{mode=d.m;k=0;draw();});
- document.getElementById('f8step').onclick=()=>{k++;draw();};
- document.getElementById('f8play').onclick=e=>{
- if(timer){clearInterval(timer);timer=null;e.target.textContent='auto';return;}
- e.target.textContent='stop';timer=setInterval(()=>{k++;draw();},620);};
- new ResizeObserver(draw).observe(cv);draw();
-})();
-
-/* ---------- FIG 9 ---------- */
-(function(){
- const R=make3D('f9',.50),out=document.getElementById('f9out');
- let mode='project',lvl=2;
- function go(){
- let m=icosaMesh(1);
- for(let i=0;i<lvl;i++)m=mode==='project'?splitProject(m,1):loopSub(m);
- const rs=m.verts.map(V.len),rmin=Math.min(...rs),rmax=Math.max(...rs);
- R.set({verts:m.verts.map(p=>V.mul(p,.86)),faces:m.faces},
- {tint:mode==='project'?[110,170,180]:[172,140,160]});
- out.innerHTML='level <b>'+lvl+'</b> · V=<b>'+m.verts.length+'</b> F=<b>'+m.faces.length
- +'</b> (= 20·4<sup>'+lvl+'</sup>) · radius min <b>'+rmin.toFixed(4)+'</b> max <b>'
- +rmax.toFixed(4)+'</b> · '+(mode==='project'
- ?'<b style="color:'+C.edge+'">exactly on the sphere — interpolating</b>'
- :'<b style="color:'+C.vert+'">not a sphere — approximating, radius down to '
- +(rmin*100).toFixed(1)+'%</b>');
- }
- seg('f9mode',d=>{mode=d.m;go();});
- slider('f9lvl','f9lvlv',v=>{lvl=v;go();});
- go();
-})();
-
-/* ---------- math ---------- */
-function typeset(){
- if(!window.renderMathInElement)return;
- renderMathInElement(document.body,{
- delimiters:[{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],
- throwOnError:false,ignoredTags:['script','noscript','style','textarea','pre','code']
- });
-}
-if(document.readyState==='complete')typeset();
-else addEventListener('load',typeset);
-</script>
-</body>
-</html>
diff --git a/references/bookmarks.txt b/references/bookmarks.txt
@@ -9,3 +9,6 @@ https://lornajane.net/posts/2015/code-reviews-before-you-even-run-the-code
How we saved 100 terabytes of memory by optimizing 1.1.1.1’s DNS cache:
https://blog.cloudflare.com/dns-cache-memory-optimization-1111/
+
+Which Gemma 4 Variant Should Power Your MCP Agent
+https://dev.to/megberts/which-gemma-4-variant-should-power-your-mcp-agent-2nl8
diff --git a/references/books.txt b/references/books.txt
@@ -1,5 +1,13 @@
# Books
+## Mathematics
+
+Mathematics for Machine Learning (free PDF)
+https://mml-book.github.io/
+
+Online math graphing tool:
+https://www.desmos.com/
+
## TypeScript
[TypeScript Deep Dive](https://basarat.gitbook.io/typescript/future-javascript/arrow-functions) -