Skip to content

Instantly share code, notes, and snippets.

@guilhermesimoes
Last active September 10, 2023 13:12
Show Gist options
  • Save guilhermesimoes/fbe967d45ceeb350b765 to your computer and use it in GitHub Desktop.
Save guilhermesimoes/fbe967d45ceeb350b765 to your computer and use it in GitHub Desktop.
YouTube's new morphing play/pause SVG icon

As soon as I saw the new YouTube Player and its new morphing play/pause button, I wanted to understand how it was made and replicate it myself.

From my analysis it looks like YouTube is using SMIL animations. I could not get those animations to work on browsers other than Chrome and it appears that they are deprecated and will be removed. I settled for the following technique:

  1. Define the icon path elements inside a defs element so that they are not drawn.

  2. Draw one icon by defining a use element whose xlink:href attribute points to one of the paths defined in the previous step. Simply changing this attribute to point to the other icon is enough to swap them out, but this switch is not animated. To do that,

  3. Replace the use with the actual path when the page is loaded.

  4. Use d3-interpolate-path to morph one path into the other when the button is clicked. Other SVG libraries like D3, Snap.svg or Raphaël can also be used for this effect.

Finally, it's important to point out that the number and order of the points of each path affect the way in which they morph into one another. If a path is drawn clockwise and another is drawn anticlockwise or if they are not drawn using the exact same number of points, animations between them will not look smooth. This is the reason the play button - a triangle - is drawn using 8 points instead of just 3. There's definitely more to be said on this subject.

Have any tips or suggestions? Comment and fork away :)

<!DOCTYPE html>
<meta charset="utf-8">
<style>
html, body {
height: 100%;
margin: 0;
}
.container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.button {
flex: 1;
padding: 0;
height: 100%;
border: 0;
background-color: white;
outline: none;
-webkit-tap-highlight-color: transparent;
}
.button svg {
width: 100%;
height: 100%;
}
</style>
<body>
<div class="container">
<button class="button js-button">
<svg viewBox="0 0 36 36">
<defs>
<path id="pause-icon" data-state="playing" data-next-state="paused" d="M11,10 L17,10 L17,26 L11,26 M20,10 L26,10 L26,26 L20,26" />
<path id="play-icon" data-state="paused" data-next-state="playing" d="M11,10 L18,13.74 L18,22.28 L11,26 M18,13.74 L26,18 L26,18 L18,22.28" />
</defs>
<use xlink:href="#play-icon" />
</svg>
</button>
</div>
<script src="https://unpkg.com/[email protected]/build/d3-interpolate-path.js" integrity="sha512-/KgDI+uHyEILdOip4a2TTY6ErNAg/jJtTZ67hG450/jvgrZFKm9YkF/Q+mAIs6y7VTG+ODbACY4V4Dj4wM6SUg==" crossorigin="anonymous"></script>
<script type="text/javascript">
"use strict";
/* global document */
class Transition {
constructor(duration, cb) {
this.fullDuration = duration;
this.cb = cb;
this.tick = this.tick.bind(this);
}
start() {
this.startTimestamp = performance.now();
if (this.progress === undefined) {
this.progress = 0;
this.duration = this.fullDuration;
requestAnimationFrame(this.tick);
} else {
this.duration = this.progress * this.fullDuration;
this.progress = 1 - this.progress;
}
}
tick(timestamp) {
// TODO instead of a linear transition, implement D3's default ease-cubic https://github.com/d3/d3-transition#transition_ease
var progressDuration = timestamp - this.startTimestamp;
this.progress = Math.max(Math.min(progressDuration / this.duration, 1), 0);
this.cb(this.progress);
if (this.progress < 1) {
requestAnimationFrame(this.tick);
} else {
this.progress = undefined;
}
}
}
class PlayPauseButton {
constructor(el) {
this.el = el;
this.replaceUseWithPath();
this.el.addEventListener("click", this.goToNextState.bind(this));
this.transition = new Transition(300, this.tick.bind(this));
}
replaceUseWithPath() {
var useEl = this.el.querySelector("use");
var iconId = useEl.getAttribute("xlink:href");
var iconEl = this.el.querySelector(iconId);
var nextState = iconEl.getAttribute("data-next-state");
var iconPath = iconEl.getAttribute("d");
this.pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
this.pathEl.setAttribute("data-next-state", nextState);
this.pathEl.setAttribute("d", iconPath);
var svgEl = this.el.querySelector("svg");
svgEl.replaceChild(this.pathEl, useEl);
}
goToNextState() {
var iconPath = this.pathEl.getAttribute("d");
var nextIconEl = this.getNextIcon();
var nextIconPath = nextIconEl.getAttribute("d");
var nextState = nextIconEl.getAttribute("data-next-state");
this.pathEl.setAttribute("data-next-state", nextState);
this.pathInterpolator = d3.interpolatePath(iconPath, nextIconPath);
this.transition.start();
}
getNextIcon() {
var nextState = this.pathEl.getAttribute("data-next-state");
return this.el.querySelector(`[data-state="${nextState}"]`);
}
tick(progress) {
this.pathEl.setAttribute("d", this.pathInterpolator(progress));
}
};
var playPauseButton = new PlayPauseButton(document.querySelector(".js-button"));
</script>
</body>
@guilhermesimoes
Copy link
Author

guilhermesimoes commented Jan 21, 2016

Thank you for the kind words 😀 I only saw your comment now because GitHub does not have gist notifications.

My use of D3 here is minimal. I use it to select, remove and append elements but you can use jQuery or even vanilla JavaScript for that. The only significant use for D3 here is morphing one button path into the other. But you can use other SVG libraries or jQuery for that.
Really, I don't know why I used D3 here. I guess if all you have is a hammer, everything looks like a nail 😄

Still, I'd say D3 is one of the coolest and most useful libraries for the web that you could learn. And there are lots of free resources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment