Making A Star Wars Intro Generator With React and p5.js

Making A Star Wars Intro Generator With React and p5.js

Introduction

I started using the p5.js library recently because an artist friend of mine wanted some help with it (He makes some really cool projects, you can check out his website here).

The library is fun to work with. You can do a lot of creative stuff with it. I generally work with react, so I decided to make a project which combines react and p5.js. 

What did I make? A Star Wars intro crawling text generator (because I love Star Wars).

This is the tutorial for the same. 

If you follow the tutorial, you’ll be able to make the same generator on your own.


What Are We Building?

We are building a crawling text generator, Star Wars style. This is the video of the final web app. This is how our project would look at the end of the tutorial:

Cool? Liked the video of what we are about to make? Let’s get started now.


Who Is This Tutorial For?

This tutorial is for creative coders who understand the basics of React. Make sure you understand the following react concepts:

  • React states and hooks.
  • Form building using Formik and Yup.

We’ll also be styling our app using the styled-components library, so it would be better if you go through it’s documentation once.


What Is p5.js?

p5.js is a JavaScript library used for creative coding. p5.js helps beginner coders to visualize their code. It’s also great for artists and creative coders who want to use a computer to create some kicka*s art.

You can learn more about p5 here.

I learnt everything I know about p5 from Daniel Shiffman’s YouTube channel.

If you’re interested in learning more about p5 (and creative coding in general), you should check out his channel: The Coding Train.

Before continuing with this tutorial, I would suggest you make a few simple creative projects with plain p5.js first.


Integrating p5.js With React

I’m assuming that you know how to set up a basic react app. If not, you can read the official getting started documentation here.

First things first, let’s install our boilerplate react app with the create-react-app command.

npx create-react-app star-wars-intro

The second thing we need to do is to integrate ps.js with react.

There are multiple npm libraries available which help us to use p5 with react. I didn’t use these libraries because I don’t like to rely on third party libraries. But if you want you can try them out, here are the link to their github repositories:

react-p5 || react-p5-wrapper

For this project, we’ll download the official p5.js library from npm and we’ll integrate it into our react app ourselves.


Installing The p5 Package

Once you have the basic react app ready, we can install the p5 npm package.

Use the following command to install p5 in your app:

npm install p5

That’s it. This command installs p5 in your react app and now we can use it to build creative stuff.


How Does p5.js Work?

Before we integrate p5.js with react, we should have a basic understanding of how p5 works. 

p5.js has a number of predefined functions which we can use to draw anything we want. The most basic (and necessary) functions are the setup() and draw() functions. 

setup() is called once when the program runs and draw() is called continuously without stopping.  

A combination of these functions and variables through which we draw on the browser is called a sketch.

Here’s a sample p5 sketch:

let radius;

function setup(){
	createCanvas(windowWidth, windowHeight);
	background(0);
	radius = 0;
}
function draw(){
	fill(255,0,0);
	ellipse(width/2,height/2,radius,radius);
	radius++;
}

When we run this sketch, we get a red circle at the center of the window whose radius increases after every frame. The circle keeps increasing in size until the whole window becomes red.

If you don’t understand how the p5 sketch is drawing an ellipse, I suggest you learn a bit more about p5.js first and make a few projects using plain p5.

Making A Star Wars Intro Generator With React and p5.js 1
Output Of The Sketch

How Do We Add A Sketch In React?

The sample sketch above is written in plain javascript and we can’t just write the same code in react. We have to add it using a few manipulations.

Adding a sketch in react is fairly simple, we need to do three basic things:

  1. Create a function which encapsulates our sketch
  2. Create a p5 object using the p5 package that we installed earlier
  3. Pass our sketch to the p5 object as an argument

And that’s it

This is how our sketch function would look in react:

const Sketch = p5 => {
   let radius; 
   p5.setup = () => {
     p5.createCanvas(p5.windowWidth, p5.windowHeight);
     p5.background(0);
     radius = 0;
   };
 
   p5.draw = () => {
   	p5.ellipse(p5.width/2,p5.height/2,radius,radius);
      radius++;  
   };
 };

