Orbits - choreograph css animations with sass

A stylesheet talk at 2013 ThoughtWorks North America Awayday #2013naad. We’ll look at how to use some of the powerful features of SASS to choreograph modular, maintainable and highly-flexible css keyframe animations.

Live demo

Github repo

Video

Speakers at TW Awayday are asked to create a 30-second commercial to promo their talk. The talk is about the video. The video is about the talk. So meta.

YouTube not loading? Click here

We’ll be looking at..

  • Touch on some SASS features.
  • Bourbon - a SASS pattern library.
  • CSS transforms (scale and rotate).
  • CSS keyframe animations.
  • CSS :nth-of-type.
  • How to use the above to create the demo.

SASS features

Bourbon

We’ll be using Bourbon, a lightweight SASS pattern-library, to support CSS3 vendor prefixes. This will clean-up our code a bit and make it more readable/maintainable.

An important note: Including bourbon doesn’t write any code. It is merely a library of mixins that we can use if we so choose. aka not bootstrappopotamus.

Example use of bourbon

example_input.scss
1
2
3
.foo {
  @include border-radius( 5px );
}

Outputs:

example_output.css
1
2
3
4
5
.foo {
  -webkit-border-radius: 5px;
     -moz-border-radius: 5px;
          border-radius: 5px;
}

CSS Transforms

Scale

Scales an element based on its size.

1
2
3
.foo { transform: scale( 0.1 ); }

.bar { transform: scale( 2.3 ); }

Rotate

You can rotate an element around its X, Y and Z axes. There are a few ways to rotate an element.

  • rotate( deg )
  • rotateX( deg )
  • rotateY( deg )
  • rotateZ( deg )
  • rotate3d( x, y, z, deg )
1
2
3
4
5
6
7
.foo { transform: rotate( 180deg ); } // default rotates z

.foo { transform: rotateZ( 180deg ); } // rotates Z

.foo { transform: rotateX( 180deg ); } // rotates X

.foo { transform: rotateY( 180deg ); } // rotates Y

Rotate3d

You gain improved-preformance from (some) webkit browsers by using rotate3d. See DeSandro’s article in the references below.

The syntax for rotate3d requires 4-arguments: X, Y, Z, and degrees to rotate.

1
.foo { transform: rotate3d( x, y, z, deg ) }

The axes’ rotations are product of the degree-value and the axis-value.

1
.foo { transform: rotate3d( x*deg, y*deg, z*deg, deg ); }

Rotating the X-axis

The following will all rotate 180 degrees around around the X-axis.

1
2
3
4
5
6
7
.foo { transform: rotateX( 180deg ); }

.foo { transform: rotate3d( 1, 0, 0, 180deg ); }

.foo { transform: rotate3d( .5, 0, 0, 360deg ); }

.foo { transform: rotate3d( 2, 0, 0, 90deg ); }

Rotating the Y-axis

The following will all rotate 180 degrees around around the Y-axis.

1
2
3
4
5
6
7
.foo { transform: rotateY( 180deg ); }

.foo { transform: rotate3d( 0, 1, 0, 180deg ); }

.foo { transform: rotate3d(  0, .5, 0, 360deg ); }

.foo { transform: rotate3d( 0, 2, 0, 90deg ); }

Rotating the Z-axis

The following will all rotate 180 degrees around around the Z-axis.

1
2
3
4
5
6
7
8
9
.foo { transform: rotate( 180deg ); }

.foo { transform: rotateZ( 180deg ); }

.foo { transform: rotate3d( 0, 0, 1, 180deg ); }

.foo { transform: rotate3d(  0, 0, .5, 360deg ); }

.foo { transform: rotate3d( 0, 0, 2, 90deg ); }

CSS Keyframe animations

Keyframes

This does not envoke an animation, but simply defines the keyframes/steps to animate between.

1
2
3
4
@keyframes pulse {
  0%    { transform: scale(1); }
  100%  { transform: scale(2); }
}

Animation

Apply a set of keyframes to an element and tell it how long the animation will last.

1
2
3
4
.foo {
  animation-name: pulse;
  animation-duration: 3s;
}
1
2
3
.foo {
  animation: pulse 3s; // shorthand syntax
}

