It’s All In the Head: Managing the Document Head of a React Powered Site With React Helmet

The document head might not be the most glamorous part of a website, but what goes into it is arguably just as important to the success of your website as its user interface. This is, after all, where you tell search engines about your website and integrate it with third-party applications like Facebook and Twitter, not to mention the assets, ranging from analytics libraries to stylesheets, that you load and initialize there.

A React application lives in the DOM node it was mounted to, and with this in mind, it is not at all obvious how to go about keeping the contents of the document head synchronized with your routes. One way might be to use the componentDidMount lifecycle method, like so:

componentDidMount() {
  document.title = "Whatever you want it to be";
}

However, you are not just going to want to change the title of the document, you are also going to want to modify an array of meta and other tags, and it will not be long before you conclude that managing the contents of the document head in this manner gets tedious pretty quickly and prone to error, not to mention that the code you end up with will be anything but semantic. There clearly has to be a better way to keep the document head up to date with your React application. And as you might suspect given the subject matter of this tutorial, there is a simple and easy to use component called React Helmet, which was developed by and is maintained by the National Football League(!).

In this tutorial, we are going to explore a number of common use cases for React Helmet that range from setting the document title to adding a CSS class to the document body. Wait, the document body? Was this tutorial not supposed to be about how to work with the document head? Well, I have got good news for you: React Helmet also lets you work with the attributes of the <html> and <body> tags; and it goes without saying that we have to look into how to do that, too!

View Repo

One important caveat of this tutorial is that I am going to ask you to install Gatsby — a static site generator built on top of React — instead of Create React App. That's because Gatsby supports server side rendering (SSR) out of the box, and if we truly want to leverage the full power of React Helmet, we will have to use SSR!

Why, you might ask yourself, is SSR important enough to justify the introduction of an entire framework in a tutorial that is about managing the document head of a React application? The answer lies in the fact that search engine and social media crawlers do a very poor job of crawling content that is generated through asynchronous JavaScript. That means, in the absence of SSR, it will not matter that the document head content is up to date with the React application, since Google will not know about it. Fortunately, as you will find out, getting started with Gatsby is no more complicated than getting started with Create React App. I feel quite confident in saying that if this is the first time you have encountered Gatsby, it will not be your last!

Getting started with Gatsby and React Helmet

As is often the case with tutorials like this, the first thing we will do is to install the dependencies that we will be working with.

Let us start by installing the Gatsby command line interface:

npm i -g gatsby-cli

While Gatsby's starter library contains a plethora of projects that provide tons of built-in features, we are going to restrict ourselves to the most basic of these starter projects, namely the Gatsby Hello World project.

Run the following from your Terminal:

gatsby new my-hello-world-starter https://github.com/gatsbyjs/gatsby-starter-hello-world

my-hello-world-starter is the name of your project, so if you want to change it to something else, do so by all means!

Once you have installed the starter project, navigate into its root directory by running cd [name of your project]/ from the Terminal, and once there, run gatsby develop. Your site is now running at http://localhost:8000, and if you open and edit src/pages/index.js, you will notice that your site is updated instantaneously: Gatsby takes care of all our hot-reloading needs without us even having to think of — and much less touch — a webpack configuration file. Just like Create React App does! While I would recommend all JavaScript developers learn how to set up and configure a project with webpack for a granular understanding of how something works, it sure is nice to have all that webpack boilerplate abstracted away so that we can focus our energy on learning about React Helmet and Gatsby!

Next up, we are going to install React Helmet:

npm i --save react-helmet

After that, we need to install Gatsby Plugin React Helmet to enable server rendering of data added with React Helmet:

npm i --save gatsby-plugin-react-helmet

When you want to use a plugin with Gatsby, you always need to add it to the plugins array in the gatsby-config.js file, which is located at the root of the project directory. The Hello World starter project does not ship with any plugins, so we need to make this array ourselves, like so:

module.exports = {
  plugins: [`gatsby-plugin-react-helmet`]
}

Great! All of our dependencies are now in place, which means we can move on to the business end of things.

Our first foray with React Helmet

The first question that we need to answer is where React Helmet ought to live in the application. Since we are going to use React Helmet on all of our pages, it makes sense to nest it in a component together with the page header and footer components since they will also be used on every page of our website. This component will wrap the content on all of our pages. This type of component is commonly referred to as a "layout" component in React parlance.

In the src directory, create a new directory called components in which you create a file called layout.js. Once you have done this, copy and paste the code below into this file.

import React from "react"
import Helmet from "react-helmet"

