Temperature Unit
Home→ Planetary Weather→ Weight on Planets→ Atmospheric Comparison→ Coriolis Simulator→ 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:
3D Real Earth Simulator (GLB Model)
Drag to rotate Earth · Click to launch particle
Inertial Frame (Straight)
Rotating Frame (Coriolis Deflection)
Storm Vortices (Cyclones)

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.
Advertisement

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 Mode: Mode 1 = 3D Real Earth (GLB Model); Mode 2 = Flat Rotating Turntable // ============================================================================== (function () { "use strict"; // Common State let mode = "globe"; // 'globe' (3D Real Earth) or 'carousel' (2D Flat Turntable) let isRunning = true; let omega = 0.025; // angular speed (rad/frame) let latitude = 45; // degrees (-90 to +90) for globe let projectileSpeed = 2.5; // pixels or speed factor 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"; const PRESETS = { earth: { omega: 0.025, latitude: 45, speed: 2.5, name: "Earth (24h Period)", model: "/models/earth.glb", tilt: 23.44 }, mars: { omega: 0.024, latitude: 35, speed: 2.3, name: "Mars (24.6h Period)", model: "/models/mars.glb", tilt: 25.19 }, jupiter: { omega: 0.065, latitude: 20, speed: 3.2, name: "Jupiter (9.9h Fast Spin)", model: "/models/realistic_jupiter.glb", tilt: 3.13 }, venus: { omega: -0.005, latitude: 30, speed: 1.5, name: "Venus (Slow Retrograde)", model: "/models/venus.glb", tilt: 177.3 }, custom: { omega: 0.03, latitude: 45, speed: 2.5, name: "Custom Setup", model: "/models/earth.glb", tilt: 23.44 } }; // DOM Elements const viewportWrap = document.getElementById("coriolis-viewport-wrap"); const wrap3D = document.getElementById("coriolis-3d-wrap"); const canvas3D = document.getElementById("coriolis3dCanvas"); const canvas2D = document.getElementById("coriolisCanvas"); const loadingBadge = document.getElementById("coriolis-loading-badge"); const modeLabel = document.getElementById("viewport-mode-label"); const modeHint = document.getElementById("viewport-hint"); const modeCarouselBtn = document.getElementById("mode-carousel-btn"); const modeGlobeBtn = document.getElementById("mode-globe-btn"); const latControlGroup = document.getElementById("control-group-latitude"); // ============================================================================ // PART 1: 3D REAL EARTH GLB SIMULATOR (THREE.JS) // ============================================================================ let renderer3D = null; let scene3D = null; let camera3D = null; let rootG = null; let orbitG = null; let earthPivotG = null; let modelGroup = null; let activeLatRing = null; let turretMarker = null; let turretArrow = null; let cyclonesGroup = null; let trajectoriesGroup = null; let hitSphere = null; const EARTH_RADIUS = 1.62; const modelCache = {}; let projectiles3D = []; let isDragging3D = false; let hasDragged3D = false; let lastX3D = 0; let lastY3D = 0; let velX3D = 0; let velY3D = 0; let activePointerId3D = null; let lastAutoFire3D = 0; function init3DScene() { if (!wrap3D || !canvas3D || !window.THREE) return; const rect = wrap3D.getBoundingClientRect(); const w = rect.width || 800; const h = rect.height || 500; try { renderer3D = new THREE.WebGLRenderer({ canvas: canvas3D, antialias: true, alpha: true, powerPreference: "high-performance" }); } catch (e) { console.warn("WebGL not supported for Coriolis 3D:", e); return; } renderer3D.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer3D.setSize(w, h, false); renderer3D.toneMapping = THREE.ACESFilmicToneMapping; renderer3D.toneMappingExposure = 1.25; scene3D = new THREE.Scene(); camera3D = new THREE.PerspectiveCamera(38, w / h, 0.1, 1000); camera3D.position.set(0, 0, 5.2); // Lighting const amb = new THREE.AmbientLight(0xffffff, 0.95); scene3D.add(amb); const sun = new THREE.DirectionalLight(0xffffff, 2.7); sun.position.set(6, 3, 5); scene3D.add(sun); const rim = new THREE.DirectionalLight(0x38bdf8, 0.75); rim.position.set(-6, -2, -4); scene3D.add(rim); // Hierarchy rootG = new THREE.Group(); // Default Earth axial tilt (23.44 deg) rootG.rotation.z = (23.44 * Math.PI) / 180 * 0.45; scene3D.add(rootG); orbitG = new THREE.Group(); rootG.add(orbitG); earthPivotG = new THREE.Group(); orbitG.add(earthPivotG); modelGroup = new THREE.Group(); earthPivotG.add(modelGroup); // Initial procedural Earth sphere so render starts with zero latency const initialFallback = createProceduralEarthMesh(); modelGroup.add(initialFallback); // Invisible hit sphere for raycasting clicks to fire projectiles const hitGeo = new THREE.SphereGeometry(EARTH_RADIUS * 1.01, 32, 32); const hitMat = new THREE.MeshBasicMaterial({ visible: false }); hitSphere = new THREE.Mesh(hitGeo, hitMat); orbitG.add(hitSphere); // Latitude Parallels buildParallels(); // Active Latitude Ring & Turret buildActiveLatitudeRing(); buildTurretMarker(); // Cyclones / Atmospheric storm systems buildCyclones(); // Trajectories container trajectoriesGroup = new THREE.Group(); orbitG.add(trajectoriesGroup); // Load actual GLB file loadModelForPreset("earth"); // Wire Pointer events for 3D Earth setup3DInteraction(); // Resize observer const ro = new ResizeObserver(() => { const r = wrap3D.getBoundingClientRect(); const rw = r.width || 800; const rh = r.height || 500; camera3D.aspect = rw / rh; camera3D.updateProjectionMatrix(); renderer3D.setSize(rw, rh, false); }); ro.observe(wrap3D); } function createProceduralEarthMesh() { const geo = new THREE.SphereGeometry(EARTH_RADIUS, 48, 48); const canvas = document.createElement("canvas"); canvas.width = 1024; canvas.height = 512; const ctx = canvas.getContext("2d"); // Deep blue ocean gradient const grad = ctx.createLinearGradient(0, 0, 0, 512); grad.addColorStop(0, "#1E3A8A"); grad.addColorStop(0.5, "#0284C7"); grad.addColorStop(1, "#0F172A"); ctx.fillStyle = grad; ctx.fillRect(0, 0, 1024, 512); // Continent patches ctx.fillStyle = "#059669"; const continents = [ [300, 180, 120, 80], [360, 320, 90, 140], [550, 140, 150, 90], [560, 270, 140, 120], [750, 170, 200, 130], [820, 360, 110, 80] ]; continents.forEach(([x, y, w, h]) => { ctx.beginPath(); ctx.ellipse(x, y, w / 2, h / 2, 0, 0, Math.PI * 2); ctx.fill(); }); // Cloud bands ctx.fillStyle = "rgba(255, 255, 255, 0.18)"; for (let i = 0; i < 35; i++) { ctx.fillRect(0, Math.random() * 512, 1024, 4 + Math.random() * 14); } const tex = new THREE.CanvasTexture(canvas); const mat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.55, metalness: 0.1 }); return new THREE.Mesh(geo, mat); } function buildParallels() { const parallelsGroup = new THREE.Group(); earthPivotG.add(parallelsGroup); // Equator (0 deg) in glowing Cyan const eqPts = []; const R_eq = EARTH_RADIUS * 1.004; for (let i = 0; i <= 64; i++) { const a = (i / 64) * Math.PI * 2; eqPts.push(new THREE.Vector3(Math.cos(a) * R_eq, 0, Math.sin(a) * R_eq)); } const eqGeo = new THREE.BufferGeometry().setFromPoints(eqPts); const eqMat = new THREE.LineBasicMaterial({ color: 0x00e5ff, linewidth: 2, transparent: true, opacity: 0.75 }); parallelsGroup.add(new THREE.Line(eqGeo, eqMat)); // Major parallels: 30N, 60N, 30S, 60S in subtle white [30, 60, -30, -60].forEach((latDeg) => { const latRad = (latDeg * Math.PI) / 180; const y = R_eq * Math.sin(latRad); const r = R_eq * Math.cos(latRad); const pts = []; for (let i = 0; i <= 64; i++) { const a = (i / 64) * Math.PI * 2; pts.push(new THREE.Vector3(Math.cos(a) * r, y, Math.sin(a) * r)); } const geo = new THREE.BufferGeometry().setFromPoints(pts); const mat = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.22 }); parallelsGroup.add(new THREE.Line(geo, mat)); }); } function buildActiveLatitudeRing() { const pts = []; for (let i = 0; i <= 64; i++) { const a = (i / 64) * Math.PI * 2; pts.push(new THREE.Vector3(Math.cos(a), 0, Math.sin(a))); } const geo = new THREE.BufferGeometry().setFromPoints(pts); const mat = new THREE.LineBasicMaterial({ color: 0xf59e0b, // Amber gold linewidth: 3, transparent: true, opacity: 0.95 }); activeLatRing = new THREE.Line(geo, mat); earthPivotG.add(activeLatRing); updateActiveLatitudeRingPosition(); } function updateActiveLatitudeRingPosition() { if (!activeLatRing) return; const latRad = (latitude * Math.PI) / 180; const R = EARTH_RADIUS * 1.008; const y = R * Math.sin(latRad); const r = R * Math.cos(latRad); activeLatRing.position.set(0, y, 0); activeLatRing.scale.set(r, 1, r); updateTurretPosition(); } function buildTurretMarker() { const turretGroup = new THREE.Group(); // Turret base sphere const dotGeo = new THREE.SphereGeometry(0.045, 16, 16); const dotMat = new THREE.MeshBasicMaterial({ color: 0xffffff }); const dot = new THREE.Mesh(dotGeo, dotMat); turretGroup.add(dot); // Aiming arrow const dir = new THREE.Vector3(0, 1, 0); const origin = new THREE.Vector3(0, 0, 0); turretArrow = new THREE.ArrowHelper(dir, origin, 0.22, 0xf59e0b, 0.08, 0.05); turretGroup.add(turretArrow); turretMarker = turretGroup; earthPivotG.add(turretMarker); updateTurretPosition(); } function updateTurretPosition() { if (!turretMarker) return; const latRad = (latitude * Math.PI) / 180; const R = EARTH_RADIUS * 1.018; // Place at longitude 0 on active latitude const y = R * Math.sin(latRad); const r = R * Math.cos(latRad); turretMarker.position.set(0, y, r); // Update aim heading if (turretArrow) { // Local North vector = (-sin(lat), cos(lat), 0) // Local East vector = (1, 0, 0) const headingRad = (launchAngle * Math.PI) / 180; const north = new THREE.Vector3(0, Math.cos(latRad), -Math.sin(latRad)).normalize(); const east = new THREE.Vector3(1, 0, 0); const aimDir = new THREE.Vector3() .addScaledVector(north, Math.cos(headingRad)) .addScaledVector(east, Math.sin(headingRad)) .normalize(); turretArrow.setDirection(aimDir); } } function buildCyclones() { cyclonesGroup = new THREE.Group(); earthPivotG.add(cyclonesGroup); // NH Cyclone at Lat +32, Lon -45 (Atlantic) const nhCyclone = createCycloneVortex(true, 0x38bdf8); positionOnGlobe(nhCyclone, 32, -45); cyclonesGroup.add(nhCyclone); // SH Cyclone at Lat -26, Lon +70 (Indian Ocean) const shCyclone = createCycloneVortex(false, 0xf43f5e); positionOnGlobe(shCyclone, -26, 70); cyclonesGroup.add(shCyclone); } function createCycloneVortex(isCounterClockwise, colorHex) { const vortex = new THREE.Group(); vortex.userData = { isCCW: isCounterClockwise }; // Center "L" marker sphere const cGeo = new THREE.SphereGeometry(0.04, 12, 12); const cMat = new THREE.MeshBasicMaterial({ color: 0xef4444 }); vortex.add(new THREE.Mesh(cGeo, cMat)); // 4 Spiraling arms for (let i = 0; i < 4; i++) { const pts = []; const baseA = (i * Math.PI) / 2; for (let r = 0.28; r >= 0.05; r -= 0.02) { const spin = (0.28 - r) * (isCounterClockwise ? 4.5 : -4.5); const a = baseA + spin; pts.push(new THREE.Vector3(Math.cos(a) * r, 0, Math.sin(a) * r)); } const geo = new THREE.BufferGeometry().setFromPoints(pts); const mat = new THREE.LineBasicMaterial({ color: colorHex, transparent: true, opacity: 0.85 }); vortex.add(new THREE.Line(geo, mat)); } return vortex; } function positionOnGlobe(obj, latDeg, lonDeg) { const latRad = (latDeg * Math.PI) / 180; const lonRad = (lonDeg * Math.PI) / 180; const R = EARTH_RADIUS * 1.018; const y = R * Math.sin(latRad); const r = R * Math.cos(latRad); const x = r * Math.sin(lonRad); const z = r * Math.cos(lonRad); obj.position.set(x, y, z); // Align normal to surface obj.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), new THREE.Vector3(x, y, z).normalize()); } function loadModelForPreset(presetKey) { const p = PRESETS[presetKey] || PRESETS.earth; const url = p.model; // Update axial tilt if (rootG) { rootG.rotation.z = (p.tilt * Math.PI) / 180 * 0.45; } if (modelCache[presetKey]) { swapModel(modelCache[presetKey]); return; } showLoading(); const loader = new window.GLTFLoader(); loader.load( url, function (gltf) { const obj = gltf.scene; const box = new THREE.Box3().setFromObject(obj); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); obj.position.sub(center); const maxDim = Math.max(size.x, size.y, size.z); const scale = maxDim > 0 ? (EARTH_RADIUS * 2) / maxDim : 1; obj.scale.setScalar(scale); obj.traverse((child) => { if (child.isMesh) { child.castShadow = false; child.receiveShadow = false; if (child.material) { child.material.roughness = Math.min(child.material.roughness || 0.6, 0.7); } } }); modelCache[presetKey] = obj; swapModel(obj); hideLoading(); }, undefined, function (err) { console.warn("Could not load GLB model (" + url + "), maintaining procedural sphere:", err); hideLoading(); } ); } function swapModel(obj) { if (!modelGroup) return; while (modelGroup.children.length > 0) { modelGroup.remove(modelGroup.children[0]); } modelGroup.add(obj.clone()); } function showLoading() { if (loadingBadge) loadingBadge.style.display = "flex"; } function hideLoading() { if (loadingBadge) loadingBadge.style.display = "none"; } // --------------------------------------------------------------------------- // 3D Coriolis Projectile Physics & Line Rendering // --------------------------------------------------------------------------- class Projectile3D { constructor(latDeg, headingDeg, speedVal) { this.latDeg = latDeg; this.headingDeg = headingDeg; this.speed = speedVal * 0.005; // radians per frame this.t = 0; this.maxT = 180; this.isNorth = latDeg >= 0; const latRad = (latDeg * Math.PI) / 180; const headingRad = (headingDeg * Math.PI) / 180; // Position unit vector at launch this.p0 = new THREE.Vector3(0, Math.sin(latRad), Math.cos(latRad)).normalize(); // Tangent velocity unit vector const north = new THREE.Vector3(0, Math.cos(latRad), -Math.sin(latRad)).normalize(); const east = new THREE.Vector3(1, 0, 0); this.v0 = new THREE.Vector3() .addScaledVector(north, Math.cos(headingRad)) .addScaledVector(east, Math.sin(headingRad)) .normalize(); this.inertialPoints = []; this.rotatingPoints = []; // Three.js Line Meshes const R_trail = EARTH_RADIUS * 1.025; // 1. Inertial Trail (White translucent) const inGeo = new THREE.BufferGeometry(); const inMat = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.65, linewidth: 2 }); this.inertialLine = new THREE.Line(inGeo, inMat); trajectoriesGroup.add(this.inertialLine); // 2. Rotating Apparent Trail (Cyan in North, Rose in South) const rotGeo = new THREE.BufferGeometry(); const rotColor = this.isNorth ? 0x00e5ff : 0xf43f5e; const rotMat = new THREE.LineBasicMaterial({ color: rotColor, linewidth: 3.5, transparent: true, opacity: 0.95 }); this.rotatingLine = new THREE.Line(rotGeo, rotMat); trajectoriesGroup.add(this.rotatingLine); // 3. Projectile Head Glow const headGeo = new THREE.SphereGeometry(0.038, 12, 12); const headMat = new THREE.MeshBasicMaterial({ color: rotColor }); this.headMesh = new THREE.Mesh(headGeo, headMat); trajectoriesGroup.add(this.headMesh); this.R_trail = R_trail; } update(omegaVal) { this.t += 1; const sigma = this.t * this.speed; // distance arc angle // Inertial great-circle point in space: const pInertialUnit = new THREE.Vector3() .addScaledVector(this.p0, Math.cos(sigma)) .addScaledVector(this.v0, Math.sin(sigma)) .normalize(); const pInertial = pInertialUnit.clone().multiplyScalar(this.R_trail); this.inertialPoints.push(pInertial); // Rotating Earth coordinate: // Earth has rotated by deltaTheta = (omega * 0.18) * t around Y axis. // Ground-relative coordinate rotates in the opposite direction (-deltaTheta): const deltaTheta = -(omegaVal * 0.18) * this.t; const pRotating = pInertial.clone().applyAxisAngle(new THREE.Vector3(0, 1, 0), deltaTheta); this.rotatingPoints.push(pRotating); // Update Line Geometries if (this.inertialPoints.length > 1) { this.inertialLine.geometry.setFromPoints(this.inertialPoints); this.inertialLine.visible = showInertial; } if (this.rotatingPoints.length > 1) { this.rotatingLine.geometry.setFromPoints(this.rotatingPoints); this.rotatingLine.visible = showRotating; this.headMesh.position.copy(pRotating); this.headMesh.visible = showRotating; } } dispose() { if (this.inertialLine) trajectoriesGroup.remove(this.inertialLine); if (this.rotatingLine) trajectoriesGroup.remove(this.rotatingLine); if (this.headMesh) trajectoriesGroup.remove(this.headMesh); } isDead() { return this.t >= this.maxT; } } function fireProjectile3D() { const p = new Projectile3D(latitude, launchAngle, projectileSpeed); projectiles3D.push(p); if (projectiles3D.length > 8) { const dead = projectiles3D.shift(); dead.dispose(); } } function clearAllProjectiles3D() { projectiles3D.forEach((p) => p.dispose()); projectiles3D = []; } // --------------------------------------------------------------------------- // 3D Pointer Events (Drag to inspect & Click on Earth to fire) // --------------------------------------------------------------------------- function setup3DInteraction() { if (!wrap3D) return; wrap3D.addEventListener("pointerdown", (e) => { isDragging3D = true; hasDragged3D = false; activePointerId3D = e.pointerId; lastX3D = e.clientX; lastY3D = e.clientY; velX3D = 0; velY3D = 0; }); window.addEventListener("pointermove", (e) => { if (!isDragging3D || e.pointerId !== activePointerId3D) return; const dx = e.clientX - lastX3D; const dy = e.clientY - lastY3D; if (Math.abs(dx) > 4 || Math.abs(dy) > 4) { hasDragged3D = true; } velX3D = dx * 0.005; velY3D = dy * 0.005; if (orbitG) { orbitG.rotation.y += velX3D; orbitG.rotation.x = Math.max(-1.4, Math.min(1.4, orbitG.rotation.x + velY3D)); } lastX3D = e.clientX; lastY3D = e.clientY; }); function endDrag() { isDragging3D = false; activePointerId3D = null; } window.addEventListener("pointerup", endDrag); window.addEventListener("pointercancel", endDrag); // Click on 3D Earth (without drag) -> Raycast to fire from clicked latitude! wrap3D.addEventListener("click", (e) => { if (hasDragged3D) return; if (!camera3D || !hitSphere) { fireProjectile3D(); return; } const rect = wrap3D.getBoundingClientRect(); const mouse = new THREE.Vector2( ((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1 ); const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera3D); const hits = raycaster.intersectObject(hitSphere); if (hits.length > 0) { const localPt = hits[0].point.clone(); // Calculate latitude from intersection point Y const latClicked = Math.asin(Math.max(-1, Math.min(1, localPt.y / (EARTH_RADIUS * 1.01)))) * (180 / Math.PI); latitude = Math.round(latClicked); // Update UI slider const latSlider = document.getElementById("slider-latitude"); const latValDisplay = document.getElementById("val-latitude"); if (latSlider) latSlider.value = latitude; if (latValDisplay) latValDisplay.textContent = latitude + "°"; updateActiveLatitudeRingPosition(); fireProjectile3D(); } else { fireProjectile3D(); } }); } // ============================================================================ // PART 2: 2D FLAT TURNTABLE / CAROUSEL SIMULATOR (HTML5 CANVAS) // ============================================================================ const ctx2D = canvas2D ? canvas2D.getContext("2d") : null; let diskRotation2D = 0; let projectiles2D = []; let simTime2D = 0; let lastAutoFire2D = 0; function resizeCanvas2D() { if (!canvas2D || !ctx2D) return; const rect = canvas2D.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas2D.width = rect.width * dpr; canvas2D.height = rect.height * dpr; ctx2D.resetTransform(); ctx2D.scale(dpr, dpr); } class Projectile2D { constructor(x0, y0, vx, vy) { this.x0 = x0; this.y0 = y0; this.vx = vx; this.vy = vy; this.t = 0; this.maxT = 240; this.inertialPath = []; this.rotatingPath = []; } update() { this.t += 1; const ix = this.x0 + this.vx * this.t; const iy = this.y0 + this.vy * this.t; this.inertialPath.push({ x: ix, y: iy }); const cosA = Math.cos(-diskRotation2D); const sinA = Math.sin(-diskRotation2D); const rx = ix * cosA - iy * sinA; const ry = ix * sinA + iy * cosA; this.rotatingPath.push({ x: rx, y: ry }); } isDead() { return this.t >= this.maxT; } } function fireProjectile2D() { if (!canvas2D) return; const radAngle = ((launchAngle - 90) * Math.PI) / 180; const vx = Math.cos(radAngle) * projectileSpeed; const vy = Math.sin(radAngle) * projectileSpeed; projectiles2D.push(new Projectile2D(0, 0, vx, vy)); if (projectiles2D.length > 12) projectiles2D.shift(); } function drawCarousel2D(w, h) { if (!ctx2D) return; const cx = w / 2; const cy = h / 2; const R = Math.min(cx, cy) * 0.72; ctx2D.save(); ctx2D.translate(cx, cy); // Disk Glow const glow = ctx2D.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)"); ctx2D.fillStyle = glow; ctx2D.beginPath(); ctx2D.arc(0, 0, R, 0, Math.PI * 2); ctx2D.fill(); // Boundary ctx2D.strokeStyle = "rgba(56, 189, 248, 0.4)"; ctx2D.lineWidth = 2; ctx2D.beginPath(); ctx2D.arc(0, 0, R, 0, Math.PI * 2); ctx2D.stroke(); // Rotating Spokes ctx2D.save(); ctx2D.rotate(diskRotation2D); for (let r = R * 0.25; r < R; r += R * 0.25) { ctx2D.strokeStyle = "rgba(255, 255, 255, 0.08)"; ctx2D.lineWidth = 1; ctx2D.beginPath(); ctx2D.arc(0, 0, r, 0, Math.PI * 2); ctx2D.stroke(); } for (let i = 0; i < 8; i++) { const theta = (i * Math.PI) / 4; ctx2D.strokeStyle = i % 2 === 0 ? "rgba(56, 189, 248, 0.25)" : "rgba(255, 255, 255, 0.08)"; ctx2D.lineWidth = i % 2 === 0 ? 1.5 : 1; ctx2D.beginPath(); ctx2D.moveTo(0, 0); ctx2D.lineTo(Math.cos(theta) * R, Math.sin(theta) * R); ctx2D.stroke(); } ctx2D.restore(); // Direction arrow ctx2D.strokeStyle = omega >= 0 ? "#38BDF8" : "#F59E0B"; ctx2D.lineWidth = 3; ctx2D.beginPath(); if (omega >= 0) { ctx2D.arc(0, 0, R + 18, diskRotation2D, diskRotation2D + 0.6, false); } else { ctx2D.arc(0, 0, R + 18, diskRotation2D, diskRotation2D - 0.6, true); } ctx2D.stroke(); // Center Launch Turret ctx2D.fillStyle = "#FFFFFF"; ctx2D.beginPath(); ctx2D.arc(0, 0, 7, 0, Math.PI * 2); ctx2D.fill(); // Aiming Vector const radAim = ((launchAngle - 90) * Math.PI) / 180; ctx2D.strokeStyle = "rgba(255, 255, 255, 0.5)"; ctx2D.lineWidth = 2; ctx2D.setLineDash([4, 4]); ctx2D.beginPath(); ctx2D.moveTo(0, 0); ctx2D.lineTo(Math.cos(radAim) * 60, Math.sin(radAim) * 60); ctx2D.stroke(); ctx2D.setLineDash([]); // Projectiles projectiles2D.forEach((p) => { if (showInertial && p.inertialPath.length > 1) { ctx2D.strokeStyle = "rgba(255, 255, 255, 0.7)"; ctx2D.lineWidth = 2; ctx2D.setLineDash([5, 5]); ctx2D.beginPath(); ctx2D.moveTo(p.inertialPath[0].x, p.inertialPath[0].y); for (let i = 1; i < p.inertialPath.length; i++) { ctx2D.lineTo(p.inertialPath[i].x, p.inertialPath[i].y); } ctx2D.stroke(); ctx2D.setLineDash([]); } if (showRotating && p.rotatingPath.length > 1) { ctx2D.strokeStyle = omega >= 0 ? "#00E5FF" : "#F59E0B"; ctx2D.lineWidth = 3; ctx2D.beginPath(); ctx2D.moveTo(p.rotatingPath[0].x, p.rotatingPath[0].y); for (let i = 1; i < p.rotatingPath.length; i++) { ctx2D.lineTo(p.rotatingPath[i].x, p.rotatingPath[i].y); } ctx2D.stroke(); } }); ctx2D.restore(); } // ============================================================================ // PART 3: MAIN SIMULATION LOOP & TELEMETRY // ============================================================================ 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; 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) { 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)"); } } function animate() { requestAnimationFrame(animate); if (mode === "globe") { // 3D Earth Globe Loop if (isRunning) { if (earthPivotG) { earthPivotG.rotation.y += omega * 0.18; } // Momentum damping for orbit interaction if (!isDragging3D && orbitG) { velX3D *= 0.92; velY3D *= 0.92; orbitG.rotation.y += velX3D; orbitG.rotation.x = Math.max(-1.4, Math.min(1.4, orbitG.rotation.x + velY3D)); } // Spin storm cyclones if (cyclonesGroup) { cyclonesGroup.visible = showAtmosphere; cyclonesGroup.children.forEach((c) => { const isCCW = c.userData.isCCW; c.rotation.y += isCCW ? 0.045 : -0.045; }); } // Update 3D projectiles for (let i = projectiles3D.length - 1; i >= 0; i--) { projectiles3D[i].update(omega); if (projectiles3D[i].isDead()) { projectiles3D[i].dispose(); projectiles3D.splice(i, 1); } } // Periodic demonstration auto-fire const now = Date.now(); if (now - lastAutoFire3D > 2200 && projectiles3D.length < 3) { fireProjectile3D(); lastAutoFire3D = now; } } if (renderer3D && scene3D && camera3D) { renderer3D.render(scene3D, camera3D); } } else { // 2D Turntable Carousel Loop if (isRunning) { simTime2D += 1; diskRotation2D += omega; for (let i = projectiles2D.length - 1; i >= 0; i--) { projectiles2D[i].update(); if (projectiles2D[i].isDead()) { projectiles2D.splice(i, 1); } } if (simTime2D - lastAutoFire2D > 90 && projectiles2D.length < 3) { fireProjectile2D(); lastAutoFire2D = simTime2D; } } if (ctx2D && canvas2D) { const rect = canvas2D.getBoundingClientRect(); ctx2D.clearRect(0, 0, rect.width, rect.height); drawCarousel2D(rect.width, rect.height); } } updateTelemetryUI(); } // ============================================================================ // PART 4: MODE SWITCHING & CONTROLS BINDING // ============================================================================ function switchMode(newMode) { mode = newMode; if (mode === "globe") { if (wrap3D) wrap3D.style.display = "block"; if (canvas2D) canvas2D.style.display = "none"; if (modeGlobeBtn) { modeGlobeBtn.classList.add("active"); modeGlobeBtn.setAttribute("aria-selected", "true"); } if (modeCarouselBtn) { modeCarouselBtn.classList.remove("active"); modeCarouselBtn.setAttribute("aria-selected", "false"); } if (latControlGroup) latControlGroup.style.display = "flex"; if (modeLabel) modeLabel.textContent = "3D Real Earth Simulator (GLB Model)"; if (modeHint) modeHint.innerHTML = "Drag to rotate Earth · Click to launch particle"; clearAllProjectiles3D(); fireProjectile3D(); } else { if (wrap3D) wrap3D.style.display = "none"; if (canvas2D) { canvas2D.style.display = "block"; resizeCanvas2D(); } if (modeCarouselBtn) { modeCarouselBtn.classList.add("active"); modeCarouselBtn.setAttribute("aria-selected", "true"); } if (modeGlobeBtn) { modeGlobeBtn.classList.remove("active"); modeGlobeBtn.setAttribute("aria-selected", "false"); } if (latControlGroup) latControlGroup.style.display = "none"; if (modeLabel) modeLabel.textContent = "Flat Rotating Turntable (Disk View)"; if (modeHint) modeHint.innerHTML = "Click canvas to fire projectile"; projectiles2D = []; fireProjectile2D(); } } function initControls() { if (modeGlobeBtn) { modeGlobeBtn.addEventListener("click", () => switchMode("globe")); } if (modeCarouselBtn) { modeCarouselBtn.addEventListener("click", () => switchMode("carousel")); } // Omega Slider 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(); }); } // Latitude Slider 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 + "°"; updateActiveLatitudeRingPosition(); clearActivePreset(); }); } // Speed Slider 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); }); } // Angle Slider 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 + "°"; updateTurretPosition(); }); } // 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; }); } const chkAtmo = document.getElementById("chk-atmosphere"); if (chkAtmo) { chkAtmo.addEventListener("change", (e) => { showAtmosphere = e.target.checked; }); } // Action buttons const btnFire = document.getElementById("btn-fire"); if (btnFire) { btnFire.addEventListener("click", () => { if (mode === "globe") fireProjectile3D(); else fireProjectile2D(); }); } const btnClear = document.getElementById("btn-clear"); if (btnClear) { btnClear.addEventListener("click", () => { clearAllProjectiles3D(); projectiles2D = []; }); } const btnPlayPause = document.getElementById("btn-pause-play"); if (btnPlayPause) { btnPlayPause.addEventListener("click", () => { isRunning = !isRunning; btnPlayPause.innerHTML = isRunning ? "⏸ Pause Simulation" : "▶ Play Simulation"; }); } // 2D Canvas click handler if (canvas2D) { canvas2D.addEventListener("click", () => { fireProjectile2D(); }); } window.addEventListener("resize", () => { if (mode === "carousel") resizeCanvas2D(); }); } 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; const omegaSlider = document.getElementById("slider-omega"); const omegaValDisplay = document.getElementById("val-omega"); if (omegaSlider) omegaSlider.value = omega; if (omegaValDisplay) omegaValDisplay.textContent = omega.toFixed(3); const latSlider = document.getElementById("slider-latitude"); const latValDisplay = document.getElementById("val-latitude"); if (latSlider) latSlider.value = latitude; if (latValDisplay) latValDisplay.textContent = latitude + "°"; const speedSlider = document.getElementById("slider-speed"); const speedValDisplay = document.getElementById("val-speed"); if (speedSlider) speedSlider.value = projectileSpeed; if (speedValDisplay) speedValDisplay.textContent = projectileSpeed.toFixed(1); updateActiveLatitudeRingPosition(); // Load corresponding 3D GLB model loadModelForPreset(name); clearAllProjectiles3D(); projectiles2D = []; if (mode === "globe") fireProjectile3D(); else fireProjectile2D(); }; // Expose global fire hook window.fireCoriolisProjectile = function () { if (mode === "globe") fireProjectile3D(); else fireProjectile2D(); }; // --------------------------------------------------------------------------- // Boot Sequence // --------------------------------------------------------------------------- function boot() { init3DScene(); resizeCanvas2D(); initControls(); switchMode("globe"); // 3D Earth Globe is default! animate(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })(); if (window.location.protocol.startsWith('http') && window.location.hostname.includes('digitalphablet.com')) { try { var ads = document.querySelectorAll('.ad-slot-product ins.adsbygoogle'); ads.forEach(function(ad) { if (!ad.getAttribute('data-adsbygoogle-status')) { (adsbygoogle = window.adsbygoogle || []).push({}); } }); } catch (e) {} }