Temperature Unit
Home→ Planetary Weather→ Atmospheric Comparison→ About→ Launch 3D Solar System
Interactive Physics Laboratory

The Coriolis Effect Simulator

Deconstruct the apparent force that shapes hurricane rotations, ocean gyres, planetary jet streams, and ballistic trajectories. Switch seamlessly between stationary inertial space and rotating reference frames.

Coriolis Acceleration
ac = -2(ω × v)
Cross-product of rotation & velocity
Atmospheric Parameter
f = 2Ω sin(φ)
Zero at Equator • Maximum at Poles
Northern Hemisphere
Deflects Right
Counter-clockwise cyclonic storms
Southern Hemisphere
Deflects Left
Clockwise cyclonic storms
Celestial Presets:
Real-Time Physics Canvas
Click / Tap anywhere to fire test particle
Inertial Path (Straight)
Rotating Observer Path (Curved)

What Is the Coriolis Effect? Complete Physical Explanation

The Coriolis effect is the apparent deflection of moving objects when viewed from within a rotating reference frame. First described mathematically in 1835 by the French mathematician and mechanical engineer Gaspard-Gustave de Coriolis in connection with waterwheels and kinetic energy transfer, the effect is not caused by a physical contact force like friction or electromagnetism. Instead, it is an inertial or fictitious force that arises entirely because the observer’s coordinate system is continually accelerating and changing direction relative to deep space.

To understand why the Coriolis effect occurs on Earth, consider the speed of rotation at different latitudes. Earth is a sphere rotating eastward on its axis once every 24 hours (strictly, one sidereal day of 23 hours, 56 minutes, and 4.09 seconds). Because the circumference of Earth is greatest at the Equator (approximately 40,075 kilometers or 24,901 miles), a person standing at the Equator is traveling eastward at approximately 1,670 km/h (1,038 mph).

As you travel north or south toward the poles, the distance around Earth along a parallel of latitude shrinks by a factor of cos(latitude). At 60° North (near Oslo or Anchorage), the eastward rotational speed is cut in half to approximately 835 km/h (519 mph). At the North and South Poles themselves, the eastward tangential velocity drops to exactly zero.

ac = -2 (ω × v)
Where ac is the Coriolis acceleration vector, ω is the angular velocity vector of the rotating coordinate frame, and v is the velocity vector of the moving mass measured relative to the rotating frame.

When air, water, an aircraft, or a long-range artillery shell moves northward from the Equator toward the pole, it does not lose its initial eastward rotational momentum. As it travels over ground that is rotating eastward more slowly than itself, the object races ahead of the surface beneath it, resulting in a pronounced veer toward the east (to the right of its original direction of travel in the Northern Hemisphere). Conversely, an object traveling southward toward the Equator passes over ground rotating faster than itself, falling behind to the west (still to the right of its forward motion).

Why Hurricanes Spin Counter-Clockwise in the North and Clockwise in the South

A common paradox for physics students is why hurricanes (tropical cyclones) rotate counter-clockwise in the Northern Hemisphere if the Coriolis force deflects moving air to the right. The answer lies in the competition between two primary atmospheric forces: the Pressure Gradient Force (PGF) and the Coriolis Force.

Phenomenon Northern Hemisphere (NH) Southern Hemisphere (SH) Equator (0° Latitude)
Coriolis Deflection Direction Deflects 90° to the Right of motion Deflects 90° to the Left of motion Zero horizontal deflection (f = 0)
Low Pressure (Cyclone / Hurricane) Counter-Clockwise (Cyclonic) Clockwise (Cyclonic) Hurricanes cannot form within ±5°
High Pressure (Anticyclone) Clockwise (Anticyclonic) Counter-Clockwise (Anticyclonic) Weak, disorganized pressure cells
Major Ocean Basin Gyres Clockwise Circulation (e.g. Gulf Stream) Counter-Clockwise Circulation Equatorial Counter-Current
Geostrophic Wind Vector Parallel to isobars with low to the left Parallel to isobars with low to the right Direct down-gradient flow across isobars

