To get P5 to work with React, I am using the P5Wrapper import.
I got a simple starfield animation to work on my tile, but the performance is an issue. The animation slows to a crawl at 512 "star" objects, so I scaled it back to 128. However, even at 128, the FPS seems much too low, averaging below 30 FPS. I am looking for ways to speed up P5's performance in React so that the animations can run closer to 60 FPS.
P5 code:
function sketch (p) {
const star = () => {
const x = p.random(-TILE_SIZE/2, TILE_SIZE/2)
const y = p.random(-TILE_SIZE/2, TILE_SIZE/2)
const z = p.random(TILE_SIZE)
return { x, y, z }
}
const stars = new Array(128)
p.setup = () => {
p.createCanvas(TILE_SIZE, TILE_SIZE)
for (let i = 0; i < stars.length; i++) {
stars[i] = star()
}
}
const update = (coord) => {
const { z } = coord
let newZ = z - 8
if (newZ < 1) {
newZ = p.random(TILE_SIZE)
}
return { ...coord, z: newZ }
}
const show = (coord) => {
const { x, y, z } = coord
p.fill(255)
p.noStroke()
const sx = p.map(x / z, 0, 1, 0, TILE_SIZE)
const sy = p.map(y / z, 0, 1, 0, TILE_SIZE)
const r = p.map(z, 0, TILE_SIZE, 4, 0)
p.ellipse(sx, sy, r, r)
}
p.draw = () => {
p.background(0)
p.translate(TILE_SIZE/2, TILE_SIZE/2)
for (let i = 0; i < stars.length; i++) {
stars[i] = update(stars[i])
show(stars[i])
}
}
}
How P5Wrapper is used:
import P5Wrapper from 'react-p5-wrapper'
...
render (
<ItemContainer key={uuidv4()}>
<header>
{name}
<p>{description}</p>
</header>
<P5Wrapper sketch={sketch} />
</ItemContainer>
)
How the starfield tile actually looks (2 tiles).
I am planning to add more animations depending on performance. Or switching to SVG.
Resolved the frame-rate issue without changing the actual animation logic. There could still be plenty of performance optimization that is still needed.
I noticed that the animation gets progressively slower as I un-mount and re-mount the component. Digging into the Github issue results in this post about performance degradation. The poster provided a PR and commit that was never merged and the repository haven't been updated for over a year.
Instead, it's better to remove the package and just create your own component following the poster's update:
import React from 'react'
import p5 from 'p5'
export default class P5Wrapper extends React.Component {
componentDidMount() {
this.canvas = new p5(this.props.sketch, this.wrapper)
if( this.canvas.myCustomRedrawAccordingToNewPropsHandler ) {
this.canvas.myCustomRedrawAccordingToNewPropsHandler(this.props)
}
}
componentWillReceiveProps(newprops) {
if(this.props.sketch !== newprops.sketch){
this.canvas.remove()
this.canvas = new p5(newprops.sketch, this.wrapper)
}
if( this.canvas.myCustomRedrawAccordingToNewPropsHandler ) {
this.canvas.myCustomRedrawAccordingToNewPropsHandler(newprops)
}
}
componentWillUnmount() {
this.canvas.remove()
}
render() {
return <div ref={wrapper => this.wrapper = wrapper}></div>
}
}
This at the very least resolved the performance degradation issue for mounting and unmounting the component. Additionally, my frames have jumped from sub 30 to nearly 60 FPS. This could be because I also imported the latest P5 package since I no longer rely on react-p5-wrapper to do the imports.
Related
I'm making a project where I need to generate pretty huge map. I need to generate a lot of ground blocks with trees or rocks, kinda similar to
minecraft (each block must be clickable). But I have a problem with optimization. I tried cutting render distance, adding fog, and playing
with three instances (I'm using threejs fiber library for react). Could anyone help me explain how instances work and tell me how to apply
them to my project? Hre's the code to generate it in normal way, without instancing.
import React, { Suspense } from "react";
import { useEffect, useState } from "react";
import Box from "./Box";
export default function Ground({
layout,
selectBox,
selectedBox,
position,
}) {
const [boxArray, setBoxArray] = useState([]);
const size = 20;
let id = 0;
useEffect(() => {
const boxArrayCopy = [];
for (let i = -(size / 2); i < size / 2; i++) {
let hasTree = Math.random() > 0.99;
for (let j = -(size / 2); j < size / 2; j++) {
const layoutObject = layout.filter(
(layoutObject) => layoutObject.x === i && layoutObject.y === j
);
const box = (
<Box
key={id}
coords={{ x: j * 5, y: 0, z: i * 5 }}
color={layoutObject.length > 0 ? layoutObject[0].color : 0x61892f}
selectBox={selectBox}
selectedBox={selectedBox}
hasTree={hasTree}
hasStone={Math.random() < 0.2}
// just a random condition to add tree to box
/>
);
boxArrayCopy.push(box);
id++;
if (hasTree) hasTree = Math.random() > 0.5;
}
setBoxArray([...boxArrayCopy]);
}
console.log("GROUND DONE");
}, []);
return (
<group position={position ?? [0, 0, 0]}>
<Suspense fallback={null}>{boxArray}</Suspense>
</group>
);
}
I’m using this Rectangle class
class Rectangle {
constructor(x, y, width, height, color, hasCollision = false, kills = false) {
this.A = new Point(x, y)
this.B = new Point(x + width, y)
this.C = new Point(x + width, y + height)
this.D = new Point(x, y + height)
this.center = new Point(x + width / 2, y + height / 2)
this.width = width
this.height = height
this.color = color
this.hasCollision = hasCollision
this.kills = kills
}
get vertices() {
const { A, B, C, D } = this
return {
A,
B,
C,
D
}
}
get x() {
return this.A.x
}
get y() {
return this.A.y
}
static translate(rectangle, vector) {
for (let vertice of Object.values(rectangle.vertices)) {
vertice.translate(vector)
}
rectangle.center.translate(vector)
}
translate(vector) {
Rectangle.translate(this, vector)
}
hasUserFallenInTrap(user) {
if (circleIntersectsRectangle(user, this) && this.kills) {
return true
}
return false
}
display(ctx, useOwnColor = true) {
const { x, y, width, height } = this
if (useOwnColor) {
ctx.fillStyle = this.color
? this.color.hexString
: this.kills
? 'red'
: '#000000'
}
ctx.fillRect(x, y, width, height)
}
}
I need to store a bunch of Rectangles in an array so that I can display them in a canvas (wrapped in a React component). The component doesn’t update every frame, I’m using my own draw function on the canvas :
// Here is the component
class Game extends React.Component {
constructor(props) {
super(props)
const world = loadMap('world1')
this.state = {
currentWorld: world,
users: {},
user: new User(
world.spawn.center.x,
world.spawn.center.y,
12,
Color.random(),
'',
world.spawn
)
}
this.canvas = React.createRef()
this.ctx = null
}
componentDidMount() {
const { user } = this.state
server.userConnects(user)
openConnection()
this.ctx = this.canvas.current.getContext('2d')
setPointerLock(this.canvas.current, this.mouseMoved)
this.request = window.requestAnimationFrame(this.draw)
}
componentWillUnmount() {
closeConnection()
window.cancelAnimationFrame(this.request)
}
shouldComponentUpdate() {
return false
}
updateUserID = id => {
this.setState({ u })
}
mouseMoved = event => {
const { currentWorld, user } = this.state
const displacement = new Vector(
event.movementX / pixelRatio,
event.movementY / pixelRatio
)
user.translate(displacement)
resolveWorldBordersCircleCollision(user)
for (const w of currentWorld.walls) {
if (w.hasCollision) stepCollisionResolve(user, w)
}
server.updateUserPosition(user)
}
draw = () => {
const { currentWorld, users, user } = this.state
this.request = window.requestAnimationFrame(this.draw)
this.ctx.clearRect(0, 0, WIDTH, HEIGHT)
this.ctx.fillText(fpsCounter.fps, 1000, 20)
currentWorld.walls.forEach(w => {
if (w.hasCollision && resolveCollisionCircleRectangle(user, w)) {
server.updateUserPosition(user)
}
w.display(this.ctx)
})
currentWorld.movableWalls.forEach(w => {
w.walkPath()
if (w.hasCollision && resolveCollisionCircleRectangle(user, w)) {
server.updateUserPosition(user)
}
w.display(this.ctx)
})
currentWorld.traps.forEach(t => {
t.display(this.ctx)
if (t.hasUserFallenInTrap(user)) {
user.kill()
server.updateUserPosition(user)
}
})
user.display(this.ctx, false)
Object.values(users)
.filter(u => u.id !== user.id)
.forEach(u => {
u.display(this.ctx, true)
})
}
render() {
return (
<>
<canvas ref={this.canvas} id="canvas" width={WIDTH} height={HEIGHT} />
</>
)
}
}
I’m not sure how I can store and manage this array of rectangles.
I’m translating a rectangle using the class method translate.
const rect = new Rectangle(10, 10, 10, 10)
rect.translate({x: 10, y: 20})
But I can’t do that if the rectangle is in the state of a component.
calling rect.translate would mutate the state directly.
Creating a new object everytime I’m updating the state completely defeats the purpose of using this class .
using object destructuring would create a plain new object, and so I wouldn’t be able to call its display method anymore :
// changing a single rectangle for example
const { rectangles } = this.state
this.setState({ rectangles: [...rectangles.splice(0,index) , { ...rectangles[index], x: rectangles[index].x + 10, y: rectangles[index].y + 10 }, ...rectangles.splice(index + 1)]
Using an array outside of any react component appears like the only solution, but not really satisfying either for a react app.
I’m out of ideas to manage the state of my applications.
Are there better ways to store this array of Rectangle instances?
Or using this kind of object design is simply impossible in react?
The hard part is figuring out whether mutability is really needed (e.g. for performance reasons) or if the state can live inside React after giving it more thought (and a good night's sleep).
If mutability is needed, that part of the app can live outside of React as an Uncontrolled Component (the state can live in DOM or some imperative code that will update the DOM).
A parent component from React can access the out-of-react mutable state (from event handlers or lifecycle methods) by using a ref (see the article above). A simplified example, using a useRef hook:
const rndColor = () => `rgb(${Array.from(Array(3)).map(() => Math.random()*255).join(',')})`
const App = () => {
const canvasRef = React.useRef()
const handleClick = () => {
// --> rect.translate({x: 10, y: 20}) can be called here <--
canvasRef.current.style.background = rndColor()
}
return <canvas ref={canvasRef} onClick={handleClick} />
}
ReactDOM.render(<App />,document.getElementById('root'))
canvas {
width: 150px;
height: 150px;
background: orange;
}
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
I'd like to create a potentially unlimited amount of instances of a certain class within my javascript document:
let trees;
const treespawn = () => {
let x = random(50, windowWidth - 50);
let y = random(windowHeight /22, windowHeight / 1.13);
for (let i = 0; i < 20; i++) {
trees[i] = new Trees(x, y);
return trees[i];
}
}
function draw() {
background(0, 111, 10);
trees.trunk();
trees.leaves();
trees.shudder();
}
class Trees {
constructor(x, y) {
stuff--stuff--stuff
}
trunk() {
stuff--stuff--stuff
}
leaves() {
stuff--stuff--stuff
}
shudder() {
stuff--stuff--stuff
}
}
Some points:
the trees variable is declared globally because I can't think of a way to keep my class function calls in scope.
I'm using p5 hence the function draw. I've created all my other classes in the setup() function.
Basic explanation:
I'd like to create my Trees class object many times and have them appear on my canvas at the start of each level of the game I'm making. This would require around 20 trees per level inside the parameters set to let x and let y. In higher levels of my game I may want to increase the amount of trees.
You can see how simply writing out 20-30 variable declarations, creating those classes and calling all of the functions inside each class would be impractical.
I realise this must be done with some sort of array methodology.
Can anyone help me here?
Thanks in advance!
Update:
This is as far as I've gotten and no console error message but I see no trees on the canvas.
function draw() {
background(0, 111, 10);
() => {
let x = random(50, windowWidth - 50);
let y = random(windowHeight /22, windowHeight / 1.13);
for (let i = 0; i < 20; i++) {
trees[i] = new Trees(x, y);
trees[i].trunk();
trees[i].leaves();
trees[i].shudder();
}
}
}
Update
random is not a native Javascript function, so if you're not getting it from a library or from elsewhere, you might need to define it:
function random(lowerBound, upperBound) {
var range = upperBound - lowerBound + 1;
return Math.floor(Math.random() * range);
}
Try this:
class Trees {
constructor() {
stuff--stuff--stuff
}
trunk() {
stuff--stuff--stuff
}
leaves() {
stuff--stuff--stuff
}
shudder() {
stuff--stuff--stuff
}
}
const treespawn = () => {
let x = random(50, windowWidth - 50);
let y = random(windowHeight /22, windowHeight / 1.13);
return new Trees(x, y);
}
let trees = [];
for (let i = 0; i < 20; i++) {
trees.push(treespawn());
}
function draw() {
background(0, 111, 10);
trees.forEach(tree => {
tree.trunk();
tree.leaves();
tree.shudder();
});
}
I am using an html canvas in React.js to draw a picture.
I get the dimensions (in millimeters) for rectangles and paths through an api call.
Rather than passing down scaling variables to reduce the dimensions before drawing the canvas, I would like to draw the canvas assuming the dimensions are in pixels, and to then, when the component is created through a higher level call, reduce its size (i.e. scale it down to a fixed width, BUT keep the aspect ratio).
How can I do this?
Below some code to demonstrate. I use withStyles also, but I've excluded this from the sample code for the sake of brevity.
I have a CanvasComponent class:
import React from 'react'
class CanvasComponent extends React.Component {
componentDidMount() {
this.updateCanvas();
}
updateCanvas() {
const {drawing_objects, drawing, classes} = this.props
const w = drawing.dims[0]
const h = drawing.dims[1]
const ctx = this.refs.canvas.getContext('2d');
ctx.fillStyle = "#79d279"
ctx.fillRect(0,0, w, h);
let i;
let r;
for (i = 0; i < drawing_objects.length; i++) {
r = drawing_objects[i]
ctx.beginPath();
...
}
}
render() {
const {drawing_objects, drawing, classes} = this.props
const w = drawing.dims[0]
const h = drawing.dims[1]
return (
<canvas ref="canvas" width={w} height={h}/>
);
}
}
export default CanvasComponent
And when I create it from a higher level, I do something like:
import React from 'react'
import CanvasComponent from "../components/CanvasComponent"
class Index extends React.Component {
render(){
const {drawing, drawing_objects} = this.props
return
<div >
<CanvasComponent
drawing={drawing}
drawing_objects={drawing_objects}
/>
</div>
}
}
I am trying to get a react application to render the conways life method. I am still a bit new to React.js and was hoping someone could shed some light on why my code is broken and also how to fix it. And perhaps how to prevent it from happening in the future. Below is my code
import React, { Component } from 'react';
import Life from './life';
import './App.css';
/**
* Life canvas
*/
class LifeCanvas extends Component {
/**
* Constructor
*/
constructor(props) {
super(props);
this.life = new Life(props.width, props.height);
this.life.randomize();
}
/**
* Component did mount
*/
componentDidMount() {
requestAnimationFrame(() => {
this.animFrame();
});
}
/**
* Handle an animation frame
*/
animFrame() {
//
// !!!! IMPLEMENT ME !!!!
//
let width = this.props.width;
let height = this.props.height;
// Request another animation frame
// Update life and get cells
let cells = this.life.getCells();
// Get canvas framebuffer, a packed RGBA array
let canvas = this.refs.canvas;
let ctx = canvas.getContext('2D');
let imageData = ctx.getImageData(0, 0, width, height);
// Convert the cell values into white or black for the canvas
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let index = (y * width + x) * 4;
let lifeStatus = cells[y][x];
let color = lifeStatus === 0 ? 0xff : 0x00;
imageData.data[index + 0] = color; // R
imageData.data[index + 1] = color; // G
imageData.data[index + 1] = color; // B
imageData.data[index + 1] = color; // A
}
}
// Put the new image data back on the canvas
ctx.putImageData(imageData, 0, 0);
// Next generation of life
this.life.step();
requestAnimationFrame(() => {
this.animFrame();
});
}
/**
* Render
*/
render() {
return (
<canvas
ref="canvas"
width={this.props.width}
height={this.props.height}
/>
);
}
}
/**
* Life holder component
*/
class LifeApp extends Component {
/**
* Render
*/
render() {
return (
<div>
<LifeCanvas width={600} height={600} />
</div>
);
}
}
/**
* Outer App component
*/
class App extends Component {
/**
* Render
*/
render() {
return (
<div className="App">
<LifeApp />
</div>
);
}
}
export default App;
When it renders the page the error in the subject line is produced in big red letters. I am not sure what it means. Please help.
It is saying ctx is null. getContext() will return null when the passed context type is not valid. In your case you passed 2D the proper context type string is 2d
var c = document.createElement('canvas');
var ctx = c.getContext('2D');
console.log("ctx is: ",ctx);
ctx = c.getContext('2d');
console.log("ctx is now: ",ctx);