{"id":435,"date":"2018-06-29T02:39:16","date_gmt":"2018-06-29T02:39:16","guid":{"rendered":"https:\/\/blog.hassler.ec\/wp\/?p=435"},"modified":"2018-06-23T02:39:59","modified_gmt":"2018-06-23T02:39:59","slug":"tutorial-animate-the-opening-star-wars-crawl-in-a-react-app-with-greensock","status":"publish","type":"post","link":"https:\/\/blog.hassler.ec\/wp\/2018\/06\/29\/tutorial-animate-the-opening-star-wars-crawl-in-a-react-app-with-greensock\/","title":{"rendered":"Tutorial: Animate the Opening Star Wars Crawl in a React App with GreenSock"},"content":{"rendered":"<p id=\"a688\" class=\"graf graf--p graf-after--h3\">Do you like Star Wars? Do you enjoy animating things in a web browser?<\/p>\n<p id=\"ca5a\" class=\"graf graf--p graf-after--p\">So do I!<\/p>\n<p id=\"f5a8\" class=\"graf graf--p graf-after--p\">I\u2019ve wanted to dig into\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/greensock.com\/\" target=\"_blank\" rel=\"noopener nofollow\" data-href=\"https:\/\/greensock.com\/\">the GreenSock library<\/a>\u00a0for a while now, so of course, I my first instinct was to try recreating the Star Wars opening crawl with it.<\/p>\n<p id=\"80d5\" class=\"graf graf--p graf-after--p\">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\u2019ll be up and running in no time.<\/p>\n<p id=\"1008\" class=\"graf graf--p graf-after--p\">Here\u2019s what we\u2019re working towards:<\/p>\n<figure id=\"3913\" class=\"graf graf--figure graf-after--p\">\n<div class=\"aspectRatioPlaceholder is-locked\">\n<div class=\"aspectRatioPlaceholder-fill\"><\/div>\n<div class=\"progressiveMedia js-progressiveMedia graf-image is-canvasLoaded is-imageLoaded\" data-image-id=\"1*EyO1lMKy6Z1C15EZ7KFwjg.png\" data-width=\"2784\" data-height=\"1674\" data-action=\"zoom\" data-action-value=\"1*EyO1lMKy6Z1C15EZ7KFwjg.png\" data-scroll=\"native\"><canvas class=\"progressiveMedia-canvas js-progressiveMedia-canvas\" width=\"75\" height=\"45\"><\/canvas><img decoding=\"async\" class=\"progressiveMedia-image js-progressiveMedia-image\" src=\"https:\/\/cdn-images-1.medium.com\/max\/800\/1*EyO1lMKy6Z1C15EZ7KFwjg.png\" data-src=\"https:\/\/cdn-images-1.medium.com\/max\/800\/1*EyO1lMKy6Z1C15EZ7KFwjg.png\" \/><\/div>\n<\/div><figcaption class=\"imageCaption\">Animating a Star Wars-inspired crawl is a cinch with GreenSock\u00a0\ud83d\ude42<\/figcaption><\/figure>\n<p id=\"613a\" class=\"graf graf--p graf-after--figure\"><strong class=\"markup--strong markup--p-strong\">TL;DR<\/strong>\u00a0Check out a\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/redacademy.github.io\/star-wars-crawl-greensock\/\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/redacademy.github.io\/star-wars-crawl-greensock\/\">live demo of the crawl here<\/a>\u00a0and the\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\">complete repo here<\/a>.<\/p>\n<p id=\"5712\" class=\"graf graf--p graf-after--p\">For this tutorial, we\u2019ll use\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/github.com\/facebook\/create-react-app\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/github.com\/facebook\/create-react-app\">Create React App<\/a>\u00a0to quickly bootstrap a React application, so be sure to install that if you haven\u2019t already.<\/p>\n<p id=\"f894\" class=\"graf graf--p graf-after--p\"><em class=\"markup--em markup--p-em\">Note: This tutorial was partly inspired by\u00a0<\/em><a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/dev.to\/christopherkade\/developing-the-star-wars-opening-crawl-in-htmlcss-2j9e\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/dev.to\/christopherkade\/developing-the-star-wars-opening-crawl-in-htmlcss-2j9e\"><em class=\"markup--em markup--p-em\">a post by Christopher Kade<\/em><\/a><em class=\"markup--em markup--p-em\">\u00a0that 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.<\/em><\/p>\n<h3 id=\"56b8\" class=\"graf graf--h3 graf-after--p\">Getting Started<\/h3>\n<p id=\"c3d9\" class=\"graf graf--p graf-after--h3\">We\u2019ll start by creating a new React app:<\/p>\n<pre id=\"0bde\" class=\"graf graf--pre graf-after--p\">create-react-app star-wars-crawl-greensock<\/pre>\n<p id=\"65e9\" class=\"graf graf--p graf-after--pre\">Then start your app:<\/p>\n<pre id=\"1916\" class=\"graf graf--pre graf-after--p\">cd star-wars-crawl-greensock &amp;&amp; yarn start<\/pre>\n<p id=\"2739\" class=\"graf graf--p graf-after--pre\">We\u2019ll keep it simple and work with the default files in the React app we just scaffolded, but we\u2019ll swap the logo for a Star Wars-inspired one and add additional SVGs for a volume button at the end.<\/p>\n<p id=\"cda0\" class=\"graf graf--p graf-after--p\">The original opening crawl used\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"http:\/\/www.theforce.net\/fanfilms\/postproduction\/crawl\/opening.asp\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"http:\/\/www.theforce.net\/fanfilms\/postproduction\/crawl\/opening.asp\">various styles of Franklin Gothic<\/a>, but we\u2019ll keep it simple and use Open Sans, which is a readily-available, close approximation, and Saira Extra Condensed (both available on Google fonts).<\/p>\n<p id=\"83e7\" class=\"graf graf--p graf-after--p\">We\u2019ll start by adding our fonts and a minimal reset and to\u00a0<code class=\"markup--code markup--p-code\">index.css<\/code>:<\/p>\n<pre id=\"ca69\" class=\"graf graf--pre graf-after--p\">\/* index.css *\/<\/pre>\n<pre id=\"53d4\" class=\"graf graf--pre graf-after--pre\">\/* Fonts *\/<\/pre>\n<pre id=\"d91b\" class=\"graf graf--pre graf-after--pre\">@import url('https:\/\/fonts.googleapis.com\/css?family=Open+Sans:400,600,700|Saira+Extra+Condensed:500');<\/pre>\n<pre id=\"c01f\" class=\"graf graf--pre graf-after--pre\">\/* Minimal Reset *\/<\/pre>\n<pre id=\"b37c\" class=\"graf graf--pre graf-after--pre\">html, body, div, h1, h2, p, section, audio {  \r\n margin: 0;  \r\n padding: 0;\r\n border: 0;\r\n font-size: 100%;\r\n font: inherit;\r\n vertical-align: baseline;\r\n}<\/pre>\n<pre id=\"2f8a\" class=\"graf graf--pre graf-after--pre\">html {\r\n  font-size: 24px;\r\n}<\/pre>\n<pre id=\"9a4e\" class=\"graf graf--pre graf-after--pre\">body {\r\n  align-items: center;\r\n  background: black;\r\n  color: rgb(229, 177, 58);\r\n  display: flex;\r\n  font-family: \"Open Sans\", sans-serif;\r\n  font-weight: 400;\r\n  height: 100vh;\r\n  justify-content: center;\r\n  line-height: 1.25;\r\n  overflow: hidden;\r\n}<\/pre>\n<pre id=\"b1a8\" class=\"graf graf--pre graf-after--pre\">div[id=\"root\"] {\r\n  width: 100%;\r\n}<\/pre>\n<p id=\"7d6a\" class=\"graf graf--p graf-after--pre\">Now would be a good time to wipe out all of the default styles in\u00a0<code class=\"markup--code markup--p-code\">App.css<\/code>\u00a0as well, because we\u2019ll be adding our own CSS to this file as we go along.<\/p>\n<p id=\"a8e1\" class=\"graf graf--p graf-after--p\">Lastly, we\u2019ll simplify the default JSX in\u00a0<code class=\"markup--code markup--p-code\">App.js<\/code>:<\/p>\n<pre id=\"d86e\" class=\"graf graf--pre graf-after--p\">\/\/ App.js<\/pre>\n<pre id=\"b56f\" class=\"graf graf--pre graf-after--pre\">import React, { Component } from \"react\";\r\nimport '.\/App.css';<\/pre>\n<pre id=\"850e\" class=\"graf graf--pre graf-after--pre\">import logo from '.\/logo.svg';<\/pre>\n<pre id=\"a0b1\" class=\"graf graf--pre graf-after--pre\">class App extends Component {\r\n  render() {\r\n    return (\r\n      &lt;div&gt;\r\n        &lt;p&gt;Hello, GreenSock World!&lt;\/p&gt;\r\n      &lt;\/div&gt;\r\n    );\r\n  }\r\n}\r\n\r\nexport default App;<\/pre>\n<p id=\"75f9\" class=\"graf graf--p graf-after--pre\">Note that you want to replace the default\u00a0<code class=\"markup--code markup--p-code\">logo.svg<\/code>\u00a0file in the\u00a0<code class=\"markup--code markup--p-code\">src<\/code>\u00a0directory with\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\/blob\/master\/src\/logo.svg\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\/blob\/master\/src\/logo.svg\">our Star Wars-style logo available here<\/a>.<\/p>\n<p id=\"ebf0\" class=\"graf graf--p graf-after--p\">At this point, our React app should look like this:<\/p>\n<figure id=\"0099\" class=\"graf graf--figure graf-after--p\">\n<div class=\"aspectRatioPlaceholder is-locked\">\n<div class=\"aspectRatioPlaceholder-fill\"><\/div>\n<div class=\"progressiveMedia js-progressiveMedia graf-image is-canvasLoaded is-imageLoaded\" data-image-id=\"1*43VYHAt8rdotXixlSOKO0Q.png\" data-width=\"2728\" data-height=\"1620\" data-action=\"zoom\" data-action-value=\"1*43VYHAt8rdotXixlSOKO0Q.png\" data-scroll=\"native\"><canvas class=\"progressiveMedia-canvas js-progressiveMedia-canvas\" width=\"75\" height=\"42\"><\/canvas><img decoding=\"async\" class=\"progressiveMedia-image js-progressiveMedia-image\" src=\"https:\/\/cdn-images-1.medium.com\/max\/800\/1*43VYHAt8rdotXixlSOKO0Q.png\" data-src=\"https:\/\/cdn-images-1.medium.com\/max\/800\/1*43VYHAt8rdotXixlSOKO0Q.png\" \/><\/div>\n<\/div>\n<\/figure>\n<p id=\"f579\" class=\"graf graf--p graf-after--figure\">Before we dive into GSAP, it\u2019s a good idea to have a game plan. There are three separate animations we need to create. We\u2019ll need to animate:<\/p>\n<ol class=\"postList\">\n<li id=\"34f2\" class=\"graf graf--li graf-after--p\">The \u201cA long time ago in a galaxy far, far away.\u2026\u201d text first<\/li>\n<li id=\"694d\" class=\"graf graf--li graf-after--li\">The logo entrance and exit<\/li>\n<li id=\"8120\" class=\"graf graf--li graf-after--li\">And finally, the episode number\/name and the main text crawl<\/li>\n<\/ol>\n<p id=\"f448\" class=\"graf graf--p graf-after--li\">Let\u2019s tackle each of these in order\u2026<\/p>\n<h3 id=\"d790\" class=\"graf graf--h3 graf-after--p\">Animate the Intro\u00a0Text<\/h3>\n<p id=\"f8e6\" class=\"graf graf--p graf-after--h3\">Now for the fun part. Start by installing the GreenSock library in your app:<\/p>\n<pre id=\"e809\" class=\"graf graf--pre graf-after--p\">yarn add gsap<\/pre>\n<p id=\"55e5\" class=\"graf graf--p graf-after--pre\">Next, we\u2019ll add the intro text to our JSX, and describe how we want to animate it in\u00a0<code class=\"markup--code markup--p-code\">componentDidMount<\/code>:<\/p>\n<pre id=\"c9ab\" class=\"graf graf--pre graf-after--p\">\/\/ App.js<\/pre>\n<pre id=\"9269\" class=\"graf graf--pre graf-after--pre\">import React, { Component } from \"react\";\r\nimport { TweenLite } from \"gsap\";\r\n\r\nimport \".\/App.css\";\r\n\r\nclass App extends Component {\r\n  constructor(props) {\r\n    super(props);\r\n    this.intro = React.createRef(); \/\/ shiny new React 16.3 ref API!\r\n  }<\/pre>\n<pre id=\"94bf\" class=\"graf graf--pre graf-after--pre\">componentDidMount() {\r\n    TweenLite.to(\r\n      this.intro.current, \r\n      4.5, \r\n      { opacity: 1, delay: 1 }\r\n    );\r\n    TweenLite.to(\r\n      this.intro.current, \r\n      1.5, \r\n      { opacity: 0, delay: 5.5 }\r\n    );\r\n  }\r\n\r\n  render() {\r\n    return (\r\n      &lt;div className=\"container\"&gt;\r\n        &lt;section className=\"intro\" ref={this.intro}&gt;\r\n          &lt;p&gt;\r\n            A long time ago, in a galaxy far,&lt;br \/&gt; far away....\r\n          &lt;\/p&gt;\r\n        &lt;\/section&gt;\r\n      &lt;\/div&gt;\r\n    );\r\n  }\r\n}\r\n\r\nexport default App;<\/pre>\n<p id=\"a22e\" class=\"graf graf--p graf-after--pre\">We\u2019ll also need more CSS to horizontally and vertically center the intro text, adjust its size, and change its colour to blue:<\/p>\n<pre id=\"1f1f\" class=\"graf graf--pre graf-after--p\">\/* App.css *\/<\/pre>\n<pre id=\"694f\" class=\"graf graf--pre graf-after--pre\">.container {\r\n  height: 100vh;\r\n  position: relative;\r\n  width: 100%;\r\n}<\/pre>\n<pre id=\"cdfb\" class=\"graf graf--pre graf-after--pre\">.intro {\r\n  left: 50%;\r\n  opacity: 0;\r\n  position: absolute;\r\n  transform: translate(-50%, -50%);\r\n  top: 50%;\r\n  z-index: 200;\r\n}<\/pre>\n<pre id=\"b7c3\" class=\"graf graf--pre graf-after--pre\">.intro p {\r\n  color: rgb(75, 213, 238);\r\n  font-size: 1.25rem;\r\n}<\/pre>\n<p id=\"bf49\" class=\"graf graf--p graf-after--pre\">In\u00a0<code class=\"markup--code markup--p-code\">App.js<\/code>\u00a0we have our first encounter with\u00a0<code class=\"markup--code markup--p-code\">TweenLite<\/code>\u00a0from GSAP. This animation tool is very flexible.<\/p>\n<p id=\"6d4b\" class=\"graf graf--p graf-after--p\">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\u2019s\u00a0<code class=\"markup--code markup--p-code\">.to()<\/code>\u00a0method 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.<\/p>\n<p id=\"8eae\" class=\"graf graf--p graf-after--p\">We use\u00a0<code class=\"markup--code markup--p-code\">TweenLite.to()<\/code>\u00a0twice 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.<\/p>\n<p id=\"f6b1\" class=\"graf graf--p graf-after--p\">Hmm, calculating delay times is a bit akward. Can we do better? Luckily, GSAP provides a better way in the form of\u00a0<code class=\"markup--code markup--p-code\">TimelineLite<\/code>.<\/p>\n<p id=\"248a\" class=\"graf graf--p graf-after--p\">Let\u2019s refactor\u00a0<code class=\"markup--code markup--p-code\">componentDidMount<\/code>:<\/p>\n<pre id=\"96b6\" class=\"graf graf--pre graf-after--p\">\/\/ ...<\/pre>\n<pre id=\"27bc\" class=\"graf graf--pre graf-after--pre\">import { TimelineLite } from \"gsap\"; \/\/ import TimelineLite instead<\/pre>\n<pre id=\"44cd\" class=\"graf graf--pre graf-after--pre\"><em class=\"markup--em markup--pre-em\">class<\/em> App <em class=\"markup--em markup--pre-em\">extends<\/em> Component {<\/pre>\n<pre id=\"9467\" class=\"graf graf--pre graf-after--pre\">  \/\/ ...<\/pre>\n<pre id=\"8c38\" class=\"graf graf--pre graf-after--pre\">  componentDidMount() {\r\n    const tl = new TimelineLite();\r\n    \r\n    tl\r\n      .to(this.intro.current, 4.5, { <em class=\"markup--em markup--pre-em\">opacity<\/em>: 1, <em class=\"markup--em markup--pre-em\">delay<\/em>: 1 })\r\n      .to(this.intro.current, 1.5, { <em class=\"markup--em markup--pre-em\">opacity<\/em>: 0 });\r\n  }<\/pre>\n<pre id=\"4f36\" class=\"graf graf--pre graf-after--pre\">  \/\/ ...<\/pre>\n<pre id=\"a32f\" class=\"graf graf--pre graf-after--pre\">}<\/pre>\n<p id=\"f4e1\" class=\"graf graf--p graf-after--pre\">Much better!\u00a0<code class=\"markup--code markup--p-code\"><a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/greensock.com\/timelinelite\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/greensock.com\/timelinelite\">TimelineLite<\/a><\/code>\u00a0acts 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\u2019ll chain the rest of our animation on to this as we go.<\/p>\n<h3 id=\"d4f1\" class=\"graf graf--h3 graf-after--p\">Animate the\u00a0Logo<\/h3>\n<p id=\"473f\" class=\"graf graf--p graf-after--h3\">To give logo the appearance of appearing and gradually drawing back in space away from us, we\u2019ll need to animate its scale and opacity.<\/p>\n<p id=\"9cf5\" class=\"graf graf--p graf-after--p\">Start by adding another section below the intro to contain the\u00a0<code class=\"markup--code markup--p-code\">logo.svg<\/code>:<\/p>\n<pre id=\"1a80\" class=\"graf graf--pre graf-after--p\">&lt;section className=\"logo\" ref={this.logo}&gt;\r\n  &lt;img src={logo} alt=\"Code Wars logo\" \/&gt;\r\n&lt;\/section&gt;<\/pre>\n<p id=\"bb07\" class=\"graf graf--p graf-after--pre\">Add the logo section\u2019s class to the same rules that apply to the intro:<\/p>\n<pre id=\"f17f\" class=\"graf graf--pre graf-after--p\">.intro, .logo {\r\n  left: 50%;\r\n  opacity: 0;\r\n  position: absolute;\r\n  transform: translate(-50%, -50%);\r\n  top: 50%;\r\n  z-index: 200;\r\n}<\/pre>\n<p id=\"4353\" class=\"graf graf--p graf-after--pre\">And add a few additional styles to centre the\u00a0<code class=\"markup--code markup--p-code\">logo.svg<\/code>\u00a0in its parent section:<\/p>\n<pre id=\"4cf9\" class=\"graf graf--pre graf-after--p\">.logo {\r\n  align-items: center;\r\n  display: flex;\r\n  justify-content: center;\r\n  width: 18rem;\r\n}<\/pre>\n<p id=\"b140\" class=\"graf graf--p graf-after--pre\">Onto the JS\u2014create the logo ref in the\u00a0<code class=\"markup--code markup--p-code\">App<\/code>\u00a0constructor:<\/p>\n<pre id=\"ee04\" class=\"graf graf--pre graf-after--p\">this.logo = React.createRef();<\/pre>\n<p id=\"eec1\" class=\"graf graf--p graf-after--pre\">And chain on our additional animations in\u00a0<code class=\"markup--code markup--p-code\">componentDidMount<\/code>:<\/p>\n<pre id=\"bb80\" class=\"graf graf--pre graf-after--p\">import { Power2, TimelineLite } from \"gsap\"; \/\/ import Power2!<\/pre>\n<pre id=\"c905\" class=\"graf graf--pre graf-after--pre\">class App extends Component {<\/pre>\n<pre id=\"eccf\" class=\"graf graf--pre graf-after--pre\">  \/\/ ...<\/pre>\n<pre id=\"92b4\" class=\"graf graf--pre graf-after--pre\">  componentDidMount() {\r\n    const tl = new TimelineLite();<\/pre>\n<pre id=\"1fb4\" class=\"graf graf--pre graf-after--pre\">    tl\r\n      .to(this.intro.current, 4.5, { opacity: 1, delay: 1 })\r\n      .to(this.intro.current, 1.5, { opacity: 0 })\r\n      .set(this.logo.current, { opacity: 1, scale: 2.75 })\r\n      .to(this.logo.current, 8, { \r\n        scale: 0.05, \r\n        ease: Power2.easeOut\r\n      })\r\n      .to(this.logo.current, 1.5, { opacity: 0 }, \"-=1.5\");\r\n  }<\/pre>\n<pre id=\"2d01\" class=\"graf graf--pre graf-after--pre\">  \/\/ ...<\/pre>\n<pre id=\"0bb4\" class=\"graf graf--pre graf-after--pre\">}<\/pre>\n<p id=\"f697\" class=\"graf graf--p graf-after--pre\">As you can see, we chain the\u00a0<code class=\"markup--code markup--p-code\">.set()<\/code>\u00a0method to flip the opacity of the logo to\u00a0<code class=\"markup--code markup--p-code\">1<\/code>\u00a0and set the scale of the logo to\u00a0<code class=\"markup--code markup--p-code\">2.75<\/code>\u00a0instantly after the intro animation completes.<\/p>\n<p id=\"2d1b\" class=\"graf graf--p graf-after--p\">We then use the\u00a0<code class=\"markup--code markup--p-code\">.to()<\/code>\u00a0method to scale the logo down to\u00a0<code class=\"markup--code markup--p-code\">0.05<\/code>\u00a0over 8 seconds. Simultaneously reducing the logo opacity over the same duration won\u2019t look right because we don\u2019t want to fade it out until it\u2019s far off in the distance. Luckily,\u00a0<code class=\"markup--code markup--p-code\">TimelineLite<\/code>\u00a0makes it easy work around this by chaining a separate\u00a0<code class=\"markup--code markup--p-code\">.to()<\/code>\u00a0method to animate the opacity of the logo to\u00a0<code class=\"markup--code markup--p-code\">0<\/code>, passing a fourth argument of\u00a0<code class=\"markup--code markup--p-code\">'-=1.5'<\/code>\u00a0to it which will start that animation 1.5 seconds before the end of the previous animation.<\/p>\n<p id=\"00ea\" class=\"graf graf--p graf-after--p\">We also have our first encounter with a\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/greensock.com\/ease-visualizer\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/greensock.com\/ease-visualizer\">GSAP easing curve<\/a>\u00a0in our logo animation properties. I experimented with a few different curves and\u00a0<code class=\"markup--code markup--p-code\">Power2<\/code>ultimately felt most like original animation to me, but feel free to experiment with others (and don\u2019t forget to import from any curves you use from the\u00a0<code class=\"markup--code markup--p-code\">gsap<\/code>\u00a0library).<\/p>\n<h3 id=\"6841\" class=\"graf graf--h3 graf-after--p\">Animate the\u00a0Crawl<\/h3>\n<p id=\"50b5\" class=\"graf graf--p graf-after--h3\">We\u2019re almost the there! Add a final\u00a0<code class=\"markup--code markup--p-code\">section<\/code>\u00a0containing the crawl text:<\/p>\n<pre id=\"54bb\" class=\"graf graf--pre graf-after--p\">&lt;section className=\"crawl\"&gt;\r\n  &lt;div className=\"content\" ref={this.content}&gt;\r\n    &lt;h1 className=\"title\"&gt;Episode 7&lt;\/h1&gt;\r\n    &lt;h2 className=\"subtitle\"&gt;THE APP AWAKENS&lt;\/h2&gt;\r\n    &lt;p&gt;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.\r\n    &lt;\/p&gt;\r\n    &lt;p&gt;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.\r\n    &lt;\/p&gt;\r\n    &lt;p&gt;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\u2019s whereabouts....&lt;\/p&gt;\r\n  &lt;\/div&gt;\r\n&lt;\/section&gt;<\/pre>\n<p id=\"b4aa\" class=\"graf graf--p graf-after--pre\">Replicate the crawl text from your favourite episode, have some fun with this!<\/p>\n<p id=\"8403\" class=\"graf graf--p graf-after--p\">The CSS for the crawl is tricky, but luckily\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/gizmodo.com\/5542745\/so-thats-how-they-filmed-the-star-wars-opening-crawl\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/gizmodo.com\/5542745\/so-thats-how-they-filmed-the-star-wars-opening-crawl\">doesn\u2019t involve any fancy camera rigging<\/a>. Our implementation relies on a 3D transform with absolute positioning:<\/p>\n<pre id=\"34a0\" class=\"graf graf--pre graf-after--p\">\/* helps fade the text out as it recedes back in space *\/\r\n.container:before {\r\n  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%);\r\n  bottom: 0;\r\n  content: \" \";\r\n  left: 0;\r\n  right: 0;\r\n  position: absolute;\r\n  top: 0;\r\n  z-index: 100;\r\n}<\/pre>\n<pre id=\"0bb6\" class=\"graf graf--pre graf-after--pre\">.crawl {\r\n  font-size: 300%;\r\n  bottom: 0;\r\n  height: 80rem;\r\n  left: 50%;\r\n  position: absolute;\r\n  transform: translateX(-50%) perspective(300px) rotateX(28deg);\r\n  transform-origin: 50% 100%;\r\n  width: 90%;\r\n}<\/pre>\n<pre id=\"0d71\" class=\"graf graf--pre graf-after--pre\">.content {\r\n  position: absolute;\r\n  top: 100%;\r\n}<\/pre>\n<pre id=\"ed4e\" class=\"graf graf--pre graf-after--pre\">.title {\r\n  font-weight: 600;\r\n  margin-bottom: 5rem;\r\n  text-align: center;\r\n}<\/pre>\n<pre id=\"5f5a\" class=\"graf graf--pre graf-after--pre\">.subtitle {\r\n  font-family: \"Saira Extra Condensed\", sans-serif;\r\n  font-size: 250%;\r\n  font-weight: 500;\r\n  line-height: 1;\r\n  margin-bottom: 7rem;\r\n  transform: scale(1, 1.5);\r\n  text-align: center;\r\n}<\/pre>\n<pre id=\"b01b\" class=\"graf graf--pre graf-after--pre\">.content p {\r\n  font-weight: 700;\r\n  line-height: 1.33;\r\n  margin-bottom: 4rem;\r\n  text-align: justify;\r\n}<\/pre>\n<p id=\"cdf4\" class=\"graf graf--p graf-after--pre\">Add another ref to the\u00a0<code class=\"markup--code markup--p-code\">App<\/code>\u00a0constructor:<\/p>\n<pre id=\"74ab\" class=\"graf graf--pre graf-after--p\">this.content = React.createRef();<\/pre>\n<p id=\"d991\" class=\"graf graf--p graf-after--pre\">And chain a final\u00a0<code class=\"markup--code markup--p-code\">.to()<\/code>\u00a0to scroll the text back into space:<\/p>\n<pre id=\"aad3\" class=\"graf graf--pre graf-after--p\">componentDidMount() {\r\n  const tl = new TimelineLite();<\/pre>\n<pre id=\"6346\" class=\"graf graf--pre graf-after--pre\">  tl\r\n    .to(this.intro.current, 4.5, { opacity: 1, delay: 1 })\r\n    .to(this.intro.current, 1.5, { opacity: 0 })\r\n    .set(this.logo.current, { opacity: 1, scale: 2.75 })\r\n    .to(this.logo.current, 8, { \r\n      scale: 0.05, \r\n      ease: Power2.easeOut\r\n    })\r\n    .to(this.logo.current, 1.5, { opacity: 0 }, \"-=1.5\")\r\n    .to(this.content.current, 200, { top: \"-170%\" }); \/\/ ADD THIS!\r\n}<\/pre>\n<p id=\"5ab8\" class=\"graf graf--p graf-after--pre\">To scroll the text back, we simply need to animate its\u00a0<code class=\"markup--code markup--p-code\">top<\/code>\u00a0property over approximately 200 seconds.<\/p>\n<h3 id=\"caef\" class=\"graf graf--h3 graf-after--p\">Finishing Touch (Adding\u00a0Music)<\/h3>\n<p id=\"83c8\" class=\"graf graf--p graf-after--h3\">Our animation looks pretty good at this point, but a Star Wars-inspired crawl wouldn\u2019t be complete without a soundtrack to accompany it.<\/p>\n<p id=\"049a\" class=\"graf graf--p graf-after--p\">This would have a much easier to accomplish prior to\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/developers.google.com\/web\/updates\/2017\/09\/autoplay-policy-changes\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/developers.google.com\/web\/updates\/2017\/09\/autoplay-policy-changes\">a recent release of Chrome that blocked non-muted, autoplaying audio<\/a>\u00a0(which was partially rolled back, but will eventually be reimplemented).<\/p>\n<p id=\"cd81\" class=\"graf graf--p graf-after--p\">Ultimately, to make our soundtrack Chrome-friendly we\u2019ll 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 \u201cautoplaying normal\u201d in Chrome ?<\/p>\n<p id=\"1763\" class=\"graf graf--p graf-after--p\">We\u2019ll start by importing two SVGs to\u00a0<code class=\"markup--code markup--p-code\">App.js<\/code>\u00a0to use as a button to indicate to the viewer whether the audio is muted:<\/p>\n<pre id=\"8ab2\" class=\"graf graf--pre graf-after--p\">import volumeOff from \".\/volume_off.svg\";\r\nimport volumeOn from \".\/volume_on.svg\";<\/pre>\n<p id=\"5a21\" class=\"graf graf--p graf-after--pre\">You can find the\u00a0<code class=\"markup--code markup--p-code\"><a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\/blob\/master\/src\/volumne_on.svg\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\/blob\/master\/src\/volumne_on.svg\">volume_on.svg<\/a><\/code>\u00a0and\u00a0<code class=\"markup--code markup--p-code\"><a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\/blob\/master\/src\/volumne_off.svg\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/github.com\/redacademy\/star-wars-crawl-greensock\/blob\/master\/src\/volumne_off.svg\">volume_off.svg<\/a><\/code>\u00a0files in the repo for this tutorial. Shout-out to\u00a0<a class=\"markup--anchor markup--p-anchor\" href=\"https:\/\/thenounproject.com\/agarunov\/\" target=\"_blank\" rel=\"nofollow noopener\" data-href=\"https:\/\/thenounproject.com\/agarunov\/\">Agarunov Oktay-Abraham<\/a>\u00a0for the Noun Project icons.<\/p>\n<p id=\"dfba\" class=\"graf graf--p graf-after--p\">We\u2019ll need to add a final ref to the constructor, and we\u2019ll also need to managed some state with respect to whether the audio is muted (of course starting off as\u00a0<code class=\"markup--code markup--p-code\">true<\/code>):<\/p>\n<pre id=\"9504\" class=\"graf graf--pre graf-after--p\">constructor(props) {\r\n  super(props);\r\n  \r\n  this.intro = React.createRef();\r\n  this.logo = React.createRef();\r\n  this.content = React.createRef();\r\n  this.audio = React.createRef(); \/\/ LAST REF TO ADD!<\/pre>\n<pre id=\"14f1\" class=\"graf graf--pre graf-after--pre\">  this.state = {\r\n    muted: true\r\n  };\r\n}<\/pre>\n<p id=\"fffe\" class=\"graf graf--p graf-after--pre\">We\u2019ll also need to add some JSX for the\u00a0<code class=\"markup--code markup--p-code\">audio<\/code>\u00a0element and a\u00a0<code class=\"markup--code markup--p-code\">button<\/code>\u00a0to trigger the unmute action just after crawl\u00a0<code class=\"markup--code markup--p-code\">section<\/code>:<\/p>\n<pre id=\"6c9b\" class=\"graf graf--pre graf-after--p\">&lt;audio ref={this.audio} muted&gt;\r\n  &lt;source\r\n    type=\"audio\/mpeg\"\r\nsrc=\"https:\/\/ia801307.us.archive.org\/28\/items\/JohnWilliamsStarWarsMainThemeFULL\/John%20Williams%20-%20Star%20Wars%20Main%20Theme%20(FULL).mp3\"\r\n\/&gt;\r\n&lt;\/audio&gt;\r\n&lt;button\r\n  className=\"volume\"\r\n  type=\"button\"\r\n  onClick= {this.onVolumeClick}\r\n&gt;\r\n  {this.state.muted ? (\r\n    &lt;img src={volumeOff} alt=\"Volume is off\" \/&gt;\r\n  ) : (\r\n    &lt;img src={volumeOn} alt=\"Volume is on\" \/&gt;\r\n  )}\r\n&lt;\/button&gt;<\/pre>\n<p id=\"6aa9\" class=\"graf graf--p graf-after--pre\">You likely noticed a reference to an\u00a0<code class=\"markup--code markup--p-code\">onVolumeClick<\/code>\u00a0method above, so we\u2019ll need to add that to the\u00a0<code class=\"markup--code markup--p-code\">App<\/code>\u00a0class as well:<\/p>\n<pre id=\"579f\" class=\"graf graf--pre graf-after--p\">onVolumeClick = () =&gt; {\r\n  if (this.state.muted) {\r\n    this.audio.current.muted = false;\r\n  } else {\r\n    this.audio.current.muted = true;\r\n  }<\/pre>\n<pre id=\"0208\" class=\"graf graf--pre graf-after--pre\">  this.setState({ muted: !this.state.muted });\r\n}<\/pre>\n<p id=\"6326\" class=\"graf graf--p graf-after--pre\">And some CSS to style the SVG icon button:<\/p>\n<pre id=\"ab68\" class=\"graf graf--pre graf-after--p\">.volume {\r\n  background: transparent;\r\n  border: 0;\r\n  bottom: 10px;\r\n  cursor: pointer;\r\n  left: 10px;\r\n  position: absolute;\r\n  z-index: 1000;\r\n}<\/pre>\n<pre id=\"b478\" class=\"graf graf--pre graf-after--pre\">.volume img {\r\n  height: 24px;\r\n}<\/pre>\n<p id=\"834d\" class=\"graf graf--p graf-after--pre\">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\u00a0<code class=\"markup--code markup--p-code\">.to()<\/code>accepts can contain an\u00a0<code class=\"markup--code markup--p-code\">onComplete<\/code>\u00a0method where we can trigger the audio to begin playing.<\/p>\n<pre id=\"5dd3\" class=\"graf graf--pre graf-after--p\">tl\r\n  .to(this.intro.current, 4.5, { opacity: 1, delay: 1 })\r\n  .to(this.intro.current, 1.5, {\r\n    opacity: 0,  \r\n    onComplete: () =&gt; { \r\n      this.audio.current.play();\r\n    } \/\/ Add this to autoplay the theme music\r\n  })\r\n  .set(this.logo.current, {\r\n    opacity: 1,\r\n    scale: 2.75,\r\n    delay: 0.5 \/\/ A slight delay here syncs better with the audio\r\n  })\r\n  .to(this.logo.current, 8, { scale: 0.05, ease: Power2.easeOut })\r\n  .to(this.logo.current, 1.5, { opacity: 0 }, \"-=1.5\")\r\n  .to(this.content.current, 200, { top: \"-170%\" })<\/pre>\n<p id=\"1bac\" class=\"graf graf--p graf-after--pre\">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.<\/p>\n<p id=\"6a74\" class=\"graf graf--p graf-after--p\">Congrats on making it to the end!<\/p>\n<p id=\"2fc0\" class=\"graf graf--p graf-after--p graf--trailing\">At this point you hopefully have an idea of how easy it is to get started with GreenSock\u2014and we\u2019ve only scratched the surface of what it\u2019s capable of. Have fun making your own Star Wars-inspired crawls, and feel free to post any questions in the comment section below.<\/p>\n<p>&nbsp;<\/p>\n<h3 class=\"ui-h3 u-fontSize18 u-lineHeightTighter u-marginBottom4\"><a class=\"link link--primary u-accentColor--hoverTextNormal\" dir=\"auto\" title=\"Go to the profile of Mandi Wise\" href=\"https:\/\/medium.com\/@mandiwise\" rel=\"author cc:attributionUrl\" aria-label=\"Go to the profile of Mandi Wise\" data-user-id=\"3b02efdbd3e3\">Mandi Wise<\/a><\/h3>\n<p class=\"ui-body u-fontSize14 u-lineHeightBaseSans u-textColorDark u-marginBottom4\">Program Director for Web &amp; App Development at RED Academy<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Do you like Star Wars? Do you enjoy animating things in a web browser? So do I! I\u2019ve wanted to [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":192,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[49,12,50,48,44,29,69],"tags":[],"class_list":["post-435","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-back-end","category-bloghassler-ec","category-css","category-front-end","category-javascript","category-programacion","category-react"],"_links":{"self":[{"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/posts\/435","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/comments?post=435"}],"version-history":[{"count":1,"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/posts\/435\/revisions"}],"predecessor-version":[{"id":436,"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/posts\/435\/revisions\/436"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/media\/192"}],"wp:attachment":[{"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/media?parent=435"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/categories?post=435"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.hassler.ec\/wp\/wp-json\/wp\/v2\/tags?post=435"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}