physics simulation

EM INDUCTION

Interactive exploration of Faraday's Law, magnetic flux, and induced EMF across four classic scenarios.

1.0 T
0.5 m/s
10
1.0 m²
3.0 Ω
Active Equations
EMF = -dΦ/dt
Φ = NBA·cos(θ)
I = EMF / R
Magnetic Flux (Wb)
Induced EMF (V)
Induced Current (A)
Power Dissipated (W)
Galvanometer
0.0 mA
Induced Current Direction
Out of page
Faraday's Law: An induced EMF is generated when magnetic flux through a loop changes with time.

Lenz's Law: The induced current flows in a direction to oppose the change in magnetic flux (its own magnetic field opposes the cause).

Current Direction: Use the right-hand rule — curl fingers in the direction of current, thumb points in direction of magnetic field created by the coil.
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// ============ SIMULATION STATE ============
    const state = {
      scenario: 0,
      time: 0,
      playing: true,
      direction: 1,
      B: 1.0,
      velocity: 0.5,
      N: 10,
      A: 1.0,
      R: 3.0,
      history: {
        flux: [],
        emf: [],
        current: [],
        power: []
      },
      maxHistoryLength: 200,
      simSpeed: 1
    };

    // Canvas references
    const canvas = document.getElementById('simCanvas');
    const ctx = canvas.getContext('2d');
    const fluxCanvas = document.getElementById('fluxGraph');
    const emfCanvas = document.getElementById('emfGraph');
    const currentCanvas = document.getElementById('currentGraph');
    const powerCanvas = document.getElementById('powerGraph');
    const galvanometer = document.getElementById('galvanometer');

    const fluxCtx = fluxCanvas.getContext('2d');
    const emfCtx = emfCanvas.getContext('2d');
    const currentCtx = currentCanvas.getContext('2d');
    const powerCtx = powerCanvas.getContext('2d');
    const galCtx = galvanometer.getContext('2d');

    // ============ EVENT LISTENERS ============
    document.querySelectorAll('.tab').forEach(tab => {
      tab.addEventListener('click', (e) => {
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        e.target.classList.add('active');
        state.scenario = parseInt(e.target.dataset.scenario);
        state.time = 0;
        state.history = { flux: [], emf: [], current: [], power: [] };
        updateEquations();
      });
    });

    document.getElementById('bFieldSlider').addEventListener('input', (e) => {
      state.B = parseFloat(e.target.value);
      document.getElementById('bFieldValue').textContent = state.B.toFixed(1) + ' T';
    });

    document.getElementById('velocitySlider').addEventListener('input', (e) => {
      state.velocity = parseFloat(e.target.value);
      document.getElementById('velocityValue').textContent = state.velocity.toFixed(2) + ' m/s';
    });

    document.getElementById('turnsSlider').addEventListener('input', (e) => {
      state.N = parseInt(e.target.value);
      document.getElementById('turnsValue').textContent = state.N;
    });

    document.getElementById('areaSlider').addEventListener('input', (e) => {
      state.A = parseFloat(e.target.value);
      document.getElementById('areaValue').textContent = state.A.toFixed(1) + ' m²';
    });

    document.getElementById('resistanceSlider').addEventListener('input', (e) => {
      state.R = parseFloat(e.target.value);
      document.getElementById('resistanceValue').textContent = state.R.toFixed(1) + ' Ω';
    });

    document.getElementById('simSpeedSlider').addEventListener('input', (e) => {
      state.simSpeed = parseFloat(e.target.value);
      document.getElementById('simSpeedValue').textContent = state.simSpeed.toFixed(1);
    });

    document.getElementById('playBtn').addEventListener('click', () => {
      state.playing = !state.playing;
      document.getElementById('playBtn').textContent = state.playing ? 'Pause' : 'Play';
    });

    document.getElementById('resetBtn').addEventListener('click', () => {
      state.time = 0;
      state.history = { flux: [], emf: [], current: [], power: [] };
    });

    document.getElementById('directionBtn').addEventListener('click', (e) => {
      state.direction = 1;
      document.getElementById('directionBtn').classList.add('active');
      document.getElementById('reverseBtn').classList.remove('active');
      e.target.classList.add('active');
    });

    document.getElementById('reverseBtn').addEventListener('click', (e) => {
      state.direction = -1;
      document.getElementById('reverseBtn').classList.add('active');
      document.getElementById('directionBtn').classList.remove('active');
      e.target.classList.add('active');
    });

    // ============ PHYSICS CALCULATIONS ============
    function calculatePhysics() {
      const t = state.time;
      let flux = 0, emf = 0, current = 0, power = 0;

      if (state.scenario === 0) {
        // Magnet & Coil
        const x = state.velocity * state.direction * t;
        const d = Math.abs(x);
        // Gaussian flux profile as magnet approaches
        flux = state.N * state.A * state.B * Math.exp(-d * d / 2);
        // EMF from -dΦ/dt
        emf = state.N * state.A * state.B * d * Math.exp(-d * d / 2) * state.velocity * state.direction * 2;
      } else if (state.scenario === 1) {
        // Rotating Loop: Φ = NBA·cos(ωt)
        const omega = state.velocity * 3; // Angular velocity
        const theta = omega * t * state.direction;
        flux = state.N * state.B * state.A * Math.cos(theta);
        // EMF = -dΦ/dt = NBA·ω·sin(ωt)
        emf = state.N * state.B * state.A * omega * Math.sin(theta);
      } else if (state.scenario === 2) {
        // Changing B field
        const B_var = state.B * (0.5 + 0.5 * Math.sin(state.velocity * state.direction * t));
        flux = state.N * state.A * B_var;
        // dB/dt
        emf = state.N * state.A * state.B * state.velocity * state.direction * 0.5 * Math.cos(state.velocity * state.direction * t);
      } else if (state.scenario === 3) {
        // Moving Rod: EMF = BvL
        const L = Math.sqrt(state.A); // Rod length
        emf = state.B * state.velocity * state.direction * L;
        flux = state.B * state.A;
      }

      // Current from Ohm's law
      current = emf / state.R;
      power = current * current * state.R;

      return { flux, emf, current, power };
    }

    function updateEquations() {
      const scenarios = [
        { emf: 'EMF = -dΦ/dt', flux: 'Φ = NBA·e^(-d²/2)', current: 'I = EMF / R' },
        { emf: 'EMF = NBAω·sin(ωt)', flux: 'Φ = NBA·cos(ωt)', current: 'I = EMF / R' },
        { emf: 'EMF = NA·dB/dt', flux: 'Φ = NA·B(t)', current: 'I = EMF / R' },
        { emf: 'EMF = BvL', flux: 'Φ = BA', current: 'I = EMF / R' }
      ];

      const s = scenarios[state.scenario];
      document.getElementById('eqEmf').textContent = s.emf;
      document.getElementById('eqFlux').textContent = s.flux;
    }

    // ============ RENDERING ============
    function drawMagnetCoil() {
      const t = state.time;
      const x = state.velocity * state.direction * t * 50 - 150;

      // Draw magnet
      ctx.fillStyle = '#ff2200';
      ctx.fillRect(x, 150, 40, 120);

      ctx.fillStyle = '#ff4420';
      ctx.fillRect(x, 150, 20, 120);
      ctx.fillStyle = '#0a0a0a';
      ctx.font = 'bold 12px "Bebas Neue"';
      ctx.textAlign = 'center';
      ctx.fillText('N', x + 10, 210);
      ctx.fillText('S', x + 30, 210);

      // Draw magnetic field lines
      ctx.strokeStyle = 'rgba(255, 34, 0, 0.3)';
      ctx.lineWidth = 1;
      for (let i = -5; i <= 5; i++) {
        ctx.beginPath();
        for (let px = x - 100; px < x + 150; px += 5) {
          const d = Math.sqrt((px - x - 20) ** 2 + (i * 30) ** 2);
          const offset = 30 * Math.sin(d / 20) * Math.exp(-d / 100);
          const py = i * 30 + offset;
          if (px === x - 100) ctx.moveTo(px, py);
          else ctx.lineTo(px, py);
        }
        ctx.stroke();
      }

      // Draw coil
      ctx.strokeStyle = '#e8e0d5';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.ellipse(300, 180, 60, 30, 0, 0, Math.PI * 2);
      ctx.stroke();

      // Draw coil turns
      for (let i = 0; i < 4; i++) {
        ctx.beginPath();
        ctx.ellipse(300, 180 + i * 20, 60, 10, 0, 0, Math.PI * 2);
        ctx.stroke();
      }

      // Draw flux value
      const { flux } = calculatePhysics();
      ctx.fillStyle = '#00c896';
      ctx.font = '14px "DM Mono"';
      ctx.textAlign = 'left';
      ctx.fillText(`Φ = ${flux.toFixed(2)} Wb`, 50, 430);
    }

    function drawRotatingLoop() {
      const omega = state.velocity * 3;
      const theta = omega * state.time * state.direction;

      ctx.save();
      ctx.translate(250, 250);

      // Draw B field
      ctx.fillStyle = 'rgba(255, 136, 0, 0.15)';
      ctx.beginPath();
      for (let x = -150; x <= 150; x += 20) {
        for (let y = -150; y <= 150; y += 20) {
          ctx.rect(x - 3, y - 3, 6, 6);
        }
      }
      ctx.fill();

      // Draw rotating coil in isometric
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 2;
      ctx.rotate(theta);

      // Front face
      ctx.beginPath();
      ctx.moveTo(-60, -40);
      ctx.lineTo(60, -40);
      ctx.lineTo(60, 40);
      ctx.lineTo(-60, 40);
      ctx.closePath();
      ctx.stroke();

      // Back edge
      ctx.beginPath();
      ctx.moveTo(-60, -40);
      ctx.lineTo(-50, -35);
      ctx.lineTo(70, -35);
      ctx.lineTo(60, -40);
      ctx.stroke();

      ctx.restore();

      // Draw flux and EMF
      const { flux, emf } = calculatePhysics();
      ctx.fillStyle = '#00c896';
      ctx.font = '14px "DM Mono"';
      ctx.textAlign = 'left';
      ctx.fillText(`Φ = ${flux.toFixed(2)} Wb`, 50, 430);
      ctx.fillStyle = '#ff2200';
      ctx.fillText(`EMF = ${emf.toFixed(2)} V`, 50, 455);
    }

    function drawChangingB() {
      const B_var = state.B * (0.5 + 0.5 * Math.sin(state.velocity * state.direction * state.time));
      const intensity = Math.abs(B_var);

      // Draw changing field
      ctx.fillStyle = `rgba(255, 136, 0, ${intensity / 2})`;
      ctx.beginPath();
      for (let x = 100; x <= 400; x += 15) {
        for (let y = 150; y <= 350; y += 15) {
          if (B_var > 0) {
            ctx.fillRect(x - 4, y - 4, 8, 8);
          } else {
            ctx.beginPath();
            ctx.arc(x, y, 4, 0, Math.PI * 2);
            ctx.fill();
          }
        }
      }

      // Draw coil
      ctx.strokeStyle = '#e8e0d5';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.ellipse(250, 250, 80, 100, 0, 0, Math.PI * 2);
      ctx.stroke();

      // Draw induced current arrows
      const { current } = calculatePhysics();
      if (Math.abs(current) > 0.01) {
        ctx.strokeStyle = current > 0 ? '#00c896' : '#ff5555';
        ctx.lineWidth = 2;
        const dir = current > 0 ? 1 : -1;
        for (let i = 0; i < 4; i++) {
          const angle = (i / 4) * Math.PI * 2;
          const x1 = 250 + 80 * Math.cos(angle);
          const y1 = 250 + 100 * Math.sin(angle);
          const arrowLen = 15;
          const nextAngle = angle + (dir * 0.3);
          const x2 = x1 + arrowLen * Math.cos(nextAngle);
          const y2 = y1 + arrowLen * Math.sin(nextAngle);
          ctx.beginPath();
          ctx.moveTo(x1, y1);
          ctx.lineTo(x2, y2);
          ctx.stroke();
          // Arrowhead
          ctx.beginPath();
          ctx.moveTo(x2, y2);
          ctx.lineTo(x2 - 5 * Math.cos(nextAngle - 0.5), y2 - 5 * Math.sin(nextAngle - 0.5));
          ctx.lineTo(x2 - 5 * Math.cos(nextAngle + 0.5), y2 - 5 * Math.sin(nextAngle + 0.5));
          ctx.closePath();
          ctx.fill();
        }
      }

      const { flux, emf } = calculatePhysics();
      ctx.fillStyle = '#00c896';
      ctx.font = '14px "DM Mono"';
      ctx.textAlign = 'left';
      ctx.fillText(`B = ${B_var.toFixed(2)} T`, 50, 430);
      ctx.fillStyle = '#ff2200';
      ctx.fillText(`EMF = ${emf.toFixed(2)} V`, 50, 455);
    }

    function drawMovingRod() {
      const x = state.velocity * state.direction * state.time * 100;

      // Draw B field (into page)
      ctx.fillStyle = 'rgba(100, 150, 255, 0.1)';
      for (let i = 0; i < 10; i++) {
        for (let j = 0; j < 10; j++) {
          ctx.beginPath();
          ctx.arc(100 + i * 40, 150 + j * 40, 3, 0, Math.PI * 2);
          ctx.fill();
          ctx.font = '10px "DM Mono"';
          ctx.fillStyle = 'rgba(100, 150, 255, 0.3)';
          ctx.textAlign = 'center';
          ctx.fillText('⊗', 100 + i * 40, 155 + j * 40);
        }
      }

      // Draw rails
      ctx.strokeStyle = '#555555';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(100, 200);
      ctx.lineTo(400, 200);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(100, 320);
      ctx.lineTo(400, 320);
      ctx.stroke();

      // Draw moving rod
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 4;
      ctx.beginPath();
      ctx.moveTo(200 + x, 200);
      ctx.lineTo(200 + x, 320);
      ctx.stroke();

      // Draw resistance
      ctx.strokeStyle = '#e8e0d5';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(100, 320);
      ctx.lineTo(120, 330);
      ctx.lineTo(140, 310);
      ctx.lineTo(160, 330);
      ctx.lineTo(180, 310);
      ctx.lineTo(200, 320);
      ctx.stroke();

      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.textAlign = 'center';
      ctx.fillText('R', 150, 350);

      const { emf, current } = calculatePhysics();
      ctx.fillStyle = '#ff2200';
      ctx.font = '14px "DM Mono"';
      ctx.textAlign = 'left';
      ctx.fillText(`EMF = ${emf.toFixed(2)} V`, 50, 430);
      ctx.fillStyle = '#00c896';
      ctx.fillText(`I = ${current.toFixed(3)} A`, 50, 455);
    }

    function drawFrame() {
      // Clear canvas
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.strokeStyle = '#1e1e1e';
      ctx.strokeRect(0, 0, canvas.width, canvas.height);

      // Draw based on scenario
      if (state.scenario === 0) drawMagnetCoil();
      else if (state.scenario === 1) drawRotatingLoop();
      else if (state.scenario === 2) drawChangingB();
      else if (state.scenario === 3) drawMovingRod();
    }

    function drawGraph(canvasEl, data, color, yMin = -5, yMax = 5) {
      const gCtx = canvasEl.getContext('2d');
      const w = canvasEl.width;
      const h = canvasEl.height;

      gCtx.fillStyle = '#0a0a0a';
      gCtx.fillRect(0, 0, w, h);
      gCtx.strokeStyle = '#1e1e1e';
      gCtx.strokeRect(0, 0, w, h);

      if (data.length < 2) return;

      // Draw grid
      gCtx.strokeStyle = '#1e1e1e';
      gCtx.lineWidth = 0.5;
      for (let i = 0; i <= 4; i++) {
        const y = (i / 4) * h;
        gCtx.beginPath();
        gCtx.moveTo(0, y);
        gCtx.lineTo(w, y);
        gCtx.stroke();
      }

      // Draw data line
      gCtx.strokeStyle = color;
      gCtx.lineWidth = 2;
      gCtx.beginPath();
      for (let i = 0; i < data.length; i++) {
        const x = (i / data.length) * w;
        const y = h - ((data[i] - yMin) / (yMax - yMin)) * h;
        if (i === 0) gCtx.moveTo(x, y);
        else gCtx.lineTo(x, y);
      }
      gCtx.stroke();

      // Draw axis labels
      gCtx.fillStyle = '#555555';
      gCtx.font = '10px "DM Mono"';
      gCtx.textAlign = 'right';
      gCtx.fillText(yMax.toFixed(1), w - 5, 12);
      gCtx.fillText(yMin.toFixed(1), w - 5, h - 2);
    }

    function drawGalvanometer(currentVal) {
      const w = galvanometer.width;
      const h = galvanometer.height;
      const centerX = w / 2;
      const centerY = h / 2;

      galCtx.fillStyle = '#0a0a0a';
      galCtx.fillRect(0, 0, w, h);

      // Draw gauge body
      galCtx.strokeStyle = '#1e1e1e';
      galCtx.lineWidth = 2;
      galCtx.beginPath();
      galCtx.arc(centerX, centerY, 50, 0, Math.PI * 2);
      galCtx.stroke();

      // Draw scale
      galCtx.strokeStyle = '#555555';
      galCtx.lineWidth = 1;
      for (let i = -180; i <= 180; i += 30) {
        const rad = (i * Math.PI) / 180;
        const x1 = centerX + 45 * Math.cos(rad);
        const y1 = centerY + 45 * Math.sin(rad);
        const x2 = centerX + 50 * Math.cos(rad);
        const y2 = centerY + 50 * Math.sin(rad);
        galCtx.beginPath();
        galCtx.moveTo(x1, y1);
        galCtx.lineTo(x2, y2);
        galCtx.stroke();
      }

      // Draw needle
      const needleAngle = Math.max(-160, Math.min(160, currentVal * 20)) * (Math.PI / 180);
      galCtx.strokeStyle = currentVal > 0 ? '#00c896' : currentVal < 0 ? '#ff5555' : '#555555';
      galCtx.lineWidth = 3;
      galCtx.beginPath();
      galCtx.moveTo(centerX, centerY);
      galCtx.lineTo(
        centerX + 40 * Math.cos(needleAngle),
        centerY + 40 * Math.sin(needleAngle)
      );
      galCtx.stroke();

      // Center dot
      galCtx.fillStyle = '#e8e0d5';
      galCtx.beginPath();
      galCtx.arc(centerX, centerY, 4, 0, Math.PI * 2);
      galCtx.fill();
    }

    // ============ ANIMATION LOOP ============
    function animate() {
      if (state.playing) {
        state.time += 0.05 * state.simSpeed;
      }

      const { flux, emf, current, power } = calculatePhysics();

      // Update history
      state.history.flux.push(flux);
      state.history.emf.push(emf);
      state.history.current.push(current);
      state.history.power.push(power);

      if (state.history.flux.length > state.maxHistoryLength) {
        state.history.flux.shift();
        state.history.emf.shift();
        state.history.current.shift();
        state.history.power.shift();
      }

      // Draw main canvas
      drawFrame();

      // Draw graphs
      drawGraph(fluxCanvas, state.history.flux, '#00c896', -3, 3);
      drawGraph(emfCanvas, state.history.emf, '#ff2200', -5, 5);
      drawGraph(currentCanvas, state.history.current, '#00c896', -2, 2);
      drawGraph(powerCanvas, state.history.power, '#f5c518', 0, 2);

      // Update galvanometer
      drawGalvanometer(current);
      document.getElementById('galValue').textContent = (current * 1000).toFixed(1) + ' mA';

      // Update current direction
      const dir = current > 0 ? '⊙' : current < 0 ? '⊗' : '·';
      const dirLabel = current > 0 ? 'Out of page' : current < 0 ? 'Into page' : 'No current';
      document.getElementById('currentDir').textContent = dir;
      document.getElementById('currentDirLabel').textContent = dirLabel;

      requestAnimationFrame(animate);
    }

    // Initialize
    updateEquations();
    animate();