Vue.js Developers
Published in

Vue.js Developers

Creating an Audio Powered 8 Puzzle in Vue.js for Evanescence

Featuring Vue Directives and Howler.js

Bitter Pills

I don’t have the best memory but I believe the last time I developed a puzzle to tease something for a client was 2011, when I built a puzzle for Manchester Orchestra’s Simple Math release. Well, I finally had the opportunity to stop my puzzle drought when I was approached a few weeks back to develop an audio puzzle for Evanescence and their upcoming single, “The Bitter Truth.” Their idea was to take a standard sliding 8 puzzle on a 3x3 grid and add audio to it somehow. I just knew this was going to be fun to tackle in Vue.js so I quickly put a proposal together, got approval, and we went to work.

We ended up developing an 8 puzzle which requires users to use audio cues instead of visual cues to decipher the correct arrangement of tiles. This adds an extra dimension of logic to an already tricky puzzle. If you’re ready for a challenge, head over to the puzzle and give it a shot. If you can’t figure it out, I hear some fans have uploaded a solution to YouTube. 😅

Read on to find out how it came together.

Building the 8 Puzzle in Vue

Vue powered 8 Puzzle

To begin with, we’ll need to develop the core 8 puzzle solution using Vue.js. I’ve included the CodePen.IO above as an example of how I’ve developed it for our campaign. Let’s break down some of the key areas.

Tile Layout

First, we just want an array of data which will represent our tiles and their solved arrangement. For this demo, I’m simply splitting a string of numbers 123456789 into an array ["1","2","3","4","5","6","7","8","9"] using the string split method. I’m also using the shuffle method of Lodash to shuffle the tiles initially.

tiles: _.shuffle('123456789'.split('')),
solution: '123456789'.split('')

Now that we have our tile data, we can add them as divs to the view with a few additional properties. First, let’s give it a key and a click event which calls the move method. Then, we’ll give the 9 tile a class of empty as this will be the missing tile from our puzzle.

<div id="puzzle">
<div class="tile" v-for="(tile, i) in tiles" :key="tile" @click="move(i) :class="{ empty: tile === '9' }">
{{ tile }}
</div>
</div>

In order to lay these tiles out in a 3x3 grid, we can use the (you guessed it) CSS grid on our #puzzle holding div. Here’s how I wrote it up.

#puzzle{
display: grid;
grid-gap: 1px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
height: 240px;
width: 240px;
}

Then you can style the .tile as you like. I kept it simple. Oh, but let’s not forget to hide the .empty tile.

.tile.empty{
opacity: 0;
pointer-events: none;
}

Movement

Now, we can start working on the tile movement. Each position in the 3x3 grid can only make certain moves. For example, a tile in the top left corner could only move to the right or down. Rather than writing logic figure out movements dynamically, I took the easier path and simply defined each position’s possible movements as an array.

moves: [
[1, 3],
[0, 2, 4],
[1, 5],
[0, 4, 6],
[1, 3, 5, 7],
[2, 4, 8],
[3, 7],
[4, 6, 8],
[5, 7]
]

However, knowing the possible movements is only half of the problem. We also need to know if the the tile can make the movement. This is only true if the empty space is present in one of the possible moves. So, we’ll get an array of possible moves for the current tile and then check to see if the index of the empty tile is includes.

let emptyIndex = this.tiles.findIndex(tile => tile === '9')this.moves[i].includes(emptyIndex)

This all comes together in the move() method which finally swaps the clicked tile with the empty tile if allowed.

move(i) {
let emptyIndex = this.tiles.findIndex(tile => tile === '9')
if (this.moves[i].includes(emptyIndex)) {
let tile = this.tiles[i]

this.$set(this.tiles, i, this.tiles[emptyIndex])
this.$set(this.tiles, emptyIndex, tile)
}
}

Solution

With movements finally in place, we can compute and watch for a solution in Vue using the isEqual function of Lodash.

computed: {
solved() {
return _.isEqual(this.tiles, this.solution)
}
},
watch: {
solved(newVal) {
if (newVal) {
alert('Solved.')
}
}
}

And that’s about the simplest 8 puzzle I can come up with in Vue. 😅

Adding Audio to Tile Events

