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
'srender
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 var
iables that look like they might be good candidates for const
ants. 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 somebackground
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 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.
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 var
s 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.