Orbital Resonance

  • Pixi.js
  • Experiment
  • Personal project
  • WebGL

9:2

Try changing the values above to explore various patterns.

See more examples

Story

Ever since I was little, I was always interested in stars and planets. I remember once seeing some charts about how some of the planets’ orbital periods resonates with each other, and I wanted to visualise the drawing of these patterns.

After a quick prototype I realized that using SVG wasn’t going to cut it: there’s just too many DOM elements moving and overlapping at the same time, slowing down the frame rate way too much for comfort. For this I needed to draw pixels on a <canvas>

I also suspected that this was a job for the GPU rather than the CPU, so I started researching web frameworks that utilized WebGL. First hit on Google was Three.js, which I already knew about, but had never really played around much with. I quickly learned that Three.js is a monster of a framework, and the learning curve seemed scaringly steep.

Me, reading the Three.js docs.

After ripping out a lot of my hair in despair trying to actually understand WebGL and Three.js, I was relieved when I discovered Pixi.js. After spending some time with various simple tutorials and reading through some of their - from this beginner’s point of view - pretty good documentation, I managed to get things moving on the screen.

Once I understood how the PIXI.Graphics() and PIXI.Sprite() methods worked, I just needed to apply some trigonometry and write the animation loop and finally tweaking the speeds and opacities until I was happy with the result.

Key takeaways

  • Using Pixi.js made rendering 2D graphics accelerated by WebGL a lot easier than with Three.js or other libraries.
  • Animation performance with WebGL and <canvas> is superb compared with SVG
  • Math and geometry is super interesting

Tools used for this project

Code

import * as PIXI from 'pixi.js'

export default function run(container, resonance, size = 200) {
  const renderer = PIXI.autoDetectRenderer(size, size, {
    transparent: true,
    antialias: true,
  })
  renderer.backgroundColor = 0x061639

  renderer.view.width = size
  renderer.view.height = size
  renderer.options.antialias = true

  container.appendChild(renderer.view)

  const stage = new PIXI.Container()
  const sun = new PIXI.Graphics().lineStyle(1, 0xf3a33f, 0.04)
  const graphics = new PIXI.Graphics()
  const pattern = new PIXI.Graphics().lineStyle(1, 0xf3a33f, 0.04)
  stage.addChild(sun)
  stage.addChild(pattern)

  let tick = 0
  const duration = 120

  sun.beginFill(0xffffff, 0.07)
  sun.drawCircle(size / 2, size / 2, 16)
  sun.endFill()

  graphics.beginFill(0x8fd6c5, 0.9)
  const planet = graphics.drawCircle(size / 2, size / 2, 8)
  graphics.endFill()

  const sprite1 = new PIXI.Sprite(renderer.generateTexture(planet))
  sprite1.anchor.set(0.5, 0.5)
  const sprite2 = new PIXI.Sprite(renderer.generateTexture(planet))
  sprite2.anchor.set(0.5, 0.5)

  stage.addChild(sprite1)
  stage.addChild(sprite2)

  function animate() {
    tick++

    const radian1 =
      ((tick % ((duration * resonance[0]) / resonance[1])) / ((duration * resonance[0]) / resonance[1])) * Math.PI * 2
    const radian2 = ((tick % duration) / duration) * Math.PI * 2

    const pos1 = getPos(radian1 - Math.PI, size * 0.25, size)
    const pos2 = getPos(radian2, size * 0.35, size)

    sprite1.position.set(...pos1)
    sprite2.position.set(...pos2)

    if (tick < duration * Math.max(...resonance)) {
      pattern.moveTo(...pos1)
      pattern.lineTo(...pos2)
    }

    renderer.render(stage)
    requestAnimationFrame(animate)
  }

  animate()
}

function getPos(radian, radius, size) {
  const x = size / 2 + Math.sin(radian) * radius
  const y = size / 2 + Math.cos(radian) * radius

  return [x, y]
}

and then in Vis.vue I put the following:

<template>
  <div class="container">
    <h2 class="heading" v-text="`${resonance[0]}:${resonance[1]}`"></h2>
    <div ref="canvasContainer"></div>
  </div>
</template>

After defining the resonance and size in the component, I can run the script:

const run = require('./script.js').default
run(this.$refs.canvasContainer, this.resonance, this.size)