Tutorial: Animate the Opening Star Wars Crawl in a React App with GreenSock

Do you like Star Wars? Do you enjoy animating things in a web browser?

So do I!

I’ve wanted to dig into the GreenSock library for a while now, so of course, I my first instinct was to try recreating the Star Wars opening crawl with it.

GreenSock (aka GSAP) is a library that makes animating DOM elements with JavaScript exceptionally easy. At first, the library can seem a bit daunting, however, if you can wrap your head around a few core concepts you’ll be up and running in no time.

Here’s what we’re working towards:

Animating a Star Wars-inspired crawl is a cinch with GreenSock 🙂

TL;DR Check out a live demo of the crawl here and the complete repo here.

For this tutorial, we’ll use Create React App to quickly bootstrap a React application, so be sure to install that if you haven’t already.

Note: This tutorial was partly inspired by a post by Christopher Kade that I read on dev.to. Check out that original post for a version of the Star Wars crawl that uses CSS keyframe animation instead of GreenSock.

Getting Started

We’ll start by creating a new React app:

create-react-app star-wars-crawl-greensock

Then start your app:

cd star-wars-crawl-greensock && yarn start

We’ll keep it simple and work with the default files in the React app we just scaffolded, but we’ll swap the logo for a Star Wars-inspired one and add additional SVGs for a volume button at the end.

The original opening crawl used various styles of Franklin Gothic, but we’ll keep it simple and use Open Sans, which is a readily-available, close approximation, and Saira Extra Condensed (both available on Google fonts).

We’ll start by adding our fonts and a minimal reset and to index.css:

/* index.css */
/* Fonts */
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600,700|Saira+Extra+Condensed:500');
/* Minimal Reset */
html, body, div, h1, h2, p, section, audio {  
 margin: 0;  
 padding: 0;
 border: 0;
 font-size: 100%;
 font: inherit;
 vertical-align: baseline;
}
html {
  font-size: 24px;
}
body {
  align-items: center;
  background: black;
  color: rgb(229, 177, 58);
  display: flex;
  font-family: "Open Sans", sans-serif;
  font-weight: 400;
  height: 100vh;
  justify-content: center;
  line-height: 1.25;
  overflow: hidden;
}
div[id="root"] {
  width: 100%;
}

Now would be a good time to wipe out all of the default styles in App.css as well, because we’ll be adding our own CSS to this file as we go along.

Lastly, we’ll simplify the default JSX in App.js:

// App.js
import React, { Component } from "react";
import './App.css';
import logo from './logo.svg';
class App extends Component {
  render() {
    return (
      <div>
        <p>Hello, GreenSock World!</p>
      </div>
    );
  }
}

export default App;

Note that you want to replace the default logo.svg file in the src directory with our Star Wars-style logo available here.

At this point, our React app should look like this:

Before we dive into GSAP, it’s a good idea to have a game plan. There are three separate animations we need to create. We’ll need to animate:

  1. The “A long time ago in a galaxy far, far away.…” text first
  2. The logo entrance and exit
  3. And finally, the episode number/name and the main text crawl

Let’s tackle each of these in order…

Animate the Intro Text

Now for the fun part. Start by installing the GreenSock library in your app:

yarn add gsap

Next, we’ll add the intro text to our JSX, and describe how we want to animate it in componentDidMount:

// App.js
import React, { Component } from "react";
import { TweenLite } from "gsap";

import "./App.css";

class App extends Component {
  constructor(props) {
    super(props);
    this.intro = React.createRef(); // shiny new React 16.3 ref API!
  }
componentDidMount() {
    TweenLite.to(
      this.intro.current, 
      4.5, 
      { opacity: 1, delay: 1 }
    );
    TweenLite.to(
      this.intro.current, 
      1.5, 
      { opacity: 0, delay: 5.5 }
    );
  }

  render() {
    return (
      <div className="container">
        <section className="intro" ref={this.intro}>
          <p>
            A long time ago, in a galaxy far,<br /> far away....
          </p>
        </section>
      </div>
    );
  }
}

export default App;

We’ll also need more CSS to horizontally and vertically center the intro text, adjust its size, and change its colour to blue:

/* App.css */
.container {
  height: 100vh;
  position: relative;
  width: 100%;
}
.intro {
  left: 50%;
  opacity: 0;
  position: absolute;
  transform: translate(-50%, -50%);
  top: 50%;
  z-index: 200;
}
.intro p {
  color: rgb(75, 213, 238);
  font-size: 1.25rem;
}

In App.js we have our first encounter with TweenLite from GSAP. This animation tool is very flexible.

In a nutshell, it allows you to tween one or more properties of an object (or array of objects) over a specified time period. It’s .to() method accepts three arguments: the reference to the element you wish to animate (accessed via a ref), the length of the animation, and an object describing the properties of the animation.

