{
const WP = 340, HP = 320, SCALE = 70;
const svgNS = "http://www.w3.org/2000/svg";
const rSlider4 = Inputs.range([1.0, 2.0], {value: 1.15, step: 0.01, label: "Circle radius r"});
const offsetSlider = Inputs.range([0.0, 0.3], {value: 0.12, step: 0.01, label: "x-offset \u03b4"});
const circSlider = Inputs.range([0, Math.PI*2], {value: 0, step: 0.1, label: "Circulation \u0393"});
function joukowski(zRe, zIm) {
const d = zRe*zRe + zIm*zIm;
if(d < 1e-10) return [NaN, NaN];
return [zRe + zRe/d, zIm - zIm/d];
}
function toScreen(re, im, cx, cy, scale) {
return [cx + re*scale, cy - im*scale];
}
function renderJoukowski(r, delta, circ) {
// Build aerofoil curve
const N = 360;
const aerofoilPts = [];
for(let k=0; k<N; k++) {
const theta = (2*Math.PI*k)/(N-1);
const zRe = -delta + r*Math.cos(theta);
const zIm = r*Math.sin(theta);
const [wRe, wIm] = joukowski(zRe, zIm);
if(isFinite(wRe) && isFinite(wIm)) aerofoilPts.push([wRe, wIm]);
}
// Left panel: z-plane
const leftSvg = document.createElementNS(svgNS, "svg");
leftSvg.setAttribute("width", WP); leftSvg.setAttribute("height", HP);
leftSvg.style.cssText = "background:#f8fafc; border:1px solid #e5e7eb; border-radius:6px;";
const CX = WP/2 + 10, CY = HP/2;
// 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,CX,CY,SCALE); const [,ly1]=toScreen(0,-2.2,CX,CY,SCALE); const [,ly2]=toScreen(0,2.2,CX,CY,SCALE);
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,CX,CY,SCALE); const [lx2]=toScreen(3,0,CX,CY,SCALE); const [,ly]=toScreen(0,i,CX,CY,SCALE);
yl.setAttribute("x1",lx1); yl.setAttribute("y1",ly); yl.setAttribute("x2",lx2); yl.setAttribute("y2",ly); gridG.appendChild(yl);
}
leftSvg.appendChild(gridG);
// Streamlines in z-plane: Im(F) = U(z + r²/z) = const
// F(z) = z + r²/z for uniform flow + vortex
// Streamlines: Im(F) = Im(z) + r² * Im(1/z) = y + r²*(-y)/(x²+y²) = const
// For vortex: add circulation * i/(2π) * log(z)
// Simple: just draw horizontal streamlines mapped through the potential
const streamG = document.createElementNS(svgNS,"g");
streamG.setAttribute("stroke","rgba(59,130,246,0.4)"); streamG.setAttribute("stroke-width","1"); streamG.setAttribute("fill","none");
for(let psi=-2.5; psi<=2.5; psi+=0.35) {
// parametric: Im(w + r²/w_conj) = psi — approximate by iterating x
let pathD=""; let started=false;
for(let x=-3.5; x<=3.5; x+=0.04) {
// Im(F) ≈ y(1 - r²/(x²+y²)) = psi; solve for y numerically
// simple approximation: far from origin, y ≈ psi
// Near origin, use Newton
let y=psi;
for(let iter=0;iter<8;iter++) {
const d2=x*x+y*y; if(d2<0.01) break;
const f=y-r*r*y/d2 - psi;
const fp=1 - r*r*(d2-2*y*y)/(d2*d2);
y -= f/(fp||1e-6);
}
const d2=x*x+y*y;
if(d2 < (r-delta)*(r-delta)*0.9) { started=false; continue; } // inside circle
const [px,py]=toScreen(x,y,CX,CY,SCALE);
if(px<0||px>WP||py<0||py>HP) { started=false; continue; }
pathD += started ? ` L ${px} ${py}` : `M ${px} ${py}`; started=true;
}
if(pathD) { const pEl=document.createElementNS(svgNS,"path"); pEl.setAttribute("d",pathD); pEl.setAttribute("stroke","rgba(59,130,246,0.35)"); streamG.appendChild(pEl); }
}
leftSvg.appendChild(streamG);
// Circle
const [cirCx, cirCy] = toScreen(-delta, 0, CX, CY, SCALE);
const circleEl = document.createElementNS(svgNS,"circle");
circleEl.setAttribute("cx",cirCx); circleEl.setAttribute("cy",cirCy); circleEl.setAttribute("r",r*SCALE);
circleEl.setAttribute("fill","none"); circleEl.setAttribute("stroke","#f97316"); circleEl.setAttribute("stroke-width","2");
leftSvg.appendChild(circleEl);
// Axes
const [ox,oy]=toScreen(0,0,CX,CY,SCALE);
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,0,CX,CY,SCALE); const [ax2]=toScreen(3,0,CX,CY,SCALE);
xa.setAttribute("x1",ax1); xa.setAttribute("y1",oy); xa.setAttribute("x2",ax2); xa.setAttribute("y2",oy); axG.appendChild(xa);
const ya=document.createElementNS(svgNS,"line"); const [,ay1]=toScreen(0,-2,CX,CY,SCALE); const [,ay2]=toScreen(0,2,CX,CY,SCALE);
ya.setAttribute("x1",ox); ya.setAttribute("y1",ay1); ya.setAttribute("x2",ox); ya.setAttribute("y2",ay2); axG.appendChild(ya);
leftSvg.appendChild(axG);
// Critical points at ±1
[1,-1].forEach(xc => {
const [cpx,cpy]=toScreen(xc,0,CX,CY,SCALE);
const cpEl=document.createElementNS(svgNS,"circle"); cpEl.setAttribute("cx",cpx); cpEl.setAttribute("cy",cpy); cpEl.setAttribute("r","4");
cpEl.setAttribute("fill","#8b5cf6"); cpEl.setAttribute("stroke","#fff"); cpEl.setAttribute("stroke-width","1.5");
leftSvg.appendChild(cpEl);
});
const lbl1=document.createElementNS(svgNS,"text"); lbl1.setAttribute("fill","#6b7280"); lbl1.setAttribute("font-size","10"); lbl1.setAttribute("font-family","sans-serif");
lbl1.setAttribute("x",5); lbl1.setAttribute("y",14); lbl1.textContent="z-plane";
leftSvg.appendChild(lbl1);
// Right panel: w-plane
const rightSvg = document.createElementNS(svgNS, "svg");
rightSvg.setAttribute("width", WP); rightSvg.setAttribute("height", HP);
rightSvg.style.cssText = "background:#f8fafc; border:1px solid #e5e7eb; border-radius:6px;";
const WCX = WP/2, WCY = HP/2;
const WSCALE = 45;
// Grid
const wgridG=document.createElementNS(svgNS,"g"); wgridG.setAttribute("stroke","#e5e7eb"); wgridG.setAttribute("stroke-width","0.5");
for(let i=-4;i<=4;i++) {
const xl2=document.createElementNS(svgNS,"line"); const [lx]=toScreen(i,0,WCX,WCY,WSCALE); const [,ly1]=toScreen(0,-3,WCX,WCY,WSCALE); const [,ly2]=toScreen(0,3,WCX,WCY,WSCALE);
xl2.setAttribute("x1",lx); xl2.setAttribute("y1",ly1); xl2.setAttribute("x2",lx); xl2.setAttribute("y2",ly2); wgridG.appendChild(xl2);
const yl2=document.createElementNS(svgNS,"line"); const [lx1]=toScreen(-4,0,WCX,WCY,WSCALE); const [lx2]=toScreen(4,0,WCX,WCY,WSCALE); const [,ly]=toScreen(0,i,WCX,WCY,WSCALE);
yl2.setAttribute("x1",lx1); yl2.setAttribute("y1",ly); yl2.setAttribute("x2",lx2); yl2.setAttribute("y2",ly); wgridG.appendChild(yl2);
}
rightSvg.appendChild(wgridG);
// w-plane axes
const [wox,woy]=toScreen(0,0,WCX,WCY,WSCALE);
const waxG=document.createElementNS(svgNS,"g"); waxG.setAttribute("stroke","#9ca3af"); waxG.setAttribute("stroke-width","1");
const wxa=document.createElementNS(svgNS,"line"); const [wax1]=toScreen(-4,0,WCX,WCY,WSCALE); const [wax2]=toScreen(4,0,WCX,WCY,WSCALE);
wxa.setAttribute("x1",wax1); wxa.setAttribute("y1",woy); wxa.setAttribute("x2",wax2); wxa.setAttribute("y2",woy); waxG.appendChild(wxa);
const wya=document.createElementNS(svgNS,"line"); const [,way1]=toScreen(0,-3,WCX,WCY,WSCALE); const [,way2]=toScreen(0,3,WCX,WCY,WSCALE);
wya.setAttribute("x1",wox); wya.setAttribute("y1",way1); wya.setAttribute("x2",wox); wya.setAttribute("y2",way2); waxG.appendChild(wya);
rightSvg.appendChild(waxG);
// Aerofoil curve
if(aerofoilPts.length > 2) {
let aeroD = "";
aerofoilPts.forEach(([wre,wim],i) => {
const [px,py]=toScreen(wre,wim,WCX,WCY,WSCALE);
aeroD += i===0 ? `M ${px} ${py}` : ` L ${px} ${py}`;
});
aeroD += " Z";
const aeroPath=document.createElementNS(svgNS,"path"); aeroPath.setAttribute("d",aeroD);
aeroPath.setAttribute("fill","rgba(249,115,22,0.15)"); aeroPath.setAttribute("stroke","#f97316"); aeroPath.setAttribute("stroke-width","2");
rightSvg.appendChild(aeroPath);
}
// Labels
const wlbl=document.createElementNS(svgNS,"text"); wlbl.setAttribute("fill","#6b7280"); wlbl.setAttribute("font-size","10"); wlbl.setAttribute("font-family","sans-serif");
wlbl.setAttribute("x",5); wlbl.setAttribute("y",14); wlbl.textContent="w-plane";
rightSvg.appendChild(wlbl);
// Shape label
let shapeLabel = "";
if(r <= 1.02 && delta < 0.02) shapeLabel = "Flat plate [−2,2]";
else if(delta < 0.02) shapeLabel = "Ellipse";
else shapeLabel = "Joukowski aerofoil";
const shapeLbl=document.createElementNS(svgNS,"text"); shapeLbl.setAttribute("fill","#f97316"); shapeLbl.setAttribute("font-size","10"); shapeLbl.setAttribute("font-family","sans-serif");
shapeLbl.setAttribute("x",5); shapeLbl.setAttribute("y",HP-6); shapeLbl.textContent=shapeLabel;
rightSvg.appendChild(shapeLbl);
// Lift indicator
const lift = circ.toFixed(2);
const liftDiv=document.createElement("div");
liftDiv.style.cssText="margin-top:0.4rem; font-family:sans-serif; font-size:0.85em; padding:0.5rem; background:#f0f9ff; border-radius:4px;";
liftDiv.innerHTML=`<strong>Kutta–Joukowski:</strong> L = \u03c1U\u0393 | \u0393 = ${lift} | <span style="color:#1e40af">Lift \u221d \u0393 = ${(parseFloat(lift)).toFixed(2)}</span>
<div style="color:#6b7280; font-size:0.82em; margin-top:0.2rem;">Purple dots on z-plane: critical points z=±1 (images become trailing/leading edges). Circulation \u0393 = 0 gives symmetric flow; \u0393 > 0 creates lift.</div>`;
const panelsDiv=document.createElement("div");
panelsDiv.style.cssText="display:flex; gap:0.5rem; flex-wrap:wrap; margin-top:0.5rem;";
panelsDiv.appendChild(leftSvg);
panelsDiv.appendChild(rightSvg);
const wrap=document.createElement("div");
wrap.appendChild(panelsDiv);
wrap.appendChild(liftDiv);
return wrap;
}
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="Joukowski map explorer: circle to aerofoil";
container.appendChild(title);
container.appendChild(rSlider4);
container.appendChild(offsetSlider);
container.appendChild(circSlider);
const vizDiv4=document.createElement("div");
container.appendChild(vizDiv4);
const note4=document.createElement("div");
note4.style.cssText="margin-top:0.5rem; font-size:0.82em; color:#6b7280; font-style:italic;";
note4.textContent="Left: circle in z-plane with uniform flow streamlines (blue). Right: image aerofoil in w-plane under w = z + 1/z. At r=1, \u03b4=0: flat plate. Increase r for ellipse; increase \u03b4 for asymmetric aerofoil. Purple dots mark critical points z=\u00b11 (where the trailing edge forms). Circulation \u0393 controls lift.";
container.appendChild(note4);
function update4() {
vizDiv4.innerHTML="";
vizDiv4.appendChild(renderJoukowski(rSlider4.value, offsetSlider.value, circSlider.value));
}
rSlider4.addEventListener("input", update4);
offsetSlider.addEventListener("input", update4);
circSlider.addEventListener("input", update4);
update4();
return container;
}