- Notifications
You must be signed in to change notification settings - Fork51
A wrapper component that allows you to utilise P5 sketches within React apps.
License
P5-wrapper/react
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A component to integrateP5.js sketches intoReact apps.
Note: Version 5 is still in development, currently 5.0.0-rc.0 has beenreleased for internal development testing ONLY. It is recommended to continueutilising version 4.4.0 until it is out of the
rc
versioning scheme.
To install, use the following command in the format appropriate to your chosenpackage manager:
[npm|yarn|pnpm] [install|add] p5 @p5-wrapper/react
Please note thatp5
,react
andreact-dom
are peer dependencies, meaningyou should ensure they are installed before installing React P5 Wrapper.
"peerDependencies":{"p5":">= 1.4.1","react":">= 18.2.0","react-dom":">= 18.2.0"},
If you would like to use Typescript, you should installp5
types in thedevelopment environment:
[npm|yarn|pnpm] [install|add] -D @types/p5
If you plan to use this component within a Next.js application, you shouldinstead useour Next.js dynamic implementationinstead. To do get started, you can run:
[npm|yarn|pnpm] [install|add] p5 @p5-wrapper/next @p5-wrapper/react
Please continue reading these docs and also look atthe Next.js dynamic implementation docsfor further supporting information.
A live demo can be viewed atP5-wrapper.github.io/react.
The repository contains furtherexamples.
To try them out for yourself fork the repository, be sure you havePNPM installed and then run the following:
git clone git@github.com:<your username>/react.gitcd reactpnpm installpnpm preview
Then just openhttp://localhost:3001
in a browser.
import*asReactfrom"react";import{ReactP5Wrapper}from"@p5-wrapper/react";functionsketch(p5){p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.draw=()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};}exportfunctionApp(){return<ReactP5Wrappersketch={sketch}/>;}
TypeScript sketches can be declared in two different ways, below you will findtwo ways to declare a sketch, both examples do the exact same thing.
In short though, theReactP5Wrapper
component requires you to pass asketch
prop. Thesketch
prop is simply a function which takes ap5
instance as it'sfirst and only argument.
import*asReactfrom"react";import{P5CanvasInstance,ReactP5Wrapper}from"@p5-wrapper/react";functionsketch(p5:P5CanvasInstance){p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.draw=()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};}exportfunctionApp(){return<ReactP5Wrappersketch={sketch}/>;}
Using theSketch
type has one nice benefit over usingP5CanvasInstance
andthat is that thep5
argument passed to the sketch function is auto-typed as aP5CanvasInstance
for you.
Side note:
In general, it comes down to personal preference as to how you declare yoursketches and there is nothing wrong with using the
P5CanvasInstance
manuallyin a regularfunction
declaration.
import*asReactfrom"react";import{ReactP5Wrapper,Sketch}from"@p5-wrapper/react";constsketch:Sketch=p5=>{p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.draw=()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};};exportfunctionApp(){return<ReactP5Wrappersketch={sketch}/>;}
We also support the use of Generics to add type definitions for your props. Ifused, the props will be properly typed when the props are passed to theupdateWithProps
method.
To utilise generics you can use one of two methods. In both of the examplesbelow, we create a custom internal type calledMySketchProps
which is a uniontype ofSketchProps
and a custom type which has arotation
key applied toit.
Side note:
We could also write the
MySketchProps
type as an interface to do exactly thesame thing if that is to your personal preference:interfaceMySketchPropsextendsSketchProps{rotation:number;}
This means, in these examples, that when therotation
prop that is provided aspart of theprops
passed to theupdateWithProps
function, it will becorrectly typed as anumber
.
import{P5CanvasInstance,ReactP5Wrapper,SketchProps}from"@p5-wrapper/react";importReact,{useEffect,useState}from"react";typeMySketchProps=SketchProps&{rotation:number;};functionsketch(p5:P5CanvasInstance<MySketchProps>){letrotation=0;p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.updateWithProps=props=>{if(props.rotation){rotation=(props.rotation*Math.PI)/180;}};p5.draw=()=>{p5.background(100);p5.normalMaterial();p5.noStroke();p5.push();p5.rotateY(rotation);p5.box(100);p5.pop();};}exportfunctionApp(){const[rotation,setRotation]=useState(0);useEffect(()=>{constinterval=setInterval(()=>setRotation(rotation=>rotation+100),100);return()=>{clearInterval(interval);};},[]);return<ReactP5Wrappersketch={sketch}rotation={rotation}/>;}
import{ReactP5Wrapper,Sketch,SketchProps}from"@p5-wrapper/react";importReact,{useEffect,useState}from"react";typeMySketchProps=SketchProps&{rotation:number;};constsketch:Sketch<MySketchProps>=p5=>{letrotation=0;p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.updateWithProps=props=>{if(props.rotation){rotation=(props.rotation*Math.PI)/180;}};p5.draw=()=>{p5.background(100);p5.normalMaterial();p5.noStroke();p5.push();p5.rotateY(rotation);p5.box(100);p5.pop();};};exportfunctionApp(){const[rotation,setRotation]=useState(0);useEffect(()=>{constinterval=setInterval(()=>setRotation(rotation=>rotation+100),100);return()=>{clearInterval(interval);};},[]);return<ReactP5Wrappersketch={sketch}rotation={rotation}/>;}
import*asReactfrom"react";import{ReactP5Wrapper}from"@p5-wrapper/react";functionsetup(p5){return()=>{p5.createCanvas(600,400,p5.WEBGL);};}functiondraw(p5){return()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};}functionsketch(p5){p5.setup=setup(p5);p5.draw=draw(p5);}exportfunctionApp(){return<ReactP5Wrappersketch={sketch}/>;}
The only required property of theReactP5Wrapper
component is thesketch
prop. Thesketch
prop is a function that will be passed a p5 instance to usefor rendering your sketches as shown inthe usage section above.
You can pass as many custom props as you want to theReactP5Wrapper
componentand these will all be passed into theupdateWithProps
method if you havedefined it within your sketch.
In the below example you see theupdateWithProps
method being used. This iscalled when the component initially renders and when the props passed to thewrapper are changed, if it is set within your sketch. This way we can render ourReactP5Wrapper
component and react to component prop changes directly withinour sketches!
import{ReactP5Wrapper}from"@p5-wrapper/react";importReact,{useEffect,useState}from"react";functionsketch(p5){letrotation=0;p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.updateWithProps=props=>{if(props.rotation){rotation=(props.rotation*Math.PI)/180;}};p5.draw=()=>{p5.background(100);p5.normalMaterial();p5.noStroke();p5.push();p5.rotateY(rotation);p5.box(100);p5.pop();};}exportfunctionApp(){const[rotation,setRotation]=useState(0);useEffect(()=>{constinterval=setInterval(()=>setRotation(rotation=>rotation+100),100);return()=>{clearInterval(interval);};},[]);return<ReactP5Wrappersketch={sketch}rotation={rotation}/>;}
To render a component on top of the sketch, you can add it as a child of theReactP5Wrapper
component and then use the exportedP5WrapperClassName
constant in your css-in-js library of choice to style one element above theother via css.
For instance, usingstyled components, we couldcenter some text on top of our sketch like so:
import{P5WrapperClassName,ReactP5Wrapper}from"@p5-wrapper/react";importstyled,{createGlobalStyle}from"styled-components";constGlobalWrapperStyles=createGlobalStyle` .${P5WrapperClassName} { position: relative; }`;constStyledCentredText=styled.span` .${P5WrapperClassName} & { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 2rem; margin: 0; text-align: center; }`;exportfunctionApp(){const[rotation,setRotation]=useState(0);useEffect(()=>{constinterval=setInterval(()=>setRotation(rotation=>rotation+100),100);return()=>{clearInterval(interval);};},[]);return(<Fragment><GlobalWrapperStyles/><ReactP5Wrappersketch={sketch}rotation={rotation}><StyledCentredText>Hello world!</StyledCentredText></ReactP5Wrapper></Fragment>);}
Of course, you can also use any other css-in-js library or by just using simplecss to achieve almost anything you can imagine just by using the wrapper classas your root selector.
Lets say you want to have a fallback UI in case thesketch
ever falls out ofsync or is undefined for some reason. If this is a use case for you then youcall use thefallback
prop to provide the necessary UI to show in the casethat thesketch
becomes undefined. An example could be as follows:
import*asReactfrom"react";import{ReactP5Wrapper}from"@p5-wrapper/react";functionsketchOne(p5){p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.draw=()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};}functionsketchTwo(p5){p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.draw=()=>{p5.background(500);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};}exportfunctionApp(){const[sketch,setSketch]=React.useState(undefined);constchooseNothing=()=>setSketch(undefined);constchooseSketchOne=()=>setSketch(sketchOne);constchooseSketchTwo=()=>setSketch(sketchTwo);return(<><ul><li><buttononClick={chooseNothing}>Choose nothing</button></li><li><buttononClick={chooseSketchOne}>Choose sketch 1</button></li><li><buttononClick={chooseSketchTwo}>Choose sketch 2</button></li></ul><ReactP5Wrapperfallback={<h1>No sketch selected yet.</h1>}sketch={sketch}/></>);}
In this case, by default the fallback UI containing<h1>No sketch selected yet.</h1>
will be rendered, then if you select asketch, it will be rendered until you choose to once again "show nothing" whichfalls back to the fallback UI.
Since version 4.4.0, it was possible to add afallback
prop, see the sectionon fallbacks.
Since version 5 it is now possible to pass anerror
andloading
prop to thewrapper which allow the user to pass different UIs for error and loading states.
- The
error
state will trigger if the sketch or the wrapper encounter anissue, otherwise a default error view will be shown. - The
loading
state will trigger while the wrapper is being lazy loaded,otherwise a default loading view will be shown.
To show a custom UI when an error occurs within the sketch or the wrapper, youcan pass a lazy function to theerror
prop.
import*asReactfrom"react";import{P5CanvasInstance,ReactP5Wrapper}from"@p5-wrapper/react";// This child will throw an error, oh no!functionErrorChild(){thrownewError("oops");}// This view will catch the thrown error and give you access to what exactly was thrown.functionErrorUI(error:any){if(errorinstanceofError){return<p>An error occured:{error.message}</p>;}return<p>An unknown error occured:{error.toString()}</p>;}functionsketch(p5:P5CanvasInstance){p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.draw=()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};}exportfunctionApp(){return(<ReactP5Wrappersketch={sketch}error={ErrorUI}><ErrorChild/></ReactP5Wrapper>);}
Instead of the sketch, this will render<p>An error occured: oops</p>
. Notethat in truth, theErrorView
willalways receiveany
values since JS /TS allow you tothrow
whatever values you want to, this is why we have to addtheerror instanceof Error
check to be sure the value we got was actually anError
instance and not some other value like anumber
,string
,object
oranything else that could be thrown by JS / TS.
As mentioned above, theerror
state will trigger if the sketch or the wrapperencounter an issue, otherwise a default error view will be shown.
To show a custom UI while the sketch UI is being lazy loaded, you can pass alazy function to theloading
prop.
import*asReactfrom"react";import{P5CanvasInstance,ReactP5Wrapper}from"@p5-wrapper/react";functionLoadingUI(){return<p>The sketch is being loaded.</p>;}functionsketch(p5:P5CanvasInstance){p5.setup=()=>p5.createCanvas(600,400,p5.WEBGL);p5.draw=()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};}exportfunctionApp(){return<ReactP5Wrappersketch={sketch}loading={LoadingUI}/>;}
In the initial period between the sketch render starting and it's lazy loadingending, theLoadingUI
will be shown!
As mentioned above, theloading
state will trigger while the wrapper is beinglazy loaded, otherwise a default loading view will be shown.
As discussed in multiple issues such as#11,#23,#61 and#62, there seems to beconfusion as to how we can use P5 plugins and constructors out of the box. Thissection aims to clarify these!
Since P5 is being used inP5 instance modeas part of this project, P5 will not automatically load global plugins like itusually might in global mode.
Let's say we want to use theP5 sound plugin in ourcomponent, we could do the following:
import*asp5from"p5";import{ReactP5Wrapper,Sketch}from"@p5-wrapper/react";importReact,{useEffect,useState}from"react";(windowasany).p5=p5;awaitimport("p5/lib/addons/p5.sound");constsketch:Sketch=p5=>{letsong:p5.SoundFile;letbutton:p5.Element;p5.setup=()=>{p5.createCanvas(600,400,p5.WEBGL);p5.background(255,0,0);button=p5.createButton("Toggle audio");button.mousePressed(()=>{if(!song){constsongPath="/piano.mp3";song=p5.loadSound(songPath,()=>{song.play();},()=>{console.error(`Could not load the requested sound file${songPath}`);});return;}if(!song.isPlaying()){song.play();return;}song.pause();});};p5.draw=()=>{p5.background(250);p5.normalMaterial();p5.push();p5.rotateZ(p5.frameCount*0.01);p5.rotateX(p5.frameCount*0.01);p5.rotateY(p5.frameCount*0.01);p5.plane(100);p5.pop();};};exportdefaultfunctionApp(){return<ReactP5Wrappersketch={sketch}/>;}
In this Typescript + React example, we can see a few key things.
- Firstly we need to set
p5
on thewindow
object manually. This is becausep5.sound
requires that it be executed client side only AND thatp5
beavailable BEFORE it is imported into the global (window
) scope. - Secondly, we ensure that audio is played after a user action, in our case thishappens on a button click. This is because in some browsers, without waitingfor a user interaction before playing audio, the audio will be blocked by thebrowser from playing at all.
- Thirdly and relevant especially to Safari users, Safari blocks audio from alltabs by default, you will need to manually change this setting in your Safarisettings. This could affect other browsers but sadly this is a browserdecision and untilP5 Sound isupdated to support newer audio APIs and browser requirements. This couldhappen at anytime in other places and is aP5 Sound issue most generallybecause it does not ask for permissions by default, even though browsers havebeen requiring it for some time.
Note: The above example requires support fortop level await,dynamic import statements andthe stream API to be supported in your browser.Furthermore,the stream API built into thebrowser requires that HTTPS is used to ensure secure data transmission.
To access P5 constructors such asp5.Vector
orp5.Envelope
, you need to usethe instance mode syntax instead. For example:
Constructor | Global mode accessor | Instance mode accessor |
---|---|---|
Vector | p5.Vector | p5.constructor.Vector |
Envelope | p5.Envelope | p5.constructor.Envelope |
So now that we know this, let's imagine we want a random 2D Vector instance. Inoursketch
function we would simply callp5.constructor.Vector.random2D()
instead ofp5.Vector.random2D()
. This is because of how theP5 instance modewas implemented by the P5 team. While I am not sure why they decided to changethe API for instance mode specifically, it is still quite simple to use theconstructs we are used to without much extra work involved.
NOTE: The source code for the component is in thesrc
directory.
To build, watch and serve the examples which will also watch the componentsource, run:
pnpm preview
About
A wrapper component that allows you to utilise P5 sketches within React apps.