{
// --- Joukowski map explorer (upgraded) ---
// LEFT: z-plane circle + Euler-traced streamlines (parametric, no Newton iteration).
// RIGHT: w-plane aerofoil + mapped streamlines + lift arrow + critical point dashed link.
// Shape label appears above the w-panel as a bold heading.
const WP = 320, HP = 300, SCALE = 65;
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];
}
// --- Complex velocity for flow around cylinder + circulation ---
// dF/dz = U(1 - r²/z²) + i*Gamma/(2*pi*z)
// vx = Re(dF/dz), vy = -Im(dF/dz)
function complexVelocity(zr, zi, r, circ) {
const d2 = zr*zr + zi*zi; if(d2<1e-12)return[NaN,NaN];
const z2r=zr*zr-zi*zi,z2i=2*zr*zi; const z2d=z2r*z2r+z2i*z2i;
const r2oz2r=r*r*z2r/z2d,r2oz2i=-r*r*z2i/z2d;
const t1r=1-r2oz2r,t1i=-r2oz2i;
const fac=circ/(2*Math.PI);
const t2r=-fac*(-zi/d2),t2i=fac*(zr/d2);
return[t1r+t2r,t1i+t2i];
}
function traceEuler(x0,y0,r,delta,circ,dt,maxSteps){
const pts=[];let x=x0,y=y0;
for(let k=0;k<maxSteps;k++){
const dx=x+delta,dy=y;
if(dx*dx+dy*dy<r*r*0.92)break;
if(Math.abs(x)>4.5||Math.abs(y)>3.5)break;
pts.push([x,y]);
const[dvr,dvi]=complexVelocity(x,y,r,circ);
if(!isFinite(dvr)||!isFinite(dvi))break;
const vx=dvr,vy=-dvi;const vmag=Math.sqrt(vx*vx+vy*vy);
if(vmag<1e-8)break;
const step=dt/vmag;x+=vx*step;y+=vy*step;
}
return pts;
}
function renderJoukowski(r, delta, circ) {
const CX = WP/2 + 5, CY = HP/2;
const WCX = WP/2, WCY = HP/2;
const WSCALE = 42;
// 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]);
}
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";
// Euler-traced streamlines
const dt=0.02,maxSteps=600;
const zStreamlines=[];
for(let sy=-2.5;sy<=2.5;sy+=0.35){
const fwd=traceEuler(-3.5,sy,r,delta,circ,dt,maxSteps);
let xb=-3.5,yb=sy;const bwd=[];
for(let k=0;k<120;k++){
const dx=xb+delta,dy=yb;if(dx*dx+dy*dy<r*r*0.92)break;
if(Math.abs(xb)>4.5||Math.abs(yb)>3.5)break;
bwd.unshift([xb,yb]);
const[dvr,dvi]=complexVelocity(xb,yb,r,circ);
if(!isFinite(dvr)||!isFinite(dvi))break;
const vx=dvr,vy=-dvi;const vmag=Math.sqrt(vx*vx+vy*vy);if(vmag<1e-8)break;
xb-=vx*(dt/vmag);yb-=vy*(dt/vmag);
}
const full=[...bwd,...fwd];if(full.length>2)zStreamlines.push(full);
}
const wStreamlines=zStreamlines.map(pts=>pts.map(([zx,zy])=>{const[wr,wi]=joukowski(zx,zy);return isFinite(wr)&&isFinite(wi)?[wr,wi]:null;}).filter(Boolean));
// 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;";
// Faint grid
for(let i=-3;i<=3;i++){
const gl=document.createElementNS(svgNS,"line");
const[lx]=toScreen(i,0,CX,CY,SCALE),[,ly1]=toScreen(0,-2.2,CX,CY,SCALE),[,ly2]=toScreen(0,2.2,CX,CY,SCALE);
gl.setAttribute("x1",lx);gl.setAttribute("y1",ly1);gl.setAttribute("x2",lx);gl.setAttribute("y2",ly2);
gl.setAttribute("stroke","#f3f4f6");gl.setAttribute("stroke-width","0.5");leftSvg.appendChild(gl);
const gl2=document.createElementNS(svgNS,"line");
const[lx1]=toScreen(-3,0,CX,CY,SCALE),[lx2]=toScreen(3,0,CX,CY,SCALE),[,ly]=toScreen(0,i,CX,CY,SCALE);
gl2.setAttribute("x1",lx1);gl2.setAttribute("y1",ly);gl2.setAttribute("x2",lx2);gl2.setAttribute("y2",ly);
gl2.setAttribute("stroke","#f3f4f6");gl2.setAttribute("stroke-width","0.5");leftSvg.appendChild(gl2);
}
// Euler-traced streamlines in z-plane
zStreamlines.forEach(pts=>{
let d="";let prev=true;
pts.forEach(([x,y])=>{
const[px,py]=toScreen(x,y,CX,CY,SCALE);
if(px<-5||px>WP+5||py<-5||py>HP+5){prev=false;return;}
d+=prev?` L ${px} ${py}`:`M ${px} ${py}`;prev=true;
});
if(!d)return;
const p=document.createElementNS(svgNS,"path");
p.setAttribute("d",d.replace(/^ L/,"M"));
p.setAttribute("stroke","rgba(59,130,246,0.45)");p.setAttribute("stroke-width","1.2");p.setAttribute("fill","none");
leftSvg.appendChild(p);
});
// 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","rgba(249,115,22,0.08)");circleEl.setAttribute("stroke","#f97316");circleEl.setAttribute("stroke-width","2");
leftSvg.appendChild(circleEl);
// Axes
const [ox,oy]=toScreen(0,0,CX,CY,SCALE);
const axH=document.createElementNS(svgNS,"line");
axH.setAttribute("x1",toScreen(-3.2,0,CX,CY,SCALE)[0]);axH.setAttribute("y1",oy);
axH.setAttribute("x2",toScreen(3.2,0,CX,CY,SCALE)[0]);axH.setAttribute("y2",oy);
axH.setAttribute("stroke","#9ca3af");axH.setAttribute("stroke-width","1");leftSvg.appendChild(axH);
const axV=document.createElementNS(svgNS,"line");
axV.setAttribute("x1",ox);axV.setAttribute("y1",toScreen(0,-2.2,CX,CY,SCALE)[1]);
axV.setAttribute("x2",ox);axV.setAttribute("y2",toScreen(0,2.2,CX,CY,SCALE)[1]);
axV.setAttribute("stroke","#9ca3af");axV.setAttribute("stroke-width","1");leftSvg.appendChild(axV);
// Critical points ±1
[1,-1].forEach(xc=>{
const[cpx,cpy]=toScreen(xc,0,CX,CY,SCALE);
const cp=document.createElementNS(svgNS,"circle");
cp.setAttribute("cx",cpx);cp.setAttribute("cy",cpy);cp.setAttribute("r","5");
cp.setAttribute("fill","#8b5cf6");cp.setAttribute("stroke","#fff");cp.setAttribute("stroke-width","1.5");
leftSvg.appendChild(cp);
const lbl=document.createElementNS(svgNS,"text");
lbl.setAttribute("x",cpx+6);lbl.setAttribute("y",cpy-6);lbl.setAttribute("fill","#8b5cf6");
lbl.setAttribute("font-size","9");lbl.setAttribute("font-family","sans-serif");
lbl.textContent=xc>0?"z=+1":"z=−1";leftSvg.appendChild(lbl);
});
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
// Shape label as bold heading above panel
const shapeLabelDiv=document.createElement("div");
shapeLabelDiv.style.cssText="font-size:14px;font-weight:700;font-family:sans-serif;color:#f97316;margin-bottom:2px;text-align:center;";
shapeLabelDiv.textContent=shapeLabel;
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;";
// Faint grid
for(let i=-4;i<=4;i++){
const gl=document.createElementNS(svgNS,"line");
const[lx]=toScreen(i,0,WCX,WCY,WSCALE),[,ly1]=toScreen(0,-3.5,WCX,WCY,WSCALE),[,ly2]=toScreen(0,3.5,WCX,WCY,WSCALE);
gl.setAttribute("x1",lx);gl.setAttribute("y1",ly1);gl.setAttribute("x2",lx);gl.setAttribute("y2",ly2);
gl.setAttribute("stroke","#f3f4f6");gl.setAttribute("stroke-width","0.5");rightSvg.appendChild(gl);
const gl2=document.createElementNS(svgNS,"line");
const[lx1]=toScreen(-4,0,WCX,WCY,WSCALE),[lx2]=toScreen(4,0,WCX,WCY,WSCALE),[,ly]=toScreen(0,i,WCX,WCY,WSCALE);
gl2.setAttribute("x1",lx1);gl2.setAttribute("y1",ly);gl2.setAttribute("x2",lx2);gl2.setAttribute("y2",ly);
gl2.setAttribute("stroke","#f3f4f6");gl2.setAttribute("stroke-width","0.5");rightSvg.appendChild(gl2);
}
// Mapped streamlines in w-plane
wStreamlines.forEach(pts=>{
let d="";let prev=true;
pts.forEach(([x,y])=>{
const[px,py]=toScreen(x,y,WCX,WCY,WSCALE);
if(px<-5||px>WP+5||py<-5||py>HP+5){prev=false;return;}
d+=prev?` L ${px} ${py}`:`M ${px} ${py}`;prev=true;
});
if(!d)return;
const p=document.createElementNS(svgNS,"path");
p.setAttribute("d",d.replace(/^ L/,"M"));
p.setAttribute("stroke","rgba(59,130,246,0.45)");p.setAttribute("stroke-width","1.2");p.setAttribute("fill","none");
rightSvg.appendChild(p);
});
// Aerofoil curve
if(aerofoilPts.length>2){
const aeroD=aerofoilPts.map(([wr,wi],i)=>{
const[px,py]=toScreen(wr,wi,WCX,WCY,WSCALE);
return`${i===0?"M":"L"} ${px} ${py}`;
}).join(" ")+" Z";
const aeroPath=document.createElementNS(svgNS,"path");
aeroPath.setAttribute("d",aeroD);
aeroPath.setAttribute("fill","rgba(249,115,22,0.12)");aeroPath.setAttribute("stroke","#f97316");aeroPath.setAttribute("stroke-width","2");
rightSvg.appendChild(aeroPath);
}
// Critical point image at w=2, dashed link
const[cpImgX,cpImgY]=toScreen(2,0,WCX,WCY,WSCALE);
const cpImgDot=document.createElementNS(svgNS,"circle");
cpImgDot.setAttribute("cx",cpImgX);cpImgDot.setAttribute("cy",cpImgY);cpImgDot.setAttribute("r","5");
cpImgDot.setAttribute("fill","#8b5cf6");cpImgDot.setAttribute("stroke","#fff");cpImgDot.setAttribute("stroke-width","1.5");
rightSvg.appendChild(cpImgDot);
const dashLink=document.createElementNS(svgNS,"line");
dashLink.setAttribute("x1",cpImgX+22);dashLink.setAttribute("y1",cpImgY-18);
dashLink.setAttribute("x2",cpImgX);dashLink.setAttribute("y2",cpImgY);
dashLink.setAttribute("stroke","#8b5cf6");dashLink.setAttribute("stroke-width","1.2");dashLink.setAttribute("stroke-dasharray","3,2");
rightSvg.appendChild(dashLink);
const cpLbl=document.createElementNS(svgNS,"text");
cpLbl.setAttribute("x",cpImgX+24);cpLbl.setAttribute("y",cpImgY-20);cpLbl.setAttribute("fill","#8b5cf6");
cpLbl.setAttribute("font-size","9");cpLbl.setAttribute("font-family","sans-serif");
cpLbl.textContent="w=2 (trailing edge)";rightSvg.appendChild(cpLbl);
// w-plane axes
const[wox,woy]=toScreen(0,0,WCX,WCY,WSCALE);
const waxH=document.createElementNS(svgNS,"line");
waxH.setAttribute("x1",toScreen(-4,0,WCX,WCY,WSCALE)[0]);waxH.setAttribute("y1",woy);
waxH.setAttribute("x2",toScreen(4,0,WCX,WCY,WSCALE)[0]);waxH.setAttribute("y2",woy);
waxH.setAttribute("stroke","#9ca3af");waxH.setAttribute("stroke-width","1");rightSvg.appendChild(waxH);
const waxV=document.createElementNS(svgNS,"line");
waxV.setAttribute("x1",wox);waxV.setAttribute("y1",toScreen(0,-3,WCX,WCY,WSCALE)[1]);
waxV.setAttribute("x2",wox);waxV.setAttribute("y2",toScreen(0,3,WCX,WCY,WSCALE)[1]);
waxV.setAttribute("stroke","#9ca3af");waxV.setAttribute("stroke-width","1");rightSvg.appendChild(waxV);
// Lift arrow: green, height proportional to Gamma
const liftMag=Math.min(circ*WSCALE*0.12,HP*0.35);
if(circ>0.05){
const arrowX=toScreen(3.2,0,WCX,WCY,WSCALE)[0];
const arrowBaseY=woy+10;const arrowTipY=arrowBaseY-liftMag;
const shaft=document.createElementNS(svgNS,"line");
shaft.setAttribute("x1",arrowX);shaft.setAttribute("y1",arrowBaseY);
shaft.setAttribute("x2",arrowX);shaft.setAttribute("y2",arrowTipY);
shaft.setAttribute("stroke","#059669");shaft.setAttribute("stroke-width","2.5");rightSvg.appendChild(shaft);
const ah=document.createElementNS(svgNS,"polygon");
ah.setAttribute("points",`${arrowX},${arrowTipY-8} ${arrowX-5},${arrowTipY+4} ${arrowX+5},${arrowTipY+4}`);
ah.setAttribute("fill","#059669");rightSvg.appendChild(ah);
const ll=document.createElementNS(svgNS,"text");
ll.setAttribute("x",arrowX+7);ll.setAttribute("y",arrowTipY+4);ll.setAttribute("fill","#059669");
ll.setAttribute("font-size","10");ll.setAttribute("font-family","sans-serif");ll.setAttribute("font-weight","600");
ll.textContent="L = \u03c1U\u0393";rightSvg.appendChild(ll);
}
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);
// Lift indicator bar
const liftDiv=document.createElement("div");
liftDiv.style.cssText="margin-top:0.4rem;font-family:sans-serif;font-size:0.85em;padding:0.5rem 0.75rem;background:#f0f9ff;border-radius:4px;";
const liftBarW=circ>0.05?Math.min(circ/6.3*80,80).toFixed(0):4;
const liftBarCol=circ>0.05?"#059669":"#d1d5db";
liftDiv.innerHTML=`<strong>Kutta\u2013Joukowski:</strong> <span style="display:inline-block;width:${liftBarW}px;height:8px;background:${liftBarCol};border-radius:3px;vertical-align:middle;margin-right:6px;"></span> L = \u03c1U\u0393 | \u0393 = ${parseFloat(circ).toFixed(2)}
<div style="color:#6b7280;font-size:0.82em;margin-top:0.2rem;">Green arrow (w-plane) grows with \u0393. Purple dots: critical points z=\u00b11 in z-plane and trailing edge w=2 in w-plane. Dashed line shows the mapping.</div>`;
const panelsDiv=document.createElement("div");
panelsDiv.style.cssText="display:flex;gap:0.75rem;flex-wrap:wrap;margin-top:0.5rem;align-items:flex-start;";
panelsDiv.appendChild(leftSvg);
const rightCol=document.createElement("div");
rightCol.appendChild(shapeLabelDiv);rightCol.appendChild(rightSvg);
panelsDiv.appendChild(rightCol);
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 (w = z + 1/z)";
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, streamlines from Euler integration of potential flow. Right: aerofoil in w-plane with mapped streamlines and lift arrow. At r=1, \u03b4=0: flat plate. Increase \u03b4 for asymmetric aerofoil with cusped trailing edge. Purple dots: critical points z=\u00b11 in z-plane (w=2 trailing edge in w-plane). Green arrow grows with \u0393.";
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;
}