We create a function called Sketch. Sketch has a parameter which provides us access to all the p5 functions and variables.

To run our sketch, we create a new p5 object using the p5 npm package and we pass our sketch as an argument.

our_drawing = new p5(Sketch)

And this is how p5 works in react. Simple, right? I think we’re now ready to actually start writing the code for the star wars intro generator.


Running Our Sample Sketch

Let’s come back to our react app now. First, we’ll try to run our sample sketch to make sure that the library has been installed properly.

Create a folder named components in the source folder of the app. In that folder add a new file and name it StarWarsSketch.js

Add the following code to this file:

import React, { useEffect } from "react";
import * as p5 from "p5";
 
const StarWarsSketch = () => {
 const Sketch = p5 => {
   Let radius;	
   p5.setup = () => {
     p5.createCanvas(p5.windowWidth, p5.windowHeight);
     p5.background(0);
     Radius = 0;
   };
 
   p5.draw = () => {
   	p5.ellipse(p5.width/2,p5.height/2,radius,radius);
      radius++;  
   };
 };
 
 useEffect(() => {
  new p5(Sketch);
 // eslint-disable-next-line react-hooks/exhaustive-deps
 }, []);
 
 return (
  	<></>
 );
};
 
export default StarWarsSketch;

Now in the App.js file, import this StarWarsSketch component and add it to the return statement.

This is how your App.js file should look as of now:

import React from 'react';
import StarWarsSketch from './components/StarWarsSketch'
const App = () => {
 return (
   <StarWarsSketch/>
 );
}
 
export default App;

Now run your app. You should be seeing a circle at the center of the screen which keeps increasing in size. Okay, so let me explain how this chunk of code works.

What we are basically doing is, as soon as the StarWarsSketch component is mounted, we create a new p5 object. This object needs an argument – for that we’ve defined our Sketch function.

Whatever we had defined in the Sketch function, will be drawn on our screen.

Note: We don’t need to return anything from the StarWarsSketch component because as soon as we create a p5 object, it takes over the DOM.


Building The Star Wars Text Crawler

Our sample sketch is working now. Hooray! Now we have to change our sketch function so that it starts to look like the Star Wars intro generator that we set out to make.

Here’s the list of things we need to accomplish in order to get the Star Wars effect:

  1. We need a title that starts big (in size) and keeps getting smaller until it vanishes.
  2. We need a summary that floats through the space.
  3. We need to reduce the opacity of the summary text to zero when the text has crawled enough.
  4. We need the right fonts for both the title and the summary.

To achieve the above listed things, we’ll do the following:

Make The Canvas 3D

To give the summary text a floating effect, we have to go 3D! We can’t generate the floating in space effect with 2D graphics.

Consider our canvas to be a 3D cube. To give a floating effect to our text, we need to rotate it along the X axis by 45 degrees.

There’s no easy way to explain this concept through writing. I’ll make a video soon to explain this concept.

But for the time being, you can take a cube and try rotating it along different axes to understand how rotating along the X axis might give us the desired result.

To make a canvas 3D in p5.js, we need to pass a third argument in the createCanvas function.

p5.createCanvas(p5.windowWidth, p5.windowHeight, p5.WEBGL);

WEBGL is a constant in p5 which enables 3D render by introducing the third dimension: Z.

Note: In a 2D render, the point (0,0) denotes the top left corner of the window. But in a 3D render, the point (0,0,0) denotes the center of the window.

Declare The Variables

Next, we’ll declare all the variables we need.

   let textFont;
   let titleFont;
   let textX;
   let textY;
   let titleSize;
   let textAlpha;
   let titleText = 'Star\nWars';
   let summaryText  = 'it is a period of civil war. Rebel spaceships, striking from a hidden base, have won their first victory against the evil Galactic Empire. During the battle, Rebel spies managed to steal secret plans to the Empire’s ultimate weapon, the DEATH STAR, an armored space station with enough power to destroy an entire planet. Pursued by the Empire’s sinister agents, Princess Leia races home aboard her starship, custodian of the stolen plans that can save her people and restore freedom to the galaxy...';

