Animate it Like @Apple

Published by in October, 2017

Categorized in: Dev Stuff

Tags: animation  sass  

What have I been working on at Apple, Inc.? For the past six months I've been building Quick Tours. Let's take a look at how we handle complex sass animations.

The four entities modern browsers animate at 60 frames per second are position, scale, rotation, and opacity. In order to maintain smooth 60fps animation, we animate transforms and opacity for those four entities, because transforms and opacity are hardware accelerated. 99.9% of animations we produced do not affect browser layout or painting. For more on this concept look at the resources below.

For Apple Quick Tours, we deliver content to the user with a traditional slider, and we use keyframe animations for elements that will be animated on the page. We add an "active" body class to the slide that the user selects, and then key our animations off of that.

So let's get right down to it, say you have an image you want to scale in from nothing to it's natural size.


.my-div {
  width:300px;
  height: 200px;
  background: red;
  transform: scale(0);
  animation: scale-me 1s 3s ease-in-out forwards;
}

@keyframes scale-me {
   100% {
        transform: scale(1);
    }
}

That should look familiar, and if your familiar with keyframe animations, I've told the .my_div to use the keyframe animation I called "scale-me" and delay the animation by 3 seconds, and set the animation duration to 1 seconds, using the animation timing function "ease-in-out" and the animation direction torwards."

Now inside our slider, we'd re-write the scss to look like the following:


.my-div {
  width:300px;
  height: 200px;
  background: red;
  transform: scale(0);
  .active & {
     animation: scale-me 3s .5s ease-in-out forwards;
  }
}

@keyframes scale-me {
   100% {
        transform: scale(1);
    }
}

See the Pen Animate like Apple - 1 by David Davis (@david-j-davis) on CodePen.

Now taking it to the next level, let's add some more complex animations, translate some elements in and delay and then scale those elements out, but we won't apply any animation until the user clicks an anchor, and we'll trigger the animation from there.

Since css and javascript are independent of each other, we will wait for the user to click something, and once that has happened we will apply a class to the DOM letting us know that it's time to start our animations. We also make use of scss variables to set our animation delays and animation durations for all animations on the page.

To make sense of this, have a look here. Say we have a set of three circles contained in a div, each one will translate into view, but we want to offset each of them with a delay, and we want them all to share the same duration:


//animation variables
$circle-one-delay:1s;
$circle-two-delay:1.1s;
$circle-three-delay:1.3s;

$circle-translate-duration: .5s;

Next we want to define the animation for translating on the page, but notice that I'm translating the "X" coordinate to 0. This is intentional, we want our final end state of the animation to be 0 and not some number like -65px. We do this because we want our animations to be a smooth 60fps and want them to be "cheap" for the browser. For more info on this concept, take a look at Paul Lewis' article, Flip your Animations.


@keyframes slide-me {
	100% {
		transform: translate(0,0);
	}
}

Now let's add a second animation onto each of our elements, let's scale them out and set their opacity to 0 to try and achieve an "explode" effect. Let's again offset the delay for each. They will all share the same animation duration, and notice that this second animation will add the delays from the first animation using those variables:


$circle-one-explode-delay: $circle-one-delay + $circle-two-delay + $circle-three-delay;
$circle-two-explode-delay: $circle-one-explode-delay + .15s;
$circle-three-explode-delay: $circle-two-explode-delay + .15s;

$circle-explode-duration: .7s;

Let's set our explode keyframe animation, notice I needed to set the translate(0,0) at 0%, because we're animating an element that has already been translated, it needs to know what position to start from if not the original state:


@keyframes explode-me {
    0% {
	transform:translate(0, 0);
    }
    100% {
        transform: scale(2);
        opacity: 0;
    }
}

Let's style our three circle elements, and notice a few things here, often our elements for animation or going to sit absolute on top of content, so I set our final resting place in this case for our .circle element to be bottom:0 and left:0. Then in keeping with our "flipped" animation we apply a transform translate in the X,Y direction, and that will in affect act as our animation start state, and we're going to animate to the bottom:0, left:0 position. Notice also that this animation won't trigger until the "expanded" class is added to the DOM.


.circle {
	width:100px;
	height:100px;
	background:grey;
	border-radius:50%;
	position:absolute;
	bottom:0;
	left:0;
	transform:translate(-100px, 0);
	.expanded & {
		animation: slide-me $circle-translate-duration $circle-one-delay ease-in-out forwards, explode-me  $circle-explode-duration $circle-one-explode-delay ease-in-out forwards;
	}
}

.circle-top {
	width:150px;
	height:150px;
	background:grey;
	border-radius:50%;
	position:absolute;
	box-shadow: 0px 0px 5px 5px rgba(0,0,0,.2);
	top:0;
	left:0;
	transform:translate(0, -100px);
	.expanded & {
		animation: slide-me $circle-translate-duration  $circle-two-delay ease-in-out forwards, explode-me  $circle-explode-duration $circle-two-explode-delay ease-in-out forwards;
	}
}

.circle-right {
	width:130px;
	height:130px;
	background:lightgray;
	border-radius:50%;
	position:absolute;
	box-shadow: 0px 0px 5px 5px rgba(0,0,0,.2);
	right:0;
	bottom:0;
	transform:translate(130px, 0);
	.expanded & {
		animation: slide-me $circle-translate-duration $circle-three-delay ease-in-out forwards, explode-me  $circle-explode-duration $circle-three-explode-delay ease-in-out forwards 
	}
}

Putting it together, here's what I've got. Click on the "Expand Content" to see our circles animate in, and explode out. This idea of "flipping" your animations takes some getting used to. It's efficient for the browser, and you'll find that you can efficiently reuse all of your animations because they will always be set to the same end-state.

See the Pen Animate it Like Apple - more complex by David Davis (@david-j-davis) on CodePen.

Now, for some real world examples, let me introduce you to some of the work I produced at Apple in the last six months for the Fall 2017/Winter 2018 new software and hardware release. (unfortunately the new hardware isn't released yet,
so for those tours we will have to wait).

References:

  1. http://wiki.bash-hackers.org/commands/builtin/printf
  2. https://aerotwist.com/blog/flip-your-animations/