DEV Community

Cover image for Throwing around text - Kinetic typography part 2: Tricking gravity thanks to matter.js! πŸ“ƒπŸ›«πŸ€―
Pascal Thormeier
Pascal Thormeier

Posted on

Throwing around text - Kinetic typography part 2: Tricking gravity thanks to matter.js! πŸ“ƒπŸ›«πŸ€―

Part 2 of my series about kinetic typography! Let's move some text around with HTML, CSS and JS! If you missed how I came about throwing around text and deforming it with only web stuff, be sure to check out part 1!

πŸŽ‰πŸŽ‰ I also want to celebrate 2500 followers! Thank you for your support everyone! It means so much to me and keeps me inspired to write even more! πŸŽ‰πŸŽ‰

Ok, so, this time we're using JS, so much I can tell. Part 1 was CSS only, but believe me, once we get the mathematical options in that JS offers us, we can go wild. Let's get going!

Text falling... up?

I consulted A Popular Search Engineβ„’ again to find some good examples. Some that are a little more complex than the one we already did. In the Creatopy blog I found this little Gif by Siddhart Mate:

The word

Now, what I like about this animation is the way the letters behave. It's not necessarily the falling itself, but the way they fall into each other once they reach the top/bottom of the image. It detaches the letters from the word, essentially making them independant from the meaning they represent when they are in the correct order and position. It has some very artistic touch to it.

And that's exactly the reason why we're rebuilding this today.

Rigid bodies obeying physics

So, if half the image defies gravity, how would one make text obey the laws of physics in the first place? Physics engines are nothing new (see a ton of computer games) and there sure as heck is one for JS, namely matter.js. Let's download that first and do some boilerplating:



npm i matter-js pathseg poly-decomp


Enter fullscreen mode Exit fullscreen mode


<!DOCTYPE html>
<html>
<head>
</head>
<body>

  <div id="left"></div>
  <div id="right"></div>

  <script src="./node_modules/matter-js/build/matter.js"></script>
  <script src="./node_modules/pathseg/pathseg.js"></script>
  <script src="example2.js"></script>

</body>
</html>


Enter fullscreen mode Exit fullscreen mode

I also installed pathseg and poly-decomp to allow for concave shapes (such as the letter U) and to be able to convert SVG paths to coordinates for bodies.

Now, let's get going right away with matter.js.

Matter.js offers a lot of things. First we deconstruct the Matter object first for simpler access and to see what we'll actually need:



const {
  Engine,
  Render,
  Runner,
  Composite,
  Bodies,
  Body,
  Svg,
  Vertices,
  World
} = window.Matter


Enter fullscreen mode Exit fullscreen mode

Then, instead of creating all the letters from scratch, let's use an existing font instead and convert that to SVG. I had to actually retrace the letters, but I'm sure there's some sophisticated tool out there that can do exactly that. Apparently matter.js doesn't really like hollow bodies when converting SVG paths to vertices.



const A = 'M 18 114 46 114 70 37 81 74 57 74 51 94 87 94 93 114 121 114 81 7 57 7 z'
const U = 'M 16 7 16 82 C 17 125 101 125 99 82 L 99 82 99 7 74 7 74 82 C 73 100 41 99 41 82 L 41 82 41 7 16 7 z'
const W = 'M 6 7 29 114 56 114 70 53 84 114 111 114 134 7 108 7 96 74 81 7 59 7 44 74 32 7 6 7 z'
const N = 'M 16 114 16 7 42 7 80 74 80 7 106 7 106 114 80 114 42 48 42 114 16 114 z'
const P = 'M 20 8 20 114 46 114 46 28 66 28 C 83 28 83 59 66 58 L 66 58 46 58 46 78 67 78 C 116 78 116 7 65 8 L 65 8 z'
const D = 'M 19 7 57 7 C 120 13 120 109 57 114 L 57 114 45 114 45 94 56 94 C 85 93 86 30 56 27 L 56 27 45 27 45 114 19 114 19 7 z'
const O = 'M 13 59 C 9 -12 109 -12 107 59 L 107 59 80 59 C 84 14 34 14 39 59 L 39 59 C 33 107 86 107 80 59 L 80 59 107 59 C 109 133 9 133 13 59 L 13 59 z'
const R = 'M 21 114 21 7 64 7 C 122 8 105 67 85 69 L 85 69 107 113 80 113 61 76 47 76 47 56 65 56 C 84 57 84 26 65 27 L 65 27 47 27 47 114 z'