textFont and titleFont specify the different fonts for our title and summary text.

textX and textY specify the coordinates of our summary text. We’ll keep changing the Y coordinate throughout the program so that the summary keeps moving upwards.

titleSize specifies the size of our title. We’ll start with a big number and then keep reducing it till it goes to 0.

textAlpha is the opacity of the summary text. We’ll start with 100% opacity and reduce it once the text is about to end.

titleText and summaryText are strings that we want to show in our custom crawler.

Load The Fonts

We need to download the Star Wars fonts before we can import them in our program. I downloaded these fonts from dafont.com.

You can download these fonts from the below link or you can use some other fonts.

Font download link.

Download the .ttf or .otf files and place them in the public folder of the react app. 

We’ll use the preload() function provided by p5 to load our fonts. The preload() function is called even before setup().

p5.preload = () => {
     textFont = p5.loadFont("./Starjedi.ttf");
     titleFont = p5.loadFont("./Starjhol.ttf");
   };

With this, our fonts will be loaded in our p5 sketch and we can use them when writing our text.

Modify The Setup And Draw Functions

With all the explaining I’ve done already, I think it would be easy to understand the below code. Go through it once, I’ll still explain the confusing bits.

p5.setup = () => {
     p5.createCanvas(p5.windowWidth, p5.windowHeight, p5.WEBGL);
     p5.background(0);
     textX = -p5.width / 2 + p5.width * 0.15;
     textY = p5.height / 2;
     titleSize = p5.width / 3;
     textAlpha = 255;
   };
 
   p5.draw = () => {
     p5.background(0);
     p5.fill(255, 232, 31);
     p5.textSize(titleSize);
     p5.textAlign(p5.CENTER);
     p5.textFont(titleFont);
     p5.textLeading(titleSize);
     p5.text(titleText, 0, 0);
     if (titleSize < p5.width / 10) {
       p5.fill(255, 232, 31, textAlpha);
       p5.textSize(p5.width / 30);
       p5.textAlign(p5.CENTER);
       p5.textFont(textFont);
       p5.rotateX(p5.PI / 4);
       p5.textLeading(p5.width / 30);
       p5.text(
         summaryText,
         textX,
         textY,
         p5.width * 0.7,
         p5.height * 10
       );
       textY -= 1.5;
       if (textY < -p5.width / 1.7) {
         textAlpha -= 0.5;
       }
     }
     if (titleSize > 0) {
       titleSize--;
     }
   };

In the setup function, we define our 3D canvas which covers the whole window.

We also define the starting coordinates of the summaryText. Instead of defining the center point of the text, we will instead define a text box. We will define the starting top left coordinate and bottom right coordinates of the box.

Our summary text will be wrapped inside this text box now.

Then we come to the draw function. We write our title in the middle of the screen with a very big font size, then we keep reducing the font size until it becomes 0.

We also used a function called textLeading() while writing our text. textLeading is a function in p5 through which we can change the spacing between two lines.

When the title becomes significantly small, we start crawling the summary text. We rotate our text by 45 degrees along the X axis to give it a “floating in space effect”.

We start the summary at the bottom of the window and then we keep reducing the Y coordinate to move it upwards.

Making A Star Wars Intro Generator With React and p5.js 2
Graphic Explaining The Dimensions and Coordinates Of The Summary Text Box

And lastly, we set the alpha value (opacity) of the summary text at 255 (maximum). When the summary text reaches a good enough height, we start to reduce this alpha value.

This is it! Our sketch for the Star Wars crawling intro is ready. All that is left now is to create a form so we can take the input for the title and summary from the user.

Screenshot of the Star Wars crawling intro generator
Screenshot Of The Sketch

Adding A Form To Take User Input

Let’s create a new form component now. We’ll use two npm packages for creating this form: Formik and Yup.

