Super Mario


I recently came across a rather lovely JS and CSS effect called the "Animated Background Gradient" by Mario Klingemann.

Through clever use of mathematics, Mario offers us a way to constantly update the background attribute of a given CSS selector, and this allows the beautiful gradual transition between several colours.

There was only one problem, as far as I was concerned: This pen / code sample uses JQuery, and I wanted to make use of this in a React and TypeScript project.

To me, this seemed like a great opportunity to play with both React and TypeScript, and hopefully learn a few new things along the way.

To follow along, all you need is a React and TypeScript project, which if you don't yet have, you can create in one line:

npx create-react-app lovely-background-tutorial --scripts-version=react-scripts-ts

This makes use of the excellent Create React App TypeScript variant.

After creation, you should have something like this:

➜  lovely-background-tutorial tree -L 1
.
├── images.d.ts
├── node_modules
├── package.json
├── public
├── README.md
├── src
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
├── tslint.json
└── yarn.lock

3 directories, 8 files

There's very little else you need to do except open this project in your favourite editor of choice and get yourself pumped up to write some hot sexy JS.

Setting The Scene

You can do this however you'd like. For simplicity I want to strip everything out, and focus only on this one component. Everything else is noise.

I'm going to:

  • delete the src/logo.svg file
  • delete all the contents of src/App.css, but keep the file
  • delete most of the contents of the App's render method:
// src/App.tsx

import * as React from 'react';
import './App.css';

class App extends React.Component {
  public render() {
    return (
      <div className="App">
      </div>
    );
  }
}

export default App;

Going completely bare bones, we could do all of the remaining portion of this tutorial inside the App component. However, I'm not going too. I would recommend, even when following a small React / TypeScript tutorial like this, to get into the habit of separating out into smaller, dedicated components.

In this case, I'm going to create two new files:

  • src/AnimatedBackground.tsx
  • src/AnimatedBackground.css

And use a very standard starting point:

// src/AnimatedBackground.tsx

import * as React from 'react';
import './AnimatedBackground.css';

class AnimatedBackground extends React.Component {
  public render() {
    return (
      <div className="AnimatedBackground">
        hello
      </div>
    );
  }
}

export default AnimatedBackground;

It might be better to move both the .tsx and .css files to a sub-directory of src, but that's really overkill for this tutorial. Separating the concepts into different files is good enough for me at this point, but feel free to structure your code however you like.

There's only one thing remaining, and that's to use the AnimatedBackground component inside our App component:

import * as React from 'react';
import AnimatedBackground from "./AnimatedBackground";
import './App.css';

class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <AnimatedBackground />
      </div>
    );
  }
}

export default App;

Now, if you yarn start your project, you should see a vastly white page, with "hello" in the top left corner. Success.

CSS Setup

We will tackle the CSS first, as it's the easiest of the two problems ahead.

The idea is that we will have a div with a given class / className of our choice. Onto that div we want to programmatically change / update the background CSS style every X milliseconds. This is how the illusion of a moving / transitioning / animated gradient is achieved.

In order for this to work, the div needs a height set in CSS. We're going with 800px as it is easy to see. Change accordingly for your needs.

/* src/AnimatedBackground.css */

.AnimatedBackground
{
    width: 100%;
    height: 800px;
    padding: 0;
    margin: 0;
}

Mind blowing, I'm sure.

That's the CSS taken care of. From here on out, it's all about the JavaScript to TypeScript process.

Typing Some Script

Mario has kindly taken care of all the hard maths for us. That's good, 'cus there's almost no way I'd be able to have figured this out myself.

Our job is not to worry about the maths.

Our job is to convert Mario's code from JavaScript to TypeScript inside a React environment.

Let's quickly review Mario's JS code in full:

var colors = new Array(
  [62,35,255],
  [60,255,60],
  [255,35,98],
  [45,175,230],
  [255,0,255],
  [255,128,0]);

var step = 0;
//color table indices for: 
// current color left
// next color left
// current color right
// next color right
var colorIndices = [0,1,2,3];

//transition speed
var gradientSpeed = 0.002;

