{
// ── VIZ 2 Rewrite: Transform pair explorer ───────────────────────────────
// Panel A: time-domain f(t) Panel B: |F(ω)| magnitude Panel C: phase ∠F(ω)
// Wave types: rectangular pulse | Gaussian | Exponential decay
const d3 = await require("d3@7");
const AMBER = "#f59e0b";
const BLUE = "#6b90c4";
const GREY = "#9ca3af";
const GREY_F = "#e5e7eb";
const W = 560, H = 280;
const PL = 44, PR = 12, PT = 22, PB = 16;
const IW = W - PL - PR;
const HA = 90, HB = 90, HC = 50, gap = 12;
const container = document.createElement("div");
container.style.cssText = "font-family:inherit; max-width:620px;";
// Controls
const ctrlRow = document.createElement("div");
ctrlRow.style.cssText = "display:flex; gap:1.2rem; align-items:center; margin-bottom:0.4rem; flex-wrap:wrap;";
// Wave type selector
const waveTypes = [["rect","Rectangular pulse"],["gauss","Gaussian"],["exp","Exp. decay e^{−|t|/τ}"]];
let currentWT = "rect";
const radioGrp = document.createElement("div");
radioGrp.style.cssText = "display:flex; gap:0.6rem; font-size:0.85em; flex-wrap:wrap;";
waveTypes.forEach(([val,lab]) => {
const lbl = document.createElement("label");
lbl.style.cssText = "display:flex; align-items:center; gap:0.25rem; cursor:pointer;";
const r = document.createElement("input");
r.type = "radio"; r.name = "ftViz2"; r.value = val; r.checked = val==="rect";
r.style.accentColor = AMBER;
r.onchange = () => { currentWT = val; updateParamLabel(); draw(); };
lbl.appendChild(r); lbl.appendChild(document.createTextNode(lab));
radioGrp.appendChild(lbl);
});
ctrlRow.appendChild(radioGrp);
// Parameter slider (a / σ / τ)
const paramRow = document.createElement("div");
paramRow.style.cssText = "display:flex; gap:0.5rem; align-items:center; font-size:0.85em;";
const paramLbl = document.createElement("span");
paramLbl.style.cssText = "color:#374151; min-width:4ch;";
paramLbl.textContent = "a =";
const paramDisp = document.createElement("strong");
paramDisp.style.cssText = "color:" + AMBER + "; min-width:3ch;";
paramDisp.textContent = "1.0";
const paramSlider = document.createElement("input");
paramSlider.type = "range"; paramSlider.min = 2; paramSlider.max = 30; paramSlider.value = 10;
paramSlider.style.cssText = "width:160px; accent-color:" + AMBER + ";";
paramRow.appendChild(paramLbl); paramRow.appendChild(paramSlider); paramRow.appendChild(paramDisp);
ctrlRow.appendChild(paramRow);
// Time-shift toggle
const shiftLbl = document.createElement("label");
shiftLbl.style.cssText = "display:flex; align-items:center; gap:0.3rem; font-size:0.82em; cursor:pointer;";
const shiftCheck = document.createElement("input");
shiftCheck.type = "checkbox";
shiftCheck.onchange = draw;
shiftLbl.appendChild(shiftCheck);
shiftLbl.appendChild(document.createTextNode("Shift by t₀=1"));
ctrlRow.appendChild(shiftLbl);
container.appendChild(ctrlRow);
function updateParamLabel() {
const lbl = currentWT==="rect"?"a":currentWT==="gauss"?"σ":"τ";
paramLbl.textContent = lbl + " =";
}
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${W} ${H}`)
.attr("width","100%").style("display","block");
const gA = svg.append("g").attr("transform", `translate(${PL},${PT})`);
const gB = svg.append("g").attr("transform", `translate(${PL},${PT+HA+gap})`);
const gC = svg.append("g").attr("transform", `translate(${PL},${PT+HA+gap+HB+gap})`);
// Shared ω range — rescale based on current a
function omegaRange(param) { return 4 * Math.PI / param; }
// ── Wave functions ───────────────────────────────────────────────────────
function fTime(t, param, wt, shift) {
const t_ = shift ? t - 1 : t;
if (wt==="rect") return Math.abs(t_) < param ? 1 : 0;
if (wt==="gauss") return Math.exp(-t_*t_ / (2*param*param));
return Math.exp(-Math.abs(t_)/param);
}
// Fourier transforms (analytical)
function FRect(omega, a, shift) {
const mag = Math.abs(omega) < 1e-8 ? 2*a : 2*Math.sin(omega*a)/omega;
const phase = shift ? -omega * 1 : 0;
return { mag: Math.abs(mag), phase: phase + (mag < 0 ? Math.PI : 0) };
}
function FGauss(omega, sigma, shift) {
const mag = sigma * Math.sqrt(2*Math.PI) * Math.exp(-omega*omega*sigma*sigma/2);
const phase = shift ? -omega * 1 : 0;
return { mag, phase };
}
function FExp(omega, tau, shift) {
const mag = 2*tau / (1 + omega*omega*tau*tau);
const phase = shift ? -omega * 1 : 0;
return { mag, phase };
}
function computeF(omega, param, wt, shift) {
if (wt==="rect") return FRect(omega, param, shift);
if (wt==="gauss") return FGauss(omega, param, shift);
return FExp(omega, param, shift);
}
// Axis setup helpers
function makeAxes(g, xScale, yScale, height, nTicksY, title) {
g.append("text").attr("x",IW/2).attr("y",-6).attr("text-anchor","middle")
.attr("font-size","10px").attr("fill","#374151").text(title);
g.append("line").attr("x1",0).attr("x2",IW).attr("y1",yScale(0)).attr("y2",yScale(0))
.attr("stroke",GREY_F).attr("stroke-width",0.8);
g.append("g").call(d3.axisLeft(yScale).ticks(nTicksY))
.call(gg => { gg.select(".domain").remove(); gg.selectAll("text").style("font-size","9px").attr("fill",GREY); gg.selectAll("line").attr("stroke",GREY_F); });
g.append("g").call(d3.axisBottom(xScale).ticks(7))
.attr("transform",`translate(0,${height})`)
.call(gg => { gg.select(".domain").attr("stroke",GREY_F); gg.selectAll("text").style("font-size","9px").attr("fill",GREY); gg.selectAll("line").attr("stroke",GREY_F); });
}
// Panel A: time domain
const tMin = -6, tMax = 6;
const xA = d3.scaleLinear().domain([tMin, tMax]).range([0, IW]);
const yA = d3.scaleLinear().domain([-0.15, 1.2]).range([HA, 0]);
makeAxes(gA, xA, yA, HA, 3, "Time domain f(t)");
const timePath = gA.append("path").attr("fill","none").attr("stroke",AMBER).attr("stroke-width",2.5);
const widthBracket = gA.append("g");
const bracketLine = widthBracket.append("line").attr("stroke",GREY).attr("stroke-width",1).attr("stroke-dasharray","3,2");
const bracketLabel = widthBracket.append("text").attr("font-size","9px").attr("fill",GREY).attr("text-anchor","middle");
// Panel B: magnitude
const xB = d3.scaleLinear().domain([-1, 1]).range([0, IW]);
const yB = d3.scaleLinear().domain([0, 3]).range([HB, 0]);
makeAxes(gB, xB, yB, HB, 3, "|F(ω)| — frequency magnitude");
const magPath = gB.append("path").attr("fill","none").attr("stroke",AMBER).attr("stroke-width",2.5);
const zeroMarks = [gB.append("line").attr("stroke","#6b7280").attr("stroke-width",1.5).attr("stroke-dasharray","3,2"),
gB.append("line").attr("stroke","#6b7280").attr("stroke-width",1.5).attr("stroke-dasharray","3,2")];
const zeroLabels = [gB.append("text").attr("font-size","8.5px").attr("fill","#6b7280").attr("text-anchor","middle"),
gB.append("text").attr("font-size","8.5px").attr("fill","#6b7280").attr("text-anchor","middle")];
const bwArrow = gB.append("text").attr("font-size","9px").attr("fill",BLUE).attr("font-style","italic");
// Panel C: phase
const xC = d3.scaleLinear().domain([-1, 1]).range([0, IW]);
const yC = d3.scaleLinear().domain([-Math.PI-0.3, Math.PI+0.3]).range([HC, 0]);
makeAxes(gC, xC, yC, HC, 2, "Phase ∠F(ω)");
const phasePath = gC.append("path").attr("fill","none").attr("stroke",BLUE).attr("stroke-width",1.8);
const phaseLabel = gC.append("text").attr("font-size","9px").attr("fill",BLUE).attr("x",4).attr("y",10);
function draw() {
const param = +paramSlider.value / 10;
paramDisp.textContent = param.toFixed(1);
const shift = shiftCheck.checked;
const omMax = omegaRange(param);
// Update x-axes domain for B and C
xB.domain([-omMax, omMax]);
xC.domain([-omMax, omMax]);
// Redraw axes ticks
gB.selectAll(".x-axis").remove();
gB.append("g").attr("class","x-axis").call(d3.axisBottom(xB).ticks(7).tickFormat(v => v.toFixed(1) === "0.0" ? "0" : v.toFixed(1)))
.attr("transform",`translate(0,${HB})`)
.call(gg => { gg.select(".domain").attr("stroke",GREY_F); gg.selectAll("text").style("font-size","8.5px").attr("fill",GREY); gg.selectAll("line").attr("stroke",GREY_F); });
gC.selectAll(".x-axis").remove();
gC.append("g").attr("class","x-axis").call(d3.axisBottom(xC).ticks(7).tickFormat(v => v.toFixed(1) === "0.0" ? "ω=0" : v.toFixed(1)))
.attr("transform",`translate(0,${HC})`)
.call(gg => { gg.select(".domain").attr("stroke",GREY_F); gg.selectAll("text").style("font-size","8.5px").attr("fill",GREY); gg.selectAll("line").attr("stroke",GREY_F); });
// Panel A: time domain
const tPts = d3.range(400).map(i => {
const t = tMin + (tMax-tMin)*i/399;
return [xA(t), yA(fTime(t, param, currentWT, shift))];
});
timePath.datum(tPts).attr("d", d3.line().x(d=>d[0]).y(d=>d[1]));
// Width bracket
const bLab = currentWT==="rect"?"2a":currentWT==="gauss"?"2σ":"τ";
const bWidth = currentWT==="rect" ? 2*param : currentWT==="gauss" ? 2*param : param;
bracketLine.attr("x1",xA(-bWidth/2)).attr("x2",xA(bWidth/2)).attr("y1",yA(-0.08)).attr("y2",yA(-0.08));
bracketLabel.attr("x",xA(0)).attr("y",yA(-0.12)).text(bLab + " = " + bWidth.toFixed(1));
// Panel B & C: spectrum
const omPts = d3.range(600).map(i => {
const om = -omMax + 2*omMax*i/599;
return om;
});
const maxMag = d3.max(omPts, om => computeF(om, param, currentWT, false).mag);
yB.domain([0, maxMag * 1.15]);
magPath.datum(omPts.map(om => {
const {mag} = computeF(om, param, currentWT, shift);
return [xB(om), yB(mag)];
})).attr("d", d3.line().x(d=>d[0]).y(d=>d[1]));
phasePath.datum(omPts.map(om => {
const {phase} = computeF(om, param, currentWT, shift);
return [xC(om), yC(phase)];
})).attr("d", d3.line().x(d=>d[0]).y(d=>d[1]));
phaseLabel.text(shift ? "Phase ramp = −ωt₀ (time-shift effect)" : "Phase = 0 (symmetric, real signal)");
// Zero-crossing markers (rect only)
if (currentWT === "rect" && !shift) {
const z1 = Math.PI / param;
zeroMarks[0].attr("x1",xB(z1)).attr("x2",xB(z1)).attr("y1",0).attr("y2",HB).style("display","block");
zeroMarks[1].attr("x1",xB(-z1)).attr("x2",xB(-z1)).attr("y1",0).attr("y2",HB).style("display","block");
zeroLabels[0].attr("x",xB(z1)).attr("y",12).text("π/a").style("display","block");
zeroLabels[1].attr("x",xB(-z1)).attr("y",12).text("−π/a").style("display","block");
const bwDir = param < 1.5 ? "narrower pulse → wider spectrum" : "wider pulse → narrower spectrum";
bwArrow.attr("x",IW/2).attr("y",HB-6).attr("text-anchor","middle").text(bwDir);
} else {
zeroMarks.forEach(m => m.style("display","none"));
zeroLabels.forEach(l => l.style("display","none"));
bwArrow.text(currentWT==="gauss" ? "Gaussian: width 2σ ↔ bandwidth 2/σ" : "");
}
}
paramSlider.oninput = draw;
draw();
const legend = document.createElement("div");
legend.style.cssText = "display:flex; gap:1.2rem; font-size:0.78em; margin-top:0.2rem; flex-wrap:wrap;";
[["─── f(t) (time domain)", AMBER], ["─── ∠F(ω) (phase)", BLUE],
["| Zero crossings at ±π/a", "#6b7280"]]
.forEach(([lab, col]) => {
const s = document.createElement("span"); s.style.color = col; s.textContent = lab;
legend.appendChild(s);
});
container.appendChild(svg.node());
container.appendChild(legend);
return container;
}