Time to start building

Setup the #milkyway and body background-color

Set the body’s background-color using a variable from our color palette and darken() function.

The darken and lighten functions allow you to move through the HSL color-space. This can be quite useful when need to create some contrast and already have a color-palette.

_milkyway.scss
1
2
3
body {
  background: darken($blackblue, 10);
}

Create a #milkyway container

We’ll create a reusable @mixin to ‘fill’ the screen with the #milkyway. This will helpful later as we’ll want to perform a zoom animation on everything inside the #milkyway later.

_utils.scss
1
2
3
4
5
6
7
@mixin fill($position:fixed) {
  position: $position;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}
_milkyway.scss
1
2
3
#milkyway {
  @include fill;
}

Note that this didn’t change the appearance of the screen.

The Sun

Variable-driven size and position

Define the $sun-size.

The $sun-size will come in handy when we need to align elements with our sun and/or base other elements on the $sun-size. We’ll use this variable for height, width.

Make a reusable @mixin for a circle

To create a circle, we’ll need an object that is of equal height and width and has a 50% border-radius.

_utils.scss
1
2
3
4
5
@mixin circle($size) {
  height: $size;
  width: $size;
  @include border-radius( 50% );
}
_variables.scss
1
2
$sun-color: $lightorange;
$sun-size: 300px;
_sun.scss
1
2
3
4
#sun {
  @include circle($sun-size);
  @include radial-gradient(50% 50%, circle closest-side, $sun-color, rgba($sun-color, 0.0) );
}

Center the sun

We also need to create a mixin to center the sun in the window. Since we have defined the size of the sun, we can set its position, top, left, bottom and right and margin properties.

We’ll make 3 @mixins for this so that we can reuse pieces of it (that’ll come in later).

_utils.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@mixin vertical-middle {
  top: 0;
  bottom: 0;
  margin-top: auto;
  margin-bottom: auto;
}

@mixin horizontal-center {
  left: 0;
  right: 0;
  margin-left: auto;
  margin-right: auto;
}

@mixin center {
  @include horizontal-center;
  @include vertical-middle;
}
_sun.scss
1
2
3
4
5
6
#sun {
  position: fixed;
  @include center;
  @include circle($sun-size);
  @include radial-gradient(50% 50%, circle closest-side, $sun-color, rgba($sun-color, 0.0) );
}

Zoom-in animation

_keyframe_animations.scss
1
2
3
4
@include keyframes(zoomin) {
  from { @include transform( scale(0) ); }
  to { @include transform( scale(1) ); }
}
_variables.scss
1
2
$zoomin-animation-delay: 0;
$zoomin-animation-duration: 4s;
_milkyway.scss
1
2
3
4
5
#milkyway {
  @include fill;
  @include transform( scale(1) ); // sets inital scale and helps avoid flickering.
  @include animation( zoomin $zoomin-animation-duration $zoomin-animation-delay linear );
}

zoomin animation

Sun pulse animation

_keyframe_animations.scss
1
2
3
@include keyframes(pulse) {
  to { @include transform( scale(0.9) ); }
}
_variables.scss
1
$pulse-animation-duration: 1.8s;
_sun.scss
1
2
3
#sun {
  @include animation( pulse $pulse-animation-duration );
}

We want the sun to pulse, but we know that our use won’t be able to see the animation until after the #milkyway is finished zooming in. So, we’ll apply an animation-delay and make it equal to the $zoomin-animation-duration.

_variables.scss
1
2
$pulse-animation-duration: 1.8s;
$pulse-animation-delay: $zoomin-animation-duration;
_sun.scss
1
2
3
#sun {
  @include animation( pulse $pulse-animation-duration $pulse-animation-delay );
}

Once the pulse-animation begins, we want the animation to continue infinitely and to alternate the direction of the pulse.

_variables.scss
1
2
$pulse-animation-delay: $zoomin-animation-duration;
$pulse-animation-duration: 1.8s;
_sun.scss
1
2
3
#sun {
  @include animation( pulse $pulse-animation-duration $pulse-animation-delay alternate infinite linear );
}

Orbits & Planets

Orbit