function updateGradient()
{

  if ( $===undefined ) return;

var c0_0 = colors[colorIndices[0]];
var c0_1 = colors[colorIndices[1]];
var c1_0 = colors[colorIndices[2]];
var c1_1 = colors[colorIndices[3]];

var istep = 1 - step;
var r1 = Math.round(istep * c0_0[0] + step * c0_1[0]);
var g1 = Math.round(istep * c0_0[1] + step * c0_1[1]);
var b1 = Math.round(istep * c0_0[2] + step * c0_1[2]);
var color1 = "rgb("+r1+","+g1+","+b1+")";

var r2 = Math.round(istep * c1_0[0] + step * c1_1[0]);
var g2 = Math.round(istep * c1_0[1] + step * c1_1[1]);
var b2 = Math.round(istep * c1_0[2] + step * c1_1[2]);
var color2 = "rgb("+r2+","+g2+","+b2+")";

 $('#gradient').css({
   background: "-webkit-gradient(linear, left top, right top, from("+color1+"), to("+color2+"))"}).css({
    background: "-moz-linear-gradient(left, "+color1+" 0%, "+color2+" 100%)"});

  step += gradientSpeed;
  if ( step >= 1 )
  {
    step %= 1;
    colorIndices[0] = colorIndices[1];
    colorIndices[2] = colorIndices[3];

    //pick two new target color indices
    //do not pick the same as the current one
    colorIndices[1] = ( colorIndices[1] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;
    colorIndices[3] = ( colorIndices[3] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;

  }
}

setInterval(updateGradient,10);

Very quickly we can see a few variables that look like they might be good candidates for constants. These are:

  • colors
  • step
  • colorIndices
  • gradientSpeed

Now it may turn out that these are values that can change, but for the moment, we'll assume they are constants, and can there be const.

There are two other 'top level' aspects to review:

  • updateGradient

This the primary function, and core part of the script. This is where we will spend most of our time.

  • setInterval(updateGradient, 10)

This is the 'loop'. Every 10 milliseconds the updateGradient function will be called, and using the four variables / constants above, the new CSS background attribute is calculated, and set.

It therefore stands that one of those four constants is probably variable. If the output of the function changes given the same inputs, we'd be in a world of confusing pain.

One of our first tasks, therefore, will be to figure out which of these constants is actually a changing variable / starting point / set of defaults.

Transitioning To React

At this point we have a rather large function, updateGradient, which does a few interesting things:

  • Figures out the colour to transition too;
  • Mutates the #gradient element to set some background CSS;
  • Sets up the next colorIndices to correctly work out the transition maths

It looks like the colorIndices is something that will need to change. To me, this says colorIndices might be better being a typical React component prop. But we'll clarify this as we go.

I'm going to copy / paste the full function in to AnimatedBackground.tsx:

import * as React from 'react';
import './AnimatedBackground.css';

class AnimatedBackground extends React.Component {
    public render() {

        var colors = new Array(
            [62,35,255],
            [60,255,60],
            [255,35,98],
            [45,175,230],
            [255,0,255],
            [255,128,0]);

        var step = 0;
//color table indices for: 
// current color left
// next color left
// current color right
// next color right
        var colorIndices = [0,1,2,3];

//transition speed
        var gradientSpeed = 0.002;

        function updateGradient()
        {

            if ( $===undefined ) return;

            var c0_0 = colors[colorIndices[0]];
            var c0_1 = colors[colorIndices[1]];
            var c1_0 = colors[colorIndices[2]];
            var c1_1 = colors[colorIndices[3]];

            var istep = 1 - step;
            var r1 = Math.round(istep * c0_0[0] + step * c0_1[0]);
            var g1 = Math.round(istep * c0_0[1] + step * c0_1[1]);
            var b1 = Math.round(istep * c0_0[2] + step * c0_1[2]);
            var color1 = "rgb("+r1+","+g1+","+b1+")";

            var r2 = Math.round(istep * c1_0[0] + step * c1_1[0]);
            var g2 = Math.round(istep * c1_0[1] + step * c1_1[1]);
            var b2 = Math.round(istep * c1_0[2] + step * c1_1[2]);
            var color2 = "rgb("+r2+","+g2+","+b2+")";

            $('#gradient').css({
                background: "-webkit-gradient(linear, left top, right top, from("+color1+"), to("+color2+"))"}).css({
                background: "-moz-linear-gradient(left, "+color1+" 0%, "+color2+" 100%)"});

            step += gradientSpeed;
            if ( step >= 1 )
            {
                step %= 1;
                colorIndices[0] = colorIndices[1];
                colorIndices[2] = colorIndices[3];

                //pick two new target color indices
                //do not pick the same as the current one
                colorIndices[1] = ( colorIndices[1] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;
                colorIndices[3] = ( colorIndices[3] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;

            }
        }

        setInterval(updateGradient,10);

        return (
            <div className="AnimatedBackground">
                hello
            </div>
        );
    }
}

export default AnimatedBackground;

Oh mercy.

A good IDE like WebStorm is going to highlight some mistakes right away.

webstorm highlighting errors in your typescript

WebStorm highlights the $ lines:

if ( $===undefined ) return;

TypeScript has spotted a problem.

"TS2304: Cannot find name '$'"

Dollar, or JQuery, is not a thing in our project. We are using React.

This is only a guard statement though. So let's just remove it.

But this is a guard because without JQuery, this code won't work. After all, this is our task to fix.

We also need to address this line:

$('#gradient').css({
    background: "-webkit-gradient(linear, left top, right top, from("+color1+"), to("+color2+"))"}).css({
    background: "-moz-linear-gradient(left, "+color1+" 0%, "+color2+" 100%)"});

This is two chained calls to set some css, which will cause us different headaches as background is being set to two different things.

This second line is important, but not immediately so. Let's comment it out.

TS6133: 'color1' is declared but its value is never read

More problems. TypeScript now identifies that both color1 and color2 are defined but never used.

This is fine, because we just commented out the place where these two vars were used. Ok, comment out these two lines also.

As soon as we do this we see a bunch more of the proceeding var definitions start to show the TS6133 error, that the variable is defined but never used.

We may just have found our first extraction.

What if we extract all these first few bits and pieces, and then return color1, and color2?

This should satisfy TypeScript, and get us a bit further:

        function determineNextColourCombo(): [string,string]
        {
            var c0_0 = colors[colorIndices[0]];
            var c0_1 = colors[colorIndices[1]];
            var c1_0 = colors[colorIndices[2]];
            var c1_1 = colors[colorIndices[3]];

            var istep = 1 - step;
            var r1 = Math.round(istep * c0_0[0] + step * c0_1[0]);
            var g1 = Math.round(istep * c0_0[1] + step * c0_1[1]);
            var b1 = Math.round(istep * c0_0[2] + step * c0_1[2]);
            var color1 = "rgb("+r1+","+g1+","+b1+")";

            var r2 = Math.round(istep * c1_0[0] + step * c1_1[0]);
            var g2 = Math.round(istep * c1_0[1] + step * c1_1[1]);
            var b2 = Math.round(istep * c1_0[2] + step * c1_1[2]);
            var color2 = "rgb("+r2+","+g2+","+b2+")";

            return [color1, color2];
        }

        function updateGradient()
        {
            determineNextColourCombo();

            // $('#gradient').css({
            //     background: "-webkit-gradient(linear, left top, right top, from("+color1+"), to("+color2+"))"}).css({
            //     background: "-moz-linear-gradient(left, "+color1+" 0%, "+color2+" 100%)"});

            step += gradientSpeed;
            if ( step >= 1 )
            {
                step %= 1;
                colorIndices[0] = colorIndices[1];
                colorIndices[2] = colorIndices[3];

                //pick two new target color indices
                //do not pick the same as the current one
                colorIndices[1] = ( colorIndices[1] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;
                colorIndices[3] = ( colorIndices[3] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;

            }
        }

        setInterval(updateGradient,10);

There's still a bunch of things wrong here, but it's a positive first step.

We extracted the logic for determining the next colour combo - math provided for us by Mario - into a distinct function.

This function now makes uses of TypeScript's return type declaration: [string, string]. A tuple.

We know for a fact that this function is going to return an array with two strings in:

  • "rgb("+r1+","+g1+","+b1+")";
  • "rgb("+r2+","+g2+","+b2+")";

We aren't actually using that data anywhere, but that's fine for now.

At this stage the updateGradient function is heavily reliant on all the local state spread throughout the render method. We have access to all this state from the updateGradient function because of closure.

Notice how updateGradient has access to all the variables inside the render method it resides in. Because updateGradient is a nested function within render, this function gains access to anything in its outer scope.

Likewise, the determineNextColourCombo function gains access to everything inside updateGradient (where this function is invoked from), and the render method.

A Happy Compile

Ok, let's get things thing compiling.

yarn start

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(7,9): Forbidden 'var' keyword, use 'let' or 'const' instead

Nil desperandum, mon frere.

A judicious use of find / replace will quickly resolve this issue.

If I set everything from var to const, then recompile (which happens automatically on file change):

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(56,13): Cannot assign to 'step' because it is a constant or a read-only property.

Telling us that line 56 is at fault:

step += gradientSpeed;

Can't redefine a const. It's constant.

-const step = 0;
+let step = 0;

We then get some commenting linting issues:

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(16,3): comment must start with a space

Fixing those is a case of following the prompts.

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(22,19): variable name must be in lowerCamelCase, PascalCase or UPPER_CASE

This seems extremely tedious, I'm sure. But it actually benefits everyone in the long term. To fix this do a find / replace on e.g. c0_ with c0, and c1_ with c1. Watch the video for further instructions, if at all unsure.

Compiled successfully!

You can now view lovely-background-tutorial in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://192.168.0.10:3000/

Note that the development build is not optimized.
To create a production build, use yarn build.

At this point, we should be compiling, and back to displaying hello on screen.

So far, so good, though there is still plenty left to do.

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

Episodes

# Title Duration
1 Super Mario 12:09
2 How, and Why 17:06
3 Finishing Up 06:10