{
const W = 400, H = 360, CX = 180, CY = 180, SCALE = 65;
const svgNS = "http://www.w3.org/2000/svg";
// Preset path configs: array of [re, im] control points defining a closed loop
const paths = {
"Loop around origin (winding = +1)": [
[1.5, 0], [1.2, 1.2], [0, 1.8], [-1.2, 1.2], [-1.5, 0],
[-1.2, -1.2], [0, -1.8], [1.2, -1.2]
],
"Loop missing origin (winding = 0)": [
[1.0, 0.8], [1.8, 0.4], [2.0, 1.2], [1.4, 1.8], [0.6, 1.4]
],
"Double loop (winding = +2)": [
[1.8, 0], [1.4, 1.0], [0, 1.8], [-1.4, 1.0], [-1.8, 0],
[-1.4, -1.0], [0, -1.8], [1.4, -1.0],
[1.2, 0.3], [0.5, 0.8], [-0.5, 0.5], [-0.8, 0], [-0.5, -0.5],
[0.5, -0.8], [1.2, -0.3]
]
};
const pathSelector = Inputs.select(Object.keys(paths), {
value: Object.keys(paths)[0],
label: "Path"
});
const branchToggle = Inputs.toggle({label: "Show branch cut", value: false});
const tSlider = Inputs.range([0, 1], {value: 0, step: 0.01, label: "Progress t"});
function catmullRomPoint(pts, t) {
const n = pts.length;
const totalT = n;
const tScaled = t * n;
const i = Math.floor(tScaled) % n;
const localT = tScaled - Math.floor(tScaled);
const p0 = pts[(i - 1 + n) % n];
const p1 = pts[i];
const p2 = pts[(i + 1) % n];
const p3 = pts[(i + 2) % n];
const t2 = localT*localT, t3 = localT*t2;
const re = 0.5*((2*p1[0]) + (-p0[0]+p2[0])*localT + (2*p0[0]-5*p1[0]+4*p2[0]-p3[0])*t2 + (-p0[0]+3*p1[0]-3*p2[0]+p3[0])*t3);
const im = 0.5*((2*p1[1]) + (-p0[1]+p2[1])*localT + (2*p0[1]-5*p1[1]+4*p2[1]-p3[1])*t2 + (-p0[1]+3*p1[1]-3*p2[1]+p3[1])*t3);
return [re, im];
}
function computeWindingAndArgTrace(pts, numSteps) {
let totalDeltaArg = 0;
let prevArg = null;
const trace = [];
for (let step = 0; step <= numSteps; step++) {
const t = step / numSteps;
const [re, im] = catmullRomPoint(pts, t);
const arg = Math.atan2(im, re);
if (prevArg !== null) {
let dArg = arg - prevArg;
while (dArg > Math.PI) dArg -= 2*Math.PI;
while (dArg < -Math.PI) dArg += 2*Math.PI;
totalDeltaArg += dArg;
}
prevArg = arg;
trace.push([t, totalDeltaArg]);
}
return trace;
}
function toScreen(re, im) {
return [CX + re*SCALE, CY - im*SCALE];
}
function render(pathName, showBranch, tVal) {
const pts = paths[pathName];
const STEPS = 300;
const argTrace = computeWindingAndArgTrace(pts, STEPS);
const windingAtEnd = argTrace[STEPS][1] / (2*Math.PI);
const windingRounded = Math.round(windingAtEnd);
// SVG
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", W); svg.setAttribute("height", H);
svg.style.cssText = "background:#fafafa; border:1px solid #e5e7eb; border-radius:6px; display:block;";
// Grid
const gridG = document.createElementNS(svgNS, "g");
gridG.setAttribute("stroke", "#e5e7eb"); gridG.setAttribute("stroke-width", "0.5");
for (let i=-3; i<=3; i++) {
const xl = document.createElementNS(svgNS,"line");
const [lx] = toScreen(i,0); const [,ly1] = toScreen(0,-3); const [,ly2] = toScreen(0,3);
xl.setAttribute("x1",lx); xl.setAttribute("y1",ly1); xl.setAttribute("x2",lx); xl.setAttribute("y2",ly2);
gridG.appendChild(xl);
const yl = document.createElementNS(svgNS,"line");
const [lx1] = toScreen(-3,0); const [lx2] = toScreen(3,0); const [,ly] = toScreen(0,i);
yl.setAttribute("x1",lx1); yl.setAttribute("y1",ly); yl.setAttribute("x2",lx2); yl.setAttribute("y2",ly);
gridG.appendChild(yl);
}
svg.appendChild(gridG);
// Axes
const [ox, oy] = toScreen(0,0);
const axG = document.createElementNS(svgNS,"g"); axG.setAttribute("stroke","#9ca3af"); axG.setAttribute("stroke-width","1");
const xa = document.createElementNS(svgNS,"line");
const [ax1] = toScreen(-3.2,0); const [ax2] = toScreen(3.2,0);
xa.setAttribute("x1",ax1); xa.setAttribute("y1",oy); xa.setAttribute("x2",ax2); xa.setAttribute("y2",oy);
const ya = document.createElementNS(svgNS,"line");
const [,ay1] = toScreen(0,-2.6); const [,ay2] = toScreen(0,2.6);
ya.setAttribute("x1",ox); ya.setAttribute("y1",ay1); ya.setAttribute("x2",ox); ya.setAttribute("y2",ay2);
axG.appendChild(xa); axG.appendChild(ya);
svg.appendChild(axG);
// Branch cut: negative real axis
if (showBranch) {
const bc = document.createElementNS(svgNS,"line");
const [bx1] = toScreen(-3.2,0); const [bx2] = toScreen(0,0);
bc.setAttribute("x1",bx1); bc.setAttribute("y1",oy); bc.setAttribute("x2",bx2); bc.setAttribute("y2",oy);
bc.setAttribute("stroke","#ef4444"); bc.setAttribute("stroke-width","2.5");
bc.setAttribute("stroke-dasharray","6,3");
svg.appendChild(bc);
const bcLabel = document.createElementNS(svgNS,"text");
const [blx] = toScreen(-2,0);
bcLabel.setAttribute("x",blx); bcLabel.setAttribute("y",oy-6);
bcLabel.setAttribute("fill","#ef4444"); bcLabel.setAttribute("font-size","10");
bcLabel.setAttribute("font-family","sans-serif"); bcLabel.textContent="branch cut";
svg.appendChild(bcLabel);
}
// Full path
let pathD = "";
for (let s=0; s<=STEPS; s++) {
const t = s/STEPS;
const [re,im] = catmullRomPoint(pts, t);
const [px,py] = toScreen(re,im);
pathD += (s===0 ? `M ${px} ${py}` : ` L ${px} ${py}`);
}
pathD += " Z";
const pathEl = document.createElementNS(svgNS,"path");
pathEl.setAttribute("d",pathD); pathEl.setAttribute("fill","none");
pathEl.setAttribute("stroke","#f97316"); pathEl.setAttribute("stroke-width","2");
svg.appendChild(pathEl);
// Animated point
const currentStep = Math.round(tVal * STEPS);
const [cre,cim] = catmullRomPoint(pts, tVal);
const [cpx,cpy] = toScreen(cre,cim);
const pt = document.createElementNS(svgNS,"circle");
pt.setAttribute("cx",cpx); pt.setAttribute("cy",cpy); pt.setAttribute("r","6");
pt.setAttribute("fill","#3b82f6"); pt.setAttribute("stroke","#fff"); pt.setAttribute("stroke-width","2");
svg.appendChild(pt);
// Origin mark
const origPt = document.createElementNS(svgNS,"circle");
origPt.setAttribute("cx",ox); origPt.setAttribute("cy",oy); origPt.setAttribute("r","4");
origPt.setAttribute("fill","#1f2937"); origPt.setAttribute("stroke","#fff"); origPt.setAttribute("stroke-width","1.5");
svg.appendChild(origPt);
const origLabel = document.createElementNS(svgNS,"text");
origLabel.setAttribute("x",ox+5); origLabel.setAttribute("y",oy-6);
origLabel.setAttribute("fill","#1f2937"); origLabel.setAttribute("font-size","10");
origLabel.setAttribute("font-family","sans-serif"); origLabel.textContent="O";
svg.appendChild(origLabel);
// Winding number display
const windBox = document.createElementNS(svgNS,"rect");
windBox.setAttribute("x",W-95); windBox.setAttribute("y",5);
windBox.setAttribute("width",88); windBox.setAttribute("height",44);
windBox.setAttribute("fill","#1e40af"); windBox.setAttribute("rx","5");
svg.appendChild(windBox);
const windLabel = document.createElementNS(svgNS,"text");
windLabel.setAttribute("x",W-51); windLabel.setAttribute("y",22);
windLabel.setAttribute("fill","white"); windLabel.setAttribute("font-size","11");
windLabel.setAttribute("text-anchor","middle"); windLabel.setAttribute("font-family","sans-serif");
windLabel.textContent = "winding #";
svg.appendChild(windLabel);
const windNum = document.createElementNS(svgNS,"text");
windNum.setAttribute("x",W-51); windNum.setAttribute("y",41);
windNum.setAttribute("fill","white"); windNum.setAttribute("font-size","18");
windNum.setAttribute("text-anchor","middle"); windNum.setAttribute("font-family","sans-serif");
windNum.setAttribute("font-weight","bold");
windNum.textContent = (tVal < 0.99) ? "..." : windingRounded.toString();
svg.appendChild(windNum);
// Arg trace mini-chart
const chartX = 0, chartY = H - 70, chartW = W, chartH = 65;
const chartBg = document.createElementNS(svgNS,"rect");
chartBg.setAttribute("x",chartX); chartBg.setAttribute("y",chartY);
chartBg.setAttribute("width",chartW); chartBg.setAttribute("height",chartH);
chartBg.setAttribute("fill","rgba(241,245,249,0.9)");
svg.appendChild(chartBg);
const maxArgAbs = Math.max(2*Math.PI*1.5, Math.abs(argTrace[STEPS][1]) * 1.2);
function argToChart(t, argVal) {
const cx = chartX + t * chartW;
const cy = chartY + chartH/2 - (argVal / maxArgAbs) * (chartH/2 - 6);
return [cx, cy];
}
// Zero line
const [zx1, zy1] = argToChart(0, 0); const [zx2] = argToChart(1, 0);
const zeroLine = document.createElementNS(svgNS,"line");
zeroLine.setAttribute("x1",zx1); zeroLine.setAttribute("y1",zy1);
zeroLine.setAttribute("x2",zx2); zeroLine.setAttribute("y2",zy1);
zeroLine.setAttribute("stroke","#9ca3af"); zeroLine.setAttribute("stroke-width","0.8");
zeroLine.setAttribute("stroke-dasharray","3,3");
svg.appendChild(zeroLine);
// Arg trace line up to current t
let traceD = "";
for (let s=0; s<=currentStep && s < argTrace.length; s++) {
const [tc, argVal] = argTrace[s];
const [cx, cy] = argToChart(tc, argVal);
traceD += (s===0 ? `M ${cx} ${cy}` : ` L ${cx} ${cy}`);
}
const traceEl = document.createElementNS(svgNS,"path");
traceEl.setAttribute("d",traceD); traceEl.setAttribute("fill","none");
traceEl.setAttribute("stroke","#8b5cf6"); traceEl.setAttribute("stroke-width","2");
svg.appendChild(traceEl);
const chartLabel = document.createElementNS(svgNS,"text");
chartLabel.setAttribute("x",chartX+4); chartLabel.setAttribute("y",chartY+12);
chartLabel.setAttribute("fill","#6b7280"); chartLabel.setAttribute("font-size","9");
chartLabel.setAttribute("font-family","sans-serif"); chartLabel.textContent="arg(z(t))";
svg.appendChild(chartLabel);
return svg;
}
const container = document.createElement("div");
container.style.cssText = "border:1px solid #e5e7eb; border-radius:8px; padding:1rem; margin:1rem 0;";
const title = document.createElement("div");
title.style.cssText = "font-weight:600; margin-bottom:0.5rem; font-family:sans-serif;";
title.textContent = "Winding number and the branch cut";
container.appendChild(title);
container.appendChild(pathSelector);
container.appendChild(branchToggle);
container.appendChild(tSlider);
const vizDiv = document.createElement("div");
container.appendChild(vizDiv);
const note = document.createElement("div");
note.style.cssText = "margin-top:0.5rem; font-size:0.82em; color:#6b7280; font-style:italic;";
note.textContent = "Move the slider to animate the point around the path. The purple trace shows arg(z) accumulating. When the path closes: if it enclosed the origin, arg increased by 2π (winding number = 1). If not, arg returned to its start. The red dashed line is the branch cut — where Arg z jumps by 2π.";
container.appendChild(note);
function update() {
vizDiv.innerHTML = "";
vizDiv.appendChild(render(pathSelector.value, branchToggle.value, tSlider.value));
}
pathSelector.addEventListener("input", update);
branchToggle.addEventListener("input", update);
tSlider.addEventListener("input", update);
update();
return container;
}