I came across this update from Scott Kellum’s and Sal Hernandez’s project Typetura via my Medium feed this morning, and what a delight?!
(Also, wow, I really have been out of the game for a minute.)
Typetura.js is a fluid design solution, for any property, based on any input. It’s not for just typography across screen sizes. Transition between anything — width, height, scroll position, cursor position, and more.https://t.co/EoouX0PkGC
This is quite exciting! Typetura wants to deal with some of the main problems that come up when utilizing fluid type in your CSS.
> Typetura is a fluid typesetting tool. Use the slider at the top of the screen to select the breakpoint you want to style, then use the panel on the left of the screen to style your page.https://t.co/6cjgdEylwY
Typetura was created to make fluid typography mainstream. To do this there were two problems to solve. First, develop an implementation that is feature rich and easy to implement with CSS. Second, create a design tool that designers can use to illustrate how they want fluid typography to look.
I love a tool that tries to remove friction and make technologies easier to use.
To ensure the implementation was easy to use and understand, Typetura needed a simple, declarative syntax in vanilla CSS. This means no complicated math or Sass tricks.
Design software is constructed around fixed art boards, but there needs to be a way for designers to communicate how designs transition between sizes… Typetura is a tool that enables designers to work with a fluid canvas.
I came across this update from Scott Kellum’s and Sal Hernandez’s project Typetura via my Medium feed this morning, and what a delight?!
(Also, wow, I really have been out of the game for a minute.)
Typetura.js is a fluid design solution, for any property, based on any input. It’s not for just typography across screen sizes. Transition between anything — width, height, scroll position, cursor position, and more.https://t.co/EoouX0PkGC
This is quite exciting! Typetura wants to deal with some of the main problems that come up when utilizing fluid type in your CSS.
> Typetura is a fluid typesetting tool. Use the slider at the top of the screen to select the breakpoint you want to style, then use the panel on the left of the screen to style your page.https://t.co/6cjgdEylwY
Typetura was created to make fluid typography mainstream. To do this there were two problems to solve. First, develop an implementation that is feature rich and easy to implement with CSS. Second, create a design tool that designers can use to illustrate how they want fluid typography to look.
I love a tool that tries to remove friction and make technologies easier to use.
To ensure the implementation was easy to use and understand, Typetura needed a simple, declarative syntax in vanilla CSS. This means no complicated math or Sass tricks.
Design software is constructed around fixed art boards, but there needs to be a way for designers to communicate how designs transition between sizes… Typetura is a tool that enables designers to work with a fluid canvas.
Scott O’Hara digs into the <figure> and <figcaption> elements. Gotta love a good ol’ HTML deep dive.
I use these on just about every blog post here on CSS-Tricks, and as I’ve suspected, I’ve basically been doing it wrong forever. My original thinking was that a figcaption was just as good as the alt attribute. I generally use it to describe the image.
<figure>
<img src="starry-night.jpg" alt="">
<figcaption>The Starry Night, a famous painting by Vincent van Gogh</figcaption>
</figure>
I intentionally left off the alt text, because the figcaption is saying what I would want to say in the alt text and I thought duplicating it would be annoying (to a screen reader user) and unnecessary. Scott says that’s bad as the empty alt text makes the image entirely undiscoverable by some screen readers and the figure is describing nothing as a result.
The correct answer, I think, is to do more work:
<figure>
<img src="starry-night.jpg" alt="An abstract painting with a weird squiggly tree thing in front of a swirling starry nighttime sky.">
<figcaption>The Starry Night, a famous painting by Vincent van Gogh</figcaption>
</figure>
It’s a good goal, and I should do better about this. It’s just laziness that gets in the way, and laziness that makes me wish there was a pattern that allowed me to write a description once that worked for both. Maybe something like Nino Ross Rodriguez just shared today where artificial intelligence can take some of the lift. But that’s kinda not the point here. The point is that you can’t write it once because <figcaption> and alt do different things.
Web developers and content editors alike often forget or ignore one of the most important parts of making a website accessible and SEO performant: image alt text. You know, that seemingly small image attribute that describes an image:
<img src="/cute/sloth/image.jpg" alt="A brown baby sloth staring straight into the camera with a tongue sticking out." >
📷 Credit: Huffington Post
If you regularly publish content on the web, then you know it can be tedious trying to come up with descriptive text. Sure, 5-10 images is doable. But what if we are talking about hundreds or thousands of images? Do you have the resources for that?
Let’s look at some possibilities for automatically generating alt text for images with the use of computer vision and image recognition services from the likes Google, IBM, and Microsoft. They have the resources!
Reminder: What is alt text good for?
Often overlooked during web development and content entry, the alt attribute is a small bit of HTML code that describes an image that appears on a page. It’s so inconspicuous that it may not appear to have any impact on the average user, but it has very important uses indeed:
Web Accessibility for Screen Readers: Imagine a page with lots of images and not a single one contains alt text. A user surfing in using a screen reader would only hear the word “image” blurted out and that’s not very helpful. Great, there’s an image, but what is it? Including alt enables screen readers to help the visually impaired “see” what’s there and have a better understanding of the content of the page. They say a picture is worth a thousand words — that’s a thousand words of context a user could be missing.
Display text if an image does not load: The World Wide Web seems infallible and, like New York City, that it never sleeps, but flaky and faulty connections are a real thing and, if that happens, well, images tend not to load properly and “break.” Alt text is a safeguard in that it displays on the page in place of where the “broken” image is, providing users with content as a fallback.
SEO performance: Alt text on images contributes to SEO performance as well. Though it doesn’t exactly help a site or page skyrocket to the top of the search results, it is one factor to keep in mind for SEO performance.
Knowing how important these things are, hopefully you’ll be able to include proper alt text during development and content entry. But are your archives in good shape? Trying to come up with a detailed description for a large backlog of images can be a daunting task, especially if you’re working on tight deadlines or have to squeeze it in between other projects.
What if there was a way to apply alt text as an image is uploaded? And! What if there was a way to check the page for missing alt tags and automagically fill them in for us?
There are available solutions!
Computer vision (or image recognition) has actually been offered for quite some time now. Companies like Google, IBM and Microsoft have their own APIs publicly available so that developers can tap into those capabilities and use them to identify images as well as the content in them.
There are developers who have already utilized these services and created their own plugins to generate alt text. Take Sarah Drasner’s generator, for example, which demonstrates how Azure’s Computer Vision API can be used to create alt text for any image via upload or URL. Pretty awesome!
There’s also Automatic Alternative Text by Jacob Peattie, which is a WordPress plugin that uses the same Computer Vision API. It’s basically an addition to the workflow that allows the user to upload an image and generated alt text automatically.
Tools like these generally help speed-up the process of content management, editing and maintenance. Even the effort of thinking of a descriptive text has been minimized and passed to the machine!
Getting Your Hands Dirty With AI
I have managed to have played around with a few AI services and am confident in saying that Microsoft Azure’s Computer Vision produces the best results. The services offered by Google and IBM certainly have their perks and can still identify images and proper results, but Microsoft’s is so good and so accurate that it’s not worth settling for something else, at least in my opinion.
Creating your own image recognition plugin is pretty straightforward. First, head down to Microsoft Azure Computer Vision. You’ll need to login or create an account in order to grab an API key for the plugin.
Once you’re on the dashboard, search and select Computer Vision and fill in the necessary details.
Starting out
Wait for the platform to finish spinning up an instance of your computer vision. The API keys for development will be available once it’s done.
Keys: Also known as the Subscription Key in the official documentation
Let the interesting and tricky parts begin! I will be using vanilla JavaScript for the sake of demonstration. For other languages, you can check out the documentation. Below is a straight-up copy and paste of the code and you can use to replace the placeholders.
var request = new XMLHttpRequest();
request.open('POST', 'https://[LOCATION]/vision/v1.0/describe?maxCandidates=1&language=en', true);
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestHeader('Ocp-Apim-Subscription-Key', '[SUBSCRIPTION_KEY]');
request.send(JSON.stringify({ "url": "[IMAGE_URL]" }));
request.onload = function () {
var resp = request.responseText;
if (request.status >= 200 && request.status < 400) {
// Success!
console.log('Success!');
} else {
// We reached our target server, but it returned an error
console.error('Error!');
}
console.log(JSON.parse(resp));
};
request.onerror = function (e) {
console.log(e);
};
Alright, let’s run through some key terminology of the AI service.
Location: This is the subscription location of the service that was selected prior to getting the subscription keys. If you can’t remember the location for some reason, you can go to the Overview screen and find it under Endpoint.
Overview > Endpoint : To get the location value
Subscription Key: This is the key that unlocks the service for our plugin use and can be obtained under Keys. There’s two of them, but it doesn’t really matter which one is used.
Image URL: This is the path for the image that’s getting the alt text. Take note that the images that are sent to the API must meet specific requirements:
File type must be JPEG, PNG, GIF, BMP
File size must be less than 4MB
Dimensions should be greater than 50px by 50px
Easy peasy
Thanks to big companies opening their services and API to developers, it’s now relatively easy for anyone to utilize computer vision. As a simple demonstration, I uploaded the image below to Microsoft Azure’s Computer Vision API.
From there, you could pick out the alt text that could be potentially used for an image. How you build upon this capability is your business:
You could create a CMS plugin and add it to the content workflow, where the alt text is generated when an image is uploaded and saved in the CMS.
You could write a JavaScript plugin that adds alt text on-the-fly, after an image has been loaded with notably missing alt text.
You could author a browser extension that adds alt text to images on any website when it finds images with it missing.
You could write code that scours your existing database or repo of content for any missing alt text and updates them or opens pull requests for suggested changes.
Take note that these services are not 100% accurate. They do sometimes return a low confidence rating and a description that is not at all aligned with the subject matter. But, these platforms are constantly learning and improving. After all, Rome wasn’t built in a day.
To make a sign in form with good UX requires UI state management, meaning we’d like to minimize the cognitive load to complete it and reduce the number of required user actions while making an intuitive experience. Think about it: even a relatively simple email and password sign in form needs to handle a number of different states, like empty fields, errors, password requirements, loading and success.
Thankfully, state management is what React was made for and I was able to create a sign in form with it using an approach that features XState, a JavaScript state management library using finite machines.
State management? Finite machines? We’re going to walk through these concepts together while putting together a solid sign in form.
Jumping ahead, here’s what we’re going to build together:
First, let’s set up
We’ll need a few tools before getting started. Here’s what to grab:
We already mentioned that XState is a state management JavaScript library. Its approach uses finite state machines which makes it ideal for this sort of project. For example:
It is a thoroughly tried and tested approach to state management. Finite state machines have been around for 30+ years.
The machine config is the core of XState. It is a statechart and it will define the logic of our form. I have broken it down into the following parts, which we’ll go over one by one.
1. The States
We need a way to control what to show, hide, enable and disable. We will control this using named-states, which include:
dataEntry: This is the state when the user can enter an email and password into the provided fields. We can consider this the default state. The current field will be highlighted in blue.
awaitingResponse: This is after the browser makes a request to the authentication service and we are waiting for the response. We’ll disable the form and replace the button with a loading indicator when the form is in this state.
emailErr: Whoops! This state is thrown when there is a problem with the email address the user has entered. We’ll highlight that field, display the error, and disable the other field and button.
passwordErr: This is another error state, this time when there is a problem with the password the user has entered. Like the previous error, we’ll highlight the field, display the error, and disable the rest of the form.
serviceErr: We reach this state when we are unable contact the authentication service, preventing the submitted data to be checked. We’ll display an error along with a „Retry” button to re-attempt a service connection.
signedIn: Success! This is when the user has successfully authenticated and proceeds past the sign in form. Normally, this would take the user to some view, but we’ll simply confirm authentication since we’re focusing solely on the form.
See the machinConfig.js file in the SignIn directory? Crack that open so we can define our states. We list them as properties of a states object. We also need to define an initial state, which mentioned earlier, will be the dataEntry state, allowing the user to enter data into the form fields.
Each part of this article will show the code of machineConfig.js along with a diagram produced from the code using XState’s visualizer.
2. The Transitions
Now that we have defined our states, we need to define how to change from one state to another and, in XState, we do that with a type of event called a transition. We define transitions within each state. For example, If the ENTER_EMAIL transition is triggered when we’re in the emailErr state, then the system will move to state dataEntry.
Note that nothing would happen if a different type of transition was triggered (such as ENTER_PASSWORD) while in the emailErr state. Only transitions that are defined within the state are valid.
When a transition has no target, it is an external (by default) self-transition. When triggered, the state will exit and re-enter itself. As an example, the machine will change from dataEntry back to dataEntry when the ENTER_EMAIL transition is triggered.
Here’s how that is defined:
dataEntry: {
on: {
ENTER_EMAIL: {}
}
}
Sounds weird, I know, but we’ll explain it a little later. Here’s the machineConfig.js file so far.
We need a way to save what the user enters into the input fields. We can do that in XState with context, which is an object within the machine that enables us to store data. So, we’ll need to define that in our file as well.
Email and password are both empty strings by default. When the user enters their email or password, this is where we’ll store it.
We will need a way to be more specific about our errors. Instead of simply telling the user there is an email error, we need to tell them what kind of error happened. Perhaps it’s email with the wrong format or there is no account linked to the entered email — we should let the user know so there’s no guessing. This is where we can use hierarchical states which are essentially state machines within state machines. So, instead of having a emailErr state, we can add sub-states, such as emailErr.badFormat or emailErr.noAccount.
For the emailErr state, we have defined two sub-states: badFormat and noAccount. This means the machine can no longer only be in the emailErr state; it would be either in the emailErr.badFormat state or the emailErr.noAccount state and having them parsed out allows us to provide more context to the user in the form of unique messaging in each sub-state.
When the user blurs an input or clicks submit, we need to check if the email and/or password are valid. If even one of those values is in a bad format, we need to prompt the user to change it. Guards allows us to transition to a state depending on those types of conditions.
Here, we’re using the EMAIL_BLUR transition to change the state to emailErr.badFormat only if the condition isBadEmailFormat returns true. We are doing a similar thing to PASSWORD_BLUR.
We’re also changing the SUBMIT transition’s value to an array of objects with a target and condition property. When the SUBMIT transition is triggered, the machine will go through each of the conditions, from first to last, and change the state of the first condition that returns true. For example, if isBadEmailFormat returns true, the machine will change to state emailErr.badFormat. However, if isBadEmailFormat returns false, the machine will move to the next condition statement and check if it returns true.
All of the work we’ve done so far would be for nought if we didn’t make a request to an authentication service. The result of what’s entered and submitted to the form will inform many of the states we defined. So, invoking that request should result in one of two states:
Transition to the signedIn state if it returns successfully, or
transition to one of our error states if it fails.
The invoke method allows us to declare a promise and transition to different states, depending on what that promise returns. The src property takes a function that has two parameters: context and event (but we’re only using context here). We return a promise (our authentication request) with the values of email and password from the context. If the promise returns successfully, we will transition to the state defined in the onDone property. If an error is returned, we will transition to the state defined in the onError property.
const machineConfig = {
...
states: {
...
// We’re in a state of waiting for a response
awaitingResponse: {
// Make a call to the authentication service
invoke: {
src: 'requestSignIn',
// If successful, move to the signedIn state
onDone: {
target: 'signedIn'
},
// If email input is unsuccessful, move to the emailErr.noAccount sub-state
onError: [
{
cond: 'isNoAccount',
target: 'emailErr.noAccount'
},
{
// If password input is unsuccessful, move to the passwordErr.incorrect sub-state
cond: 'isIncorrectPassword',
target: 'passwordErr.incorrect'
},
{
// If the service itself cannot be reached, move to the serviceErr state
cond: 'isServiceErr',
target: 'serviceErr'
}
]
},
},
...
7. Actions
We need a way to save what the user enters into the email and password fields. Actions enable side effects to be triggered when a transition occurs. Below, we have defined an action (cacheEmail) within the ENTER_EMAIL transition of the dataEntry state. This means if the machine is in dataEntry and the transition ENTER_EMAIL is triggered, the action cacheEmail will also be triggered.
const machineConfig = {
...
states: {
...
// On submit, target the two fields
dataEntry: {
on: {
ENTER_EMAIL: {
actions: 'cacheEmail'
},
ENTER_PASSWORD: {
actions: 'cachePassword'
},
},
...
},
// If there’s an email error on that field, trigger email cache action
emailErr: {
on: {
ENTER_EMAIL: {
actions: 'cacheEmail',
...
}
}
},
// If there’s a password error on that field, trigger password cache action
passwordErr: {
on: {
ENTER_PASSWORD: {
actions: 'cachePassword',
...
}
}
},
...
8. Final State
We need to way to indicate if the user has successfully authenticated and, depending on the result, trigger the next stage of the user journey. Two things are required for this:
We declare that one of the states is the final state, and
define an onDone property that can trigger actions when that final state is reached.
Within the signedIn state, we add type: final. We also add an onDone property with action onAuthentication. Now, when the state signedIn is reached, the action onAuthentication will be triggered and the machine will be done (no longer executable).
A great feature of XState is that the machine configuration is completely independent of the actual implementation. This means we can test it now and get confidence with what we’ve made before connecting it to the UI and backend service. We can copy and paste the machine config file into XState’s visualizer and get a auto-generated statechart diagram that not only outlines all the defined states with arrows that illustrate how they’re all connected, but allows us to interact with the chart as well. This is built-in testing!
Connecting the machine to a React component
Now that we’ve written our statechart, it’s time to connect it to our UI and backend service. An XState machine options object allows us to map strings we declared in the config to functions.
We’ll begin by defining a React class component with three refs:
We declared the following actions in our machine config:
focusEmailInput
focusPasswordInput
focusSubmitBtn
cacheEmail
cachePassword
onAuthentication
Actions are mapped in the machine config’s actions property. Each function takes two arguments: context (ctx) and event (evt).
focusEmailInput and focusPasswordInput are pretty straightforward, however, there is a bug. These elements are being focused when coming from a disabled state. The function to focus these elements is firing right before the elements are re-enabled. The delay function gets around that.
cacheEmail and cachePassword need to update the context. To do this, we use the assign function (provided by XState). Whatever is returned by the assign function is added to our context. In our case, it is reading the input’s value from the event object and then adding that value to the context’s email or password. From there property.assign is added to the context. Again, in our case, it is reading the input’s value from the event object and adding that value to the context’s email or password property.
We declared the following guards in our machine config:
isBadEmailFormat
isPasswordShort
isNoAccount
isIncorrectPassword
isServiceErr
Guards are mapped in the machine configuration’s guards property. The isBadEmailFormat and isPasswordShort guards make use of the context to read the email and password entered by the user then pass them on to the appropriate functions. isNowAccount, isIncorrectPassword and isServiceErr make use of the event object to read what kind of error was returned from the call to the authentication service.
We declared the following service in our machine configuration (within our invoke definition): requestSignIn.
Services are mapped in the machine configuration’s services property. In this case, the function is a promise and is passed to the email password from the context.
Now that we have our machine config and options at the ready, we can create the actual machine! In order to use XState in a real world scenario, that requires an interpreter. react-xstate-js is an interpreter that connects React with XState using the render props approach. (Full disclosure, I developed this library.) It takes two props — config and options — and returns an XState service and state object.
// SignIn/index.jsx
...
import { Machine } from 'react-xstate-js'
import machineConfig from './machineConfig'
class SignIn extends Component {
...
render() {
<Machine config={machineConfig} options={this.machineOptions}>
{({ service, state }) => null}
</Machine>
}
}
Let’s make the UI!
OK, we have a functional machine but the user needs to see the form in order to use it. That means it’s time to create the markup for the UI component. There are two things we need to do to communicate with our machine:
1. Read the state
To determine what state we are in, we can use the state’s matches method and return a boolean. For example: state.matches('dataEntry').
2. Fire a transition
To fire a transition, we use the service’s send method. It takes an object with the transitions type we want to trigger as well as any other key and value pairs we want in the evt object. For example: service.send({ type: 'SUBMIT' }).
And there you have it. A sign in form that has a great user experience controlled by XState. Not only were we able to create a form a user can interact with, we also put a lot of thought into the many states and types of interactions that’s need to be considered, which is a good exercise for any piece of functionality that would go into a component.
Hit up the comments form if there’s something that doesn’t make sense or if there’s something else you think might need to be considered in the form. Would love to hear your thoughts!
Here’s a possibility! Perhaps you are testing your JavaScript with a framework like Jasmine. That’s nice because you can write lots of tests to cover your application, get a nice little UI to see the output, and even integrate it with build and deploy tools to make your ongoing development work safer.
Now, perhaps there is this zany developer on your team who keeps changing API endpoints on you — quite literally breaking things in the process. You decide to write a test that hits those endpoints and makes sure you’re getting back from it what you expect. Straightforward enough. The only slightly tricky part is that API requests are async. To really test it, the test needs to have some way to wait for the results before testing the expectations.
That can be handled in Jasmine through a beforeEach(), which can wait to complete until you call a done() function. Here’s the whole thing:
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.
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!
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:
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:
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:
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.
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.
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.
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.
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!
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!
If you hadn’t seen it, Heydon posted a rather clever flexbox layout pattern that, in a sense, mimics what you could do with a container query by forcing an element to stack at a certain container width. I was particularly interested, as I was fighting a little layout situation at the time I saw this and thought it could be a solution. Let’s take a peak.
„Ad Double” Units
I have these little advertising units on the design of this site. I can and do insert them into a variety of places on the site. Sometimes they are in a column like this:
Ad doubles appearing in a column of content
Sometimes I put them in a place that is more like a full-width environment:
Ad doubles going wide.
And sometimes they go in a multi-column layout that is created by a flexible CSS grid.
Ad doubles in a grid layout that changes column numbers at will.
So, really, they could be just about any width.
But there is a point at which I’d like the ads to stack. They don’t work side by side anymore when they get squished in a narrow column, so I’d like to have them go over/under instead of left/right.
I don’t care how wide the screen is, I care about the space these go in
I caught myself writing media queries to make these ads flop from side by side to stacked. I’d „fix” it in one place only to break it in another because that same media query doesn’t work in another context. I needed a damn container query!
This is the beauty of Heydon’s albatross technique. The point at which I want them to break is about 560px, so that’s what I set out to use.
The transition
I was already using flexbox to lay out these Ad Doubles, so the only changes were to make it wrap them, put in the fancy 4-property albatross magic, and adjust the margin handling so that it doesn’t need a media query to reset itself.
The coolest part, to me, is how it shows you the way an individual flex item arrives at the size it’s being rendered. As we well know, this can get a bit wacky, as lots of things can affect it like flex-basis, flex-grow, flex-shrink, max-width, min-width, etc.
Product teams from AirBnb and New York Times to Shopify and Artsy (among many others) are converging on a new set of best practices and technologies for building the web apps that their businesses depend on. This trend reflects core principles and solve underlying problems that we may share, so it is worth digging deeper.
Internal consistency: Created with static typing tools like TypeScript.
Data manipulation: These work with GraphQL-speaking clients like Apollo.
Data representation: Displayed with a library for reusable components and behaviors, like React.
Naming things is hard, and our industry has struggled to name this new generation of tooling for web apps. The inimitable Orta Theroux calls it an Omakase; I slimmed it down and opted for a simpler backronym pulled from letters in the tooling outlined above: STAR (Design Systems, TypeScript, Apollo, and React).
Product teams from AirBnb and New York Times to Shopify and Artsy (among many others) are converging on a new set of best practices and technologies for building the web apps that their businesses depend on. This trend reflects core principles and solve underlying problems that we may share, so it is worth digging deeper.
Internal consistency: Created with static typing tools like TypeScript.
Data manipulation: These work with GraphQL-speaking clients like Apollo.
Data representation: Displayed with a library for reusable components and behaviors, like React.
Naming things is hard, and our industry has struggled to name this new generation of tooling for web apps. The inimitable Orta Theroux calls it an Omakase; I slimmed it down and opted for a simpler backronym pulled from letters in the tooling outlined above: STAR (Design Systems, TypeScript, Apollo, and React).