Form building in React becomes really simple with Formik. In Jared Palmer’s words (Formik’s Author), Formik helps us to “Build forms in React, without the tears”

Yup is a validation library which integrates seamlessly with Formik. It helps us to validate our form inputs.

I won’t go deep into how Formik and Yup work. You can follow this tutorial here to get a better understanding of how to build forms.

I’ll quickly explain the steps we need to take for our project.

Creating The Form Component

Install the two libraries using the following command

npm install formik yup

Create a new file in the components folder, name it DataInputForm.js

Add the following code to it:

import React from "react";
import { Formik } from "formik";
import * as Yup from "yup";
 
const DataInputForm = props => {
 const starWarsSchema = Yup.object().shape({
   titleText: Yup.string()
     .label("Title")
     .required("Empty title not allowed")
     .max(21, "Title can be max 20 characters"),
   summaryText: Yup.string()
     .label("Summary")
     .required()
     .min(20, "Summary must be at least 20 characters")
     .max(501, "Summary can have 500 characters at maximum")
 });
 return (
   <Formik
     initialValues={{ titleText: "", summaryText: "" }}
     onSubmit={values => {
       console.log(`form submitted, values: ${JSON.stringify(values)}`);
     }}
     validationSchema={starWarsSchema}
   >
     {props => {
       const {
         values,
         errors,
         isSubmitting,
         handleChange,
         handleBlur,
         handleSubmit
       } = props;
       return (
         <form onSubmit={handleSubmit}>
           <div>
             <div>
               <label style={{ display: "block" }}>Title</label>
               <input
                 id="titleText"
                 placeholder="Enter your desired title"
                 type="text"
                 onBlur={handleBlur}
                 value={values.titleText}
                 onChange={handleChange}
               />
               <div>{errors.titleText}</div>
             </div>
             <div>
               <label style={{ display: "block" }}>Summary</label>
               <textarea
                 id="summaryText"
                 placeholder="Enter your summary"
                 type="text"
                 onBlur={handleBlur}
                 value={values.summaryText}
                 onChange={handleChange}
                 rows={10}
               />
               <div>{errors.summaryText}</div>
             </div>
             <div>
               <button type="submit" disabled={isSubmitting}>
                 Start Crawling Intro
               </button>
             </div>
           </div>
         </form>
       );
     }}
   </Formik>
 );
};
 
export default DataInputForm;

What are we doing in this file?

We have created a form with two inputs: TitleText and SummaryText. These two input fields have been validated using Yup. We check the minimum and maximum length of the text fields.

The form is only submitted when all fields have been successfully validated. Once the form is submitted, we log the values of the form in our browser console.

Again, I won’t go into the details of how Formik works. If you want a better understanding of Formik then read the article I linked above.

Modifying the App.js file

We’ll modify our App.js file to display our form.

import React, {useState} from "react";
import StarWarsSketch from "./components/StarWarsSketch";
import DataInputForm from "./components/DataInputForm";
const App = () => {
 const [isStarted, setIsStarted] = useState(false);
 return (
   <div>
     {!isStarted && (
       <>
         <div>
           <h2>Star Wars Crawling Intro</h2>
         </div>
         <DataInputForm/>
       </>
     )}
     {isStarted && (
       <StarWarsSketch/>
     )}
   </div>
 );
};
 
export default App;

We’ve created an isStarted state for our sketch. The sketch isn’t displayed until isStarted is true.

Initially, isStarted is false. So, we’ll only see our form.

On successful form submission, we’ll change the value of isStarted to true (we’ll do this next).

As of now, we have a form but nothing happens on a successful form submission.

star wars crawling intro form
This Is How The Form Looks As Of Now

Starting The Sketch And Passing The Data From The Form To The Sketch

Modifying The App.js File

In our App.js file, we’ll declare two state variables: swTitle and swSummary. And we’ll also create a function onFormSubmitted().

We’ll pass the onFormSubmitted function to our form component as a prop. Through this function, once the form is submitted, we’ll set the state variables.

