{"id":22120202,"url":"https://github.com/joshbrew/nbody","last_synced_at":"2026-06-25T13:31:45.159Z","repository":{"id":205188466,"uuid":"713629858","full_name":"joshbrew/nbody","owner":"joshbrew","description":"Quick 2D n-body planet canvas render with gamey rocket physics","archived":false,"fork":false,"pushed_at":"2024-02-11T23:11:14.000Z","size":103,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-24T06:33:16.250Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshbrew.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-02T23:13:48.000Z","updated_at":"2023-11-04T18:22:03.000Z","dependencies_parsed_at":"2024-12-02T08:03:06.103Z","dependency_job_id":"b3784796-6a8b-4ff0-aaf8-7339502e0240","html_url":"https://github.com/joshbrew/nbody","commit_stats":null,"previous_names":["joshbrew/nbody"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/joshbrew/nbody","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fnbody","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fnbody/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fnbody/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fnbody/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshbrew","download_url":"https://codeload.github.com/joshbrew/nbody/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fnbody/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34778079,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-01T14:21:26.394Z","updated_at":"2026-06-25T13:31:45.107Z","avatar_url":"https://github.com/joshbrew.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nBody physics test with scaling\n\n[Live Demo](https://nbody2dtest.netlify.app)\n\nWith Nodejs installed:\n\n`npm i -g tinybuild` then `npm start`\n\nMove mouse to predict trajectory path\n\nClick to fire. \n\nThe physics are gamified so the behavior is more interesting\n![cap](./cap.PNG)\n\nThis is just a quick and dirty canvas rendering test for an n-body planetary physics sim and some logarithmic scaling for orbital distances and planet radii, including exaggerating moon orbits based on the dominant body. \n\nIt's not perfect by any means as the orbits can teleport if a new dominant body is picked up, the timestep is not continuous, and the scaling could be fine tuned.\n\n\n```js\nimport { innerSolarSystemConfig, outerSolarSystemConfig } from \"./src/planetConfig\";\n\n// Constants\nconst G = 6.67430e-11; // Gravitational constant\nlet timeStep = 3600; // One hour in seconds\nconst AU = 1.496e11; // One Astronomical Unit (distance from Earth to Sun) in meters\n\n// Add a rocket object. The rocket won't attract planets but will be attracted, so mass can be arbitrary to increase gravity\nconst SaturnV = {\n    mass: 480000, // Arbitrary mass of the rocket in kilograms\n    x: 0, // Initial X position will be set based on Earth's position\n    y: 0, // Initial Y position will be set based on Earth's position\n    vx: 0, // Initial X velocity of the rocket\n    vy: 0, // Initial Y velocity of the rocket\n    fx: 0, // Force applied in the X direction\n    fy: 0, // Force applied in the Y direction\n    color: 'red'\n};\nlet rocketInitialImpulse = 3e4; // Arbitrary large number for noticeable effect, this is multiplied over timeStep (e.g. 1Hr)\nlet rocketLaunched = false;\n\ndocument.body.insertAdjacentHTML('afterbegin',`\n    \u003cdiv\u003e\n      Rocket simulation with tweaked physics and log-scaling for fun-factor.\u003cbr/\u003e\n      Rocket Mass (kg): \u003cinput type='number' id='mass' value='${SaturnV.mass}'/\u003e\u003cbr/\u003e\n      Rocket Initial Force (multiplies over initial time step): \u003cinput type='number' id='force' value='${rocketInitialImpulse}'/\u003e\u003cbr/\u003e\n      Time step (s): \u003cinput type='number' id='timeStep' value='${timeStep}'/\u003e\n    \u003c/div\u003e\n    \u003ccanvas style=\"width:100%;\" id=\"solarSystem\" width=\"800\" height=\"800\"\u003e\u003c/canvas\u003e\n    \n`);\n\nconst canvas = document.getElementById('solarSystem');\nconst ctx = canvas.getContext('2d');\n\ndocument.getElementById('mass').onchange = (ev) =\u003e {\n  SaturnV.mass = parseFloat(ev.target.value);\n}\n\ndocument.getElementById('force').onchange = (ev) =\u003e {\n  rocketInitialImpulse = parseFloat(ev.target.value);\n}\n\ndocument.getElementById('timeStep').onchange = (ev) =\u003e {\n  timeStep = parseFloat(ev.target.value);\n}\n\n\n/** e.g.\n * planet: {\n *  name:'Eeyarth'\n *  mass:3e23,\n * \n *  distance:1, \n *    //OR\n *  x: 1.2*AU,\n *  y: 0.5*AU,\n * \n *  velocity: 47.87e3,\n *    //OR\n *  velocityX:50e3,\n *  velocityY:20e2,\n * \n *  color:'blue'\n * }\n * \n */\n\n// Solar system planets configurations\nconst solarSystemConfig = [\n  //inner planets and moon\n  ...innerSolarSystemConfig,\n  // Outer planets and their moons\n  ...outerSolarSystemConfig\n  // Add other planets if needed\n];\n\n// Generate the planets\nconst SolarSystem = generateSolarSystem(solarSystemConfig);\n  \n// Find the largest body to exclude it from the exaggeration\nconst largestBody = SolarSystem.reduce((prev, current) =\u003e (prev.mass \u003e current.mass) ? prev : current);\n  \n// Find the farthest planet to set the scale factor accordingly\nlet scaleFactor, logf, orbitExaggerationFactor=100;\nconst farthestPlanetDistance = Math.max(...solarSystemConfig.map(config =\u003e Math.abs(config.distance)));\nconst farthestPlanetDistanceM = farthestPlanetDistance*AU;\n\nlet mouseX, mouseY;\nconst timeSimulation = 300*timeStep; // nSteps\n\ncanvas.addEventListener('mousemove',(ev)=\u003e{\n    const rect = canvas.getBoundingClientRect();\n    mouseX = ev.clientX - rect.left - canvas.width / 2;\n    mouseY = ev.clientY - rect.top - canvas.height / 2;\n});\n\nfunction generateSolarSystem(planetConfigs) {\n  return planetConfigs.map(config =\u003e ({\n    name: config.name,\n    mass: config.mass,\n    x: config.x ? config.x : config.distance * AU, //provide x and y in meters or distance in AU\n    y: config.y ? config.y : config.distanceY ? config.distanceY : 0,\n    vx: config.velocityX ? config.velocityX : 0,\n    vy: config.velocity ? config.velocity : config.velocityY ? config.velocityY : 0,\n    color:config.color\n  }));\n}\n\n\nfunction distanceEasing(distanceFromCenter,sf=scaleFactor) {\n  let exp = 0.55;\n  if(distanceFromCenter \u003c farthestPlanetDistanceM) {\n    return distanceFromCenter \u003e 1 ? Math.pow(\n      distanceFromCenter*logf*sf, \n      (exp + (farthestPlanetDistance*0.0025)*(1-(distanceFromCenter/farthestPlanetDistanceM)))\n    ) : 0; \n  } else {\n    let value = Math.pow(\n      farthestPlanetDistanceM*logf*sf, \n      exp\n    );\n    return value + ((distanceFromCenter/farthestPlanetDistanceM)-1);\n  }\n}\n\nfunction drawBody(ctx, bodyX, bodyY, mass, canvasWidth, canvasHeight, color) {\n  const angle = Math.atan2(bodyY, bodyX);\n  // Apply an exponential/logarithmic transformation to the distances from the center\n  const distanceFromCenter = Math.sqrt(bodyX * bodyX + bodyY * bodyY);\n  const logDistance = distanceEasing(distanceFromCenter);\n  const screenX = (canvasWidth / 2) + Math.cos(angle) * scaleFactor * logDistance;\n  const screenY = (canvasHeight / 2) + Math.sin(angle) * scaleFactor * logDistance;\n\n  let scaled = Math.log10(mass)*0.10; //arbitrary logarithmic scaling factor for planet radii\n  const planetRadius = Math.pow(scaled, scaled)*.4; \n  ctx.beginPath();\n  ctx.arc(screenX, screenY, planetRadius, 0, Math.PI * 2);\n  ctx.fillStyle = color ? color : 'gray';\n  ctx.fill();\n}\n\n\nfunction drawSystem(\n  planets=SolarSystem, \n  rocket=SaturnV,\n  dt=timeStep\n) {\n    if (!scaleFactor) {\n      // The maximum distance we expect to encounter in the system, which will be scaled down to fit the canvas\n      const maxExpectedDistance = Math.log10(farthestPlanetDistance * AU + 1);\n      console.log(maxExpectedDistance);\n      scaleFactor = Math.min(canvas.width, canvas.height) / (2 * maxExpectedDistance);\n      logf = 1/(farthestPlanetDistanceM*0.5);\n      orbitExaggerationFactor = (farthestPlanetDistance \u003e scaleFactor ? scaleFactor*3.33 : farthestPlanetDistance*10); \n            \n    }\n  \n    ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear the canvas\n  \n    // Draw the planets\n    planets.forEach(planet =\u003e {\n    \n        let exaggeratedX = planet.x;\n        let exaggeratedY = planet.y;\n    \n        if (planet !== largestBody \u0026\u0026 planet.mostInfluentialBody \u0026\u0026 planet.mostInfluentialBody !== largestBody) {\n            const dx = planet.x - planet.mostInfluentialBody.x;\n            const dy = planet.y - planet.mostInfluentialBody.y;\n            exaggeratedX = planet.mostInfluentialBody.x + dx * orbitExaggerationFactor;\n            exaggeratedY = planet.mostInfluentialBody.y + dy * orbitExaggerationFactor;\n            planet.exaggeratedX = exaggeratedX;\n            planet.exaggeratedY = exaggeratedY;\n        }\n\n        drawBody(\n          ctx, \n          exaggeratedX, \n          exaggeratedY, \n          planet.mass, \n          canvas.width, \n          canvas.height, \n          planet.color\n        );\n\n        if(mouseX \u0026\u0026 mouseY \u0026\u0026 planet.name === 'Earth') {\n            // Draw a computed trajectory from Earth based on mouse direction\n            // Calculate the direction of the force based on mouse position\n            const angle = Math.atan2(mouseY, mouseX);\n\n            // Create a simulated rocket with initial position and velocity based on mouse position\n            let simulatedRocket = {\n                x: planet.x + Math.cos(angle) * planet.x * 0.2 * Math.sign(planet.x),\n                y: planet.y + Math.sin(angle) * planet.y * 0.2 * Math.sign(planet.y),\n                vx: planet.vx, // Initial velocity based on mouse position\n                vy: planet.vy, // Divide to scale the velocity\n                mass: rocket.mass\n            };\n\n            // Apply the initial force direction to the rocket for trajectory simulation\n            let vScalar = rocketInitialImpulse * dt * dt / simulatedRocket.mass;\n            simulatedRocket.vx += Math.cos(angle) * vScalar; //rocket thrust endures for a whole timeStep\n            simulatedRocket.vy += Math.sin(angle) * vScalar;\n\n            const distanceFromCenter = Math.sqrt(simulatedRocket.x ** 2 + simulatedRocket.y ** 2);\n            const logDistance = distanceEasing(distanceFromCenter);\n      \n            let simRocketAngle = Math.atan2(simulatedRocket.y, simulatedRocket.x);\n\n            // Convert polar coordinates to Cartesian coordinates for the trajectory point\n            let simRocketScreenX = (canvas.width / 2) + Math.cos(simRocketAngle) * logDistance * scaleFactor;\n            let simRocketScreenY = (canvas.height / 2) + Math.sin(simRocketAngle) * logDistance * scaleFactor;\n            // Draw the trajectory\n            ctx.beginPath();\n            // Start at the rocket's current location on the screen, not at the exaggerated position\n            ctx.moveTo(simRocketScreenX, simRocketScreenY);\n\n            let ssClone = structuredClone(planets); //clone the planet object so we can project trajectories forward\n            for (let t = 0; t \u003c timeSimulation; t += dt) {\n              updateSystem(ssClone, simulatedRocket, dt);\n              const distanceFromCenter = Math.sqrt(simulatedRocket.x ** 2 + simulatedRocket.y ** 2);\n              const logDistance = distanceEasing(distanceFromCenter);\n        \n              let simRocketAngle = Math.atan2(simulatedRocket.y, simulatedRocket.x);\n\n              // Convert polar coordinates to Cartesian coordinates for the trajectory point\n              let simRocketScreenX = (canvas.width / 2) + Math.cos(simRocketAngle) * logDistance * scaleFactor;\n              let simRocketScreenY = (canvas.height / 2) + Math.sin(simRocketAngle) * logDistance * scaleFactor;\n              \n              ctx.lineTo(simRocketScreenX, simRocketScreenY);\n            }\n\n            ctx.strokeStyle = 'yellow';\n            ctx.lineWidth = 1;\n            ctx.stroke();\n\n        }\n\n    });\n\n    \n    // Draw the rocket as a triangle\n    if (rocketLaunched) {\n        // Apply the same logarithmic scaling for the rocket\n        const distanceFromCenter = Math.sqrt(rocket.x ** 2 + rocket.y ** 2);\n        const logDistance = distanceEasing(distanceFromCenter); \n        const angle = Math.atan2(rocket.y, rocket.x);\n        const rocketX = (canvas.width / 2) + (Math.cos(angle) * logDistance * scaleFactor);\n        const rocketY = (canvas.height / 2) + (Math.sin(angle) * logDistance * scaleFactor);\n        const vangle = Math.atan2(rocket.vy, rocket.vx); // Direction of the velocity vector\n        const size = 7; // Size of the triangle representing the rocket\n\n        // Calculate the tip of the rocket\n        const tipX = rocketX + size * Math.cos(vangle);\n        const tipY = rocketY + size * Math.sin(vangle);\n\n        // Calculate the back corners of the rocket\n        const rearLeftX = rocketX - size * (Math.cos(vangle) - 0.5 * Math.sin(vangle));\n        const rearLeftY = rocketY - size * (Math.sin(vangle) + 0.5 * Math.cos(vangle));\n        const rearRightX = rocketX - size * (Math.cos(vangle) + 0.5 * Math.sin(vangle));\n        const rearRightY = rocketY - size * (Math.sin(vangle) - 0.5 * Math.cos(vangle));\n\n        ctx.beginPath();\n        ctx.moveTo(tipX, tipY); // Move to the tip of the triangle\n        ctx.lineTo(rearLeftX, rearLeftY); // Draw line to the rear left of the triangle\n        ctx.lineTo(rearRightX, rearRightY); // Draw line to the rear right of the triangle\n        ctx.closePath(); // Close the path to create the third side of the triangle\n        ctx.fillStyle = SaturnV.color;\n        ctx.fill();\n    } else {\n\n    }\n}\n\n\nfunction updateSystem(\n  planets=SolarSystem, \n  rocket=SaturnV,\n  dt=timeStep,\n  mainBody=largestBody\n) {\n    // Variables to calculate center of mass\n    let totalMass = 0;\n    let weightedX = 0;\n    let weightedY = 0;\n    let mainBodyMassLog;\n  \n    if (rocket) {\n        // Reset forces on the rocket\n        rocket.fx = 0;\n        rocket.fy = 0;\n        mainBodyMassLog = Math.log(Math.log(mainBody.mass));\n    }\n  \n    // Calculate the gravitational force between all pairs of bodies\n    for (let i = 0; i \u003c planets.length; i++) {\n        const planetA = planets[i];\n        planetA.maxForce = 0;\n        for (let j = 0; j \u003c planets.length; j++) {\n            if (i === j) continue; // Skip self\n    \n            const planetB = planets[j];\n    \n            const dx = planetA.x - planetB.x;\n            const dy = planetA.y - planetB.y;\n            const distance = Math.sqrt(dx * dx + dy * dy);\n    \n            if (distance === 0) throw new Error('Collision detected between ' + planetA.name + ' and ' + planetB.name);\n    \n            const force = (G * planetB.mass) / (distance * distance);\n    \n            // Update max force and most influential body for planet A\n            if (\n                force \u003e planetA.maxForce || \n                (planetB !== mainBody \u0026\u0026 force \u003e planetA.maxForce*0.25) //prefer nearby bodies (i.e. moons to planets to exaggerate orbits)\n            ) {\n                planetA.maxForce = force;\n                planetA.mostInfluentialBody = planetB;\n            }\n    \n            // Assuming the force is mutual, we don't need to update for planet B\n            // as it will be handled in its own turn in the outer loop\n    \n            const ax = force * dx / distance;\n            const ay = force * dy / distance;\n    \n            // Update velocities of planetA based on the force exerted by planetB\n            planetA.vx -= ax * dt;\n            planetA.vy -= ay * dt;\n        }\n        // Update the mass and weighted position for center of mass calculation\n        totalMass += planetA.mass;\n        weightedX += planetA.x * planetA.mass;\n        weightedY += planetA.y * planetA.mass;\n    \n        // Now that we have checked all other bodies, planetA knows its most influential body\n        // You can perform additional logic here using planetA.mostInfluentialBody if needed\n\n        if(rocket) {\n        \n          //let's use exaggerated orbits \n            const dx = rocket.x - (planetA.exaggeratedX ? planetA.exaggeratedX : planetA.x);\n            const dy = rocket.y - (planetA.exaggeratedY ? planetA.exaggeratedY : planetA.y);\n            const distance = Math.sqrt(dx * dx + dy * dy);\n\n            if (distance === 0) continue; // Avoid self-interaction or collision\n\n            const force = (G * planetA.mass) / (\n              Math.pow( //this is just to make it more fun rather than be accurate\n                distance,\n                (1.98 - (planetA.mass \u003c mainBody.mass ? 3*(mainBodyMassLog-Math.log(Math.log(planetA.mass))\n              ) : 0)))\n            );\n            // Calculate the acceleration of the rocket due to planet's gravity\n            const ax = force * dx / distance; //mass cancelled out already\n            const ay = force * dy / distance;\n\n            // Update the force vectors for the rocket\n            rocket.vx -= ax * dt;\n            rocket.vy -= ay * dt;\n\n            //dumb hit check\n            if(rocket === SaturnV \u0026\u0026 distance \u003c (0.02*AU)) {\n              console.log('Rocket hit', planetA.name);\n              rocketLaunched = false; //hit!\n            }\n\n        }\n    }\n  \n    // Update the positions of all planets based on their updated velocities\n    planets.forEach(planet =\u003e {\n      planet.x += planet.vx * dt;\n      planet.y += planet.vy * dt;\n    });\n  \n    // Calculate center of mass\n    const centerX = weightedX / totalMass;\n    const centerY = weightedY / totalMass;\n\n\n    if (rocket) {\n        // Update the velocity and position of the rocket based on the accumulated force\n        rocket.x += rocket.vx * dt;\n        rocket.y += rocket.vy * dt;\n    }\n\n  \n    // Optionally, use the center of mass to perform system-wide operations\n  \n    // Return the center of mass (if needed elsewhere)\n    return { x: centerX, y: centerY }; //returns the barycenter of all the moving bodies\n}\n    \n\nfunction animate() {\n    updateSystem(SolarSystem, rocketLaunched ? SaturnV : null, timeStep, largestBody); // Update the system based on physics\n    drawSystem(); // Draw the system with scaling applied\n    requestAnimationFrame(animate); // Call the next frame\n}\n  \nanimate(); // Start the animation\n\n\n\n\n// Function to apply the force to the rocket based on mouse position\nfunction applyForceToRocket(event) {\n        // Calculate the direction of the force based on mouse position\n        const rect = canvas.getBoundingClientRect();\n        const mouseX = event.clientX - rect.left - canvas.width / 2;\n        const mouseY = event.clientY - rect.top - canvas.height / 2;\n\n        // This is an arbitrary force calculation for the mouse interaction\n        const angle = Math.atan2(mouseY, mouseX);\n   \n        // Set the initial position and velocity of the rocket to be near Earth\n        const earth = SolarSystem.find(planet =\u003e planet.name === \"Earth\");\n        if (earth) {\n            SaturnV.x = earth.x + Math.cos(angle) * earth.x * 0.2 * Math.sign(earth.x);\n            SaturnV.y = earth.y + Math.sin(angle) * earth.y * 0.2 * Math.sign(earth.y);\n            SaturnV.vx = earth.vx;\n            SaturnV.vy = earth.vy;\n        }\n        \n        // Apply force in the direction of the mouse click\n        const vScalar = rocketInitialImpulse * timeStep * timeStep / SaturnV.mass;\n        SaturnV.vx += Math.cos(angle) * vScalar;\n        SaturnV.vy += Math.sin(angle) * vScalar;\n\n        rocketLaunched = true;\n\n        console.log('launched!')\n}\n\n// Listen for mouse clicks on the canvas to trigger the rocket force application\ncanvas.addEventListener('click', applyForceToRocket);\n\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshbrew%2Fnbody","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshbrew%2Fnbody","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshbrew%2Fnbody/lists"}