Physics Simulation
Magnetic Field
Interactive visualization of magnetic fields using the Biot-Savart law. Simulate current-carrying conductors and observe real-time field patterns.
Position: — | Field: — mT
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let animationFrameId;
// State
let state = {
config: 'infinite-wire',
current: 2,
direction: 'out',
displayMode: 'arrows',
viewPlane: 'xy',
showArrows: true,
showCurrent: true,
showGridlines: false,
wirePos: { x: 0, y: 0 },
dragging: false,
mousePos: { x: 0, y: 0 },
time: 0
};
// Constants (SI units)
const MU_0 = 4 * Math.PI * 1e-7; // T⋅m/A
const SCALE = 30; // pixels per meter
const GRID_SIZE = 30; // grid points
const ARROW_SCALE = 15; // pixel scale for arrow length
// Initialize canvas
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width - 32; // account for padding
canvas.height = Math.min(600, canvas.width * 0.75);
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Configuration buttons
document.querySelectorAll('.config-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.config-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.config = btn.dataset.config;
updatePanelVisibility();
});
});
// Direction buttons
document.querySelectorAll('.direction-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.direction-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.direction = btn.dataset.dir;
});
});
// Display mode buttons
document.querySelectorAll('.display-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.display-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.displayMode = btn.dataset.display;
});
});
// Plane buttons
document.querySelectorAll('.plane-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.plane-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.viewPlane = btn.dataset.plane;
});
});
// Current slider
document.getElementById('current').addEventListener('input', (e) => {
state.current = parseFloat(e.target.value);
document.getElementById('currentValue').textContent = state.current.toFixed(1);
});
// Checkboxes
document.getElementById('showArrows').addEventListener('change', (e) => {
state.showArrows = e.target.checked;
});
document.getElementById('showCurrent').addEventListener('change', (e) => {
state.showCurrent = e.target.checked;
});
document.getElementById('showGridlines').addEventListener('change', (e) => {
state.showGridlines = e.target.checked;
});
// Mouse events
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
state.mousePos.x = e.clientX - rect.left;
state.mousePos.y = e.clientY - rect.top;
if (state.dragging && state.config === 'infinite-wire') {
const cx = canvas.width / 2;
const cy = canvas.height / 2;
state.wirePos.x = (state.mousePos.x - cx) / SCALE;
state.wirePos.y = (state.mousePos.y - cy) / SCALE;
}
updateMouseInfo();
});
canvas.addEventListener('mousedown', (e) => {
if (state.config === 'infinite-wire') {
state.dragging = true;
}
});
canvas.addEventListener('mouseup', () => {
state.dragging = false;
});
canvas.addEventListener('mouseleave', () => {
state.dragging = false;
});
function updatePanelVisibility() {
const directionField = document.getElementById('directionField');
const planeField = document.getElementById('planeField');
if (['infinite-wire', 'two-wires', 'solenoid'].includes(state.config)) {
directionField.style.display = 'block';
} else {
directionField.style.display = 'none';
}
if (['loop', 'helmholtz'].includes(state.config)) {
planeField.style.display = 'block';
} else {
planeField.style.display = 'none';
}
}
// Biot-Savart calculations
function computeFieldInfiniteWire(x, y) {
const dx = x - state.wirePos.x;
const dy = y - state.wirePos.y;
const r = Math.sqrt(dx * dx + dy * dy);
if (r < 0.05) return { bx: 0, by: 0 };
const B = (MU_0 * state.current) / (2 * Math.PI * r);
const sign = state.direction === 'out' ? 1 : -1;
return {
bx: -sign * B * dy / r,
by: sign * B * dx / r
};
}
function computeFieldTwoWires(x, y) {
const sep = 1;
const field1 = computeFieldAtPoint(x, y, -sep/2, 0, state.current, state.direction);
const field2 = computeFieldAtPoint(x, y, sep/2, 0, state.current, state.direction === 'out' ? 'in' : 'out');
return {
bx: field1.bx + field2.bx,
by: field1.by + field2.by
};
}
function computeFieldAtPoint(x, y, wireX, wireY, current, direction) {
const dx = x - wireX;
const dy = y - wireY;
const r = Math.sqrt(dx * dx + dy * dy);
if (r < 0.05) return { bx: 0, by: 0 };
const B = (MU_0 * current) / (2 * Math.PI * r);
const sign = direction === 'out' ? 1 : -1;
return {
bx: -sign * B * dy / r,
by: sign * B * dx / r
};
}
function computeFieldCircularLoop(x, y, z, loopRadius = 1) {
const r_sq = x * x + y * y;
const z_sq = z * z;
const denom = (loopRadius * loopRadius + z_sq) ** 1.5;
if (denom < 1e-6) return { bx: 0, by: 0, bz: 0 };
const B_z = (MU_0 * state.current * loopRadius * loopRadius) / (2 * denom);
const B_r = (MU_0 * state.current * loopRadius * z * Math.sqrt(r_sq)) / (2 * denom * (r_sq || 1e-6));
const r = Math.sqrt(r_sq) || 1e-6;
return {
bx: B_r * x / r,
by: B_r * y / r,
bz: B_z
};
}
function computeFieldSolenoid(x, y) {
const radius = 0.8;
const length = 3;
let B_z = 0;
for (let i = -length/2; i < length/2; i += 0.2) {
const loopField = computeFieldCircularLoop(x, y, i, radius);
B_z += loopField.bz;
}
return { bx: 0, by: 0, bz: B_z * 0.2 };
}
function computeFieldHelmholtz(x, y, z) {
const R = 1;
const field1 = computeFieldCircularLoop(x, y, z - R/2, R);
const field2 = computeFieldCircularLoop(x, y, z + R/2, R);
return {
bx: field1.bx + field2.bx,
by: field1.by + field2.by,
bz: field1.bz + field2.bz
};
}
function getFieldAtPosition(x, y) {
switch (state.config) {
case 'infinite-wire':
return computeFieldInfiniteWire(x, y);
case 'two-wires':
return computeFieldTwoWires(x, y);
case 'loop':
return computeFieldCircularLoop(x, y, 0);
case 'solenoid':
return computeFieldSolenoid(x, y);
case 'helmholtz':
return computeFieldHelmholtz(x, y, 0);
default:
return { bx: 0, by: 0 };
}
}
function getMagnitude(field) {
return Math.sqrt(field.bx * field.bx + field.by * field.by);
}
function getColor(magnitude) {
const normalized = Math.min(magnitude / 0.01, 1);
const hue = 240 * (1 - normalized); // blue to red
const saturation = 100;
const lightness = 30 + normalized * 20;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
function drawVectorField() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const gridSpacing = Math.floor(canvas.width / GRID_SIZE);
let maxField = 0;
const fieldValues = [];
// First pass: compute max field
for (let py = 0; py < canvas.height; py += gridSpacing) {
for (let px = 0; px < canvas.width; px += gridSpacing) {
const x = (px - cx) / SCALE;
const y = (py - cy) / SCALE;
const field = getFieldAtPosition(x, y);
const mag = getMagnitude(field);
fieldValues.push(mag);
maxField = Math.max(maxField, mag);
}
}
// Draw arrows
let idx = 0;
for (let py = 0; py < canvas.height; py += gridSpacing) {
for (let px = 0; px < canvas.width; px += gridSpacing) {
const x = (px - cx) / SCALE;
const y = (py - cy) / SCALE;
const field = getFieldAtPosition(x, y);
const mag = getMagnitude(field);
if (mag > 1e-6) {
const angle = Math.atan2(field.by, field.bx);
const arrowLen = Math.max(3, Math.log(mag * 1000 + 1) * 3);
const endX = px + Math.cos(angle) * arrowLen;
const endY = py + Math.sin(angle) * arrowLen;
const color = getColor(mag);
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(endX, endY);
ctx.stroke();
// Arrowhead
const headlen = 4;
ctx.beginPath();
ctx.moveTo(endX, endY);
ctx.lineTo(endX - headlen * Math.cos(angle - Math.PI / 6), endY - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(endX - headlen * Math.cos(angle + Math.PI / 6), endY - headlen * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
}
idx++;
}
}
// Draw wire
drawWireConfiguration();
}
function drawFieldLines() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
if (state.config === 'infinite-wire') {
const wireX = cx + state.wirePos.x * SCALE;
const wireY = cy + state.wirePos.y * SCALE;
// Draw concentric circles
ctx.strokeStyle = '#444444';
ctx.lineWidth = 1;
for (let r = SCALE * 0.3; r < Math.max(canvas.width, canvas.height); r += SCALE * 0.4) {
ctx.beginPath();
ctx.arc(wireX, wireY, r, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.fillStyle = '#555555';
ctx.beginPath();
ctx.arc(wireX, wireY, 4, 0, 2 * Math.PI);
ctx.fill();
}
drawWireConfiguration();
}
function drawHeatmap() {
const imageData = ctx.createImageData(canvas.width, canvas.height);
const data = imageData.data;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
let maxField = 0;
// Find max field
for (let i = 0; i < canvas.width; i += 2) {
for (let j = 0; j < canvas.height; j += 2) {
const x = (i - cx) / SCALE;
const y = (j - cy) / SCALE;
const field = getFieldAtPosition(x, y);
const mag = getMagnitude(field);
maxField = Math.max(maxField, mag);
}
}
// Draw heatmap
for (let i = 0; i < canvas.width; i += 2) {
for (let j = 0; j < canvas.height; j += 2) {
const x = (i - cx) / SCALE;
const y = (j - cy) / SCALE;
const field = getFieldAtPosition(x, y);
const mag = getMagnitude(field);
const normalized = maxField > 0 ? Math.min(mag / maxField, 1) : 0;
const hue = 240 * (1 - normalized);
const saturation = 100;
const lightness = 50 * normalized;
const rgb = hslToRgb(hue / 360, saturation / 100, lightness / 100);
for (let di = 0; di < 2 && i + di < canvas.width; di++) {
for (let dj = 0; dj < 2 && j + dj < canvas.height; dj++) {
const idx = ((j + dj) * canvas.width + (i + di)) * 4;
data[idx] = rgb.r;
data[idx + 1] = rgb.g;
data[idx + 2] = rgb.b;
data[idx + 3] = 255;
}
}
}
}
ctx.putImageData(imageData, 0, 0);
drawWireConfiguration();
}
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function drawWireConfiguration() {
const cx = canvas.width / 2;
const cy = canvas.height / 2;
ctx.save();
if (state.showGridlines) {
ctx.strokeStyle = '#1e1e1e';
ctx.lineWidth = 0.5;
for (let x = -10; x <= 10; x++) {
const px = cx + x * SCALE;
ctx.beginPath();
ctx.moveTo(px, 0);
ctx.lineTo(px, canvas.height);
ctx.stroke();
}
for (let y = -10; y <= 10; y++) {
const py = cy + y * SCALE;
ctx.beginPath();
ctx.moveTo(0, py);
ctx.lineTo(canvas.width, py);
ctx.stroke();
}
}
if (state.config === 'infinite-wire') {
drawInfiniteWire(cx, cy);
} else if (state.config === 'two-wires') {
drawTwoWires(cx, cy);
} else if (state.config === 'loop') {
drawLoop(cx, cy);
} else if (state.config === 'solenoid') {
drawSolenoid(cx, cy);
} else if (state.config === 'helmholtz') {
drawHelmholtzCoil(cx, cy);
}
if (state.showCurrent && (state.config === 'infinite-wire' || state.config === 'two-wires')) {
drawAnimatedCurrent();
}
ctx.restore();
}
function drawInfiniteWire(cx, cy) {
const x = cx + state.wirePos.x * SCALE;
const y = cy + state.wirePos.y * SCALE;
ctx.fillStyle = '#ff2200';
ctx.beginPath();
ctx.arc(x, y, 6, 0, 2 * Math.PI);
ctx.fill();
// Direction symbol
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
ctx.font = 'bold 14px Arial';
ctx.fillStyle = '#ff2200';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(state.direction === 'out' ? '⊙' : '⊗', x, y);
}
function drawTwoWires(cx, cy) {
ctx.fillStyle = '#ff2200';
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
// Wire 1
const x1 = cx - SCALE * 0.5;
const y1 = cy;
ctx.beginPath();
ctx.arc(x1, y1, 6, 0, 2 * Math.PI);
ctx.fill();
ctx.font = 'bold 12px Arial';
ctx.fillStyle = '#ff2200';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(state.direction === 'out' ? '⊙' : '⊗', x1, y1);
// Wire 2
const x2 = cx + SCALE * 0.5;
const y2 = cy;
ctx.beginPath();
ctx.arc(x2, y2, 6, 0, 2 * Math.PI);
ctx.fill();
ctx.fillText(state.direction === 'out' ? '⊗' : '⊙', x2, y2);
}
function drawLoop(cx, cy) {
const radius = SCALE * 0.8;
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = '#ff2200';
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('I', cx - radius - 10, cy);
}
function drawSolenoid(cx, cy) {
const wireRadius = SCALE * 0.6;
const numCoils = 5;
const spacing = SCALE * 0.3;
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
for (let i = 0; i < numCoils; i++) {
const x = cx - (numCoils - 1) * spacing / 2 + i * spacing;
ctx.beginPath();
ctx.arc(x, cy, wireRadius, 0, 2 * Math.PI);
ctx.stroke();
}
// Axis line
ctx.strokeStyle = '#555555';
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(cx - numCoils * spacing, cy);
ctx.lineTo(cx + numCoils * spacing, cy);
ctx.stroke();
ctx.setLineDash([]);
}
function drawHelmholtzCoil(cx, cy) {
const radius = SCALE * 0.7;
const sep = SCALE * 0.7;
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
// Coil 1
ctx.beginPath();
ctx.arc(cx - sep/2, cy, radius, 0, 2 * Math.PI);
ctx.stroke();
// Coil 2
ctx.beginPath();
ctx.arc(cx + sep/2, cy, radius, 0, 2 * Math.PI);
ctx.stroke();
// Axis
ctx.strokeStyle = '#555555';
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(cx - radius - sep, cy);
ctx.lineTo(cx + radius + sep, cy);
ctx.stroke();
ctx.setLineDash([]);
}
function drawAnimatedCurrent() {
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const speed = state.time * 0.02;
ctx.fillStyle = '#ffaa00';
if (state.config === 'infinite-wire') {
const wireX = cx + state.wirePos.x * SCALE;
const wireY = cy + state.wirePos.y * SCALE;
// Draw moving charge particles
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * 2 * Math.PI + speed;
const r = SCALE * 0.3;
const px = wireX + Math.cos(angle) * r;
const py = wireY + Math.sin(angle) * r;
ctx.beginPath();
ctx.arc(px, py, 2, 0, 2 * Math.PI);
ctx.fill();
}
} else if (state.config === 'two-wires') {
for (let wire = 0; wire < 2; wire++) {
const wireX = cx + (wire === 0 ? -1 : 1) * SCALE * 0.5;
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * 2 * Math.PI + speed;
const r = SCALE * 0.25;
const px = wireX + Math.cos(angle) * r;
const py = cy + Math.sin(angle) * r;
ctx.beginPath();
ctx.arc(px, py, 1.5, 0, 2 * Math.PI);
ctx.fill();
}
}
}
}
function updateMouseInfo() {
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const x = (state.mousePos.x - cx) / SCALE;
const y = (state.mousePos.y - cy) / SCALE;
document.getElementById('mousePos').textContent = `(${x.toFixed(2)}, ${y.toFixed(2)})`;
const field = getFieldAtPosition(x, y);
const mag = getMagnitude(field) * 1000; // convert to mT
document.getElementById('fieldValue').textContent = mag.toFixed(3);
}
function updateStatistics() {
const cx = canvas.width / 2;
const cy = canvas.height / 2;
// Center field
let centerField = getFieldAtPosition(0, 0);
let centerMag = getMagnitude(centerField) * 1000;
// Max and uniformity
let maxMag = 0;
let sumMag = 0;
let count = 0;
for (let x = -2; x <= 2; x += 0.5) {
for (let y = -2; y <= 2; y += 0.5) {
const field = getFieldAtPosition(x, y);
const mag = getMagnitude(field);
maxMag = Math.max(maxMag, mag);
sumMag += mag;
count++;
}
}
const avgMag = sumMag / count;
const uniformity = count > 0 ? (avgMag / maxMag) * 100 : 0;
document.getElementById('centerField').textContent = centerMag.toFixed(2);
document.getElementById('maxField').textContent = (maxMag * 1000).toFixed(2);
document.getElementById('uniformity').textContent = uniformity.toFixed(1);
}
function animate() {
state.time++;
switch (state.displayMode) {
case 'arrows':
drawVectorField();
break;
case 'lines':
drawFieldLines();
break;
case 'heatmap':
drawHeatmap();
break;
}
updateStatistics();
animationFrameId = requestAnimationFrame(animate);
}
// Start animation
updatePanelVisibility();
animate();