Enter fullscreen mode Exit fullscreen mode

Ok, given, they still look a bit pants when rendered, but I'm sure there's some way to make them render correctly...

Challenge: Can anyone figure out what font I used? Hint: It's a Google Font.

To convert these paths to actual bodies, we create a function to transform paths to vertices and then vertices to bodies:



const toVertices = path => {
  const pathEl = document.createElementNS(
    'http://www.w3.org/2000/svg',
    'path'
  )
  pathEl.setAttribute('d', path)
  return Svg.pathToVertices(pathEl, 1)
}

const toBody = function (letter) {
  const vertices = toVertices(letter)

  return Bodies.fromVertices(0, 0, vertices, {
    render: {
      fillStyle: '#fff',
      strokeStyle: '#fff',
      lineWidth: 1,
    }
  })
}


Enter fullscreen mode Exit fullscreen mode

Those functions can then be used to create the words as arrays of bodies:



const bodiesUpward = [
  toBody(U),
  toBody(P),
  toBody(W),
  toBody(A),
  toBody(R),
  toBody(D),
]

const bodiesDownward = [
  toBody(D),
  toBody(O),
  toBody(W),
  toBody(N),
  toBody(W),
  toBody(A),
  toBody(R),
  toBody(D),
]


Enter fullscreen mode Exit fullscreen mode

Awesome. Now we need to create two worlds: One for the left side and one for the right side.



// Create the engines
const leftEngine = Engine.create()
const rightEngine = Engine.create()

// Get both worlds
const leftWorld = leftEngine.world
const rightWorld = rightEngine.world

// Create the render instances with the same options
const options = {
  wireframes: false,
  width: 400,
  height: 600,
  background: '#000'
}

const leftRender = Render.create({
  element: document.querySelector('#left'),
  engine: leftEngine,
  options
})
const rightRender = Render.create({
  element: document.querySelector('#right'),
  engine: leftEngine,
  options
})

Render.run(leftRender)
const leftRunner = Runner.create()
Runner.run(leftRunner, leftEngine)

Render.run(rightRender)
const rightRunner = Runner.create()
Runner.run(rightRunner, rightEngine)


Enter fullscreen mode Exit fullscreen mode

These are now two different worlds we can render stuff on. A world doesn't have any boundaries by default, so we need to add a floor and a ceiling to the left and the right world respectively. We also adjust the gravity to make things "fall" up and down:



// Stuff falls down
leftEngine.gravity.y = 1

// Stuff goes up
rightEngine.gravity.y = -1

// The floor and ceiling are rectangles
World.add(leftWorld, Bodies.rectangle(0, -1, 800, 1, { isStatic: true }))
World.add(rightWorld, Bodies.rectangle(0, 601, 800, 1, { isStatic: true }))


Enter fullscreen mode Exit fullscreen mode

Then we add all the letters to their respective worlds:



bodiesUpward.forEach(body =>{
  World.add(leftWorld, body)
})

bodiesDownward.forEach(body =>{
  World.add(rightWorld, body)
})


Enter fullscreen mode Exit fullscreen mode

Now comes the fun part: Positioning the letters, rotating them and letting them fall. Since we want this to happen over and over, we introduce an interval that repositions all of the letters and lets them fall again:



const positionLeftBodies = () =>{
  let leftY = 0
  bodiesUpward.forEach(body => {
    Body.setPosition(body, {
      x: 200,
      y: leftY,
    })
    Body.setAngle(body, -Math.PI / 2) // 90deg in Radians

    // Important to not have any "left-over" movement.
    Body.setVelocity(body, { x: 0, y: 0 })

    leftY -= 100
    console.log(leftY)
  })
}

