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="http://www.w3.org/2000/svg" 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"/> </svg>
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++) { makeNumber(); for (let i = 0; i < 5; i++) { y1Pos = i === 0 ? y2Pos - 25 : y2Pos - 15; // first line in each pack is slightly taller makeLine(y1Pos); startX += spacing; } count++; }
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"); demo.appendChild(newLine); gsap.set(newLine, { attr: { x1: startX, x2: startX, y1: yp, y2: y2Pos } }); } // creates the numbers function makeNumber() { let txt = document.createElementNS(svgns, "text"); demo.appendChild(txt); 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 }); animNumbers .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( dragMin, dragMax, dur, 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’.