Insight

Smokestacks, an SVG animation tutorial

A man in a white button up shirt stands smiling in front of a metal wall
Philip Curley Senior Product Developer

Overview

The end of Flash means that some of our development work is taking older interactive experiences like games and porting them over to web technologies supported by modern browsers and mobile devices. This is particularly exciting because it means we get to open up files that were compiled nearly 2 decades ago! It’s like working with a dusty artifact.

For all the fun it is to work with old interactives, it also has its frustrating challenges. The underlying animations which used ActionScript are not simple to port over to a modern Javascript implementation. In a recent Flash conversion of an interactive we created for a client called Polluting Sites in Northern Manhattan, I encountered a good animation which I decided would require a rebuild in Javascript and SVG.

I approached this problem knowing that I need to control each individual smoke particle. I can see that each particle is an individual circle which translates away from the point of origin, grows, and fades out of view in order to emulate a plume of smoke dissipating into the atmosphere. Additionally, I need multiple smokestacks across the map so I’ll need to be able to copy the animation multiple times.

In this example I’ll be utilizing the d3.js library in order to simplify transitions. I’ll work with a separate Smoke class and a Particle class to keep the code logic a bit more organized. Let’s look at the Particle class first.

class Particle {
  constructor (object, count, x, y, size) {
    this.object = object
    this.x = x
    this.y = y + (size * Math.random())
    this.scale = parseFloat(3/10) * size
    this.duration = Math.random() * 10000
    this.grow = this.scale + (size * Math.random())
    this.velX = (Math.random() < 0.5 ? -1 : 1) * parseFloat(1/2) * size
    this.maxVelY = size / 2
    this.minVelY = 2 * size
    this.velY = (size * 2 * Math.random()) + size * 2
    this.count = count
    this.id = `smoke-${this.object.id}-${this.count}`

    this.emit()
  }

  emit () {
    d3.select(this.object)
      .append('circle')
      .attr('id', `smoke-${this.object.id}-${this.count}`)
      .attr('class', 'particle')
      .attr('cx', this.x)
      .attr('cy', this.y)
      .attr('r', this.scale)
      .attr('fill', '#000000')
      .attr('opacity', '0.5')
      .transition().duration(this.duration)
      .attr('cy', this.y - this.velY)
      .attr('cx', this.x + this.velX)
      .attr('r', this.grow)
      .attr('opacity', '0')
      .on('end', () => {
        document.getElementById(`smoke-${this.object.id}-${this.count}`).remove()
      })
  }
}

 

Particle describes and handles every individual dark, smoke circle created. As you can see there are a lot of options and Math.Random() is doing a ton of work to keep things looking as “natural” as possible so that no two particles appear exactly identical.

In our constructor we are passed the starting coordinates x and y, and a size so that we can control the size of the smoke animation. The object to which this particle is emitted is also passed for reference.

In the Particle’s emit() method you will see that d3.js is creating a circle element to define our particle and sets the initial attributes of how the particle will look to begin. Using d3.js transition() method, each particle will have its position, radius, and opacity changed over the duration of the particle to simulate dissipation. In order to rise we create a velX and velY which define the offset of the particle position. The radius of the particle grows from scale to grow and the opacity effect is static across all the particles moving from 0.5 to 0.

Using the transition() and duration() methods, when the transition ends we instruct the particle to destroy itself to keep the DOM clean and to create room for more particles to be made by our Smoke class.

class Smoke {
  constructor (id = 1) {
    this.id = id
    this.particles = []
    this.limit = 30 // Max number of particles.
    this.step = 10 // Max particles created per interval.
    this.created = 0 // A incrementing numeric id for each particle.
    this.interval = 400 // How often to add new particles.
  }

  startAnimation () {
    setInterval(() => {
      const filter = this.particles.filter(p => {
        return document.getElementById(p.id) !== null
      })

      this.particles = filter
      let currentStep = 0

      while (this.particles.length < this.limit && currentStep < this.step) {
        const point = document.querySelector(`#smoke`)
        this.particles.push(new Particle(point, this.created, 0, -15, 25))
        this.created += 1
        currentStep += 1
      }
    }, this.interval)
  }
}

 

The Smoke class is a bit simpler as there is less to configure. It’s the controller for all the local particles emanating from a single element on the page. It defines a limit for the max number of particles and also how often of an interval new particles are added.

In order to initialize the smoke, attach the Smoke class to an existing element and call the startAnimation() method.

As demonstrated, we have a very simple but flexible recreation of a smoke effect. Because we’re working with an animation which needs to look “natural” and “random,” there are a lot of variables to consider. This implementation gives us the ability to tweak things as necessary to change the effect. Furthermore, variables can be added and changed based on what we want in the future.

If you want to see more, checkout the code for this blog example on GitHub.

Hopefully this has been a useful example of how we can use web technologies like SVG and Javascript to recreate our favorite Flash animations of the past. Interactive Knowledge is great at this work and if you have a Flash animation or game you’re interested in reviving then please contact us!