const positionRightBodies = () => {
  let rightY = 600
  bodiesDownward.forEach(body => {
    Body.setPosition(body, {
      x: 200,
      y: rightY,
    })
    Body.setAngle(body, -Math.PI / 2) // 90deg in Radians

    // Important to not have any "left-over" movement.
    Body.setVelocity(body, { x: 0, y: 0 })

    rightY += 120
  })
}

positionLeftBodies()
positionRightBodies()

setInterval(() => {
  positionLeftBodies()
  positionRightBodies()
}, 6000)


Enter fullscreen mode Exit fullscreen mode

And this is what it looks like in action:

And that's it for the second part of the series!


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❀️ or a πŸ¦„! Also, consider following me if you want to see how the other examples turn out! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, you can offer me a coffee β˜• or follow me on Twitter 🐦! You can also support me directly via Paypal!

Buy me a coffee button

Top comments (12)

Collapse
 
thegrizzler profile image
theGrizzler

Hey, I created this account just to comment here. I followed along with the article but I seem to have been stuck. In toBody() Bodies.fromVertices() expects a Matter.Vector[][], but toVertices returns a Matter.Vector[]. Even Vertices.scale(), used in the codepen, returns Matter.Vector[]. I tried looking up but matterjs has terrible docs apparently. If you could help me I would appreciate it a lot. Thanks.

Collapse
 
thormeier profile image
Pascal Thormeier

Hey, thank you for the feedback and for your time. I'm sure we can debug this.

First, Codepen uses matter.js 0.18.0. Since the post is already over two years old, I assume you're using a newer version. What version do you have installed?

Collapse
 
thegrizzler profile image
theGrizzler • Edited

Hey again. I was using version 0.19.0 when I tried this. This was in sveltekit. I did not continue beyond the toVertices() and toBody() func code you provided since that is where I encountered the errors. I could provide the actual code I had if you need.

Collapse
 
div_100 profile image
Divyansh Choudhary

I second this @eioluseyi

Collapse
 
eioluseyi profile image
EIO β€’ Emmanuel Imolorhe

Challenge: Can anyone figure out what font I used? Hint: It's a Google Font.

I think the font is a regular sans font for which you made an edit to the letter "D" and the path for the letter O didn't close completely.
I call Roboto!

Collapse
 
thormeier profile image
Pascal Thormeier

Correct! I at least tried to fix these letters, Matter.js is doing weird things with composite/hollow bodies, that's why the letter "D" and "O" look off. But, it's indeed Roboto!

Collapse
 
eioluseyi profile image
EIO β€’ Emmanuel Imolorhe

Oh..
Let's hope that subsequent updates would fix that on their end.
However, the resulting effects on those letters are not that bad either; plus it made the challenge even tougher! πŸ˜‚

Collapse
 
jerzakm profile image
Martin J • Edited

Cool! This is a little similar to something I've done a few months back with Rapier.rs

If you plan to do more things with physics I highly recommend checking rapier, from my experience performance is much better than matter.js (wasm!).

demo elastic-banach-bbcb85.netlify.app/ (press let's get physical to start, only desktop)
repo github.com/jerzakm/rapier-dom-physics

Collapse
 
thormeier profile image
Pascal Thormeier

Oh wow, that's amazing! Although it's a bit hard to press the "close" button on the magenta box when physics is turned on 😜 Will definitely have a look, thank you for the recommendation!

Collapse
 
jerzakm profile image
Martin J

Oh yeah, it's definitely hacky and not user friendly. Yet another unfinished idea waiting for it's turn on a forever growing list :D

Collapse
 
eioluseyi profile image
EIO β€’ Emmanuel Imolorhe
Collapse
 
thormeier profile image
Pascal Thormeier

Thank you for linking this tool! Gotta retry soon with that and see if it works better than my Sisyphean approach with trial and error until it looks halfway decent lol