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();