Before we can make a planet, we need to make an orbit. Then we’ll put the planet inside the orbit. This will let us animate the orbit and the planet will naturally move with its orbit.

  • Base the $orbit-size on $sun-size.
  • Reuse the circle @mixin.
  • Reuse the center @mixin.
_variables.scss
1
2
$sun-size: 300px;
$orbit-size: $sun-size*1.2;
_orbit.scss
1
2
3
4
5
6
.orbit {
  position: fixed;
  @include center;
  border: 1px solid rgba(255,255,255, .2);
  @include circle($orbit-size);
}

Planet

  • Base the $planet-size on $sun-size.
  • Reuse the circle @mixin.
  • Position the planet absolutely inside its orbit.
  • Center the planet vertically using the verticle-middle @mixin.
  • Place the planet directly over the orbit’s border on the right side.
_variables.scss
1
2
$sun-size: 300px;
$planet-size: $sun-size/10;
_planet.scss
1
2
3
4
5
6
7
.planet {
  position: absolute;
  right: -$planet-size/2;
  @include vertical-middle;
  @include circle($planet-size);
  background: $darkblue;
}

Create multiple orbits/planets

Use ruby/haml to quickly create multiple orbits/planets.

index.haml
1
2
3
4
#orbits
  - 9.times do
    .orbit
      .planet

Define the number of orbits in your variables file.

_variables.scss
1
$orbit-count: 9;

Use a @for loop to enlarge the size of each orbit.

_orbit.scss
1
2
3
4
5
6
7
.orbit {
  @for $i from 1 through $orbit-count {
    &:nth-of-type( #{$i} ) {
      @include circle( $orbit-size * (1 + .2*$i) ) );
    }
  }
}

Animate the orbits.

Completing a 360 degree rotation

In order to create a complete 360-degree rotation, we have to guide the rotation through 3-steps. Usually, you would have to write this out, but here we’ll use a @for loop.

  • Define the value of your full $rotation.
  • Define the number of steps to complete your rotation ($rotation-keyframe-count).
  • Calculate the value of a $rotation-keyframe.
_keyframe_animations.scss
1
2
3
4
5
6
7
8
9
@include keyframes(orbit) {
  $rotation: 360;
  $rotation-keyframe-count: 3;
  $rotation-keyframe: $rotation/$rotation-keyframe-count;

  @for $i from 1 through $rotation-keyframe-count {
    #{$i * (100%/$rotation-keyframe-count)} { @include transform( rotate3d(0,0,1, #{$i*$rotation-keyframe}deg) ); }
  }
}
screen.css
1
2
3
4
5
6
7
8
9
/* css output of 3-keyframe */

@keyframes orbit {
  33.33333% { transform: rotate3d(0, 0, 1, 120deg); }

  66.66667% { transform: rotate3d(0, 0, 1, 240deg); }

  100%      { transform: rotate3d(0, 0, 1, 360deg); }
}

I don’t really like the fact that my percentages don’t come out clean. This may cause some hiccups in easing-calculations. Fortunately we used a loop, so we can easily change the $rotation-keyframe-count from 3 to 4.

_keyframe_animations.scss
1
2
3
4
5
6
7
8
9
10
11
@include keyframes(orbit) {
  $rotation: 360;
  $rotation-keyframe-count: 4;
  $rotation-keyframe-value: $rotation/$rotation-keyframe-count;

  @for $i from 1 through $rotation-keyframe-count {
    #{$i * (100%/$rotation-keyframe-count)} { @include transform( rotate3d(0,0,1, #{$i*$rotation-keyframe-value}deg) ); }
  }

  50% { opacity: .8; }
}
screen.css
1
2
3
4
5
6
7
8
9
10
11
12
13
/* css output from 4-step rotation */

@keyframes orbit {
  25%   { transform: rotate3d(0, 0, 1, 90deg); }

  50%   { transform: rotate3d(0, 0, 1, 180deg); }

  75%   { transform: rotate3d(0, 0, 1, 270deg); }

  100%  { transform: rotate3d(0, 0, 1, 360deg); }

  50% { opacity: .8; }
}

Ah. Much cleaner.

Apply orbit keyframes to .orbit