We use TweenLite.to() twice so we can first animate the opacity of the intro text in for 4.5 seconds after a 1 second delay, then fade it out after a 5.5 second delay, because we need to wait for the first animation to complete.

Hmm, calculating delay times is a bit akward. Can we do better? Luckily, GSAP provides a better way in the form of TimelineLite.

Let’s refactor componentDidMount:

// ...
import { TimelineLite } from "gsap"; // import TimelineLite instead
class App extends Component {
  // ...
  componentDidMount() {
    const tl = new TimelineLite();
    
    tl
      .to(this.intro.current, 4.5, { opacity: 1, delay: 1 })
      .to(this.intro.current, 1.5, { opacity: 0 });
  }
  // ...
}

Much better! TimelineLite acts like a container for sequencing a series of tweens over time, starting one after another by default (with the ability to overlap them if necessary). We’ll chain the rest of our animation on to this as we go.

Animate the Logo

To give logo the appearance of appearing and gradually drawing back in space away from us, we’ll need to animate its scale and opacity.

Start by adding another section below the intro to contain the logo.svg:

<section className="logo" ref={this.logo}>
  <img src={logo} alt="Code Wars logo" />
</section>

Add the logo section’s class to the same rules that apply to the intro:

.intro, .logo {
  left: 50%;
  opacity: 0;
  position: absolute;
  transform: translate(-50%, -50%);
  top: 50%;
  z-index: 200;
}

And add a few additional styles to centre the logo.svg in its parent section:

.logo {
  align-items: center;
  display: flex;
  justify-content: center;
  width: 18rem;
}

Onto the JS—create the logo ref in the App constructor:

this.logo = React.createRef();

And chain on our additional animations in componentDidMount:

import { Power2, TimelineLite } from "gsap"; // import Power2!
class App extends Component {
  // ...
  componentDidMount() {
    const tl = new TimelineLite();
    tl
      .to(this.intro.current, 4.5, { opacity: 1, delay: 1 })
      .to(this.intro.current, 1.5, { opacity: 0 })
      .set(this.logo.current, { opacity: 1, scale: 2.75 })
      .to(this.logo.current, 8, { 
        scale: 0.05, 
        ease: Power2.easeOut
      })
      .to(this.logo.current, 1.5, { opacity: 0 }, "-=1.5");
  }
  // ...
}

As you can see, we chain the .set() method to flip the opacity of the logo to 1 and set the scale of the logo to 2.75 instantly after the intro animation completes.

We then use the .to() method to scale the logo down to 0.05 over 8 seconds. Simultaneously reducing the logo opacity over the same duration won’t look right because we don’t want to fade it out until it’s far off in the distance. Luckily, TimelineLite makes it easy work around this by chaining a separate .to() method to animate the opacity of the logo to 0, passing a fourth argument of '-=1.5' to it which will start that animation 1.5 seconds before the end of the previous animation.

We also have our first encounter with a GSAP easing curve in our logo animation properties. I experimented with a few different curves and Power2ultimately felt most like original animation to me, but feel free to experiment with others (and don’t forget to import from any curves you use from the gsap library).

Animate the Crawl

We’re almost the there! Add a final section containing the crawl text:

<section className="crawl">
  <div className="content" ref={this.content}>
    <h1 className="title">Episode 7</h1>
    <h2 className="subtitle">THE APP AWAKENS</h2>
    <p>The Development Team Lead has vanished. In her absence, the sinister FUNCTIONAL BUG has risen from the ashes of the CI Tool and will not rest until the last developer has been destroyed.
    </p>
    <p>With the support of the QA TEAM, the Software Developer leads a brave RESISTANCE. He is desperate to find his Lead and gain her help in restoring peace and justice to the repository.
    </p>
    <p>The Developer has sent his most daring editor theme on a secret mission to the production branch, where an old ally has discovered a clue to the Lead’s whereabouts....</p>
  </div>
</section>

Replicate the crawl text from your favourite episode, have some fun with this!

The CSS for the crawl is tricky, but luckily doesn’t involve any fancy camera rigging. Our implementation relies on a 3D transform with absolute positioning:

/* helps fade the text out as it recedes back in space */
.container:before {
  background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0) 100%);
  bottom: 0;
  content: " ";
  left: 0;
  right: 0;
  position: absolute;
  top: 0;
  z-index: 100;
}
.crawl {
  font-size: 300%;
  bottom: 0;
  height: 80rem;
  left: 50%;
  position: absolute;
  transform: translateX(-50%) perspective(300px) rotateX(28deg);
  transform-origin: 50% 100%;
  width: 90%;
}
.content {
  position: absolute;
  top: 100%;
}
.title {
  font-weight: 600;
  margin-bottom: 5rem;
  text-align: center;
}
.subtitle {
  font-family: "Saira Extra Condensed", sans-serif;
  font-size: 250%;
  font-weight: 500;
  line-height: 1;
  margin-bottom: 7rem;
  transform: scale(1, 1.5);
  text-align: center;
}
.content p {
  font-weight: 700;
  line-height: 1.33;
  margin-bottom: 4rem;
  text-align: justify;
}

