Conducting a Dance Fever Reading for Florence and The Machine

Using the Spotify Platform and Greensock

Lee Martin
9 min readMay 28, 2022

As soon as I got the project, I let my girlfriend, Anne, know I was going to be building a tarot card inspired reading for Florence and The Machine’s new record Dance Fever. In addition to being a painter, Anne works at a vintage shop in New Orleans part time. A week or two later, as Anne was working at the shop, she encountered someone from another vintage shop in New Orleans and they got to talking about this woman’s upcoming trip to London. (Anne and I just visited in December.) As it turns out, this woman was a die-hard Florence fan and was headed to the UK to catch a string of the initial Dance Fever tour dates. Awesome! I love hearing about the biggest fans of a band and it got me even more excited to be building the app. Flash forward to the days before we launched the project. I was posting some of the UX experiments on Twitter to get fans excited for the project and I noticed I received a comment from a diehard Florence fan, based in New Orleans. I had to ask… Are you the same woman my girlfriend met that day? As it turns out, no. She was that woman’s partner and they’re engaged now! In fact, they were engaged right before a Florence show.

Congrats to Danielle and Bethany!

So, on the topic of universal energies, we did in fact build and launch a Dance Fever Reading app for Florence and The Machine. The reading tells fans which song has chosen them by comparing the audio features of a user’s recent listening history on Spotify to those of the songs on the record. Get your reading today and read on to learn more about how it came together.

Algorithm

This is not the first time I’ve developed a song matching algorithm using the Spotify platform. I first tried this with SG Lewis and was looking forward to revisiting the algorithm for Florence. As a refresher, Spotify provides audio feature parameters (danceability, energy, tempo, valence, etc) for every track on their platform. Our app compares the features of the top tracks a user has been streaming to those of the tracks on the record. With up to 50 top recently streamed songs, it turns out to be a lot of comparisons and creates a nice (albeit robotic) comparison of taste. The algorithm of this app was evolved to make even more comparisons but effectively came down to a bit of code which looked something like this.

// Album tracks
let albumTracks = [...]
// User tracks
let meTracks = [...]
// Audio features
const audioFeatures = [
“danceability”,
“energy”,
“loudness”,
“speechiness”,
“acousticness”,
“instrumentalness”,
“liveness”,
“valence”,
“tempo”
]
// Give every album track an initial score of 0
albumTracks.forEach(albumTrack => {
// Set score to 0
albumTrack.score = 0
})// Loop through all of the user’s tracks
meTracks.forEach(metTrack => {
// Loop through each audio feature
audioFeatures.forEach(audioFeature => {
// Map distance between each album track and user track feature
let featureDistances = albumTracks.map(albumTrack => {
// Absolute distance
return Math.abs(albumTrack.features[audioFeature] - meTrack.features[audioFeature])
}) // Loop through each feature distance
featureDistances.forEach((featureDistance, i) => {
// If feature distance is smallest
if (featureDistance == _.min(featureDistances)) {
// Add to album track score
albumTracks[i].score += 1.0
}
})
})
})
// Sort album tracks by highest score
const sortedTracks = albumTracks.sort((a, b) => b.score - a.score)
// Chosen track
// sortedTracks[0]

We loop through all of the user tracks and calculate the absolute distance of each audio feature as compared to each track on the album. In other words, how close is the tempo of this user track to the tempo of this album track? Once all these distances are calculated, we loop through them and increment the associated album tracks score if it contains the minimally calculated distance. So, this track was closest in valence, give it a point. This is done over and over again until we can sort all the album tracks by their score and select the one with the highest value.

Pretty simple. The only thing you need to be mindful of is making too many average comparisons as this will always yield the track with the most average audio feature parameters. If you see a particular song surfacing often, you’ll probably want to adjust the algorithm to be even more selective. You can do this by using less user tracks or being more strict about handing out points.

Card Components

Early Figma prototype image

Our app revolves around the cards which represent each of the tracks of the Dance Fever. The visuals for these cards are pulled from the incredible art direction of Autumn De Wilde and design of Thunderwing Studio. I love building small UX components based around the tactile delightfulness of cards. I did some of this on the same SG Lewis project and A LOT of this while building the Me Rex Megarbear player. It all starts with creating a reusable Vue component for the card itself.

Card

To create a flippable card in HTML and CSS, we only need three divs: one container element and two children for each face of the card.

<div class=”card”>
<div class=”face front”></div>
<div class=”face back”></div>
</div>

The faces are absolutely positioned in the container which preserves any 3D transforms. I’m using aspect-ratio to make sure the container maintains the card design’s aspect ratio. Since the card will be part of a responsive interface, I’ll typically size the card itself with another container parent later on. Finally, the back face is flipped around using a transform rotation and hidden behind the front face using backface-visibility.

.card{
aspect-ratio: 751 / 888;
height: 100%;
position: relative;
transform-style: preserve-3d;
width: 100%;
}
.face{
backface-visibility: hidden;
background-size: cover;
height: 100%;
position: absolute;
width: 100%;
}
.front{
background-image: url(front.jpg);
}
.back{
background-image: url(back.jpg);
}

You can now flip this card by simply applying a 180deg rotation transform to the container div but I like using a bit of Greensock for more control.

Card Flip

Click to flip

As I mentioned, I like to use a bit of Greensock to flip the card around because it gives me more control and potential events to listen for. To make the flip more realistic, the card is first raised slightly to make room for the flip, before it is once again placed down. Here’s all it takes with a little Greensock timeline.

// Initialize timeline
let tl = gsap.timeline()

// Raise card
tl.to(this.$refs.card, {
z: 200
})