Once the state variables are set, we then change the isStarted state to true. Then the sketch component will be mounted (since isStarted is changed to true). We’ll pass the title and summary to the sketch component through props.

To do all of this, add the following code to the export function of the App.js file.

const [swTitle, setSwTitle] = useState("");
 const [swSummary, setSwSummary] = useState("");
 useEffect(() => {
   if (swTitle.length > 0 && swSummary.length > 0) {
     setIsStarted(true);
   }
 }, [swSummary, swTitle]);
 const onFormSubmitted = values => {
   let temp = "";
   temp = values.titleText.split(" ");
   temp = temp.join("\n");
   temp = temp.toLowerCase();
   setSwTitle(temp);
   temp = values.summaryText.split("\n");
   temp = temp.join("\n\n\n");
   temp = temp.toLowerCase();
   setSwSummary(temp);
 };

We also need to modify the return statement. This is how our return should look like now:

return (
   <div>
     {!isStarted && (
       <>
         <div>
           <h2>Star Wars Crawling Intro</h2>
         </div>
         <DataInputForm onFormSubmitted={onFormSubmitted}/>
       </>
     )}
     {isStarted && <StarWarsSketch titleText={swTitle} summaryText={swSummary}/>}
   </div>
 );

We are passing the onFormSubmit function to the form component and the state variables (for the text and summary to the sketch component).

Modifying The DataInputForm Component

We just need to make one change in our form component. When the submit button is pressed, the onFormSubmitted function should be called.

Change the onSubmit (in Formik) to this:

onSubmit={props.onFormSubmitted}

Don’t forget to add props as a parameter in the main DataInputForm function (the function which is being exported).

Modifying The StarWarsSketch Component

We just need to change two lines of code in this component.

In the draw function, where we were drawing our title text, change it to this:

p5.text(props.titleText, 0, 0);

And change the summary text to this:

p5.text(
         props.summaryText,
         textX,
         textY,
         p5.width * 0.7,
         p5.height * 10
       );

Instead of using predefined title and summary, we are now using the title and summary from our props (which were passed from the form).


Adding Sound To The Sketch

To add sound, we’ll use the react-sound package.

Install the package using the following npm command:

npm install react-sound

In the StarWarsSketch.js file, add the following import statement.

import Sound from "react-sound";

And modify the return statement to add the sound.

return (
   <div>
     <Sound
       url="star-wars-intro-music.mp3"
       playStatus={Sound.status.PLAYING}
       playFromPosition={0}
     />
   </div>
 );

Add an mp3 file to your public folder and name it star-wars-intro-music.mp3

You can download the mp3 file that I used from here.

The background music will start playing as soon as our sketch starts.

The functionality of the app is ready now, the only thing that remains is to style the app.


Styling The App

The sketch component has already been styled according to our needs using p5. We just need to style our homepage (App.js + DataInputForm.js files).

We are only going to use the styled-components package for making our web app beautiful.

Let’s install the styled-components package first.

npm install styled-components

Now, create a new file in the components folder and name it StyledComponents.js

Add the following code to the file:

import styled from "styled-components";
 
export const StyledInputText = styled.input`
 border: 1px solid #ccc;
 background-color: #f4f4f4;
 padding: 15px 0px 15px 5px;
 border-radius: 20px;
 font-size: 18px;
 width: 100%;
 margin-top: 10px;
`;
 
export const StyledInputTextarea = styled.textarea`
 border: 1px solid #ccc;
 background-color: #f4f4f4;
 padding: 15px 0px 15px 5px;
 border-radius: 20px;
 font-size: 18px;
 width: 100%;
 margin-top: 10px;
`;
 
export const MainContainer = styled.div`
 width: 100%;
`;
 
export const FormContainer = styled.div`
 width: 45%;
 margin: 50px auto;
 @media (max-width: 768px) {
   width: 80%;
 }
`;
 
export const FormInputContainer = styled.div`
 width: 100%;
 margin: 20px auto;
`;
 