In a hurricane, warm ocean water causes rapid updrafts, creating an intense central pocket of low atmospheric pressure. Surrounding air from all directions rushes radially inward toward the low-pressure eye due to the pressure gradient force.

In the Northern Hemisphere, as air parcels rush toward the central low, the Coriolis effect deflects each air parcel to its right. Instead of meeting directly in the center, the air flows into an orbiting swirl around the eye in a counter-clockwise circle. In the Southern Hemisphere, deflection to the left produces an identical mechanism rotating clockwise.

Why Hurricanes Cannot Form at the Equator (The ITCZ "Safe Zone")

Tropical cyclones require a minimum Coriolis parameter f = 2Ω sin(φ) to organize incoming winds into a vortex rather than dissipating the pressure gradient. Because sin(0°) = 0, the horizontal Coriolis force is precisely zero at the Equator. Tropical depressions within 5° of the Equator (roughly 550 km north or south) cannot initiate cyclonic spin, making the equatorial doldrums entirely free of hurricanes and typhoons.

Debunking the Bathtub & Toilet Drain Myth

Scientific Fact: The Coriolis Effect Does NOT Control Household Sinks or Toilets

A ubiquitous urban myth claims that sink drains and flush toilets swirl counter-clockwise in North America or Europe, and clockwise in Australia or South Africa. This is demonstrably false. On the scale of a 30-centimeter sink basin, the Coriolis acceleration is approximately 10⁻⁷ m/s²—over 10,000,000 times weaker than random residual angular momentum, pipe curvature, and asymmetric bowl shape.

Atmospheric scientists evaluate whether the Coriolis force dominates fluid dynamics using a dimensionless ratio known as the Rossby Number (Ro):

Ro = U / (2 Ω L) = U / (f · L)
Where U is characteristic flow velocity, L is characteristic spatial length scale, and f = 2Ω sin(φ) is the Coriolis parameter.

When the Rossby number is very small (Ro << 1), Coriolis accelerations dominate inertial forces, dictating fluid trajectories (as seen in synoptic-scale weather systems spanning 1,000 kilometers over 24 hours, where Ro ≈ 0.05). Conversely, in a sink or toilet where L ≈ 0.3 meters and water empties in 5 seconds (U ≈ 0.5 m/s), the Rossby number is roughly Ro ≈ 30,000. Inertial and viscous forces are thousands of times stronger than Earth’s rotation. Only in rigorous laboratory experiments with perfectly symmetric 2-meter tanks left settling for several days in temperature-controlled rooms can the Coriolis swirl be isolated at household scales.

Planetary Comparison: The Coriolis Effect Across Other Worlds