// Flip card
tl.to(this.$refs.card, {
duration: 2,
rotationY: "+=180",
ease: "circ.out"
})

// Lower card
tl.to(this.$refs.card, {
z: 0
})

On the front page of the app, I created a grid of all the cards and then randomly flipped one at a time. This sorta reminds me of the Super Mario 3 card matching mini game. Wait, should I recreate that using Dance Fever cards? I’ll put it on the idea list.

Card Reveal

Another rather complicated bit of UX is the card reveal animation. Once our algorithm finds the user’s chosen card, the Dance Fever cards reappear, fan out, and then the chosen card is pulled from the hand and shown to the user. Once again, we can use a gsap timeline to handle this. In general, it is pretty easy until you start trying to make it responsive for different device sizes. It took a while for me to dial it in and I also had to squash a few browser specific bugs like how Safari drops z indexes when making 3D transforms. Anyway, here’s a little gsap code to get you there.

// Cards parent
let $cardHolder = document.getElementById(‘cards’)
// Card holder width
let cardHolderWidth = $cardsHolder.getBoundingClientRect().width
// Card elements
let $cards = Array.from($cardHolder.children)
// Card sizes
let cardWidth = $cards[0].getBoundingClientRect().width
let cardHeight = $cards[0].getBoundingClientRect().height
// Chosen card
let $card = $cards[index]
// Initialize timeline
let tl = gsap.timeline()
// Fan out cards
tl.to($cards, {
duration: 2,
x: function (i, target) {
return i * ((cardHolderWidth - cardWidth) / ($cards.length - 1))
}
})
// Raise card up
tl.to($card, {
ease: “power2.out”,
duration: 2,
y: ‘-105%’
})
// Bring card to center and flip over
tl.to($card, {
duration: 6,
ease: "circ.out",
xPercent: -50,
yPercent: -50,
left: "50%",
top: "50%",
x: 0,
y: 0,
scale: 2,
rotationY: "-=180",
})
// Raise card above hand
tl.to($card, {
duration: 0.25,
z: cardHeight,
}, '<')

Of special note is the fan out animation which uses a function based x position to animate each of the cards to a nice relative position from the left of the hand based on their index position. (This allows them to fan out from left to right.) Another thing worth mentioning is the final animation which adjusts the z position to the card’s height. This helps us get around the Safari z index issue I mentioned earlier which prevents the cards from clipping each other visually.

Bonus: Card Fan

As a bonus topic, I’d love to touch on the little card fan loader which appears when the algorithm is loading. Instead of using our Vue component for this, I decided to handle it in HTML <canvas> because I was a bit more comfortable with complicated rotation logic there. This works by establishing an array of 15 independent card rotations which are first tweened by a pair of gsap animations. Once out and again in.

// Initialize cards
let cards = Array(15).fill(null).map(() => {
return {
rotate: 0
}
})
// Initialize timeline
let tl = gsap.timeline({
ease: "linear",
repeat: -1
})

// Fan cards out
tl.to(cards, {
duration: 1,
rotate: (i, target) => {
// Calculate rotation
let rotate = i * (360/(cards.length-1))

// Return rotation adjustment
return `+=${rotate}`
}
})

// Fan cards in
tl.to(tcards, {
duration: 1,
rotate: (i, target) => {
// Calculate rotation
let rotate = ((cards.length-1)-i) * (360/(cards.length-1))

// Return rotation adjustment
return `+=${rotate}`
}
})

This isn’t animating any actual HTML elements. Just the array of card rotations. In order for something to appear, we’ll use HTML canvas to render the cards dynamically. Note: I preloaded the cardImage beforehand using a simple JavaScript Promises based loader.

// Request animation frame
requestAnimationFrame(this.render)

// Get canvas
let canvas = document.getElementById('canvas')

// Get context
let context = canvas.getContext('2d')
// Clear canvas
context.clearRect(0, 0, canvas.width, canvas.height)

// Loop through cards
this.cards.forEach(card => {
// Set canvas rotation
context.translate(canvas.width/2, canvas.height/2)
context.rotate(Math.round(card.rotate) * Math.PI / 180)
context.translate(-canvas.width/2, -canvas.height/2)

// Calculate card size
let cardHeight = canvas.height * 0.45
let cardWidth = cardHeight * (cardImage.width / cardImage.height)

// Calculate params
let x = (canvas.width / 2) - (cardWidth / 2)
let y = (canvas.height / 2) - cardHeight
let width = cardWidth
let height = cardHeight

// Draw card image
context.drawImage(cardImage, 0, 0, cardImage.width, cardImage.height, x, y, width, height)

// Reset transform
context.setTransform(1, 0, 0, 1, 0, 0)

})

Using requestAnimationFrame, we can create a constant loop of animation. Within this animation, we loop through each card and first set the rotation of the canvas to the card’s unique rotation. We also want to calculate a dynamic size and position of the rotated card to keep things responsive. Once we have these variables, we can draw our card image to the canvas and thanks to the canvas rotation, it will appear rotated. We then need to reset the canvas transformation before drawing the next card. With gsap and canvas working in tandem, you get a nice fluid circular card fan animation.

Thanks

Florence

Special shout out to Nic Taylor from Thunderwing for helping me hone the “Dance Fever” aesthetic. There were so many incredible people involved with this precious project, including Oliver Hunter, Julie Vastola, Elliot Althoff, Alexander Coslov, Luke Ferrar, Timothy Hrycyshyn, Mackinlay Ingham, Kiley Schlappich, and Kathy Tra Thanks again for making me a small part of your magical campaigns. Finally, thanks to Florence and her team for allowing me to help on this perfectly crafted album. Dance Fever it out everywhere now.

--

--

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