export default ({ children }) => (
  <>
    <Helmet>
      <title>Cool</title>
    </Helmet>
    <div>
      <header>
        <h1></h1>
        <nav>
          <ul>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

Let's break down that code.

First off, if you are new to React, you might be asking yourself what is up with the empty tags that wrap the React Helmet component and the header and footer elements. The answer is that React will go bananas and throw an error if you try to return multiple elements from a component, and for a long time, there was no choice but to nest elements in a parent element — commonly a div — which led to a distinctly unpleasant element inspector experience littered with divs that serve no purpose whatsoever. The empty tags, which are a shorthand way for declaring the Fragment component, were introduced to React as a solution to this problem. They let us return multiple elements from a component without adding unnecessary DOM bloat.

That was quite a detour, but if you are like me, you do not mind a healthy dose of code-related trivia. In any case, let us move on to the <Helmet> section of the code. As you are probably able to deduce from a cursory glance, we are setting the title of the document here, and we are doing it in exactly the same way we would in a plain HTML document; quite an improvement over the clunky recipe I typed up in the introduction to this tutorial! However, the title is hard coded, and we would like to be able to set it dynamically. Before we take a look at how to do that, we are going to put our fancy Layout component to use.

Head over to src/pages/ and open ìndex.js. Replace the existing code with this:

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout>
    <div>I live in a layout component, and life is pretty good here!</div>
  </Layout>

That imports the Layout component to the application and provides the markup for it.

Making things dynamic

Hard coding things in React does not make much sense because one of the major selling points of React is that makes it's easy to create reusable components that are customized by passing props to them. We would like to be able to use props to set the title of the document, of course, but what exactly do we want the title to look like? Normally, the document title starts with the name of the website, followed by a separator and ends with the name of the page you are on, like Website Name | Page Name or something similar. You are probably right, in thinking, we could use template literals for this, and right you are!

Let us say that we are creating a website for a company called Cars4All. In the code below, you will see that the Layout component now accepts a prop called pageTitle, and that the document title, which is now rendered with a template literal, uses it as a placeholder value. Setting the title of the document does not get any more difficult than that!

import React from "react"
import Helmet from "react-helmet"

export default ({ pageTitle, children }) => (
  <>
    <Helmet>
      <title>{`Cars4All | ${pageTitle}`}</title>
    </Helmet>
    <div>
      <header>
        <h1>Cars4All</h1>
        <nav>
          <ul>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

Let us update ìndex.js accordingly by setting the pageTitle to "Home":

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout pageTitle="Home">
    <div>I live in a layout component, and life is pretty good here!</div>
  </Layout>

If you open http://localhost:8000 in the browser, you will see that the document title is now Cars4All | Home. Victory! However, as stated in the introduction, we will want to do more in the document head than set the title. For instance, we will probably want to include charset, description, keywords, author and viewport meta tags.

How would we go about doing that? The answer is exactly the same way we set the title of the document:

import React from "react"
import Helmet from "react-helmet"

export default ({ pageMeta, children }) => (
  <>
    <Helmet>
      <title>{`Cars4All | ${pageMeta.title}`}</title>
      
      {/* The charset, viewport and author meta tags will always have the same value, so we hard code them! */}
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="author" content="Bob Trustly" />

      {/* The rest we set dynamically with props */}
      <meta name="description" content={pageMeta.description} />
      
      {/* We pass an array of keywords, and then we use the Array.join method to convert them to a string where each keyword is separated by a comma */}
      <meta name="keywords" content={pageMeta.keywords.join(',')} />
    </Helmet>
    <div>
      <header>
        <h1>Cars4All</h1>
        <nav>
          <ul>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

As you may have noticed, the Layout component no longer accepts a pageTitle prop, but a pageMeta one instead, which is an object that encapsulates all the meta data on a page. You do not have to do bundle all the page data like this, but I am very averse to props bloat. If there is data with a common denominator, I will always encapsulate it like this. Regardless, let us update index.js with the relevant data:

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout
    pageMeta={{
      title: "Home",
      keywords: ["cars", "cheap", "deal"],
      description: "Cars4All has a car for everybody! Our prices are the lowest, and the quality the best-est; we are all about having the cake and eating it, too!"
    }}
  >
    <div>I live in a layout component, and life is pretty good here!</div>
  </Layout>

If you open http://localhost:8000 again, fire up DevTools and dive into the document head, you will see that all of the meta tags we added are there. Regardless of whether you want to add more meta tags, a canonical URL or integrate your site with Facebook using the Open Graph Protocol, this is how you about about it. One thing that I feel is worth pointing out: if you need to add a script to the document head (maybe because you want to enhance the SEO of your website by including some structured data), then you have to render the script as a string within curly braces, like so:

<script type="application/ld+json">{`
{
  "@context": "http://schema.org",
  "@type": "LocalBusiness",
  "address": {
  "@type": "PostalAddress",
  "addressLocality": "Imbrium",
  "addressRegion": "OH",
  "postalCode":"11340",
  "streetAddress": "987 Happy Avenue"
  },
  "description": "Cars4All has a car for everybody! Our prices are the lowest, and the quality the best-est; we are all about having the cake and eating it, too!",
  "name": "Cars4All",
  "telephone": "555",
  "openingHours": "Mo,Tu,We,Th,Fr 09:00-17:00",
  "geo": {
  "@type": "GeoCoordinates",
  "latitude": "40.75",
  "longitude": "73.98"
  }, 			
  "sameAs" : ["http://www.facebook.com/your-profile",
  "http://www.twitter.com/your-profile",
  "http://plus.google.com/your-profile"]
}
`}</script>

For a complete reference of everything that you can put in the document head, check out Josh Buchea's great overview.

The escape hatch

For whatever reason, you might have to overwrite a value that you have already set with React Helmet — what do you do then? The clever people behind React Helmet have thought of this particular use case and provided us with an escape hatch: values set in components that are further down the component tree always take precedence over values set in components that find themselves higher up in the component tree. By taking advantage of this, we can overwrite existing values.

Say we have a fictitious component that looks like this:

import React from "react"
import Helmet from "react-helmet"

export default () => (
  <>
    <Helmet>
      <title>The Titliest Title of Them All</title>
    </Helmet>
    <h2>I'm a component that serves no real purpose besides mucking about with the document title.</h2>
  </>
)

And then we want to include this component in ìndex.js page, like so:

import React from "react"
import Layout from "../components/layout"
import Fictitious from "../components/fictitious"

export default () => 
  <Layout
    pageMeta={{
      title: "Home",
      keywords: ["cars", "cheap", "deal"],
      description: "Cars4All has a car for everybody! Our prices are the lowest, and the quality the best-est; we are all about having the cake and eating it, too!"
    }}
  >
    <div>I live in a layout component, and life is pretty good here!</div>
    <Fictitious />
  </Layout>

Because the Fictitious component hangs out in the underworld of our component tree, it is able to hijack the document title and change it from "Home" to "The Titliest Title of Them All." While I think it is a good thing that this escape hatch exists, I would caution against using it unless there really is no other way. If other developers pick up your code and have no knowledge of your Fictitious component and what it does, then they will probably suspect that the code is haunted, and we do not want to spook our fellow developers! After all, fighter jets do come with ejection seats, but that is not to say fighter pilots should use them just because they can.

Venturing outside of the document head

As mentioned earlier, we can also use React Helmet to change HTML and body attributes. For example, it's always a good idea to declare the language of your website, which you do with the HTML lang attribute. That's set with React Helmet like this:

<Helmet>

  /* Setting the language of your page does not get more difficult than this! */
  <html lang="en" />
    
  /* Other React Helmet-y stuff...  */
</Helmet>

Now let us really tap into the power of React Helmet by letting the pageMeta prop of the Layout component accept a custom CSS class that is added to the document body. Thus far, our React Helmet work has been limited to one page, so we can really spice things up by creating another page for the Cars4All site and pass a custom CSS class with the Layout component's pageMeta prop.

First, we need to modify our Layout component. Note that since our Cars4All website will now consist of more than one page, we need to make it possible for site visitors to navigate between these pages: Gatsby's Link component to the rescue!

Using the Link component is no more difficult than setting its to prop to the name of the file that makes up the page you want to link to. So if we want to create a page for the cars sold by Cars4All and we name the page file cars.js, linking to it is no more difficult than typing out <Link to="/cars/">Our Cars</Link>. When you are on the Our Cars page, it should be possible to navigate back to the ìndex.js page, which we call Home. That means we need to add <Link to="/">Home</Link> to our navigation as well.

In the new Layout component code below, you can see that we are importing the Link component from Gatsby and that the previously empty unordered list in the head element is now populated with the links for our pages. The only thing left to do in the Layout component is add the following snippet:

<body className={pageMeta.customCssClass ? pageMeta.customCssClass : ''}/>

...to the <Helmet> code, which adds a CSS class to the document body if one has been passed with the pageMeta prop. Oh, and given that we are going to pass a CSS class, we do, of course, have to create one. Let's head back to the src directory and create a new directory called css in which we create a file called main.css. Last, but not least, we have to import it into the Layout component, because otherwise our website will not know that it exists. Then add the following CSS to the file:

.slick {
  background-color: yellow;
  color: limegreen;
  font-family: "Comic Sans MS", cursive, sans-serif;
}

Now replace the code in src/components/layout.js with the new Layout code that we just discussed:

import React from "react"
import Helmet from "react-helmet"
import { Link } from "gatsby"
import "../css/main.css"

export default ({ pageMeta, children }) => (
  <>
    <Helmet>
      {/* Setting the language of your page does not get more difficult than this! */}
      <html lang="en" />
      
     {/* Add the customCssClass from our pageMeta prop to the document body */}
     
     <body className={pageMeta.customCssClass ? pageMeta.customCssClass : ''}/>
      
      <title>{`Cars4All | ${pageMeta.title}`}</title>
      
      {/* The charset, viewport and author meta tags will always have the same value, so we hard code them! */}
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="author" content="Bob Trustly" />

      {/* The rest we set dynamically with props */}
      <meta name="description" content={pageMeta.description} />
      
      {/* We pass an array of keywords, and then we use the Array.join method to convert them to a string where each keyword is separated by a comma */}
      <meta name="keywords" content={pageMeta.keywords.join(',')} />
    </Helmet>
    <div>
      <header>
        <h1>Cars4All</h1>
        <nav>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/cars/">Our Cars</Link></li>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

We are only going to add a custom CSS class to the document body in the cars.js page, so there is no need to make any modifications to the ìndex.js page. In the src/pages/ directory, create a file called cars.js and add the code below to it.

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout
    pageMeta={{
      title: "Our Cars",
      keywords: <a href="">"toyota", "suv", "volvo"],
      description: "We sell Toyotas, gas guzzlers and Volvos. If we don't have the car you would like, let us know and we will order it for you!!!",
      customCssClass: "slick"
    }}
  >
    <h2>Our Cars</h2>
    <div>A car</div>
    <div>Another car</div>
    <div>Yet another car</div>
    <div>Cars ad infinitum</div>
  </Layout>

If you head on over to http://localhost:8000, you will see that you can now navigate between the pages. Moreover, when you land on the cars.js page, you will notice that something looks slightly off... Hmm, no wonder I call myself a web developer and not a web designer! Let's open DevTools, toggle the document head and navigate back to the ìndex.js page. The content is updated when changing routes!

The icing on the cake

If you inspect the source of your pages, you might feel a tad bit cheated. I promised a SSR React website, but none of our React Helmet goodness can be found in the source.

What was the point of my foisting Gatsby on you, you might ask? Well, patience young padowan! Run gatsby build in Terminal from the root of the site, followed by gatsby serve.

Gatsby will tell you that the site is now running on http://localhost:9000. Dash over there and inspect the source of your pages again. Tadá, it's all there! You now have a website that has all the advantages of a React SPA without giving up on SEO or integrating with third-party applications and what not. Gatsby is amazing, and it is my sincere hope that you will continue to explore what Gatsby has to offer.

On that note, happy coding!

The post It’s All In the Head: Managing the Document Head of a React Powered Site With React Helmet appeared first on CSS-Tricks.

How I Learned to Stop Worrying and Love Git Hooks

The merits of Git as a version control system are difficult to contest, but while Git will do a superb job in keeping track of the commits you and your teammates have made to a repository, it will not, in itself, guarantee the quality of those commits. Git will not stop you from committing code with linting errors in it, nor will it stop you from writing commit messages that convey no information whatsoever about the nature of the commits themselves, and it will, most certainly, not stop you from committing poorly formatted code.

Fortunately, with the help of Git hooks, we can rectify this state of affairs with only a few lines of code. In this tutorial, I will walk you through how to implement Git hooks that will only let you make a commit provided that it meets all the conditions that have been set for what constitutes an acceptable commit. If it does not meet one or more of those conditions, an error message will be shown that contains information about what needs to be done for the commit to pass the checks. In this way, we can keep the commit histories of our code bases neat and tidy, and in doing so make the lives of our teammates, and not to mention our future selves, a great deal easier and more pleasant.

As an added bonus, we will also see to it that code that passes all the tests is formatted before it gets committed. What is not to like about this proposition? Alright, let us get cracking.

Prerequisites

In order to be able to follow this tutorial, you should have a basic grasp of Node.js, npm and Git. If you have never heard of something called package.json and git commit -m [message] sounds like code for something super-duper secret, then I recommend that you pay this and this website a visit before you continue reading.

Our plan of action

First off, we are going to install the dependencies that make implementing pre-commit hooks a walk in the park. Once we have our toolbox, we are going to set up three checks that our commit will have to pass before it is made:

  • The code should be free from linting errors.
  • Any related unit tests should pass.
  • The commit message should adhere to a pre-determined format.

Then, if the commit passes all of the above checks, the code should be formatted before it is committed. An important thing to note is that these checks will only be run on files that have been staged for commit. This is a good thing, because if this were not the case, linting the whole code base and running all the unit tests would add quite an overhead time-wise.

In this tutorial, we will implement the checks discussed above for some front-end boilerplate that uses TypeScript and then Jest for the unit tests and Prettier for the code formatting. The procedure for implementing pre-commit hooks is the same regardless of the stack you are using, so by all means, do not feel compelled to jump on the TypeScript train just because I am riding it; and if you prefer Mocha to Jest, then do your unit tests with Mocha.

Installing the dependencies

First off, we are going to install Husky, which is the package that lets us do whatever checks we see fit before the commit is made. At the root of your project, run:

npm i husky --save-dev

However, as previously discussed, we only want to run the checks on files that have been staged for commit, and for this to be possible, we need to install another package, namely lint-staged:

npm i lint-staged --save-dev

Last, but not least, we are going to install commitlint, which will let us enforce a particular format for our commit messages. I have opted for one of their pre-packaged formats, namely the conventional one, since I think it encourages commit messages that are simple yet to the point. You can read more about it here.

npm install @commitlint/{config-conventional,cli} --save-dev

## If you are on a device that is running windows
npm install @commitlint/config-conventional @commitlint/cli --save-dev

After the commitlint packages have been installed, you need to create a config that tells commitlint to use the conventional format. You can do this from your terminal using the command below:

echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

Great! Now we can move on to the fun part, which is to say implementing our checks!

Implementing our pre-commit hooks

Below is an overview of the scripts that I have in the package.json of my boilerplate project. We are going to run two of these scripts out of the box before a commit is made, namely the lint and prettier scripts. You are probably asking yourself why we will not run the test script as well, since we are going to implement a check that makes sure any related unit tests pass. The answer is that you have to be a little bit more specific with Jest if you do not want all unit tests to run when a commit is made.

"scripts": {
  "start": "webpack-dev-server --config ./webpack.dev.js --mode development",
  "build": "webpack --config ./webpack.prod.js --mode production",
  "test": "jest",
  "lint": "tsc --noEmit",
  "prettier": "prettier --single-quote --print-width 80 "./**/*.{js,ts}" --write"
}

As you can tell from the code we added to the package.json file below, creating the pre-commit hooks for the lint and prettier scripts does not get more complicated than telling Husky that before a commit is made, lint-staged needs to be run. Then you tell lint-staged to run the lint and prettier scripts on all staged JavaScript and TypeScript files, and that is it!

"scripts": {
  "start": "webpack-dev-server --config ./webpack.dev.js --mode development",
  "build": "webpack --config ./webpack.prod.js --mode production",
  "test": "jest",
  "lint": "tsc --noEmit",
  "prettier": "prettier --single-quote --print-width 80 "./**/*.{js,ts}" --write"
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "./**/*.{ts}": [
    "npm run lint",
    "npm run prettier"
  ]
}

At this point, if you set out to anger the TypeScript compiler by passing a string to a function that expects a number and then try to commit this code, our lint check will stop your commit in its tracks and tell you about the error and where to find it. This way, you can correct the error of your ways, and while I think that, in itself, is pretty powerful, we are not done yet!

By adding "jest --bail --coverage --findRelatedTests" to our configuration for lint-staged, we also make sure that the commit will not be made if any related unit tests do not pass. Coupled with the lint check, this is the code equivalent of wearing two safety harnesses while fixing broken tiles on your roof.

What about making sure that all commit messages adhere to the commitlint conventional format? Commit messages are not files, so we can not handle them with lint-staged, since lint-staged only works its magic on files staged for commit. Instead, we have to return to our configuration for Husky and add another hook, in which case our package.json will look like so:

"scripts": {
  "start": "webpack-dev-server --config ./webpack.dev.js --mode development",
  "build": "webpack --config ./webpack.prod.js --mode production",
  "test": "jest",
  "lint": "tsc --noEmit",
  "prettier": "prettier --single-quote --print-width 80 "./**/*.{js,ts}" --write"
},
"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",  //Our new hook!
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "./**/*.{ts}": [
    "npm run lint",
    "jest --bail --coverage --findRelatedTests", 
    "npm run prettier"
  ]
}

If your commit message does not follow the commitlint conventional format, you will not be able to make your commit: so long, poorly formatted and obscure commit messages!

If you get your house in order and write some code that passes both the linting and unit test checks, and your commit message is properly formatted, lint-staged will run the Prettier script on the files staged for commit before the commit is made, which feels like the icing on the cake. At this point, I think we can feel pretty good about ourselves; a bit smug even.

Implementing pre-commit hooks is not more difficult than that, but the gains of doing so are tremendous. While I am always skeptical of adding yet another step to my workflow, using pre-commit hooks has saved me a world of bother, and I would never go back to making my commits in the dark, if I am allowed to end this tutorial on a somewhat pseudo-poetical note.

The post How I Learned to Stop Worrying and Love Git Hooks appeared first on CSS-Tricks.

Creating Your Own Gravity and Space Simulator

Space is vast. Space is awesome. Space is difficult to understand — or so people tend to think. But in this tutorial I am going to show you that this is not the case. Quite the contrary; the laws that govern the motion of the stars, planets, asteroids and even entire galaxies are incredibly simple. You could argue that if our Universe was created by a developer, she sure was concerned about writing clean code that would be easy to maintain and scale.

What we are going to do is create a simulation of the inner region of our solar system using nothing but plain old JavaScript. It will be a gravitational n-body simulation where every mass feels the gravity of all the other masses being simulated. To spice things up, I will also show how you can enable users of your simulator to add planets of their own to the simulation with nothing but a little bit of mouse drag action, and in doing so, cause all sorts of cosmic mayhem. A gravity or space simulator would not be worthy of its name without motion trails, so I will show you how to create some fancy looking trails, too, in addition to some other shenanigans that will make the simulator a little bit more fun for the average user.

See the Pen
Gravity Simulator Tutorial
by Darrell Huffman (@thehappykoala)
on CodePen.

You will find the complete source code for this project in the Pen above. There is nothing fancy going on there. No bundling of modules, or transpilation of TypeScript or JSX into JavaScript; just HTML markup, CSS, and a healthy dose of JavaScript.

I came up with the idea for this while working on a project that is close to my heart, namely Harmony of the Spheres. Harmony of the Spheres is open source and very much a work in progress, so if you enjoy this tutorial and got your appetite for all things space and physics related going, check out the repository and fire away a pull request if you find a bug or have a cool new feature that you would like to see implemented.

For this tutorial, it is assumed that you have a basic grasp of JavaScript and the syntax and features that were introduced with ES6. Also, if you are able to draw a rectangle onto a canvas element, that would help, too. If you are not yet in possession of this knowledge, I suggest you head over to MDN and start reading up on ES6 classes, arrow functions, shorthand notation for defining key-value pairs for object literals and const and let. If you are not quite sure how to set up a canvas animation, go check out the documentation on the Canvas API on MDN.

Part 1: Writing a Gravitational N-Body Algorithm

To achieve the goal outlined above, we are going to draw on numerical integration, which is an approach to solving gravitational n-body problems where you take the positions and velocities of all objects at a given time (T), calculate the gravitational force they exert on each other and update their velocities and positions at time (T + dt, dt being shorthand for delta time), or in other words, the change in time between iterations. Repeating this process, we can trace the trajectories of a set of masses through space and time.

We will use a Cartesian coordinate system for our simulation. The Cartesian coordinate system is based on three mutually perpendicular coordinate axes: the x-axis, the y-axis, and the z-axis. The three axes intersect at the point called the origin, where x, y and z are equal to 0. An object in a Cartesian space has a unique position that is defined by its x, y and z values. The benefit of using the Cartesian coordinate system for our simulation is that the Canvas API, with which we will visualize our simulation, uses it, too.

For the purpose of writing an algorithm for solving the gravitational n-body problem, it is necessary to have an understanding of what is meant by velocity and acceleration. Velocity is the change in position of an object with time, while acceleration is the change in an object's velocity with time. Newton's first law of motion stipulates that every object will remain at rest or in uniform motion in a straight line unless compelled to change its state by the action of an external force. The Earth does not move in a straight line, but orbits the Sun, so clearly it is accelerating, but what is causing this acceleration? As you have probably guessed, given the subject matter of this tutorial, the answer is the gravitational forces exerted on Earth by the Sun, the other planets in our solar system and every other celestial object in the Universe.

Before we discuss gravity, let us write some pseudo code for updating the positions and velocities of a set of masses in Cartesian space. We store our masses as objects in an array where each object represents a mass with x, y and z position and velocity vectors. Velocity vectors are prefixed with a v — v for velocity!

const updatePositionVectors = (masses, dt) => {
  const massesLen = masses.length;

  for (let i = 0; i < massesLen; i++) {
    const massI = masses[i];

    mass.x += mass.vx * dt;
    mass.y += mass.vy * dt;
    mass.z += mass.vz * dt;
  }
};

const updateVelocityVectors = (masses, dt) => {
  const massesLen = masses.length;

  for (let i = 0; i < massesLen; i++) {
    const massI = masses[i];

    massI.vx += massI.ax * dt;
    massI.vy += massI.ay * dt;
    massI.vz += massI.az * dt;
  }
};

Looking at the code above, we can see that — as outlined in our discussion on numerical integration — every time we advance the simulation by a given time step, dt, we update the velocities of the masses being simulated and, with those velocities, we update the positions of the masses. The relationship between position and velocity is also made clear in the code above, as we can see that in one step of our simulation, the change in, for example, the x position vector of our mass is equal to the product of the mass's x velocity vector and dt. Similarly, we can make out the relationship between velocity and acceleration.

How, then, do we get the x, y and z acceleration vectors for a mass so that we can calculate the change in its velocity vectors? To get the contribution of massJ to the x acceleration vector of massI, we need to calculate the gravitational force exerted by massJ on massI, and then, to obtain the x acceleration vector, we simply calculate the product of this force and the distance between the two masses on the x axis. To get the y and z acceleration vectors, we follow the same procedure. Now we just have to figure out how to calculate the gravitational force exerted by massJ on massI to be able to write some more pseudo code. The formula we are interested in looks like this:

f = g * massJ.m / dSq * (dSq + s)^1/2

The formula above tells us that the gravitational force exerted by massJ on massI is equal to the product of the gravitational constant (g) and the mass of massJ (massJ.m) divided by the product of the sum of the squares of the distance between massI and massJ on the x, y and z axises (dSq) and the square root of dSq + s, where s is what is referred to as a softening constant (softeningConstant). Including a softening constant in our gravity calculations prevents a situation where the gravitational force exerted by massJ becomes infinite because it is too close to massI. This "bug," if you will, in the Newtonian theory of gravity arises for the reason that Newtonian gravity treats masses as point objects, which they are not in reality. Moving on, to get the net acceleration of massI along, for example, the x axis, we simply sum the acceleration induced on it by every other mass in the simulation.

Let us transform the above into code for updating the acceleration vectors of all the masses in the simulation.

const updateAccelerationVectors = (masses, g, softeningConstant) => {
  const massesLen = masses.length;

  for (let i = 0; i < massesLen; i++) {
    let ax = 0;
    let ay = 0;
    let az = 0;

    const massI = masses[i];

    for (let j = 0; j < massesLen; j++) {
      if (i !== j) {
        const massJ = masses[j];

        const dx = massJ.x - massI.x;
        const dy = massJ.y - massI.y;
        const dz = massJ.z - massI.z;

        const distSq = dx * dx + dy * dy + dz * dz;

        f = (g * massJ.m) / (distSq * Math.sqrt(distSq + softeningConstant));

        ax += dx * f;
        ay += dy * f;
        az += dz * f;
      }
    }

    massI.ax = ax;
    massI.ay = ay;
    massI.az = az;
  }
};

We iterate over all the masses in the simulation, and for every mass we calculate the contribution to its acceleration by the other masses in a nested loop and increment the acceleration vectors accordingly. Once we are out of the nested loop, we update the acceleration vectors of massI, which we can then use to calculate its new velocity vectors! Whowie. That was a lot. We now know how to update the position, velocity and acceleration vectors of n bodies in a gravity simulation using numerical integration.

But wait; there is something missing. That is right, we have talked about distance, mass and time, but we have never specified what units we ought to use for these quantities. As long as we are consistent, the choice is arbitrary, but generally speaking, it is a good idea to go for units that are suitable for the scales under consideration, so as to avoid awkwardly long numbers. In the context of our solar system, scientists tend to use astronomical units for distance, solar masses for mass and years for time. Adopting this set of units, the value of the gravitational constant (g in the formula for calculating the gravitational force exerted by massJ on massI) is 39.5. For the position and velocity vectors of the Sun and planets of the inner solar system — Mercury, Venus, Earth and Mars — we turn to NASA JPL's HORIZONS Web-Interface where we change the output setting to vector tables and the units to astronomical units and days. For whatever reason, Horizons does not serve vectors with years as the unit of time, so we have to multiply the velocity vectors by 365.25, the number of days in a year, to obtain velocity vectors that are consistent with our choice of years as the unit of time.

To think, that with the simple equations and laws discussed above, we can calculate the motion of every galaxy, star, planet and moon contained within this dazzling cosmic panorama captured by the Hubble Telescope, is nothing short of awe-inspiring. It is not for nothing Newton’s theory of gravity is referred to as "Newton’s law of universal gravitation."

A JavaScript class seems like an excellent way of encapsulating the methods we wrote above together with the data on the masses and the constants we need for our simulation, so let us do some refactoring:

class nBodyProblem {
  constructor(params) {
    this.g = params.g;
    this.dt = params.dt;
    this.softeningConstant = params.softeningConstant;

    this.masses = params.masses;
  }

  updatePositionVectors() {
    const massesLen = this.masses.length;

    for (let i = 0; i < massesLen; i++) {
      const massI = this.masses[i];

      massI.x += massI.vx * this.dt;
      massI.y += massI.vy * this.dt;
      massI.z += massI.vz * this.dt;
    }

    return this;
  }

  updateVelocityVectors() {
    const massesLen = this.masses.length;

    for (let i = 0; i < massesLen; i++) {
      const massI = this.masses[i];

      massI.vx += massI.ax * this.dt;
      massI.vy += massI.ay * this.dt;
      massI.vz += massI.az * this.dt;
    }
  }

  updateAccelerationVectors() {
    const massesLen = this.masses.length;

    for (let i = 0; i < massesLen; i++) {
      let ax = 0;
      let ay = 0;
      let az = 0;

      const massI = this.masses[i];

      for (let j = 0; j < massesLen; j++) {
        if (i !== j) {
          const massJ = this.masses[j];

          const dx = massJ.x - massI.x;
          const dy = massJ.y - massI.y;
          const dz = massJ.z - massI.z;

          const distSq = dx * dx + dy * dy + dz * dz;

          const f =
            (this.g * massJ.m) /
            (distSq * Math.sqrt(distSq + this.softeningConstant));

          ax += dx * f;
          ay += dy * f;
          az += dz * f;
        }
      }

      massI.ax = ax;
      massI.ay = ay;
      massI.az = az;
    }

    return this;
  }
}

That looks much nicer! Let us create an instance of this class. To do so, we need to specify three constants, namely the gravitational constant (g), the time step of the simulation (dt) and the softening constant (softeningConstant). We also need to populate an array with mass objects. Once we have all of those, we can create an instance of the nBodyProblem class, which we will call the innerSolarSystem, since, well, our simulation is going to be of the inner solar system!

const g = 39.5;
const dt = 0.008; // 0.008 years is equal to 2.92 days
const softeningConstant = 0.15;

const masses = [{
    name: "Sun", // We use solar masses as the unit of mass, so the mass of the Sun is exactly 1
    m: 1,
    x: -1.50324727873647e-6,
    y: -3.93762725944737e-6,
    z: -4.86567877183925e-8,
    vx: 3.1669325898331e-5,
    vy: -6.85489559263319e-6,
    vz: -7.90076642683254e-7
  }
  // Mercury, Venus, Earth and Mars data can be found in the pen for this tutorial
];

const innerSolarSystem = new nBodyProblem({
  g,
  dt,
  masses: JSON.parse(JSON.stringify(masses)), 
  softeningConstant
});

At this moment, you are probably looking at how I instantiated the nBodyProblem class and asking yourself what is up with the JSON parsing and string-ifying nonsense. The reason for why I went about passing the data contained in the masses array to the nBodyProblem constructor in this way is that we want our users to be able to reset the simulation. However, if we pass the masses array itself to the constructor of the nBodyProblem class when we create an instance of it, and then set the value of the masses property of this instance to be equal to the masses array when the user clicks the reset button, the simulation would not have been reset; the state of the masses from the end of the previous simulation run would still be there, and so would any masses the user had added. To solve this problem, we need to pass a clone of the masses array when we instantiate the nBodyProblem class or reset the simulation, so as to avoid modifying the masses array, which we need to keep pristine and untouched, and the easiest way of cloning it is to simply parse a string-ified version of it.

Okay, moving on: to advance the simulation by one step, we simply call:

innerSolarSystem.updatePositionVectors()
                .updateAccelerationVectors()
                .updateVelocityVectors();

Congratulations. You are now one step closer to collecting a Nobel prize in physics!

Part 2: Creating a Visual Manifestation for our Masses

We could represent our masses with cute little circles created with the Canvas API's arc method, but that would look kind of dull, and we would not get a sense of the trajectories of our masses through space and time, so let us write a JavaScript class that will be our template for how our masses manifest themselves visually. It will create a circle that leaves a predetermined number of smaller and faded circles where it has been before, which conveys a sense of motion and direction to the user. The farther you get from the current position of the mass, the smaller and more faded out the circles will become. In this way, we will have created a pretty looking motion trail for our masses.

The constructor accepts three arguments, namely the drawing context for our canvas element (ctx), the length of the motion trail (trailLength) that represents the number of previous positions of our mass that the trail will visualize and finally the radius (radius) of the circle that represents the current position of our mass. In the constructor we will also initialize an empty array that we will call positions, which will — quell surprise — store the current and previous positions of the mass that are included in the motion trail.

At this point, our manifestation class looks like this:

class Manifestation {

  constructor(ctx, trailLength, radius) {
    this.ctx = ctx;
    
    this.trailLength = trailLength;

    this.radius = radius;

    this.positions = [];
  }
  
}

How do we go about populating the positions array with positions and making sure that we do not store more positions than the number specified by the trailLength property? The answer is that we add a method to our class that accepts the x and y coordinates of the mass's position as arguments and stores them in an object in the array using the array push method, which appends an element to an array. This means that the current position of the mass will be the last element in the positions array. To make sure we do not store more positions than specified when we instantiated the class, we check if the length of the positions array is greater than the trailLength property. If it is, we use the array shift method to remove the first element, which represents the oldest stored position of the positions array.

class Manifestation {

  constructor() { /* The code for the constructor outlined above */ }

  storePosition(x, y) {
    this.positions.push({ x, y });

    if (this.positions.length > this.trailLength) 
      this.positions.shift();
  }
  
}

Okay, let us write a method that draws our motion trail. As you have probably guessed, it will accept two arguments, namely the x and y positions of the mass we are drawing the trail for. The first thing we need to do is to store the new position in the positions array and discard any superfluous positions stored in it. Then we iterate over the positions array and draw a circle for every position and voilà, we have ourselves a motion trail! But it does not look very nice, and I promised you that our trail would be pretty with circles that would become increasingly smaller and faded out according to how close they were to the current position of our mass in time.

What we need is, clearly, a scale factor whose size depends on how far away the position we are drawing is from the current position of our mass in time! An excellent way of obtaining an appropriate scale factor, for our intents and purposes, is to simply divide the index (i) of the circle being drawn by the length of the positions array. For example, if the number of elements allowed in the positions array is 25, element number 23 in that array will get a scale factor of 23 / 25, which gives us 0.92. Element number 5, on the other hand, will get a scale factor of 5 / 25, which gives us 0.2; the scale factor decreases the further we get from the current position of our mass, which is the relationship we want! Do note that we need a condition that makes sure that if the circle being drawn represents the current position, the scale factor is set to 1, as we do not want that circle to be either faded or smaller, for that matter. With all this in mind, let us write the code for the draw method of our Manifestation class.

class Manifestation {

  constructor() { /* The code for the constructor outlined above */ }

  storePosition() { /* The code for the storePosition method discussed above */ } 

  draw(x, y) {
    this.storePosition(x, y);

    const positionsLen = this.positions.length;

    for (let i = 0; i < positionsLen; i++) {
      let transparency;
      let circleScaleFactor;

      const scaleFactor = i / positionsLen;

      if (i === positionsLen - 1) {
        transparency = 1;
        circleScaleFactor = 1;
      } else {
        transparency = scaleFactor / 2;
        circleScaleFactor = scaleFactor;
      }

      this.ctx.beginPath();
      this.ctx.arc(
        this.positions[i].x,
        this.positions[i].y,
        circleScaleFactor * this.radius,
        0,
        2 * Math.PI
      );
      this.ctx.fillStyle = `rgb(0, 12, 153, ${transparency})`;

      this.ctx.fill();
    }
  }
  
}

Part 3: Visualizing Our Simulation

Let us write some canvas boilerplate and bind it together with the gravitational n-body algorithm and the motion trails, so that we can get an animation of our inner solar system simulation up and running. As mentioned in the introduction to this tutorial, I do not discuss the Canvas API in any great depth, as this is not an introductory tutorial on the Canvas API, so if you find yourself looking rather bemused and or perplexed, make haste and change this state of affairs by heading over to MDN’s documentation on the subject.

Before we continue, though, here is the HTML markup for our simulator:

<section id="controls-wrapper">
  <label>Mass of Added Planet</label>
  <select id="masses-list">
    <option value="0.000003003">Earth</option> 
    <option value="0.0009543">Jupiter</option>
    <option value="1">Sun</option>
    <option value="0.1">Red Dwarf Star</option>
  </select>
  <button id="clear-masses">Reset</button>
</section>
<canvas id="canvas"></canvas>

Now, we turn to the interesting part: the JavaScript. We start by getting a reference to the canvas element and then we proceed by getting its drawing context. Next, we set the dimensions of our canvas element. When it comes to canvas animations on the web, I do not spare any expenses in terms of screen real estate, so let us set the width and height properties of the canvas element to the width and height of the browser window, respectively. You will notice that I have drawn on a peculiar syntax for setting the width and height of the canvas element in that I have declared, in one statement, that the width variable is equal to the width property of the canvas element which, in turn, is equal to the width of the window. Some developers frown upon the use of this syntax, but I find it to be semantically beautiful. If you do not feel the same way, you can deconstruct that statement into two statements. Generally speaking, do whatever you feel most comfortable with, or if you find yourself collaborating with others, what the team has agreed on.

const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");

const width = (canvas.width = window.innerWidth);
const height = (canvas.height = window.innerHeight);

At this point, we are going to declare some constants for our animation. More specifically, there are three of them. The first is the radius (radius) of the circle, which represents the current position of a mass, in pixels. The second is the length of our motion trail (trailLength), which is the number of previous positions that it includes. Last, but not least, we have the scale (scale) constant, which represents the number of pixels per astronomical unit; Earth is one astronomical unit from the Sun, so if we did not introduce this scale factor, our inner solar system would look very claustrophobic, to say the least.

const scale = 70;
const radius = 4;
const trailLength = 35;

Let us now turn to the visual manifestations of the masses we are simulating. We have written a class that encapsulates their behavior, but how do we instantiate and work with these manifestations in our code? The most convenient and elegant way would be to populate every element of the masses array we are simulating with an instance of the Manifestation class, so let us write a simple method that iterates over these masses and does just that, which we then invoke.

const populateManifestations = masses => {
  masses.forEach(
    mass =>
    (mass["manifestation"] = new Manifestation(
      ctx,
      trailLength,
      radius
    ))
  );
};

populateManifestations(innerSolarSystem.masses);

Our simulator is meant to be a playful affair, so it is only to be expected that users will spawn masses left and right and that after a minute, or so, the inner solar system will look like an unrecognizable cosmic mess, which is why I think it would be decent of us to provide them with the ability to reset the simulation. To achieve this goal, we start by attaching an event listener to the reset button, and then we write a callback for this event listener that sets the value of the masses property of the innerSolarSystem object to a clone of the masses array. As we cloned the masses array, we no longer have the manifestations of our masses in it, so we call the populateManifestations method to make sure that our users have something to look at after having reset the simulation.

document.querySelector('#reset-button').addEventListener('click', () => {
  innerSolarSystem.masses = JSON.parse(JSON.stringify(masses));
  populateManifestations(innerSolarSystem.masses);       
}, false);

Okay, enough setting things up. Let us breathe some life into the inner solar system by writing a method that, with the help of the requestAnimationFrame API, will run 60 steps of our simulation a second and animate the results with motion trails and labels for the planets of the inner solar system and the Sun.

The first thing this method does is advance the inner solar system by one step and it does so by updating the position, acceleration and velocity vectors of its masses. Then we prepare the canvas element for the next animation cycle by clearing it of what was drawn in the preceding animation cycle using the Canvas API’s clearRect method.

Next, we iterate over the masses array and invoke the draw method of each mass manifestation. Moreover, if the mass being drawn has a name, we draw it onto the canvas, so that the user can see where the original planets are after things have gone haywire. Looking at the code in the loop, you will probably notice that we are not setting, for example, the value of the mass’s x coordinate on the canvas to massI times scale, and that we are in fact setting it to the width of the viewport divided by two plus massI times scale. Why is this? The answer is that the origin (x = 0, y = 0) of the canvas coordinate system is set to the top left corner of the canvas element, so to center our simulation on the canvas where it is clearly visible to the user, we must include this offset.

After the loop, at the end of the animate method, we call requestAnimationFrame with the animate method as the callback, and then the whole process discussed above is repeated again, creating yet another frame — and run in quick succession, these frames have brought the inner solar system to life. But wait, we have missed something! If you were to run the code I have walked you through thus far, you would not see anything at all. Fortunately, all we have to do to change this sad state of affairs is to proverbially give the inner solar system a kick in its rear end (no, I am not going to fall for the temptation of inserting a Uranus joke here; grow up!) by invoking the animate method!

const animate = () => {
  innerSolarSystem
    .updatePositionVectors()
    .updateAccelerationVectors()
    .updateVelocityVectors();

  ctx.clearRect(0, 0, width, height);

  const massesLen = innerSolarSystem.masses.length;

  for (let i = 0; i < massesLen; i++) {
    const massI = innerSolarSystem.masses[i];

    const x = width / 2 + massI.x * scale;
    const y = height / 2 + massI.y * scale;

    massI.manifestation.draw(x, y);

    if (massI.name) {
      ctx.font = "14px Arial";
      ctx.fillText(massI.name, x + 12, y + 4);
      ctx.fill();
    }
  }

  requestAnimationFrame(animate);
};

animate();
Our visualization of Mercury, Venus, Earth and Mars going about their day-to-day business of running circles around the sun. Looks pretty neat.

Woah! We have now gotten to the point where our simulation is animated, with the masses represented by dainty little blue circles stalked by marvelous looking motion trails. That is pretty cool in itself, if you were to ask me; but I did promise to also show how you can enable the user to add masses of their own to the simulation with a little bit of mouse drag action, so we are not done quite yet!

Part 4: Adding Masses with the Mouse

The idea here is that the user should be able to press down on the mouse button and draw a line by dragging it; the line will start where the user pressed down and end at the current position of the mouse cursor. When the user releases the mouse button, a new mass is spawned at the position of the screen where the user pressed down the mouse button, and the direction the mass will move is determined by the direction of the line; the length of the line determines the velocity vectors of the mass. So, how do we go about implementing this? Let us run through what we need to do, step by step. The code for steps one through six go above the animate method, while the code for step seven is a small addition to the animate method.

1. We need two variables that will store the x and y coordinates where the user pressed down the mouse button on the screen.

let mousePressX = 0;
let mousePressY = 0;

2. We need two variables that store the current x and y coordinates of the mouse cursor on the screen.

let currentMouseX = 0;
let currentMouseY = 0;

3. We need one variable that keeps track of whether the mouse is being dragged or not. The mouse is being dragged in the time that passes from when the user has pressed down the mouse button to the point where he releases it.

let dragging = false;

4. We need to attach a mousedown listener to the canvas element that logs the x and y coordinates of where the mouse was pressed down and sets the dragging variable to true.

canvas.addEventListener(
  "mousedown",
  e => {
    mousePressX = e.clientX;
    mousePressY = e.clientY;
    dragging = true;
  },
  false
);

5. We need to attach a mousemove listener to the canvas element that logs the current x and y coordinates of the mouse cursor.

canvas.addEventListener(
  "mousemove",
  e => {
    currentMouseX = e.clientX;
    currentMouseY = e.clientY;
  },
  false
);

6. We need to attach a mouseup listener to the canvas element that sets the drag variable to false, and pushes a new object representing a mass into the innerSolarSystem.masses array where the x and y position vectors are the point where the user pressed down the mouse button divided by value of the scale variable.

If we did not divide these vectors by the scale variable, the added masses would end up way out in the solar system, which is not what we want. The z position vector is set to zero and so is the z velocity vector. The x velocity vector is set to the x coordinate where the mouse was released subtracted by the x coordinate where the mouse was pressed down, and then you divide this number by 35. I will be honest and admit that 35 is a magical number that just happens to give you reasonable velocities when you add masses with the mouse to the inner solar system. Same procedure for the y velocity vector. The mass (m) of the mass we are adding is set by the user with a select element that we have populated with the masses of some famous celestial objects in the HTML markup. Last, but not least, we populate the object representing our mass with an instance of the Manifestation class so that the user can see it on the screen!

const massesList = document.querySelector("#masses-list");

canvas.addEventListener(
  "mouseup",
  e => {
    const x = (mousePressX - width / 2) / scale;
    const y = (mousePressY - height / 2) / scale;
    const z = 0;
    const vx = (e.clientX - mousePressX) / 35;
    const vy = (e.clientY - mousePressY) / 35;
    const vz = 0;

    innerSolarSystem.masses.push({
      m: parseFloat(massesList.value),
      x,
      y,
      z,
      vx,
      vy,
      vz,
      manifestation: new Manifestation(ctx, trailLength, radius)
    });

    dragging = false;
  },
  false
);

7. In the animate function, after the loop where we draw our manifestations and, before we call requestAnimationFrame, check if the mouse is being dragged. If that is the case, we’ll draw a line between the position where the mouse was pressed down and the mouse cursors current position.

const animate = () => {
  // Preceding code in the animate method down to and including the loop where we draw our mass manifestations

  if (dragging) {
    ctx.beginPath();
    ctx.moveTo(mousePressX, mousePressY);
    ctx.lineTo(currentMouseX, currentMouseY);
    ctx.strokeStyle = "red";
    ctx.stroke();
  }

  requestAnimationFrame(animate);
};
The inner solar system is about to get a lot more interesting — we can now add masses to our simulation!

Adding masses to our simulation with your mouse is not more difficult than that! Now, grab your mouse and unleash some mayhem on the inner solar system.

Part 5: Fencing off the Inner Solar System

As you will probably have noticed after adding some masses to the simulation, celestial objects are very shenanigan-prone in that they have a tendency to dance their way out of the viewport, especially if the added masses are very massive or they have too high of a velocity, which is kind of annoying. The natural solution to this problem is, of course, to fence off the inner solar system so that if a mass reaches the edge of the viewport, it will bounce back in! Sounds like quite a project, implementing this functionality, but fortunately doing so is a rather simple affair. At the end of the loop where we iterate over the masses and draw them in the animate method, we have insert two conditions: one that checks if our mass is outside the bounds of the viewport on the x-axis, and another that does the same check for the y axis. If the position of our mass is outside of the viewport on the x axis we reverse its x velocity vector so that it bounces back into the viewport, and the same logic applies if our mass is outside of the viewport on the y axis. With these two conditions, the animate method will look like so:

const animate = () => {
  // Advance the simulation by one step; clear the canvas

  for (let i = 0; i < massesLen; i++) {
  
    // Preceding loop code

    if (x < radius || x > width - radius) massI.vx = -massI.vx;

    if (y < radius || y > height - radius) massI.vy = -massI.vy;
  }

  requestAnimationFrame(animate);
};
Absolute madness! Venus, you silly planet, what are you doing out there?! You are supposed to be orbiting the Sun!

Ping, pong! It is almost as though we are playing a game of cosmic billiards with all those masses bouncing off the fence that we have built for the inner solar system!

Concluding Remarks

People have a tendency to think of orbital mechanics — which is what we have played around with in this tutorial — as something that is beyond the understanding of mere mortals such as yours truly. Truth, though, is that orbital mechanics follows a very simple and elegant set of rules, as this tutorial is a testament to. With a little bit of JavaScript and high-school mathematics and physics, we have reconstructed the inner solar system to a reasonable degree of accuracy, and gone beyond that to make things a little bit more spicy and, therefore, more interesting. With this simulator, you can answer silly what-if questions along the lines of, "What would happen if I flung a star with the mass of the Sun into our inner solar system?" or develop a feeling for Kepler's laws of planetary motion by, for example, observing the relationship between the distance of a mass from the Sun and its velocity.

I sure had fun writing this tutorial, and it is my sincere hope that you had as much fun reading it!

The post Creating Your Own Gravity and Space Simulator appeared first on CSS-Tricks.