The Coriolis effect is a universal law of classical mechanics applicable to every rotating astronomical body. Exploring other planets in our solar system provides extreme testbeds:

  • Jupiter (The Fast Giant): Despite having an equatorial diameter 11 times that of Earth, Jupiter completes a full axial rotation in just 9 hours, 55 minutes (Ω = 1.76 × 10⁻⁴ rad/s, nearly 2.5 times Earth's angular speed). This immense rotation velocity fragments Jupiter's atmosphere into dozens of ultra-narrow, alternating zonal jet streams with wind speeds exceeding 500 km/h, and supports giant anticyclones like the 350-year-old Great Red Spot.
  • Mars (The Terrestrial Twin): Mars has a rotational period (sol) of 24 hours, 37 minutes, almost identical to Earth's. However, with an atmospheric density less than 1% of Earth's and extreme diurnal temperature swings, dust storms spawned by Coriolis deflection can wrap around the entire Martian globe in seasonal dust fronts.
  • Venus (The Retrograde Stagnant): Venus rotates extraordinarily slowly, requiring 243 Earth days to complete one turn, and spins in a retrograde (clockwise) direction. Consequently, the Coriolis parameter on Venus is minuscule (f ≈ 6 × 10⁻⁷ s⁻¹), resulting in a single massive Hadley cell that transports heat directly from the Equator to the poles without forming mid-latitude Coriolis storms.

Impact on Long-Range Artillery and Sniper Ballistics

In military ballistics, projectile fire over distances exceeding 1,000 meters requires precise corrections for the Coriolis effect. For naval gunnery and precision sniper fire:

  • Horizontal Deflection: In the Northern Hemisphere, a shell fired in any horizontal direction (north, south, east, or west) drifts steadily to the right of the target reticle. At a range of 20 kilometers (typical for modern 155mm naval artillery), Coriolis deflection can shift the point of impact by more than 50 to 100 meters if not compensated for by fire control computers.
  • The Eötvös Effect (Vertical Deflection): When firing eastward, the projectile’s eastward velocity adds to Earth's eastward rotational velocity. The increased centrifugal force decreases the projectile’s effective weight, causing it to strike higher than anticipated. When firing westward, the projectile effectively travels slower than Earth's surface, increasing weight and causing the shell to strike low.

Frequently Asked Questions About the Coriolis Effect

✦ Does the Coriolis effect affect airplanes during flight?
Yes, but in commercial aviation, pilots do not manually "steer left" to counter it. An aircraft travels through the moving fluid of the atmosphere, which is already carrying Earth's rotational momentum. However, flight management systems (FMS) and autopilot navigation continuously compute wind vectors (such as high-altitude jet streams) that are shaped by the Coriolis effect.
✦ Can the Coriolis effect be felt by human senses on Earth?
No. Earth's rotation rate is extraordinarily gentle: one revolution per 86,164 seconds (about 0.00007292 radians per second, or half the speed of the hour hand on a clock). Humans can only feel Coriolis acceleration on fast-spinning carnival rides, centrifuges, or future rotating space stations, where head movements induce vestibular illusions and motion sickness.
✦ What is the difference between Centrifugal Force and Coriolis Force?
Both are inertial forces arising in rotating reference frames. However, the centrifugal force depends only on position: it always acts radially outward from the axis of rotation regardless of whether an object is stationary or in motion (F_cf = m · ω² · r). The Coriolis force depends strictly on velocity: it is zero for stationary objects and acts perpendicular to both the rotation axis and the velocity vector (F_c = -2m · (ω × v)).
✦ Why are ocean currents curved instead of moving in straight lines?
Prevailing planetary winds blow surface water across the oceans. As water moves, the Coriolis force deflects it (45° at the surface), and each successive layer of water beneath deflects even further, creating an underwater spiral known as the Ekman Spiral. This drives giant circular ocean gyres (such as the North Atlantic Gyre and Gulf Stream) that regulate global climate.
// ============================================================================== // The Coriolis Effect Simulator Core Engine // Dual View: Mode 1 = Flat Rotating Carousel/Disk; Mode 2 = Rotating Planetary Sphere // ============================================================================== (function() { const canvas = document.getElementById('coriolisCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); // State let mode = 'carousel'; // 'carousel' or 'globe' let isRunning = true; let animId = null; // Parameters let omega = 0.025; // angular speed (rad/frame) let latitude = 45; // degrees (-90 to +90) for globe let projectileSpeed = 2.4; // pixels/frame let launchAngle = 0; // heading angle in degrees (0 = North/Up, 90 = East/Right) let showInertial = true; let showRotating = true; let showAtmosphere = true; let activePreset = 'earth'; // Internal simulation clock & objects let simTime = 0; let diskRotation = 0; // current angle of rotating frame let projectiles = []; // array of fired particles // Presets catalog const PRESETS = { earth: { omega: 0.025, latitude: 45, speed: 2.5, name: 'Earth (24h Period)' }, mars: { omega: 0.024, latitude: 35, speed: 2.3, name: 'Mars (24.6h Period)' }, jupiter: { omega: 0.065, latitude: 20, speed: 3.2, name: 'Jupiter (9.9h Fast Spin)' }, venus: { omega: -0.005, latitude: 30, speed: 1.5, name: 'Venus (Slow Retrograde)' }, custom: { omega: 0.03, latitude: 45, speed: 2.5, name: 'Custom Setup' } }; // Canvas resizing with crisp DPI handling function resizeCanvas() { const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.resetTransform(); ctx.scale(dpr, dpr); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); // Projectile Class class Projectile { constructor(x0, y0, vx, vy, isGlobe = false, startLat = 0) { this.x0 = x0; this.y0 = y0; this.vx = vx; this.vy = vy; this.isGlobe = isGlobe; this.startLat = startLat; this.t = 0; this.maxT = 240; // lifetime frames this.inertialPath = []; // coordinates in fixed frame this.rotatingPath = []; // coordinates in rotating frame this.initialDiskAngle = diskRotation; } update() { this.t += 1; if (!this.isGlobe) { // Flat Carousel Physics: // In inertial frame, particle moves in a straight line: const ix = this.x0 + this.vx * this.t; const iy = this.y0 + this.vy * this.t; this.inertialPath.push({ x: ix, y: iy }); // In rotating frame (theta = diskRotation - initialDiskAngle): const currentAngle = diskRotation; const cosA = Math.cos(-currentAngle); const sinA = Math.sin(-currentAngle); const rx = ix * cosA - iy * sinA; const ry = ix * sinA + iy * cosA; this.rotatingPath.push({ x: rx, y: ry }); } else { // Planetary Latitude Deflection: // Coriolis parameter f = 2 * omega * sin(lat) const phi = (this.startLat * Math.PI) / 180; const f = 2 * (omega * 1.5) * Math.sin(phi); // Integrate trajectory with Coriolis acceleration a = -f * (k x v) // dvx/dt = f * vy, dvy/dt = -f * vx if (this.rotatingPath.length === 0) { this.currRx = this.x0; this.currRy = this.y0; this.currVx = this.vx; this.currVy = this.vy; this.inertialVx = this.vx; this.inertialVy = this.vy; this.currIx = this.x0; this.currIy = this.y0; } // Inertial step: straight line this.currIx += this.inertialVx; this.currIy += this.inertialVy; this.inertialPath.push({ x: this.currIx, y: this.currIy }); // Rotating step: curve due to Coriolis this.currVx += f * this.currVy; this.currVy -= f * this.currVx; this.currRx += this.currVx; this.currRy += this.currVy; this.rotatingPath.push({ x: this.currRx, y: this.currRy }); } } isDead() { return this.t >= this.maxT; } } // Fire projectile function function fireProjectile() { const rect = canvas.getBoundingClientRect(); const cx = rect.width / 2; const cy = rect.height / 2; const radAngle = ((launchAngle - 90) * Math.PI) / 180; const vx = Math.cos(radAngle) * projectileSpeed; const vy = Math.sin(radAngle) * projectileSpeed; if (mode === 'carousel') { // Start near center of disk or offset const p = new Projectile(0, 0, vx, vy, false); projectiles.push(p); } else { // Mode globe: start at latitude-dependent Y position const radius = Math.min(cx, cy) * 0.75; const latRad = (latitude * Math.PI) / 180; const startY = -radius * Math.sin(latRad); const startX = 0; const p = new Projectile(startX, startY, vx, vy, true, latitude); projectiles.push(p); } // Keep maximum 12 projectiles at a time to retain performance if (projectiles.length > 12) { projectiles.shift(); } } // Periodic auto-firing every 1.5 seconds if less than 3 active let lastAutoFire = 0; // Draw Carousel Mode function drawCarousel(w, h) { const cx = w / 2; const cy = h / 2; const R = Math.min(cx, cy) * 0.72; // Draw Rotating Turntable / Disk ctx.save(); ctx.translate(cx, cy); // Disk Outer Glow const glow = ctx.createRadialGradient(0, 0, R * 0.7, 0, 0, R); glow.addColorStop(0, 'rgba(10, 28, 84, 0.4)'); glow.addColorStop(1, 'rgba(56, 189, 248, 0.15)'); ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(0, 0, R, 0, Math.PI * 2); ctx.fill(); // Disk Boundary ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, R, 0, Math.PI * 2); ctx.stroke(); // Rotating Grid Lines & Radius Spokes ctx.save(); ctx.rotate(diskRotation); // Concentric rings on disk for (let r = R * 0.25; r < R; r += R * 0.25) { ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.stroke(); } // Spokes (8 spokes) for (let i = 0; i < 8; i++) { const theta = (i * Math.PI) / 4; ctx.strokeStyle = i % 2 === 0 ? 'rgba(56, 189, 248, 0.25)' : 'rgba(255, 255, 255, 0.08)'; ctx.lineWidth = i % 2 === 0 ? 1.5 : 1; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.cos(theta) * R, Math.sin(theta) * R); ctx.stroke(); } // Rotating Frame Target Marker ctx.fillStyle = '#00E5FF'; ctx.beginPath(); ctx.arc(R * 0.85, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(224, 231, 255, 0.8)'; ctx.font = '10px monospace'; ctx.fillText('Target (Rotating)', R * 0.85 + 8, 3); ctx.restore(); // end disk rotation // Rotation Direction Indicator Arrow around perimeter ctx.save(); ctx.strokeStyle = omega >= 0 ? '#38BDF8' : '#F59E0B'; ctx.lineWidth = 3; const arrowAngle = diskRotation; ctx.beginPath(); if (omega >= 0) { ctx.arc(0, 0, R + 18, arrowAngle, arrowAngle + 0.6, false); } else { ctx.arc(0, 0, R + 18, arrowAngle, arrowAngle - 0.6, true); } ctx.stroke(); // Arrow head const headA = omega >= 0 ? arrowAngle + 0.6 : arrowAngle - 0.6; const hx = Math.cos(headA) * (R + 18); const hy = Math.sin(headA) * (R + 18); ctx.fillStyle = omega >= 0 ? '#38BDF8' : '#F59E0B'; ctx.beginPath(); ctx.arc(hx, hy, 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'; ctx.font = '11px sans-serif'; const labelText = omega >= 0 ? '⟲ Counter-Clockwise (Ω > 0)' : '⟳ Clockwise (Ω < 0)'; ctx.fillText(labelText, -ctx.measureText(labelText).width / 2, -R - 26); ctx.restore(); // Draw Center Launch Turret ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(0, 0, 7, 0, Math.PI * 2); ctx.fill(); // Draw Aiming Vector const radAim = ((launchAngle - 90) * Math.PI) / 180; ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.cos(radAim) * 60, Math.sin(radAim) * 60); ctx.stroke(); ctx.setLineDash([]); // Render All Active Projectiles projectiles.forEach(p => { // 1. Inertial Straight Path (White/Gray dashed) if (showInertial && p.inertialPath.length > 1) { ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(p.inertialPath[0].x, p.inertialPath[0].y); for (let i = 1; i < p.inertialPath.length; i++) { ctx.lineTo(p.inertialPath[i].x, p.inertialPath[i].y); } ctx.stroke(); ctx.setLineDash([]); // Inertial current head const curI = p.inertialPath[p.inertialPath.length - 1]; ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(curI.x, curI.y, 4, 0, Math.PI * 2); ctx.fill(); } // 2. Rotating Curved Apparent Path (Vibrant Cyan or Amber depending on spin) if (showRotating && p.rotatingPath.length > 1) { ctx.strokeStyle = omega >= 0 ? '#00E5FF' : '#F59E0B'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(p.rotatingPath[0].x, p.rotatingPath[0].y); for (let i = 1; i < p.rotatingPath.length; i++) { ctx.lineTo(p.rotatingPath[i].x, p.rotatingPath[i].y); } ctx.stroke(); // Rotating current head const curR = p.rotatingPath[p.rotatingPath.length - 1]; ctx.fillStyle = omega >= 0 ? '#00E5FF' : '#F59E0B'; ctx.beginPath(); ctx.arc(curR.x, curR.y, 6, 0, Math.PI * 2); ctx.fill(); ctx.shadowColor = omega >= 0 ? '#00E5FF' : '#F59E0B'; ctx.shadowBlur = 12; ctx.fill(); ctx.shadowBlur = 0; } }); ctx.restore(); } // Draw Globe / Planetary Sphere Mode function drawGlobe(w, h) { const cx = w / 2; const cy = h / 2; const R = Math.min(cx, cy) * 0.74; ctx.save(); ctx.translate(cx, cy); // Planet Sphere Background & Diurnal Shading const sphereGrad = ctx.createRadialGradient(-R * 0.25, -R * 0.25, R * 0.2, 0, 0, R); sphereGrad.addColorStop(0, '#1E40AF'); sphereGrad.addColorStop(0.65, '#0F172A'); sphereGrad.addColorStop(1, '#020617'); ctx.fillStyle = sphereGrad; ctx.beginPath(); ctx.arc(0, 0, R, 0, Math.PI * 2); ctx.fill(); // Planet Limb Glow ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)'; ctx.lineWidth = 2; ctx.stroke(); // Latitude Lines (Parallels) const latitudes = [-60, -30, 0, 30, 60]; latitudes.forEach(lat => { const y = -R * Math.sin((lat * Math.PI) / 180); const rx = R * Math.cos((lat * Math.PI) / 180); const ry = rx * 0.22; // tilt projection ctx.strokeStyle = lat === 0 ? 'rgba(0, 229, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)'; ctx.lineWidth = lat === 0 ? 2 : 1; if (lat !== 0) ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.ellipse(0, y, rx, ry, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); // Latitude text label ctx.fillStyle = lat === 0 ? '#00E5FF' : 'rgba(255, 255, 255, 0.45)'; ctx.font = '10px monospace'; const latText = lat === 0 ? 'Equator 0°' : (lat > 0 ? lat + '°N' : Math.abs(lat) + '°S'); ctx.fillText(latText, rx + 8, y + 3); }); // Polar Axis ctx.strokeStyle = 'rgba(255, 255, 255, 0.35)'; ctx.lineWidth = 1.5; ctx.setLineDash([6, 4]); ctx.beginPath(); ctx.moveTo(0, -R - 25); ctx.lineTo(0, R + 25); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = '#38BDF8'; ctx.font = '11px sans-serif'; ctx.fillText('North Pole', -28, -R - 30); ctx.fillText('South Pole', -28, R + 40); // Highlight Active Launch Latitude const curY = -R * Math.sin((latitude * Math.PI) / 180); const curRx = R * Math.cos((latitude * Math.PI) / 180); const curRy = curRx * 0.22; ctx.strokeStyle = '#F59E0B'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.ellipse(0, curY, curRx, curRy, 0, 0, Math.PI * 2); ctx.stroke(); // Launch Origin Marker ctx.fillStyle = '#F59E0B'; ctx.beginPath(); ctx.arc(0, curY, 6, 0, Math.PI * 2); ctx.fill(); // Atmospheric Storm / Cyclone Simulation vectors if enabled if (showAtmosphere) { // Draw Northern Hemisphere Low Pressure (Counter-Clockwise Cyclone) const nhY = -R * 0.45; const nhX = -R * 0.35; drawVortex(nhX, nhY, 40, true, 'Low (NH: CCW Cyclone)'); // Draw Southern Hemisphere Low Pressure (Clockwise Cyclone) const shY = R * 0.45; const shX = -R * 0.35; drawVortex(shX, shY, 40, false, 'Low (SH: CW Cyclone)'); } // Render Globe Projectiles projectiles.forEach(p => { // Inertial path if (showInertial && p.inertialPath.length > 1) { ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(p.inertialPath[0].x, p.inertialPath[0].y); for (let i = 1; i < p.inertialPath.length; i++) { ctx.lineTo(p.inertialPath[i].x, p.inertialPath[i].y); } ctx.stroke(); ctx.setLineDash([]); } // Rotating Apparent Path (Deflection curves right in North, left in South) if (showRotating && p.rotatingPath.length > 1) { const isNorth = p.startLat >= 0; ctx.strokeStyle = isNorth ? '#00E5FF' : '#F43F5E'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(p.rotatingPath[0].x, p.rotatingPath[0].y); for (let i = 1; i < p.rotatingPath.length; i++) { ctx.lineTo(p.rotatingPath[i].x, p.rotatingPath[i].y); } ctx.stroke(); // Head const cur = p.rotatingPath[p.rotatingPath.length - 1]; ctx.fillStyle = isNorth ? '#00E5FF' : '#F43F5E'; ctx.beginPath(); ctx.arc(cur.x, cur, 5, 0, Math.PI * 2); ctx.fill(); } }); ctx.restore(); } // Helper to draw cyclonic spiral vortex function drawVortex(cx, cy, radius, isCounterClockwise, label) { ctx.save(); ctx.translate(cx, cy); // Center Low marker ctx.fillStyle = 'rgba(239, 68, 68, 0.85)'; ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 11px sans-serif'; ctx.fillText('L', -4, 4); // 4 Inward spiraling wind arms for (let i = 0; i < 4; i++) { const baseA = (i * Math.PI) / 2 + (isCounterClockwise ? simTime * 0.05 : -simTime * 0.05); ctx.strokeStyle = isCounterClockwise ? 'rgba(56, 189, 248, 0.7)' : 'rgba(244, 63, 94, 0.7)'; ctx.lineWidth = 1.8; ctx.beginPath(); for (let r = radius; r >= 12; r -= 4) { const spin = (radius - r) * (isCounterClockwise ? 0.045 : -0.045); const a = baseA + spin; const x = Math.cos(a) * r; const y = Math.sin(a) * r; if (r === radius) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } ctx.fillStyle = 'rgba(255, 255, 255, 0.75)'; ctx.font = '10px sans-serif'; ctx.fillText(label, -radius, radius + 14); ctx.restore(); } // Telemetry update in UI function updateTelemetryUI() { const omegaVal = document.getElementById('telemetry-omega'); const latVal = document.getElementById('telemetry-lat'); const paramF = document.getElementById('telemetry-param-f'); const defDir = document.getElementById('telemetry-deflect-dir'); const rossbyVal = document.getElementById('telemetry-rossby'); const radLat = (latitude * Math.PI) / 180; // f = 2 * Omega * sin(lat) const fCalc = 2 * Math.abs(omega) * Math.sin(radLat); if (omegaVal) omegaVal.textContent = omega.toFixed(3) + ' rad/s'; if (latVal) latVal.textContent = latitude + '° ' + (latitude >= 0 ? 'N' : 'S'); if (paramF) paramF.textContent = (fCalc * 100).toFixed(2) + ' × 10⁻² s⁻¹'; if (defDir) { if (mode === 'carousel') { defDir.textContent = omega > 0 ? 'Deflects RIGHT' : (omega < 0 ? 'Deflects LEFT' : 'No Deflection'); defDir.style.color = omega > 0 ? '#00E5FF' : '#F59E0B'; } else { if (latitude > 0) { defDir.textContent = 'Deflects RIGHT (NH)'; defDir.style.color = '#00E5FF'; } else if (latitude < 0) { defDir.textContent = 'Deflects LEFT (SH)'; defDir.style.color = '#F43F5E'; } else { defDir.textContent = 'Zero at Equator'; defDir.style.color = '#94A3B8'; } } } if (rossbyVal) { // Ro = U / (2 * Omega * L). Let characteristic L = 100, U = speed * 10 const denominator = Math.max(Math.abs(fCalc) * 100, 0.001); const ro = (projectileSpeed * 10) / denominator; rossbyVal.textContent = ro.toFixed(2) + (ro < 0.1 ? ' (Coriolis Dominates)' : ' (Inertia Dominates)'); } } // Main Animation Frame function animate() { if (isRunning) { simTime += 1; diskRotation += omega; // Update active projectiles for (let i = projectiles.length - 1; i >= 0; i--) { projectiles[i].update(); if (projectiles[i].isDead()) { projectiles.splice(i, 1); } } // Periodic Auto Fire if idle if (simTime - lastAutoFire > 90) { fireProjectile(); lastAutoFire = simTime; } } // Render Canvas const rect = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, rect.width, rect.height); if (mode === 'carousel') { drawCarousel(rect.width, rect.height); } else { drawGlobe(rect.width, rect.height); } updateTelemetryUI(); animId = requestAnimationFrame(animate); } animId = requestAnimationFrame(animate); // Event Listeners for UI Controls const modeCarouselBtn = document.getElementById('mode-carousel-btn'); const modeGlobeBtn = document.getElementById('mode-globe-btn'); if (modeCarouselBtn && modeGlobeBtn) { modeCarouselBtn.addEventListener('click', () => { mode = 'carousel'; modeCarouselBtn.classList.add('active'); modeGlobeBtn.classList.remove('active'); projectiles = []; const latGroup = document.getElementById('control-group-latitude'); if (latGroup) latGroup.style.display = 'none'; }); modeGlobeBtn.addEventListener('click', () => { mode = 'globe'; modeGlobeBtn.classList.add('active'); modeCarouselBtn.classList.remove('active'); projectiles = []; const latGroup = document.getElementById('control-group-latitude'); if (latGroup) latGroup.style.display = 'flex'; }); } // Sliders const omegaSlider = document.getElementById('slider-omega'); const omegaValDisplay = document.getElementById('val-omega'); if (omegaSlider) { omegaSlider.addEventListener('input', (e) => { omega = parseFloat(e.target.value); if (omegaValDisplay) omegaValDisplay.textContent = omega.toFixed(3); clearActivePreset(); }); } const latSlider = document.getElementById('slider-latitude'); const latValDisplay = document.getElementById('val-latitude'); if (latSlider) { latSlider.addEventListener('input', (e) => { latitude = parseInt(e.target.value, 10); if (latValDisplay) latValDisplay.textContent = latitude + '°'; projectiles = []; clearActivePreset(); }); } const speedSlider = document.getElementById('slider-speed'); const speedValDisplay = document.getElementById('val-speed'); if (speedSlider) { speedSlider.addEventListener('input', (e) => { projectileSpeed = parseFloat(e.target.value); if (speedValDisplay) speedValDisplay.textContent = projectileSpeed.toFixed(1); }); } const angleSlider = document.getElementById('slider-angle'); const angleValDisplay = document.getElementById('val-angle'); if (angleSlider) { angleSlider.addEventListener('input', (e) => { launchAngle = parseInt(e.target.value, 10); if (angleValDisplay) angleValDisplay.textContent = launchAngle + '°'; }); } // Toggles const chkInertial = document.getElementById('chk-inertial'); if (chkInertial) { chkInertial.addEventListener('change', (e) => { showInertial = e.target.checked; }); } const chkRotating = document.getElementById('chk-rotating'); if (chkRotating) { chkRotating.addEventListener('change', (e) => { showRotating = e.target.checked; }); } // Action buttons const btnFire = document.getElementById('btn-fire'); if (btnFire) { btnFire.addEventListener('click', () => { fireProjectile(); }); } const btnClear = document.getElementById('btn-clear'); if (btnClear) { btnClear.addEventListener('click', () => { projectiles = []; }); } const btnPlayPause = document.getElementById('btn-pause-play'); if (btnPlayPause) { btnPlayPause.addEventListener('click', () => { isRunning = !isRunning; btnPlayPause.innerHTML = isRunning ? '⏸ Pause' : '▶ Play'; }); } // Presets function clearActivePreset() { document.querySelectorAll('.preset-chip').forEach(c => c.classList.remove('active')); } window.setPreset = function(name) { const p = PRESETS[name]; if (!p) return; activePreset = name; clearActivePreset(); const chip = document.getElementById('preset-' + name); if (chip) chip.classList.add('active'); omega = p.omega; latitude = p.latitude; projectileSpeed = p.speed; if (omegaSlider) omegaSlider.value = omega; if (omegaValDisplay) omegaValDisplay.textContent = omega.toFixed(3); if (latSlider) latSlider.value = latitude; if (latValDisplay) latValDisplay.textContent = latitude + '°'; if (speedSlider) speedSlider.value = projectileSpeed; if (speedValDisplay) speedValDisplay.textContent = projectileSpeed.toFixed(1); projectiles = []; }; // Expose globally window.fireCoriolisProjectile = fireProjectile; })();