We don’t want the orbits to start until after the zoomin animation and a couple pulses of the sun. So we’ll set $orbit-animation-delay to reflect that.

_variables.scss
1
2
$orbit-animation-delay: $pulse-animation-delay + $pulse-animation-duration*2;
$orbit-animation-duration: 3s;
_orbit.scss
1
2
3
.orbit {
  @include animation( orbit $orbit-animation-duration $orbit-animation-delay infinite linear );
}

Orbiting animation 01

Stagger the orbit animation

The current animation works, but it doesn’t do much for me. I want to vary the speeds of the orbits. So we’ll add some logic to multiply the animation-duration in the .orbit @for loop.

_orbit.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.orbit {
  @include animation( orbit $orbit-animation-duration $orbit-animation-delay infinite linear );


  @function nth-orbit($x, $i) {
    @return $x * (1 + .2*$i);
  }


  @for $i from 1 through $orbit-count {
    &:nth-of-type( #{$i} ) {
      @include circle( nth-orbit($orbit-size, $i) );
      @include animation-duration( nth-orbit($orbit-animation-duration, $i) );
    }
  }
}

Orbiting animation 02

Oh. That’s better.

Zoom out

We want to zoom the scene out at the end. So let’s go ahead and time that up to be after 4-orbit-animations. We’re going to hook up the zoom-out to the #milkyway.

_variables.scss
1
2
$zoomout-animation-delay: $orbit-animation-delay + $orbit-animation-duration*4;
$zoomout-animation-duration: 10s;
_keyframe_animations.scss
1
2
3
4
@include keyframes(zoomout) {
  from { @include transform( scale(1) ); }
  to { @include transform( scale(0) ); }
}
_milkyway.scss
1
2
3
4
5
6
7
#milkyway {
  @include transform( scale(1) );
  @include animation(
    zoomin $zoomin-animation-duration $zoomin-animation-delay linear, // commas separate shorthand-animations
    zoomout $zoomout-animation-duration $zoomout-animation-delay linear
  );
}

Subtitles

Loop over content-arrays with haml

First let’s get the content onto the page. When it comes to content, I often find it nice to make a quick array and loop over it. This makes tweaking/refacting markup much easier/cleaner.

_helpers.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
def whatif
  "What if I told you.."
end

def subtitles
  [
    { copy1: whatif, copy2: "stylesheets don't have to be hard-coded.", },
    { copy1: whatif, copy2: "choreographing css animations is easy." },
    { copy1: whatif, copy2: "this video is only css and a few divs." },
    { copy1: "Want to see how flexible it is?", copy2: ""},
    { copy1: "Orbits.", copy2: "These aren't your grandma's stylesheets." },
  ]
end
_subtitles.haml
1
2
3
4
5
6
#subtitles
  - subtitles.each do |stitle|
    .subtitle
      %p
        %span= stitle[:copy1]
        %span= stitle[:copy2]

Use a mixin to drive the typography

Whenever creating typography styles, I like to put them into a typography partial. In a larger web-app, this helps us to see any/all type styles that we’re using.

_typography.scss
1
2
3
4
5
6
7
@mixin subtitle-type {
  color: white;
  font-family: arial;
  text-align: left;
  font-size: 44px;
  line-height: 50px;
}
_subtitles.scss
1
2
3
#subtitles {
  @include subtitle-type;
}

Subtitles screenshot

Display table and table-cell.

Position the subtitles on the page. Vertically centering an unknown text-string with display: table; and display: table-cell; is nice.

Make the subtitle container a table and set the width.

_subtitles.scss
1
2
3
4
5
6
7
8
9
10
11
#subtitles {
  display: table;
  width: 100%;    // tables aren't blocks. so you have to set the width.
  height: 100%;
  padding: 0 20px; // give it some breathing room on the edges.
}

.subtitle {
  display: table-cell;
  vertical-align: middle;
}

Subtitles screenshot

CSS :frist-of-type

We want to break each sentence into 2-pieces for readability. So let’s add some space between the first and second halves by setting the spans to display: block and adding margin-bottom: 1em; to the first half.