With a simple 8 puzzle solution in place, it is now time to add audio to the experience. This was trickier to figure out than I thought. Initially, we thought about adding play buttons to the top corner of each tile but I explained that this would be visually confusing and potentially break the user experience, as we were already listening for clicks connected to tile movements. After a bit of thinking, I remembered an old Spotify iOS feature which allowed users to press and hold to preview audio. So, I thought maybe we could listen for long press events and clicks on the same tile element.

It didn’t take me long to find this excellent blog by Nosa Obaseki which not only covers a simple solution for long press events but also introduced me to custom Vue directives. A Vue directive allows us to add all sorts of dynamic functionality to an element. In our case, we wanted to know whether or not a long press or click occurred. It all begins by adding listeners which listen for mouse and touch events. Then, in abstract, when a user tap or clicks a tile, we start a timer to see if they keep their mouse or finger down for a certain period of time. If so, we count it as a long press. I invite you to check out Nosa’s blog but I will cover some of my solution, as it relates to the puzzle, here.

Setting up Vue Directive

To begin with, we need three variables. The pressTimer variable keeps track of whether or not a timer is currently running. The pressDelay allows us to set how many milliseconds need to elapse before a long press is registered. Finally, I noticed I also needed a pressed variable to help me navigate between long press and click events.

let pressTimer = null
let pressDelay = 500
let pressed = false

We’ll then establish three functions: start(), cancel(), and click(). Let’s start with start(). When a user’s mouse is down or has started touching one of our tiles, we’ll start a timer using the setTimeout function at the pressDelay interval. If this timeout ends up firing, we should begin playing the audio clip associated with the tile. (More on playing audio later.)

let start = (e) => {
if (e.type === 'click') {
return
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
pressed = true
// start clip
}, pressDelay)
}
}
el.addEventListener('mousedown', start)
el.addEventListener('touchstart', start)

Now, when a user’s mouse moves out of an element or their touch is canceled we should stop the clip and clear the timer.

let cancel = (e) => {
// stop clip
if (pressTimer !== null) {
clearTimeout(pressTimer)
pressTimer = null
}
}
el.addEventListener('mouseout', cancel)
el.addEventListener('touchcancel', cancel)

Finally, we’ll listen for click and touch end events. If this click event was not part of a long press event, we’ll move the tile. In either case, we make sure to call the cancel() function to clear any logic.

let click = (e) => {
cancel()
if (!pressed) {
// move tile
} else {
pressed = false
}
}
el.addEventListener('click', click)
el.addEventListener('touchend', click)

Playing Clips with Howler

The last bit of functionality we need is to actually play the clips. Since our clips are sections of a single composition which have been rearranged, I like using the sprite functionality of Howler.js to power playback. Howler allows us to take a single audio file and declare the start point and duration of each clip. Here’s what initializing that audio object looks like.

this.clips = new Howl({
src: ['clips.mp3'],
sprite: {
1: [0, 6386],
2: [7355, 6395],
3: [14688, 6402],
4: [22000, 6438],
5: [29355, 4839],
6: [35125, 1698],
7: [37768, 4818],
8: [43510, 1698]
},
onload: () => {
resolve()
}
})
this.clips.load()

Then, playing the clips simply requires the right method.

this.clips.play('1')

Thanks

As of Monday, over 2000 fans have solved the puzzle! That’s a great turnout and signs of an active and interested fanbase. Thanks to BMG, Revelation Management Group, and Evanescence for the challenge and letting me be a part of this. “Better Without You” releases Friday, March 5th.

--

--

--

Helping web professionals up their skill and knowledge of Vue.js

Recommended from Medium

Ex-Dividend Date

Ex-Dividend Date

Vimeo upload API 串接

Most Efficient Ways Of Styling React Components: Introducing Styled Components

How to Create a Slide Transition Between Separate “Pages” with HTML, CSS, and JavaScript

Jest vs Mocha vs Jasmine: Comparing The Top 3 JavaScript Testing Frameworks

Create a Basic useFetch Hook in Vue.js

vue custom useFetch hook

Custom Actions in Botframework Composer with Node.JS (Part 1)

Jewelupp Instagram Paylaşımı #17912105540067159

Instagram Post

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Lee Martin

Lee Martin

Netmaker. Playing the Internet in your favorite band for nearly two decades. Previously Silva Artist Management, SoundCloud, and Songkick.

More from Medium

How to build a Vue-based micro-frontends infrastructure

How to use vuelidate with vue tel input (vue-tel-input)

Why You Should Use VueJS

How to use Moment.js with Vue 3?