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:
- the demo >
- the full source