5
\$\begingroup\$

I'm building a simple canvas animation engine as an exercise in learning ES6. Coming from C++ and Java, I miss static properties, but I've read that "Prototypes having data properties is generally considered an anti-pattern".

The animation engine simulates \$n\$ river waves (simple lines), flowing from right to left in an HTML5 canvas, using requestAnimationFrame. Waves come in three lengths, based on a baseLength that should be configurable and have a default: 1x, 5x, and 7x. When creating a new wave, I'm picking one of the lengths at random. All waves have a constant flowing velocity on the x axis. You can see this in motion on CodePen (ES5 approach; ignore the boats).

  1. How should this simple universe be organized into classes? One "Wave" class with a constructor and draw method, and another "Waves" class with an animate method?
  2. Should the velocity (same for all waves) be stored at the Wave, or Waves level?
  3. Is the static config() method a good idea, or should each property be separate? Let's assume for simplicity we won't build an interface to modify these properties at runtime, but they are to be grouped at the top of the class for easy editing in the code.
  4. Is it better to use just one class, Waves, and have a createWave method which the constructor will call \$n\$ times to create all waves? I don't imagine ever needing to create a Wave in isolation from its river / Waves.

Here's the first stab at this in ES6:

'use strict';

class Wave {
  static config() {
    const waveBaseLength = 5;
    return {
      color: '#4F5E5B',
      waveLengths: [waveBaseLength, waveBaseLength * 7, waveBaseLength * 15],
      velocity: 5
    }
  }

  constructor(x, y, velocity) {
    this.x = x;
    this.y = y;
    let waveLengths = Wave.config().waveLengths;
    this.l = waveLengths[Math.floor(Math.random() * waveLengths.length)];
    this.velocity = velocity || 0.5;  // Chrome currently fails at `velocity=0.5` in the constructor declaration
  }

  draw(context) {
    context.beginPath();
    context.moveTo(this.x, this.y);
    context.lineTo(this.x + this.l, this.y);
    context.strokeStyle = Wave.config().color;
    context.stroke();
  }
}

class Waves {
  constructor(n, context) {
    this.waves = [];
    this.context = context;
    for (var i = 1; i <= n; i++) {
      var x = Math.random() * context.canvas.width,
        y = context.canvas.height / n * i;
      this.waves.push(new Wave(x, y));
    }
  }

  step() {
    for (var i = 0; i < this.waves.length; i++) {
      var wave = this.waves[i];

      if (wave.x + wave.l > 0) {
        // if still partially visible in the canvas, draw it
        wave.draw(this.context);
        wave.x -= wave.velocity;
      } else {
        // otherwise, recreate the wave off-screen to the right, at a different Y location to keep it interesting
        // TODO this does create a "gap" as waves come on-screen, but fixing it is not a priority
        wave.x = this.context.canvas.width;
        wave.y = Math.random() * this.context.canvas.height;
      }
    }
  }
}


let canvas = document.getElementById('river');
let context = canvas.getContext('2d');
let waves = new Waves(150, context);

window.requestAnimationFrame = window.requestAnimationFrame || window.msRequestAnimationFrame;

(function loop(timestamp) {
  context.clearRect(0, 0, canvas.width, canvas.height);

  waves.step();

  // keep animating
  window.requestAnimationFrame(loop);
})();
<canvas id="river"></canvas>

\$\endgroup\$
2
\$\begingroup\$

This is really cool! I do have a few small tips though.

First off, rather than creating an entire function, in this case, config, that returns configuration data, I'd store it as a public getter, like this:

class Wave {
  static get CONFIG() {
    const waveBaseLength = 5;
    return {
      color: '#4F5E5B',
      waveLengths: [waveBaseLength, waveBaseLength * 7, waveBaseLength * 15],
      velocity: 5
    }
  }
}

Using this, you can do stuff like Wave.CONFIG.color, and drop the un-needed parentheses.

Finally, you have a few, not so great variable names. A few include l and n. Names like these should be expanded for clarity and readability.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.