export const FormSubmitContainer = styled.div`
 width: 100%;
 text-align: center;
`;
 
export const StyledLabel = styled.label`
 color: white;
 font-size: 21px;
 font-weight: 700;
`;
 
export const StyledErrorContainer = styled.div`
 font-size: 16px;
 color: red;
 font-weight: 400;
 margin: 10px;
`;
 
export const StyledSubmitButton = styled.button`
 width: 40%;
 margin: auto;
 font-size: 18px;
 color: white;
 background-color: #191919;
 border-style: solid;
 border-color: white;
 border-width: 5px;
 padding: 20px;
 cursor: pointer;
 @media (max-width: 768px) {
   width: 100%;
 }
`;
 
export const HeadingContainer = styled.div`
 text-align: center;
 margin:30px;
`;

export const StyledHeading = styled.h1`
 color: #ffe81f;
 font-size: 48px;
`;

Now let’s come to the DataInputForm.js file.

Let’s import our styled components for the form. Add the following import statement to the DataInputForm.js file.

import {
  FormContainer,
  FormInputContainer,
  StyledInputText,
  FormSubmitContainer,
  StyledInputTextarea,
  StyledErrorContainer,
  StyledLabel,
  StyledSubmitButton,
} from "./StyledComponents";

Then change the return statement of the form in DataInputForm to this:

return (
    <Formik
      initialValues={{ titleText: "", summaryText: "" }}
      onSubmit={props.onFormSubmitted}
      validationSchema={starWarsSchema}
    >
      {(props) => {
        const {
          values,
          errors,
          isSubmitting,
          handleChange,
          handleBlur,
          handleSubmit,
        } = props;
        return (
          <form onSubmit={handleSubmit}>
            <FormContainer>
              <FormInputContainer>
                <StyledLabel>Title</StyledLabel>
                <StyledInputText
                  id="titleText"
                  placeholder="Enter your desired title"
                  type="text"
                  onBlur={handleBlur}
                  value={values.titleText}
                  onChange={handleChange}
                />
                <StyledErrorContainer>{errors.titleText}</StyledErrorContainer>
              </FormInputContainer>
              <FormInputContainer>
                <StyledLabel>Summary</StyledLabel>
                <StyledInputTextarea
                  id="summaryText"
                  placeholder="Enter your summary"
                  type="text"
                  onBlur={handleBlur}
                  value={values.summaryText}
                  onChange={handleChange}
                  rows={10}
                />
                <StyledErrorContainer>
                  {errors.summaryText}
                </StyledErrorContainer>
              </FormInputContainer>
              <FormSubmitContainer>
                <StyledSubmitButton type="submit" disabled={isSubmitting}>
                  Start Crawling Intro
                </StyledSubmitButton>
              </FormSubmitContainer>
            </FormContainer>
          </form>
        );
      }}
    </Formik>
  );

Now, come to the App.js file. Add the following import statement to the file.

import {
  MainContainer,
  HeadingContainer,
  StyledHeading,
} from "./components/StyledComponents";

Then modify the return statement to this:

return (
   <MainContainer>
     {!isStarted && (
       <>
         <HeadingContainer>
           <StyledHeading>Star Wars Crawling Intro</StyledHeading>
         </HeadingContainer>
         <DataInputForm onFormSubmitted={onFormSubmitted} />
       </>
     )}
     {isStarted && (
       <StarWarsSketch titleText={swTitle} summaryText={swSummary} />
     )}
   </MainContainer>
 );

Finally, add the following lines of code to the index.css file:

body {
 margin: 0;
 background-image: url("https://rohanbhatia96.github.io/star-wars-intro-react-p5/star-wars-background.jpg");
 background-repeat: repeat;
}

Fin!

That’s it! Our star wars crawling intro generator is now complete.

You can view the final code on GitHub.

Please share this article if it helped you. If you’re looking for a remote react developer, feel free to contact me ?

Leave a Reply

seventeen − 13 =