Building a JavaScript Yak Bak clone with Tone.js — Part 3
Chris Gustin
Posted on February 21, 2023
Building a JavaScript Yak Bak clone with Tone.js — Part 3
Part 1: https://medium.com/@cgustin/building-a-javascript-yak-bak-clone-with-tone-js-part-1-cb01b3b10529
Part 2: https://medium.com/@cgustin/building-a-javascript-yak-bak-clone-with-tone-js-part-2-1a262aa82c5
In part 1, we set up our HTML and used JavaScript and Tone.js to add functionality to our Yak Bak. In part 2, we used CSS to style it and build out a design system. At this point, we could consider this project done and move on, however an important part of product development is to look for opportunities to go beyond what the user expects and up the excitement level. Some product dev philosophies refer to these as “delighters.”
Our original scope had two “nice-to-haves” or “delighters”: features that aren’t critical for the app to function, but will add to the excitement for the user. The first was adding a reverse playback feature, and the second was giving our users the ability to change the color scheme of the Yak Bak. With the work we did in part 2 with setting up CSS color variables, this step is simpler than it sounds.
Step 1: Planning
Before we start writing code, it’s a good idea to think about how we want this feature to work. There are a couple ways we could go.
Should we pre-define color schemes and let the user pick them with a select element? This may be too restrictive for users who want to pick their own colors.
Should we use a text input to have the user type in the hex values they want for each color? This would work, but it sounds like a less than ideal user experience.
Ideally, we want the user to choose their colors from a palette using a color picker, and see those changes update in realtime. These color pickers should be displayed at the top of the screen above the Yak Bak so they’re easy to get to. Luckily, HTML5 includes a “color” input type that makes this step simple.
Step 2: HTML
Our user interface for the color changing feature will be two HTML color picker inputs at the top of the page. We’ll house these in their own div so they’re structurally separated from the Yak Bak markup. Let’s add a div with an ID of options (there are lots of name possibilities for this id, but “options” leaves room to add other features to this section down the line, whereas something like “colors” would lock is in a little more).
<div id="options"></div>
<div id="yakbak">
...
</div>
Now we’ll add the two HTML color inputs like this:
<div id="options">
<input type="color">
<input type="color">
</div>
<div id="yakbak">
...
</div>
And since we want to hook them up to our JavaScript, we’ll give each one a descriptive ID:
<div id="options">
<input type="color" id="primaryColor">
<input type="color" id="secondaryColor">
</div>
<div id="yakbak">
...
</div>
Finally, we can set the initial value for each input using the “value” property. It defaults to black, let’s instead set each one to the primary/secondary color values so it’s clear to the user what colors each input controls:
<div id="options">
<input type="color" id="primaryColor" value="#4abbb8">
<input type="color" id="secondaryColor" value="#f79b1b">
</div>
<div id="yakbak">
...
</div>
Our color pickers are in place, but they don’t do anything and they could use a little styling. Let’s knock out the CSS, and then work on the JS.
Step 3: CSS
Right now, the color picker inputs are at the far left of the screen. During the planning phase, we decided we want them centered over the Yak Bak so they’re easy for the user to get to. We can use Flexbox to move the inputs to the center. In our CSS, let’s target our “options” div and set it to display: flex to initialize Flexbox.
:root {
...
}
#options {
display: flex;
}
#yakbak {
...
}
We can use justify-content: center to move our inputs (and anything else we might add) to the center of the container.
:root {
...
}
#options {
display: flex;
justify-content: center;
}
#yakbak {
...
}
Now they’re centered, but the color inputs are touching each other and the Yak Bak. Let’s move them off of the Yak Bak first by adding a little margin-bottom .
:root {
...
}
#options {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
#yakbak {
...
}
For the inputs, we could add a left/right margin to each input to separate them, but Flexbox gives us a gap property that will let us put space between our elements without having to add an extra CSS selector and target the elements separately. Let’s set it to 10px to stay consistent with our 5px grid scheme.
:root {
...
}
#options {
display: flex;
justify-content: center;
margin-bottom: 20px;
gap: 10px;
}
#yakbak {
...
}
Our styling is done, let’s move on to the functionality.
Step 3: JavaScript
When the user clicks into one of the color inputs and changes the color, we want the colors on the Yak Bak to change too. We can use JavaScript to accomplish this. The first thing we need to do is create a reference to each input using document.getElementById
Let’s add those underneath the const yalpButton that we declared earlier:
let initialized = false;
let mic, recorder, player;
const sayButton = document.getElementById("say");
const playButton = document.getElementById("play");
const yalpButton = document.getElementById("yalp");
const primaryColorInput = document.getElementById("primaryColor");
const secondaryColorInput = document.getElementById("secondaryColor");
Now that we have a reference to each input, we need to hook them up to event listeners so we can do stuff when the user uses the inputs. At the bottom of the JavaScript file, let’s add an event listener for each color input.
According to the docs, HTML color inputs have two events we can listen for: input and change . inputfires anytime the color changes, while change only fires when the user closes the color picker (a little unintuitive, I know). We want the user to see their changes right away, rather than picking a color and closing the input to see it take effect, so we’ll listen to the input event.
primaryColorInput.addEventListener("input", function(event) {
// Do stuff when color changes
})
secondaryColorInput.addEventListener("input", function(event) {
// Do stuff when color changes
})
These look just like the event listeners from part 1, but you may have noticed we’re passing an event argument in the functions that we’ve attached to each listener. This is an optional argument that gets passed to an event listener function anytime the event is triggered and contains important information about the event that happened.
In this case, we can use the event to find out the current color value each input is set to. The event has a property called target , which refers to the element that triggered the event. And that element has a value property, since it’s an input. Let’s console.log this for now to make sure everything is working so far:
primaryColorInput.addEventListener("input", function(event) {
console.log(event.target.value);
})
secondaryColorInput.addEventListener("input", function(event) {
console.log(event.target.value);
})
Now if you open the console and change the color values on the inputs, you should see those hex values logged in realtime.
We’re a little bit closer, but we still need to get those values in to our CSS somehow, so we can see the Yak Bak itself update. Luckily, JavaScript gives us a way to target CSS color variables using document.documentElement.style.setProperty(propertyName, newValue)
By passing a CSS variable name and a value, we can update our CSS variables programmatically, and because we set up our stylesheet to use those variables instead of hardcoded values, we don’t have to target and update multiple CSS properties. Let’s hook up our primary color variable first, remembering that event.target.value has the new hex code from the color input:
// Color Inputs
primaryColorInput.addEventListener("input", function(event) {
document.documentElement.style.setProperty("--primary", event.target.value);
})
secondaryColorInput.addEventListener("input", function(event) {
console.log(event.target.value);
})
Now if you choose a new color on the primary color input, you should see the body of the Yak Bak update. Let’s get the secondary color hooked up too:
// Color Inputs
primaryColorInput.addEventListener("input", function(event) {
document.documentElement.style.setProperty("--primary", event.target.value);
})
secondaryColorInput.addEventListener("input", function(event) {
document.documentElement.style.setProperty("--secondary", event.target.value);
})
Our color inputs are hooked up and working and we’re almost done, but you may have noticed that the border color of the Yak Bak isn’t updating. This border is set to the color variable that’s a 10% darker shade of the primary color to give a little bit of depth effect. So when our primary color variable changes, we also need to calculate a 10% darker version and update our primary-dark CSS variable at the same time.
With some Googling for “javascript make color darker by percentage” (don’t judge me), I came across a StackOverflow thread where a user had come up with a function that would take in a color value and a percentage value, and return the darkened (or lightened) value, which is exactly what we need. Color theory is a little outside the scope of this tutorial, but the StackOverflow thread is chock full of great info if you’re curious. For now all you need to know is that we’re pasting this utility function at the top of our JavaScript file before the rest of our code:
function lightenDarkenColor(col, amt) {
var usePound = false;
if ( col[0] == "#" ) {
col = col.slice(1);
usePound = true;
}
var num = parseInt(col,16);
var r = (num >> 16) + amt;
if ( r > 255 ) r = 255;
else if (r < 0) r = 0;
var b = ((num >> 8) & 0x00FF) + amt;
if ( b > 255 ) b = 255;
else if (b < 0) b = 0;
var g = (num & 0x0000FF) + amt;
if ( g > 255 ) g = 255;
else if ( g < 0 ) g = 0;
var string = "000000" + (g | (b << 8) | (r << 16)).toString(16);
return (usePound?"#":"") + string.substr(string.length-6);
}
We can update our primaryColorInput event listener to dynamically update our darkened primary color value too, using our helper function to calculate the 10% darker value:
primaryColorInput.addEventListener("input", function(event) {
document.documentElement.style.setProperty('--primary', event.target.value);
document.documentElement.style.setProperty('--primary-dark', lightenDarkenColor(event.target.value, -10));
})
Now when we change the primary color, the border color updates too. Our inputs work great, but what if the user wants to reset back to the default values? It would be nice to give them an easy way to do that. Let’s add a “Reset” button in our HTML and add some JavaScript logic to make it work.
<div id="options">
<input type="color" id="primaryColor" value="#4abbb8">
<input type="color" id="secondaryColor" value="#f79b1b">
<button type="button" id="resetColors">Reset</button>
</div>
And we’ll create a reference in our JS file like this:
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");
const primaryColorInput = document.getElementById("primaryColor");
const secondaryColorInput = document.getElementById("secondaryColor");
const resetButton = document.getElementById("resetColors");
Then because we’re going to listen for and respond to the click event, we’ll set up our event listener at the bottom of our JS file like this:
resetButton.addEventListener("click", function() {
// Do stuff when clicked
console.log("Reset!");
})
Notice that we’re not passing the event parameter, because we don’t need any of the information that would be passed on the event. Instead, when the reset button is clicked, we want to set all our CSS variables back to their original state, and the two color inputs. We could hardcode those default values into our CSS, but then if we change our default color values in the future, we’ll have to remember to update not only our CSS variables, but our JS file and our HTML input values too.
Let’s instead set it up so our CSS variables are the single source of truth, and our JavaScript instead grabs and stores those values dynamically. First, in our HTML, let’s delete the hardcoded value properties that we set on each color picker.
<div id="options">
<input type="color" id="primaryColor" value="">
<input type="color" id="secondaryColor" value="">
<button type="button" id="resetColors">Reset</button>
</div>
Next, when our page loads, we want use JavaScript to get the primary and secondary color values, and set those values on each color picker. Just like JavaScript gives us a setProperty function, it also has a getProperty function for fetching CSS property values. However, we also have to call getComputedStyle on document.documentElement before we call it to make sure we’re getting the final compiled stylesheet first, then trying to access the properties on it (including our CSS variables). Underneath our const resetButton let’s add a const primaryColor and set the value using getProperty:
const resetButton = document.getElementById("resetColors");
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue("--primary").trim();
We’re also chaining .trim() to the end to remove any whitespace before or after the value that gets returned. If we didn’t do this, the hex value we get back would have a space in the front and cause errors. Let’s set our secondaryColor and primaryDark values the same way:
const resetButton = document.getElementById("resetColors");
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue("--primary").trim();
const primaryDark = getComputedStyle(document.documentElement).getPropertyValue("--primary-dark").trim();
const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue("--secondary").trim();
And we can initialize our color pickers by setting their value property equal to their corresponding color value:
const resetButton = document.getElementById("resetColors");
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue("--primary").trim();
const primaryDark = getComputedStyle(document.documentElement).getPropertyValue("--primary-dark").trim();
const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue("--secondary").trim();
primaryColorInput.value = primaryColor;
secondaryColorInput.value = secondaryColor;
Now when the page loads, the color pickers are set to the default color values automatically and everything will stay in sync.
Finally, when the reset button is clicked, we can use our stored color values to set our CSS variables and the color pickers back to their original settings:
resetButton.addEventListener("click", function() {
primaryColorInput.value = primaryColor;
secondaryColorInput.value = secondaryColor;
document.documentElement.style.setProperty('--primary', primaryColor);
document.documentElement.style.setProperty('--primary-dark', primaryDark);
document.documentElement.style.setProperty('--secondary', secondaryColor);
})
With that, we’ve finished all the features in our scope, and wrapped up our project. If you’ve followed along this far, thanks for taking the time, and I hope you learned a few new things! You can view the complete working code at https://codepen.io/cmgustin/pen/ExpRLJj
Posted on February 21, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.