Build a Reactive Trochoidal Wave with SVG and Vue

Last Updated On 24 Feb 2020 by

This tutorial covers the concept of Trochoidal Wave and visualize it with the reactive framework Vue.js.

Style and Responsive ViewBox

The demo is built using SVG to take advantage of its viewBox property which allows us to set an original dimension and let the browser handle the resizing according to the preserveAspectRatio rule we set, in this case our SVG uses the following: preserveAspectRatio=“xMidYMid meet”

So all we got left to do in term of layout is make sure the SVG fills the screen width, and we will let the SVG fill the height based on its viewBox property:

body {
  background: black;
  margin: 0;
  svg#app {
    width: 100vw;
    overflow: hidden;
  }
}

The template

Next we create the Vue template which will consist of a few for loops to render all the points

 <template>
  <svg
    id="app"
    xmlns="http://www.w3.org/2000/svg"
    :width="width"
    :height="height"
    preserveAspectRatio="xMidYMid meet"
    :view-box.camel="`0 0 ${width} ${height}`"
  >
    <g
      v-for="(line, lineIndex) in lines"
      :key="lineIndex"
      :transform="`translate(0 ${lineIndex * 20 - lines.length * 10 / 2})`"
    >
      <circle
        v-for="(point, index) in line.filter((row, index) => index + 2 < line.length)"
        :key="`dot-${index}`"
        :cx="point.x"
        :cy="point.y"
        :r="point.radius"
        stroke="rgba(255,255,255,0.25)"
        stroke-width="1"
        fill="transparent"
      />
      <polyline
        :opacity="lineIndex / lines.length / 2 + 0.25"
        :points="pointsToString(lineIndex)"
        stroke="rgba(255,255,255,1)"
        stroke-width="0.1"
        fill="#00BFFF"
      />
      <circle
        v-for="(point, index) in animatedLines[lineIndex].filter((row, index) => index > 0 && index + 1 < line.length)"
        :key="`circle-${index}`"
        :cx="point.x"
        :cy="point.y"
        :r="2"
        stroke="rgba(255,255,255,0.5)"
        stroke-width="1"
        fill="#00BFFF"
      />
    </g>
  </svg>
</template>

The Reactive Component

Because we’re using computed properties to generate the SVG elements, our loop method has very little complexity. All we have to do is update the current time and Vue JS takes care of updating the depending computed values and rendering accordingly.

export default {
  name: "App",
  data: () => ({
    lines: [], // number of wave lines
    resolution: 8, // number of point per wave line
    width: window.innerWidth,
    height: window.innerHeight,
    time: 0,
  }),
  created() {
    // loop over 3 lines and generate each points original X, Y and Radius property
    for (
      let lineIndex = 0, numLines = 3;
      lineIndex < numLines;
      lineIndex += 1
    ) {
      this.lines.push([]);
      for (
        let pointIndex = 0, X = this.resolution + 1;
        pointIndex < X;
        pointIndex += 1
      ) {
        const radius =
          (window.innerWidth / 20) * (0.2 + (1 - lineIndex / numLines) * 0.8);
        const scale = this.width / this.resolution;
        const x = (pointIndex + 1) * scale;
        const y = this.height / 2;
        this.lines[lineIndex].push({ x, y, radius });
      }
    }
    // start looping
    this.loop(0);
  },
  methods: {
    // in the loop function, all we do is update the time value,
    // the data will react from that change and update the SVG
    loop(time) {
      window.requestAnimationFrame(this.loop);
      this.time = time * 0.003;
    },
    // generate a string of coordinates in SVG polyline ready syntax
    pointsToString(lineIndex) {
      const points = [];
      const lines = this.animatedLines;
      // bottom left fixed point
      points.push({ x: 0, y: this.height * 2 });
      // left moving point
      points.push({ x: 0, y: lines[lineIndex][0].y });
      points.push(...lines[lineIndex]);
      // right moving point
      points.push({
        x: this.width,
        y: lines[lineIndex][lines[lineIndex].length - 1].y,
      });
      // bottom right fixed point
      points.push({ x: this.width, y: this.height * 2 });
      return points.map((pt) => `${pt.x},${pt.y}`);
    },
  },
  computed: {
    // we never change the original X and Y properties of each point,
    // instead we generate the projected values
    // using Math.sin/cos and the current time value
    animatedLines() {
      const step = this.width / this.resolution;
      return this.lines.map((line) =>
        line.map((point, pointIndex) => ({
          x:
            step * pointIndex + Math.sin(this.time + pointIndex) * point.radius,
          y: this.height / 2 + Math.cos(this.time + pointIndex) * point.radius,
        }))
      );
    },
    // Make sure scaling happens accordingly to our set dimensions
    viewBox() {
      return `0 0 ${this.width} ${this.height}`;
    },
  },
};

Source Code

Checkout the following resources:

About The Author

Headshot of Michael Iriarte aka Mika

Hi, I'm Michael aka Mika. I'm a software engineer with years of experience in frontend development. Thank you for visiting tips4devs.com I hope you learned something fun today! You can follow me on Twitter, see some of my work on GitHub, or read more about me on my website.