Add another ref to the App constructor:

this.content = React.createRef();

And chain a final .to() to scroll the text back into space:

componentDidMount() {
  const tl = new TimelineLite();
  tl
    .to(this.intro.current, 4.5, { opacity: 1, delay: 1 })
    .to(this.intro.current, 1.5, { opacity: 0 })
    .set(this.logo.current, { opacity: 1, scale: 2.75 })
    .to(this.logo.current, 8, { 
      scale: 0.05, 
      ease: Power2.easeOut
    })
    .to(this.logo.current, 1.5, { opacity: 0 }, "-=1.5")
    .to(this.content.current, 200, { top: "-170%" }); // ADD THIS!
}

To scroll the text back, we simply need to animate its top property over approximately 200 seconds.

Finishing Touch (Adding Music)

Our animation looks pretty good at this point, but a Star Wars-inspired crawl wouldn’t be complete without a soundtrack to accompany it.

This would have a much easier to accomplish prior to a recent release of Chrome that blocked non-muted, autoplaying audio (which was partially rolled back, but will eventually be reimplemented).

Ultimately, to make our soundtrack Chrome-friendly we’ll need to start autoplaying the theme music at a point synchronized with our animation (it will start muted), and give the viewer the ability to unmute the audio if they wish. So we can also call this finishing touch a bonus round for learning how to deal with the new “autoplaying normal” in Chrome ?

We’ll start by importing two SVGs to App.js to use as a button to indicate to the viewer whether the audio is muted:

import volumeOff from "./volume_off.svg";
import volumeOn from "./volume_on.svg";

You can find the volume_on.svg and volume_off.svg files in the repo for this tutorial. Shout-out to Agarunov Oktay-Abraham for the Noun Project icons.

We’ll need to add a final ref to the constructor, and we’ll also need to managed some state with respect to whether the audio is muted (of course starting off as true):

constructor(props) {
  super(props);
  
  this.intro = React.createRef();
  this.logo = React.createRef();
  this.content = React.createRef();
  this.audio = React.createRef(); // LAST REF TO ADD!
  this.state = {
    muted: true
  };
}

We’ll also need to add some JSX for the audio element and a button to trigger the unmute action just after crawl section:

<audio ref={this.audio} muted>
  <source
    type="audio/mpeg"
src="https://ia801307.us.archive.org/28/items/JohnWilliamsStarWarsMainThemeFULL/John%20Williams%20-%20Star%20Wars%20Main%20Theme%20(FULL).mp3"
/>
</audio>
<button
  className="volume"
  type="button"
  onClick= {this.onVolumeClick}
>
  {this.state.muted ? (
    <img src={volumeOff} alt="Volume is off" />
  ) : (
    <img src={volumeOn} alt="Volume is on" />
  )}
</button>

You likely noticed a reference to an onVolumeClick method above, so we’ll need to add that to the App class as well:

onVolumeClick = () => {
  if (this.state.muted) {
    this.audio.current.muted = false;
  } else {
    this.audio.current.muted = true;
  }
  this.setState({ muted: !this.state.muted });
}

And some CSS to style the SVG icon button:

.volume {
  background: transparent;
  border: 0;
  bottom: 10px;
  cursor: pointer;
  left: 10px;
  position: absolute;
  z-index: 1000;
}
.volume img {
  height: 24px;
}

Lastly, we need to start playing the audio at a sychronized point in the GSAP animation, which is right as the intro animation completes and the logo animation starts. Luckily the object of animation properties that .to()accepts can contain an onComplete method where we can trigger the audio to begin playing.

tl
  .to(this.intro.current, 4.5, { opacity: 1, delay: 1 })
  .to(this.intro.current, 1.5, {
    opacity: 0,  
    onComplete: () => { 
      this.audio.current.play();
    } // Add this to autoplay the theme music
  })
  .set(this.logo.current, {
    opacity: 1,
    scale: 2.75,
    delay: 0.5 // A slight delay here syncs better with the audio
  })
  .to(this.logo.current, 8, { scale: 0.05, ease: Power2.easeOut })
  .to(this.logo.current, 1.5, { opacity: 0 }, "-=1.5")
  .to(this.content.current, 200, { top: "-170%" })

I found that adding a slight delay on the logo animation at this point helped sync up the opening note of the theme with the beginning of the animation, but you may want to play around with this a bit more.

Congrats on making it to the end!

At this point you hopefully have an idea of how easy it is to get started with GreenSock—and we’ve only scratched the surface of what it’s capable of. Have fun making your own Star Wars-inspired crawls, and feel free to post any questions in the comment section below.

 

Mandi Wise

Program Director for Web & App Development at RED Academy

Scroll al inicio

Si continuas utilizando este sitio aceptas el uso de cookies. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar