Education
Cyclotron Motion
Interactive simulation of charged particles in magnetic and electric fields. Explore circular orbits, E×B drift, particle acceleration, and magnetic confinement.
+1
Particle
Force
Velocity
Trail
Current Scenario:
Shows circular motion of a charged particle in a uniform magnetic field perpendicular to its velocity.
0.00
Orbital Radius (m)
0.00
Cyclotron Freq (Hz)
0.00
Period (s)
0.00
Kinetic Energy (J)
Physics Equations:
Lorentz Force: F = q(E + v×B)
Cyclotron Radius: r = mv/(|q|B)
Cyclotron Frequency: ω_c = |q|B/m
Period: T = 2π/ω_c
E×B Drift: v_drift = E×B/B²
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('simulationCanvas');
const ctx = canvas.getContext('2d');
// Get all UI elements
const scenarioSelect = document.getElementById('scenarioSelect');
const bFieldSlider = document.getElementById('bFieldSlider');
const eFieldSlider = document.getElementById('eFieldSlider');
const chargeNegBtn = document.getElementById('chargeNegBtn');
const chargePosBtn = document.getElementById('chargePosBtn');
const chargeInput = document.getElementById('chargeInput');
const chargeValue = document.getElementById('chargeValue');
const massSlider = document.getElementById('massSlider');
const speedSlider = document.getElementById('speedSlider');
const angleSlider = document.getElementById('angleSlider');
const resetBtn = document.getElementById('resetBtn');
const pauseBtn = document.getElementById('pauseBtn');
// Simulation state
let simState = {
scenario: 'pure_b',
bField: 1.0,
eField: 0,
charge: 1, // 1 = positive, -1 = negative
chargeAmount: 1,
mass: 1.0,
speed: 2.0,
angle: 0,
x: 275,
y: 275,
vx: 0,
vy: 0,
trail: [],
paused: false,
speedMult: 1,
time: 0,
deeVoltage: 0,
deePhase: 0,
deeAngle: 0,
gapCrossing: false
};
const maxTrailLength = 500;
// Update scenario description
function updateScenarioDescription() {
const descriptions = {
pure_b: 'Charged particle moves in a circular orbit due to the Lorentz force perpendicular to its velocity.',
crossed_eb: 'A uniform electric field is applied perpendicular to the magnetic field, causing the particle to drift in the E×B direction.',
cyclotron: 'Two D-shaped electrodes accelerate the particle at each gap crossing. The particle spirals outward as it gains energy.',
magnetic_bottle: 'Non-uniform magnetic field (stronger at ends) traps the particle, causing it to bounce back and forth.'
};
document.getElementById('scenarioDescription').textContent = descriptions[simState.scenario];
}
// Initialize particle position
function initializeParticle() {
const angle = (simState.angle * Math.PI) / 180;
simState.vx = simState.speed * Math.cos(angle);
simState.vy = simState.speed * Math.sin(angle);
simState.x = 275;
simState.y = 275;
simState.trail = [];
simState.time = 0;
simState.deePhase = 0;
simState.deeAngle = 0;
}
// Calculate cyclotron radius
function calculateRadius() {
if (simState.bField === 0) return 0;
const speed = Math.sqrt(simState.vx ** 2 + simState.vy ** 2);
return (simState.mass * speed) / (Math.abs(simState.chargeAmount) * simState.bField);
}
// Calculate cyclotron frequency (rad/s)
function calculateFrequency() {
if (simState.bField === 0) return 0;
return (Math.abs(simState.chargeAmount) * simState.bField) / simState.mass;
}
// Calculate period
function calculatePeriod() {
const freq = calculateFrequency();
return freq === 0 ? 0 : (2 * Math.PI) / freq;
}
// Calculate kinetic energy
function calculateKineticEnergy() {
const speed = Math.sqrt(simState.vx ** 2 + simState.vy ** 2);
return 0.5 * simState.mass * speed ** 2;
}
// Get local B field (for magnetic bottle)
function getLocalBField(x, y) {
if (simState.scenario !== 'magnetic_bottle') {
return simState.bField;
}
// Magnetic field stronger at ends (y near 0 or 550), weaker in middle
const centerY = 275;
const distance = Math.abs(y - centerY);
const maxDistance = 275;
const ratio = distance / maxDistance;
return simState.bField * (0.5 + 1.5 * ratio * ratio);
}
// Update particle physics
function updateParticle(dt) {
const q = simState.chargeAmount * simState.charge;
const m = simState.mass;
let bField = getLocalBField(simState.x, simState.y);
let eField = simState.eField;
// Cyclotron scenario: apply voltage in gap
if (simState.scenario === 'cyclotron') {
const gapWidth = 30;
const gapAngle = Math.atan2(simState.y - 275, simState.x - 275);
const dee1Angle = 0;
const dee2Angle = Math.PI;
// Check if in gap
const angleDiff1 = Math.abs(gapAngle - dee1Angle);
const angleDiff2 = Math.abs(gapAngle - dee2Angle);
const minAngleDiff = Math.min(angleDiff1, angleDiff2);
if (minAngleDiff < 0.3) {
// Apply accelerating voltage
const voltageAmplitude = 1.5;
eField = voltageAmplitude;
}
}
// Lorentz force: F = q(E + v×B)
let fx = q * eField; // E field in y, but we'll use for acceleration
let fy = 0;
// v × B (B perpendicular to plane, pointing out)
fx += q * simState.vy * bField;
fy += -q * simState.vx * bField;
// Also add E field in y direction for crossed E×B
if (simState.scenario === 'crossed_eb') {
fy += q * eField;
}
// Acceleration
const ax = fx / m;
const ay = fy / m;
// Update velocity (Euler method)
simState.vx += ax * dt;
simState.vy += ay * dt;
// Update position
simState.x += simState.vx * dt;
simState.y += simState.vy * dt;
// Bounce off walls for magnetic bottle
if (simState.scenario === 'magnetic_bottle') {
const bounceMargin = 50;
if (simState.y < bounceMargin) {
simState.y = bounceMargin;
simState.vy = Math.abs(simState.vy);
}
if (simState.y > 550 - bounceMargin) {
simState.y = 550 - bounceMargin;
simState.vy = -Math.abs(simState.vy);
}
}
// Keep particle on canvas
simState.x = Math.max(20, Math.min(530, simState.x));
simState.y = Math.max(20, Math.min(530, simState.y));
// Add to trail
simState.trail.push({ x: simState.x, y: simState.y, t: simState.time });
if (simState.trail.length > maxTrailLength) {
simState.trail.shift();
}
simState.time += dt;
}
// Draw simulation
function draw() {
// Clear canvas
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw field background
if (simState.scenario === 'pure_b' || simState.scenario === 'crossed_eb' || simState.scenario === 'cyclotron') {
ctx.fillStyle = 'rgba(0, 100, 150, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
if (simState.scenario === 'magnetic_bottle') {
// Draw field gradient
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, 'rgba(100, 0, 150, 0.15)');
gradient.addColorStop(0.5, 'rgba(0, 100, 150, 0.05)');
gradient.addColorStop(1, 'rgba(100, 0, 150, 0.15)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Draw cyclotron dees
if (simState.scenario === 'cyclotron') {
ctx.strokeStyle = 'rgba(255, 34, 0, 0.3)';
ctx.lineWidth = 2;
const centerX = 275;
const centerY = 275;
const radius = 150;
// Dee 1 (top)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI, false);
ctx.stroke();
// Dee 2 (bottom)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, Math.PI, 2 * Math.PI, false);
ctx.stroke();
// Gap lines
ctx.strokeStyle = 'rgba(255, 34, 0, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(centerX - 15, centerY - 10);
ctx.lineTo(centerX - 15, centerY + 10);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX + 15, centerY - 10);
ctx.lineTo(centerX + 15, centerY + 10);
ctx.stroke();
}
// Draw trail
if (simState.trail.length > 1) {
for (let i = 0; i < simState.trail.length - 1; i++) {
const p1 = simState.trail[i];
const p2 = simState.trail[i + 1];
// Color fade based on trail age
const age = 1 - i / simState.trail.length;
const hue = (simState.time * 20 + i * 0.5) % 360;
ctx.strokeStyle = `hsla(${hue}, 100%, 50%, ${age * 0.6})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
}
// Draw particle
const particleRadius = 8;
const gradient = ctx.createRadialGradient(simState.x, simState.y, 0, simState.x, simState.y, particleRadius);
gradient.addColorStop(0, '#00ffff');
gradient.addColorStop(0.7, '#00d4ff');
gradient.addColorStop(1, 'rgba(0, 212, 255, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(simState.x, simState.y, particleRadius, 0, 2 * Math.PI);
ctx.fill();
// Draw velocity vector
const velocityScale = 30;
const vLength = Math.sqrt(simState.vx ** 2 + simState.vy ** 2);
if (vLength > 0.01) {
const vx = (simState.vx / vLength) * velocityScale;
const vy = (simState.vy / vLength) * velocityScale;
// Arrow shaft
ctx.strokeStyle = '#00c896';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(simState.x, simState.y);
ctx.lineTo(simState.x + vx, simState.y + vy);
ctx.stroke();
// Arrow head
const headlen = 6;
const angle = Math.atan2(vy, vx);
ctx.fillStyle = '#00c896';
ctx.beginPath();
ctx.moveTo(simState.x + vx, simState.y + vy);
ctx.lineTo(simState.x + vx - headlen * Math.cos(angle - Math.PI / 6), simState.y + vy - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(simState.x + vx - headlen * Math.cos(angle + Math.PI / 6), simState.y + vy - headlen * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
}
// Draw force vector (centripetal)
const forceLength = Math.sqrt(simState.vx ** 2 + simState.vy ** 2);
if (forceLength > 0.01 && simState.bField > 0) {
const q = simState.chargeAmount * simState.charge;
const bField = getLocalBField(simState.x, simState.y);
// Centripetal force perpendicular to velocity
const forceScale = 25;
const fx = (-simState.vy / forceLength) * forceScale * (q * bField > 0 ? 1 : -1);
const fy = (simState.vx / forceLength) * forceScale * (q * bField > 0 ? 1 : -1);
// Arrow shaft
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(simState.x, simState.y);
ctx.lineTo(simState.x + fx, simState.y + fy);
ctx.stroke();
// Arrow head
const headlen = 6;
const angle = Math.atan2(fy, fx);
ctx.fillStyle = '#ff2200';
ctx.beginPath();
ctx.moveTo(simState.x + fx, simState.y + fy);
ctx.lineTo(simState.x + fx - headlen * Math.cos(angle - Math.PI / 6), simState.y + fy - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(simState.x + fx - headlen * Math.cos(angle + Math.PI / 6), simState.y + fy - headlen * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
}
// Draw E field arrows (for crossed E×B)
if (simState.scenario === 'crossed_eb' && simState.eField > 0) {
ctx.strokeStyle = 'rgba(245, 197, 24, 0.3)';
ctx.lineWidth = 1;
const spacing = 60;
for (let y = 50; y < canvas.height; y += spacing) {
ctx.beginPath();
ctx.moveTo(50, y);
ctx.lineTo(50, y + 20);
ctx.stroke();
// Arrow head
ctx.fillStyle = 'rgba(245, 197, 24, 0.3)';
ctx.beginPath();
ctx.moveTo(50, y + 20);
ctx.lineTo(47, y + 14);
ctx.lineTo(53, y + 14);
ctx.closePath();
ctx.fill();
}
}
// Draw stats
ctx.fillStyle = '#555555';
ctx.font = '12px DM Mono';
let yPos = 25;
const speed = Math.sqrt(simState.vx ** 2 + simState.vy ** 2);
ctx.fillText(`v: ${speed.toFixed(2)}`, 10, yPos);
yPos += 15;
ctx.fillText(`r: ${calculateRadius().toFixed(2)}`, 10, yPos);
if (simState.scenario === 'cyclotron') {
ctx.fillText('◯', 510, 25);
}
}
// Update stats display
function updateStats() {
const radius = calculateRadius();
const frequency = calculateFrequency();
const period = calculatePeriod();
const energy = calculateKineticEnergy();
document.getElementById('radiusValue').textContent = radius.toFixed(3);
document.getElementById('frequencyValue').textContent = frequency.toFixed(3);
document.getElementById('periodValue').textContent = period.toFixed(4);
document.getElementById('energyValue').textContent = energy.toFixed(3);
}
// Animation loop
let lastTime = Date.now();
function animate() {
const now = Date.now();
const dt = Math.min((now - lastTime) / 1000, 0.05); // Cap dt at 50ms
lastTime = now;
if (!simState.paused) {
updateParticle(dt * 10 * simState.speedMult); // Scale time for visibility
}
draw();
updateStats();
requestAnimationFrame(animate);
}
// Event listeners
scenarioSelect.addEventListener('change', (e) => {
simState.scenario = e.target.value;
updateScenarioDescription();
initializeParticle();
});
bFieldSlider.addEventListener('input', (e) => {
simState.bField = parseFloat(e.target.value);
document.getElementById('bFieldValue').textContent = simState.bField.toFixed(1);
});
eFieldSlider.addEventListener('input', (e) => {
simState.eField = parseFloat(e.target.value);
document.getElementById('eFieldValue').textContent = simState.eField.toFixed(2);
});
chargeNegBtn.addEventListener('click', () => {
simState.charge = -1;
updateChargeDisplay();
});
chargePosBtn.addEventListener('click', () => {
simState.charge = 1;
updateChargeDisplay();
});
chargeInput.addEventListener('input', (e) => {
simState.chargeAmount = parseInt(e.target.value) || 1;
updateChargeDisplay();
});
function updateChargeDisplay() {
const sign = simState.charge === 1 ? '+' : '−';
chargeValue.textContent = sign + simState.chargeAmount;
}
massSlider.addEventListener('input', (e) => {
simState.mass = parseFloat(e.target.value);
document.getElementById('massValue').textContent = simState.mass.toFixed(1);
});
speedSlider.addEventListener('input', (e) => {
simState.speed = parseFloat(e.target.value);
document.getElementById('speedValue').textContent = simState.speed.toFixed(1);
initializeParticle();
});
angleSlider.addEventListener('input', (e) => {
simState.angle = parseInt(e.target.value);
document.getElementById('angleValue').textContent = simState.angle + '°';
initializeParticle();
});
resetBtn.addEventListener('click', () => {
initializeParticle();
simState.paused = false;
pauseBtn.textContent = 'Pause';
});
pauseBtn.addEventListener('click', () => {
simState.paused = !simState.paused;
pauseBtn.textContent = simState.paused ? 'Resume' : 'Pause';
});
document.getElementById('speedMultSlider').addEventListener('input', (e) => {
simState.speedMult = parseFloat(e.target.value);
document.getElementById('speedMultValue').textContent = simState.speedMult.toFixed(1) + '×';
});
// Initialize
initializeParticle();
updateScenarioDescription();
animate();