_subtitle.scss
1
2
3
4
5
6
.subtitle {
  span {
    display: block;
    &:first-of-type { margin-bottom: 1em; }
  }
}
_subtitles.haml
1
2
3
4
5
6
#subtitles
  - subtitles.each do |stitle|
    .subtitle
      %p
        %span= stitle[:copy1]
        %span= stitle[:copy2]

Subtitles screenshot

CSS :nth-of-type(n+foo)

We want to set the 2nd-half of the subtitles to text-align: right;. Well, the second-half, except for the last subtitle. We can select ‘all the but the frist nth-elements’ using css nth-of-type. For details on how this works, see Chris Coyier’s useful :nth-child recipes.

_variables.scss
1
2
$subtitle-count: 5;
$subtitle-half-count: round($subtitle-count/2);
_subtitles.scss
1
2
3
4
5
.subtitle {
  &:nth-of-type(n+#{$subtitle-half-count}) {
    text-align: right; // right align the 2nd half of the subtitles.
  }
}

Subtitles screenshot

Add the subtitle animation.

Setup subtitle keyframes

  • Create the subtitle keyframes. For now we’ll just fade them in and out with opacity.
  • Set the orbit’s initial opacity to 0. .orbit { opacity: 0; }
_keyframe_animations.scss
1
2
3
@include keyframes(fadein) {
  to { opacity: 1; }
}
_subtitles.scss
1
2
3
.subtitle {
  opacity: 0;
}

Subtitle animation timing

  • Calculate the sum of the total animation time.
  • Set the animation-duration for each subtitle.
  • Divide the total-time by the number of messages to get the animation-duration for each subtitle.
  • Delay each subtitle animation.

We want the subtitle animation to last the entire-duration of all other animations combined. So first we’ll calculate the $total-animation-time, then divide that total by the $subtitle-count to determine the animation-duration for each subtitle.

_variables.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$zoomin-animation-delay: 0;
$zoomin-animation-duration: 4s;

$pulse-animation-delay: $zoomin-animation-duration + $zoomin-animation-delay;
$pulse-animation-duration: 1.8s;

$orbit-animation-delay: $pulse-animation-delay + $pulse-animation-duration*2;
$orbit-animation-duration: 3s;

$zoomout-animation-delay: $orbit-animation-delay + $orbit-animation-duration*4;
$zoomout-animation-duration: 10s;

$total-animation-time: $zoomout-animation-delay + $zoomout-animation-duration;

$subtitle-count: 5;
$subtitle-animation-duration: $total-animation-time/$subtitle-count;

The animation-delay loop

Loop over the count of subtitles and delay each subtitle-animation by product of $subtitle-animation-duration*($i - 1). ($i - 1) is the count of previous subtitles.

_subtitles.scss
1
2
3
4
5
6
7
8
9
10
11
.subtitle {
  opacity: 0;
  @include animation-name(fadein);
  @include animation-duration($subtitle-animation-duration);

  @for $i from 1 through $subtitle-count {
    &:nth-of-type(#{$i}) {
      @include animation-delay( $subtitle-animation-duration * ($i - 1) );
    }
  }
}

Subtitles animation 01

Modify the last subtitle

  • Target the last subtitle.
  • Set the display to fixed to take the last-element out of the flow of its siblings.
  • Center it in the screen.
  • Apply a new table/table-cell relationship.

CSS :last-of-type

We want to center the last subtitle in the window and center align the text. We can target the last subtitle with .subtitle:last-of-type.

Change the display property to fixed. Note that as soon as we change the display property of the element, the rest of the table-cells fall into place.. beautiful.

Subtitles animation 02

We’ll create a display: table; within a display: table; to vertically center text in the last subtitle.

_subtitles.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.subtitle {
  &:last-of-type {
    position: fixed;
    display: table;
    @include horizontal-center;
    height: 100%;
    width: $subtitle-last-width;
    text-align: center;
    @include animation-duration($subtitle-animation-duration*2);
    p {
      display: table-cell;
      vertical-align: middle;
    }
  }
}

Subtitles screenshot

We’re done. Play time!

Now that our animations are all lined up. It’s time to have some fun. Change variables, mess with the orbit sizing function, add a planet-sizing function, etc.

References