notes

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 780bf0fde9e02bc65ca24f5116a2d78e4039f870
parent b86298dddd9a1efe21d2501f5d7c198760d812cb
Author: ling0x <ling0x@users.noreply.github.com>
Date:   Mon,  7 Sep 2026 12:29:13 +0100

calculus

Diffstat:
Amathematics/calculus_derivatives.txt | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmathematics/subdivision-interactive.html | 1475-------------------------------------------------------------------------------
Mreferences/bookmarks.txt | 3+++
Mreferences/books.txt | 8++++++++
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&nbsp;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> &nbsp;·&nbsp; VOCABULARY &amp; 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> &nbsp;·&nbsp; 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 &nbsp;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 &nbsp;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 &nbsp;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> &nbsp;·&nbsp; 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> &nbsp;·&nbsp; 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> &nbsp;·&nbsp; 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> &nbsp;·&nbsp; 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> &nbsp;·&nbsp; 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 &amp; 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> &nbsp;·&nbsp; 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>&amp;</code> / -<code>&amp;mut</code> is unrepresentable without <code>Rc&lt;RefCell&lt;_&gt;&gt;</code> — which -costs runtime borrow panics, cache locality, and <code>Send</code>.</p> -<p><strong>Use an arena with integer handles:</strong> <code>Vec&lt;HalfEdge&gt;</code> plus -<code>u32</code> indices. Contiguous, cache-friendly, trivially <code>Send + Sync</code>. Then -refinement is a pure <code>Mesh -&gt; Mesh</code>, the hierarchy is a -<code>Vec&lt;Mesh&gt;</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> &nbsp;·&nbsp; 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> &nbsp;·&nbsp; 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 &nbsp;Joy, <em>On-Line Geometric Modeling Notes</em> — Subdivision Curves → Surfaces → Doo-Sabin → Catmull-Clark → Loop<br> -2 &nbsp;Zorin &amp; Schröder, <em>Subdivision for Modeling and Animation</em>, SIGGRAPH '99 — Ch. 4 for schemes, analysis chapter for §05<br> -3 &nbsp;Stam 1998, <em>Exact Evaluation of Catmull-Clark Subdivision Surfaces at Arbitrary Parameter Values</em><br> -4 &nbsp;Loop &amp; Schaefer 2008, TOG 27(1) — the one for your purposes<br> -5 &nbsp;Reif 1995 — the \(C^1\) sufficiency proof<br> -6 &nbsp;Peters &amp; 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 &amp; 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) -