get your pixels movin'
The logo does tricks:

Wavy Gauge Numbers

Holy wavy numbers Batman! We have a fun one today. We’re gonna wire up a draggable element to the time() of a timeline and make the numbers on the gauge turn into a wave with the peak being at our dragger’s current x position.

The mostly empty SVG

We start with a SVG and a single path that acts as our control dragger. The rest of the gauge tick marks and numbers will be created on the fly. We could create the draggable path on the fly too, but what’s life without the excitement of vector software? I just made that in Adobe Illustrator.

<svg id="demo" xmlns="" width="1000" height="280" viewBox="0 0 1000 280"> 
  <path id="slider" d="M15.63,1.49.4,27.87A3,3,0,0,0,3,32.35H33.45A3,3,0,0,0,36,27.87L20.81,1.49A3,3,0,0,0,15.63,1.49Z" fill="#5cceee"/>

Dynamically creating the lines and numbers

This post isn’t so much about creating the elements for the gauge as it is about making and controlling the wave, but I’ll go over the basics. If you want more details, I wrote Creating Dynamic Elements with JavaScript.

In the variables, I’ve set the quantity of numbers to 20 (0-19) . The outer loop will run that many times to create a 5-pack of lines and the number. I call the makeNumber() function to create a number at the current x position. After that, five lines are created. The first one in the inner loop is 10 units taller than the next four. The x position is incremented each time by the amount of spacing set in the variables and so on, until each 5-pack of lines and a number are created.

// make a 5 pack of lines for each number
for (let j = 0; j < positions; j++) {
  for (let i = 0; i < 5; i++) {
    y1Pos = i === 0 ? y2Pos - 25 : y2Pos - 15; // first line in each pack is slightly taller
    startX += spacing;

The creation functions

This makeLine() function makes a little vertical line the length of the y1Pos variable, which is set in the inner loop from above. The makeNumbers() function sets a new number 40 units from the bottom y position of the lines.

Once the main loop is finished, I had to call each function one more time to create the last tall tick mark and one final number. The row of tick marks then starts and ends with a taller line.

// creates the line elements
function makeLine(yp) {
  let newLine = document.createElementNS(svgns, "line");
  gsap.set(newLine, {
    attr: { x1: startX, x2: startX, y1: yp, y2: y2Pos }

// creates the numbers
function makeNumber() {
  let txt = document.createElementNS(svgns, "text");
  txt.textContent = count;
  gsap.set(txt, {
    attr: { x: startX, y: y2Pos - 40, "text-anchor": "middle" }

Make the animation

Let’s start making these numbers move up and down. I settled on a 1.1 second duration and a stagger amount of 3. Feel free to adjust those in the demo to see the changes.

The timeline is paused as we’ll be controlling it with the dragger. Each text target will move up on the y axis, scale up a bit and change the fill color. The stagger amount is divided between all the targets. Since each target is set to yoyo, its duration is 2.2 seconds. Add that to the stagger amount of 3 and the total duration() of the timeline is 5.2 seconds.

let dur = 1.1; // master duration
let masterStagger = 3; // higher numbers tighten the curve
let jump = 120; // height of number jump during animation

// main timeline for the number jump
let animNumbers = gsap.timeline({ paused: true });
  .to("text", {
    duration: dur,
    y: -jump,
    scale: 1.5,
    fill: "#5cceee",
    stagger: {
      amount: masterStagger,
      yoyo: true,
      repeat: 1
    ease: "sine.inOut"
  .time(dur); // set the time to the end of the first number jump

You’ll notice the time() of the timeline is set to the duration (1.1). Why? Because I’m not interested in seeing the first target complete its move upward. When the draggable target is at 0, I want the first target to be at the peak of its jump. That occurs at 1.1 seconds. It then, yoyos back to its starting point.

Map the dragger

Using a handy GreenSock utility method, I’ll map the dragger position to the timeline. Where do the dragMin and dragMax come from? The min was set in the variables. We set it to the same value as startX. startX is incremented throughout the loop that creates the lines so we, once again, use that to set our max drag number.

That puts the min drag at the first tall tick mark and the max drag at the last tall tick mark. Exactly what we need.

let startX = 30; // first line position and fist number position
let dragMin = startX;

// final position of last line is new draggable max
// after the loop and line creation
let dragMax = startX;

When the dragger is at minimum, we set the timeline time() to the duration of 1.1. (Remember, we don’t want to see the first target move to its peak). When the dragger is at maximum, we set the time() of the timeline to its duration() – 1.1. Why? The same reason. We don’t want to see the last number move down to its initial y position.

Putting it in more succinct terms, we’re chopping off the first and last 1.1 seconds of the timeline. We don’t want to see those.

// Map the drag range to the timeline duration
let mapper = gsap.utils.mapRange(
  animNumbers.duration() - dur

The dragger

At the beginning of the code (before the startX variable had been incremented), the dragger was moved into position below the tick mark lines.

// move the draggable element into position
gsap.set("#slider", { x: startX, xPercent: -50, y: y2Pos + 20 });

When creating the dragger, we set the bounds to our known min/max (dragMin and dragMax). The dragger will then only move between the start and end tick marks. For fun, I’ve also added inertia so you can not only drag it back and forth, but you can also toss it. Neat! In either interaction, we run the updateMeter function.

// Create the draggable element and set the range 
Draggable.create("#slider", {
  type: "x",
  bounds: {
    minX: dragMin,
    maxX: dragMax
  inertia: true,
  edgeResistance: 1,
  onDrag: updateMeter,
  onThrowUpdate: updateMeter

The update function for the dragger

Remember that mapper function we created above? We’re gonna call that every time we drag or throw the dragger. We’ll feed in the dragger’s x position and get back a new time() value for the timeline. Remember, when the dragger is at its minimum, the timeline is at 1.1. That mapper really does some handy work for us.

// using the mapper, update the current time of the timeline
function updateMeter() {
  gsap.set(animNumbers, { time: mapper(this.x) });

The final result

Once we put all that together, we have a neat little gauge with a wavy set of numbers.

See the Pen Jumping Meter For Tutorial by Craig Roblewsky (@PointC) on CodePen.

Final thoughts

One of the many cool things about GSAP 3 is all the available utility methods. The mapRange() we used above is one of my favorites, but there are many more.

I hope this article gave you some ideas and wasn’t too much of a drag. See what I did there? Really funny stuff here. Until next time, keep your pixels movin’.

If you found this information useful, please help me get the word out to the interwebs. I appreciate it. You're awesome!

Published: July 5, 2020 Last Updated: August 11, 2020

You might dig these articles too

No algorithm. Just hand chosen artisanal links.