<!-- Simulation controls and canvas for the 2D FDTD single-slit diffraction demo -->
<div class="controls">
<label>Slit width: <input id="slitSlider" type="range" min="2" max="10" value="4" step="1"> <span id="slitVal">4</span> cells</label>
<button id="btnReset" type="button">Reset</button>
<button id="btnToggle" type="button">Pause</button>
</div>
<canvas id="fdtd" width="320" height="280"></canvas>
<div id="status" class="status">Simulation running…</div>
<p class="hint">The wave (red = positive field, blue = negative) travels left-to-right and diffracts through the slit in the gray barrier. Try different slit widths: a narrower slit spreads the wave more.</p>
/* Styles for the FDTD single-slit diffraction demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; padding: .5rem; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .85rem; }
label { display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 90px; }
button { font: 600 13px system-ui, sans-serif; padding: .35rem .75rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button:hover { background: #274f78; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; image-rendering: pixelated; width: 320px; height: 280px; }
.status { font-size: .85rem; font-weight: 600; margin-top: .35rem; color: #444; min-height: 1.2em; }
.hint { font-size: .8rem; color: #666; margin-top: .4rem; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2D FDTD grid dimensions and spatial step
const NX = 80, NY = 70;
const DX = 1.0, DY = 1.0;
// Courant-stable time step for 2D grid (must satisfy CFL condition) — CFL: dt = 0.5 / sqrt(1/dx^2 + 1/dy^2) ≈ 0.354
const DT = 0.5 / Math.sqrt(1 / (DX * DX) + 1 / (DY * DY));
const C = 1.0;
// Electromagnetic field arrays for Ez, Hx, Hy components
let Ez = new Float32Array(NX * NY);
let Hx = new Float32Array(NX * NY);
let Hy = new Float32Array(NX * NY);
const idx = (x, y) => x + y * NX;
// Barrier wall at WALL_X column — 1=wall, 0=open slit — barrier column and slit mask
let slitWidth = 4;
const WALL_X = Math.floor(NX * 0.45);
const wallMask = new Uint8Array(NY);
function buildWall(sw) {
// Mark all cells as wall except the slit region around center
const cy = Math.floor(NY / 2);
const half = Math.floor(sw / 2);
wallMask.fill(1);
for (let y = cy - half; y <= cy + half; y++) {
if (y >= 0 && y < NY) wallMask[y] = 0;
}
}
// Sinusoidal source parameters at left edge
const SRC_X = 3;
const FREQ = 0.08;
let t = 0;
let running = true;
let stepCount = 0;
function resetSim(sw) {
// Reset all fields to zero and rebuild the wall for the given slit width
slitWidth = sw;
Ez.fill(0); Hx.fill(0); Hy.fill(0);
t = 0; stepCount = 0;
buildWall(sw);
}
// Yee leapfrog H-field update using central differences — Yee leapfrog: H update at half-integer time steps
function updateH() {
const coeff = DT / (DX);
for (let y = 0; y < NY - 1; y++) {
for (let x = 0; x < NX - 1; x++) {
Hx[idx(x, y)] -= coeff * (Ez[idx(x, y + 1)] - Ez[idx(x, y)]);
Hy[idx(x, y)] += coeff * (Ez[idx(x + 1, y)] - Ez[idx(x, y)]);
}
}
}
// Yee leapfrog E-field update using central differences — Yee leapfrog: E update at integer time steps
function updateE() {
const coeff = DT / DX;
for (let y = 1; y < NY - 1; y++) {
for (let x = 1; x < NX - 1; x++) {
// Perfectly conducting wall absorbs the field (set Ez to zero) — wall cells: perfectly conducting (Ez = 0)
if (x === WALL_X && wallMask[y] === 1) {
Ez[idx(x, y)] = 0;
continue;
}
Ez[idx(x, y)] += coeff * (
(Hy[idx(x, y)] - Hy[idx(x - 1, y)]) -
(Hx[idx(x, y)] - Hx[idx(x, y - 1)])
);
}
}
// Inject sinusoidal plane-wave source at left boundary — soft sinusoidal source
const val = Math.sin(2 * Math.PI * FREQ * t);
for (let y = 1; y < NY - 1; y++) {
Ez[idx(SRC_X, y)] += val * 0.5;
}
// Simple absorbing boundary condition at right wall — first-order absorbing boundary at right edge
for (let y = 0; y < NY; y++) {
Ez[idx(NX - 1, y)] = Ez[idx(NX - 2, y)];
}
t += DT;
}
// Draw the Ez field to the canvas with a diverging blue-white-red colormap
const canvas = document.getElementById('fdtd');
const ctx = canvas.getContext('2d');
const CW = canvas.width / NX;
const CH = canvas.height / NY;
function render() {
const img = ctx.createImageData(canvas.width, canvas.height);
const d = img.data;
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const v = Ez[idx(x, y)];
// Map field amplitude to color — positive=red, zero=white, negative=blue — blue=negative, white=zero, red=positive
const norm = Math.max(-1, Math.min(1, v * 2));
const px = Math.floor(x * CW), py = Math.floor(y * CH);
let r, g, b;
if (norm >= 0) { r = 220; g = Math.round(220 - norm * 190); b = Math.round(220 - norm * 220); }
else { r = Math.round(220 + norm * 220); g = Math.round(220 + norm * 190); b = 220; }
// Render barrier wall cells as dark gray — draw wall as dark gray
const isWall = (x === WALL_X && wallMask[y] === 1);
if (isWall) { r = 60; g = 60; b = 60; }
for (let dy = 0; dy < Math.ceil(CH); dy++) {
for (let dx2 = 0; dx2 < Math.ceil(CW); dx2++) {
const pi = ((py + dy) * canvas.width + (px + dx2)) * 4;
if (pi + 3 >= d.length) continue;
d[pi] = r; d[pi + 1] = g; d[pi + 2] = b; d[pi + 3] = 255;
}
}
}
}
ctx.putImageData(img, 0, 0);
}
// Main animation loop — run 3 FDTD steps per frame for speed
let rafId = null;
function step() {
if (!running) return;
for (let i = 0; i < 3; i++) { updateH(); updateE(); stepCount++; }
render();
document.getElementById('status').textContent = i18nFmt('Step %n%', { n: stepCount });
rafId = requestAnimationFrame(step);
}
// Wire up slider and button event listeners
const slider = document.getElementById('slitSlider');
const slitVal = document.getElementById('slitVal');
slider.addEventListener('input', () => {
slitVal.textContent = slider.value;
});
document.getElementById('btnReset').addEventListener('click', () => {
cancelAnimationFrame(rafId);
resetSim(parseInt(slider.value, 10));
render();
document.getElementById('status').textContent = 'Simulation running…';
if (running) rafId = requestAnimationFrame(step);
});
const toggleBtn = document.getElementById('btnToggle');
toggleBtn.addEventListener('click', () => {
running = !running;
toggleBtn.textContent = running ? 'Pause' : 'Resume';
if (running) rafId = requestAnimationFrame(step);
});
// Initialize simulation and start animation
resetSim(slitWidth);
render();
rafId = requestAnimationFrame(step);
Comments
Loading comments...