Adding physics to web components
Tags: physics, typescript, gamedev, javascript
Published: 2021-05-28 17:20:48

It's Friday afternoon, so I wanted to do some crazy experiment. In a previous post I already looked into using Web Components (Custom Elements) for browser game development.

Today we're going to add physics to our HTML tags, just because it's possible! And to learn a bit about web components and [Matter.JS](https://brm.io/matter-js/)

We'll be looking at:

- Custom Elements
- Game Loop
- Adding Physics (with Matter.js)
- Project setup (with Parcel.js)

![animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5b3ol93c7womcbybg4ey.gif)

> *A simulation with bouncy barrels, stubborn crates, platforms and a player character*.


Example code is in Typescript, but you can leave out type annotations such as `a:number` and `public, private` to convert to Javascript.

---

# Custom Elements

A custom element is a HTML tag that has executable code added to it. That's really handy for game objects! We'll use that to add physics later. You can nest custom elements within each other to create a hierarchy. The tag names have to end with `-component` (at least I get an error if I leave that out)...

### HTML

```html
<game-component>
    <platform-component></platform-component>
    <crate-component></crate-component>
    <player-component></player-component>
</game-component>
```

<br>

### CSS

We will use `translate` to position our elements with javascript, so that means all elements need `position:absolute` and `display:block`. You can use a background image for the visual, it's shorter and faster than using `<img>` tags, and you can use repeating backgrounds.

```css
platform-component {
   position:absolute;
   display:block;
   background-image:url(./images/platform.png);
   width:400px; 
   height:20px;
}
```
</br>

### TYPESCRIPT 

First we have to bind our code to the HTML tag by creating a class and registering it using `customElments.define()`.

> 😬 In Javascript this is exactly the same, except for the `:number` type annotations

```typescript
export class Crate extends HTMLElement {
    constructor(x:number, y:number) {
        super()
        console.log(`I am a crate at ${x}, ${y}`)
    }
}

customElements.define('crate-component', Crate)
```

You can add it to the DOM by placing the tag in the HTML document: `<crate-component></crate-component>`. But if we do it by code we can pass constructor arguments, in this case an `x` and `y` position. This is handy if we want several crates at different positions:

```typescript
let c = new Crate(200,20)
document.body.appendChild(c)
``` 
<br>
<br>

---

# GAME LOOP

To use physics, we need a game loop. This will update the physics engine 60 times per second. The game loop will then update all the custom elements. In this example, we create a game class with a game loop that updates all crates.

```typescript
import { Crate } from "./crate"

export class Game extends HTMLElement {
    private crates : Crate[] = []
    constructor() {
        super()
        this.elements.push(new Crate(270, 20))
        this.gameLoop()
    }
    private gameLoop(){
        for (let c of this.crates){
            c.update()
        }
        requestAnimationFrame(() => this.gameLoop())
    }
}
customElements.define('game-component', Game)
```

The crate component gets an update function to `translate` its position.

```typescript
export class Crate extends HTMLElement {
    constructor(private x:number, private y:number) {
        super()
    }
    public update() {
        this.style.transform = `translate(${this.x}px, ${this.y}px)`
    }
}
customElements.define('crate-component', Crate)
```

---

# 🔥 PHYSICS

FINALLY we get to the point where we add [Matter.js](https://brm.io/matter-js/) physics! *Matter.js creates a physics engine that can run invisibly in the background*. If we add objects such as boxes, cylinders, floors and ceilings to it, it will create a physics simulation with those objects. Our elements will respond to gravity, friction, velocity, force, bounciness and get precise collision detection. 

Matter.js has a `renderer` that can draw those objects directly in a canvas, but that's boring 🥱. We'll use the positions of the physics elements to position DOM elements!

## Plan:

1 - Adding the physics world to the game class
2 - Adding physics to the crates
3 - What more can you do with physics?

---

## 1 - Adding Matter.js to the Game class

```typescript
import Matter from 'matter-js'
import { Crate } from "./crate"

export class Game extends HTMLElement {
    private engine : Matter.Engine
    private world : Matter.World
    private crates : Crate[] = []
    
    constructor() {
        super()
        this.engine = Matter.Engine.create()
        this.world = this.engine.world
        this.crates.push(
            new Crate(this.world, 270, 20, 60, 60),
            new Crate(this.world, 320, 70, 60, 60)
        )
        this.gameLoop()
    }
    private gameLoop(){
        Matter.Engine.update(this.engine, 1000 / 60)
        for (let c of this.crates){
            c.update()
        }
        requestAnimationFrame(() => this.gameLoop())
    }
} 
customElements.define('game-component', Game)
```
---

## 2 - Adding physics to the crates

The Crate class will add a physics box to the physics world. Then, it will read the physics box position in the update function, and update the crate element position in the DOM world.

```typescript
import Matter from 'matter-js'

export class Crate extends HTMLElement {
    private physicsBox: Matter.Body

    constructor(x: number, y: number, private width: number, private height: number) {
        super()
        this.physicsBox = Matter.Bodies.rectangle(x, y, this.width, this.height, options)
        Matter.Composite.add(game.getWorld(), this.physicsBox)
        document.body.appendChild(this)
    }
    public update() {
        let pos = this.physicsBox.position
        let angle = this.physicsBox.angle
        let degrees = angle * (180 / Math.PI)
        this.style.transform = `translate(${pos.x - (this.width/2)}px, ${pos.y-(this.height/2)}px) rotate(${degrees}deg)`
    }
}
customElements.define('crate-component', Crate)
```


## 3 - What more can you do with physics?

We're really just getting started using Matter.JS. To build the game you see in the images from this post you use the following concepts:

### Static elements

These are elements such as platforms and walls, that do not have forces applied to them, but still cause collisions.

```typescript
this.physicsBox = Matter.Bodies.rectangle(x, y, w, h, {isStatic:true})
```

### Velocity

By setting the *velocity* of an object manually, you can create a player or enemy character that moves according to player input.

```typescript
Matter.Body.setVelocity(this.physicsBox, { x: 5, y: this.physicsBox.velocity.y })
```

### Force

By adding *force* you can temporarily boost an object in a certain direction, for example a rocket or a bullet. You can use force to make a character jump.

```typescript
Matter.Body.applyForce(this.physicsBox, { x: this.physicsBox.position.x, y: this.physicsBox.position.y }, { x: 0, y: -0.15 })
```

---

# Project setup

You can set up the above project (with or without Typescript) using Parcel to bundle your modules:

```bash
npm install -g parcel-bundler
npm install matter-js
npm install @types/matter-js
npm install typescript
```
Then, you can run the project in watch mode using 
```bash
parcel dev/index.html
```
Or build the whole project using
```bash
parcel build dev/index.html --public-url ./
```

---

## Conclusion

I hope this post didn't become too long! I think this approach is great fun, but is it really useful compared to using a canvas for physics simulations? Well...

- Canvas elements can't have Event Listeners
- Canvas doesn't have a nice DOM tree that you can traverse

Disadvantages:

- Rendering and game structure are a bit too intertwined (you can't easily switch to canvas rendering at a late stage in development).
- If you want thousands (or tens of thousands) of objects bouncing around, a canvas is much more efficient.

---

## Links

- [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)
- [Matter.js](https://brm.io/matter-js/)
- [Parcel.js](https://parceljs.org)