I am not too good at data visualization.I want to create a Sunburst where the user can zoom. I have done the zoom with the help of my friend but I am unable to add text from data. Here is my code of zoomable Sunburst.
import React from "react";
import { Group } from "#vx/group";
import { Arc } from "#vx/shape";
import { Partition } from "#vx/hierarchy";
import { arc as d3arc } from "d3-shape";
import {
scaleLinear,
scaleSqrt,
scaleOrdinal,
schemeCategory20c
} from "d3-scale";
import { interpolate } from "d3-interpolate";
import Animate from "react-move/Animate";
import NodeGroup from "react-move/NodeGroup";
const color = scaleOrdinal(schemeCategory20c);
export default class extends React.Component {
state = {
xDomain: [0, 1],
xRange: [0, 2 * Math.PI],
yDomain: [0, 1],
yRange: [0, this.props.width / 2]
};
xScale = scaleLinear();
yScale = scaleSqrt();
arc = d3arc()
.startAngle(d => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x0))))
.endAngle(d => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x1))))
.innerRadius(d => Math.max(0, this.yScale(d.y0)))
.outerRadius(d => Math.max(0, this.yScale(d.y1)));
handleClick = d => {
this.setState({
xDomain: [d.x0, d.x1],
yDomain: [d.y0, 1],
yRange: [d.y0 ? 20 : 0, this.props.width / 2]
});
};
render() {
const {
root,
width,
height,
margin = {
top: 0,
left: 0,
right: 0,
bottom: 0
}
} = this.props;
const { xDomain, xRange, yDomain, yRange } = this.state;
if (width < 10) return null;
const radius = Math.min(width, height) / 2 - 10;
return (
<svg width={width} height={height}>
<Partition top={margin.top} left={margin.left} root={root}>
{({ data }) => {
const nodes = data.descendants();
return (
<Animate
start={() => {
this.xScale.domain(xDomain).range(xRange);
this.yScale.domain(yDomain).range(yRange);
}}
update={() => {
const xd = interpolate(this.xScale.domain(), xDomain);
const yd = interpolate(this.yScale.domain(), yDomain);
const yr = interpolate(this.yScale.range(), yRange);
return {
unused: t => {
this.xScale.domain(xd(t));
this.yScale.domain(yd(t)).range(yr(t));
},
timing: {
duration: 800
}
};
}}
>
{() => (
<Group top={height / 2} left={width / 2}>
{nodes.map((node, i) => (
<path
d={this.arc(node)}
stroke="#fff"
fill={color(
(node.children ? node.data : node.parent.data).name
)}
fillRule="evenodd"
onClick={() => this.handleClick(node)}
text="H"
key={`node-${i}`}
/>
))}
</Group>
)}
</Animate>
);
}}
</Partition>
</svg>
);
}
}
Currently this visualization does not display the name of data from data.js. I want to display that and add a tooltip. How can I achieve that?
class Sunburst extends React.Component {
componentDidMount() {
this.renderSunburst(this.props);
}
componentWillReceiveProps(nextProps) {
if (!isEqual(this.props, nextProps)) {
this.renderSunburst(nextProps);
}
}
arcTweenData(a, i, node, x, arc) {
const oi = d3.interpolate({ x0: (a.x0s ? a.x0s : 0), x1: (a.x1s ? a.x1s : 0) }, a);
function tween(t) {
const b = oi(t);
a.x0s = b.x0;
a.x1s = b.x1;
return arc(b);
}
if (i === 0) {
const xd = d3.interpolate(x.domain(), [node.x0, node.x1]);
return function (t) {
x.domain(xd(t));
return tween(t);
};
} else {
return tween;
}
}
formatNameTooltip(d) {
const name = d.data.name;
return `${name}`;
}
labelName(d) {
const name = d.data.name;
return `${name}`;
}
labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * 130;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
update(root, firstBuild, svg, partition, hueDXScale, x, y, radius, arc, node, self) {
if (firstBuild) {
firstBuild = false;
function click(d) {
node = d; // eslint-disable-line
self.props.onSelect && self.props.onSelect(d);
svg.selectAll('path').transition().duration(1000);
}
const tooltipContent = self.props.tooltipContent;
const tooltip = d3.select(`#${self.props.keyId}`)
.append(tooltipContent ? tooltipContent.type : 'div')
.style('position', 'absolute')
.style('z-index', '10')
.style('opacity', '0');
if (tooltipContent) {
Object.keys(tooltipContent.props).forEach((key) => {
tooltip.attr(key, tooltipContent.props[key]);
});
}
svg.selectAll('path')
.data(partition(root).descendants())
.enter()
.append('path')
.style('fill', (d) => {
const current = d;
if (current.depth === 0) {
return '#ffff';
}
if (current.depth === 1) {
return '#3f51b5';
}
if (current.depth > 1) {
return '#f44336';
}
})
.attr('stroke', '#fff') // lines color
.attr('stroke-width', '2') // line width
.on('click', d => click(d, node, svg, self, x, y, radius, arc))
.on('mouseover', function (d) {
if (self.props.tooltip) {
d3.select(this).style('cursor', 'pointer');
tooltip.html(() => { const name = self.formatNameTooltip(d); return name; });
return tooltip.transition().duration(50).style('opacity', 1);
}
return null;
})
.on('mousemove', () => {
if (self.props.tooltip) {
tooltip
.style('top', `${d3.event.pageY - 50}px`)
.style('left', `${self.props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`);
}
return null;
})
.on('mouseout', function () {
if (self.props.tooltip) {
d3.select(this).style('cursor', 'default');
tooltip.transition().duration(50).style('opacity', 0);
}
return null;
})
} else {
svg.selectAll('path').data(partition(root).descendants());
}
svg.selectAll('path').transition().duration(1000).attrTween('d', (d, i) => self.arcTweenData(d, i, node, x, arc));
}
renderSunburst(props) {
if (props.data) {
const self = this, // eslint-disable-line
gWidth = props.width,
gHeight = props.height,
radius = (Math.min(gWidth, gHeight) / 2) - 10,
svg = d3.select('svg').append('g').attr('transform', `translate(${gWidth / 2},${gHeight / 2})`),
x = d3.scaleLinear().range([0, 2 * Math.PI]),
y = props.scale === 'linear' ? d3.scaleLinear().range([0, radius]) : d3.scaleSqrt().range([0, radius]),
partition = d3.partition(),
arc = d3.arc()
.startAngle(d => Math.max(0, Math.min(2 * Math.PI, x(d.x0))))
.endAngle(d => Math.max(0, Math.min(2 * Math.PI, x(d.x1))))
.innerRadius(d => Math.max(0, y(d.y0)))
.outerRadius(d => Math.max(0, y(d.y1))),
hueDXScale = d3.scaleLinear()
.domain([0, 1])
.range([0, 360]),
rootData = d3.hierarchy(props.data);
const firstBuild = true;
const node = rootData;
rootData.sum(d => d.size);
self.update(rootData, firstBuild, svg, partition, hueDXScale, x, y, radius, arc, node, self); // GO!
}
}
render() {
return (
<div id={this.props.keyId} className="text-center">
<svg style={{ width: parseInt(this.props.width, 10) || 480, height: parseInt(this.props.height, 10) || 400 }} id={`${this.props.keyId}-svg`} />
</div>
);
}
}
export default Sunburst;
Related
I am trying to make floodfill function by manipulating the pixel color.
When i scale the image to the canvas size, context.getImageData returns only rgba[0,0,0,0].
/* library */
import { useCallback, useEffect, useRef } from "react";
import { useSelector } from "react-redux";
/* module from local */
import { RootState } from "../../store";
import img1 from "../../test.png";
interface srcProps {
src: string;
}
export function ImageCanvas({ src }: srcProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const contextRef = useRef<CanvasRenderingContext2D | null>(null);
const awareness = useSelector((state: RootState) => state.yjs.awareness);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
canvas.addEventListener(
"touchmove",
(e) => {
e.preventDefault();
},
{ passive: false }
);
}, []);
useEffect(() => {
const img = new Image();
img.src = img1;
img.onload = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (ctx === null) return;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
contextRef.current = ctx;
};
}, []);
const _valid = useCallback((x: number, y: number) => {
console.log("Enter _valid");
const width = canvasRef.current?.offsetWidth as number;
const height = canvasRef.current?.offsetHeight as number;
console.log(`width: ${width}, height: ${height}`);
if (x > 0 && x < width && y > 0 && y < height) {
const imageData = contextRef.current?.getImageData(x, y, 1, 1);
console.log(imageData?.data);
if (
imageData?.data[0] === 255 &&
imageData?.data[1] === 255 &&
imageData?.data[2] === 255
) {
console.log("_valid => Success");
return true;
} else {
console.log("_valid => valid Failed!!");
return false;
}
}
}, []);
const _isPixel = useCallback((x: number, y: number) => {
console.log("Enter _isPixel");
const imageData = contextRef.current?.getImageData(x, y, 1, 1);
if (imageData) {
return imageData.data.length > 0;
} else {
return false;
}
}, []);
const _setPixel = useCallback((x: number, y: number) => {
console.log("Enter _setPixel");
const imageData = contextRef.current?.getImageData(x, y, 1, 1);
const pixelData = imageData?.data;
let color = awareness.getLocalState().color;
if (color === "black") {
color = "rgba(223,154,36)";
}
const values = color.match(/\d+/g).map(Number);
console.log("This is pixel data => _setPixel");
console.log(pixelData);
if (pixelData) {
pixelData[0] = values[0];
pixelData[1] = values[1];
pixelData[2] = values[2];
pixelData[3] = 255;
}
if (imageData) {
contextRef.current?.putImageData(imageData, x, y);
}
}, []);
const _fill = useCallback((x: number, y: number) => {
const fillStack = [];
fillStack.push([x, y]);
while (fillStack.length > 0) {
const [x, y]: any = fillStack.pop();
if (_valid(x, y)) {
console.log("It is valid");
} else {
console.log("NOT!! Valid");
continue;
}
if (_isPixel(x, y)) {
console.log("This is pixel");
} else {
console.log("NOT!! pixel");
continue;
}
_setPixel(x, y);
fillStack.push([x + 1, y]);
fillStack.push([x - 1, y]);
fillStack.push([x, y + 1]);
fillStack.push([x, y - 1]);
}
}, []);
const handlePointerDown = useCallback((e: React.PointerEvent<any>) => {
_fill(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
}, []);
return (
<canvas
ref={canvasRef}
onPointerDown={handlePointerDown}
style={{ touchAction: "none" }}
className={"w-full"}
/>
);
}
export default ImageCanvas;
What i tried:
runs fill function after the image is loaded
set ctx.imageSmoothingEnabled = false;
test if the canvas size is equal to the image (then the fill function works because i can get the right pixel color)
What i expect:
i want to get the right pixel color, even the image is scale to the canvas size which is full screen size.
i wanted to make a game and followed a tutorial to make a Tetris game, while playing it i noticed that when you press the space button the whole game resets, i cant figure out how to fix that. My harddrop function works when i press space so that works but then the game resets to. As far as i know there should be no other function set to the spacebar (keyCode === 32)
If i remove the harddrop function the game still restarts if i press space
this is my code:
const Tetris = () => {
const [dropTime, setDropTime] = useState(null);
const [gameOver, setGameOver] = useState(false);
const [player, updatePlayerPos, resetPlayer, playerRotate] = usePlayer();
const [stage, setStage, rowsCleared] = useStage(player, resetPlayer);
const [score, setScore, rows, setRows, level, setLevel] = useGameStatus(
rowsCleared
);
console.log('re-render');
const movePlayer = dir => {
if (!checkCollision(player, stage, { x: dir, y: 0 })) {
updatePlayerPos({ x: dir, y: 0 });
}
};
const keyUp = ({ keyCode }) => {
if (!gameOver) {
// Activate the interval again when user releases down arrow.
if (keyCode === 40) {
setDropTime(1000 / (level + 1));
}
}
};
const startGame = () => {
// Reset everything
setStage(createStage());
setDropTime(1000);
resetPlayer();
setScore(0);
setLevel(0);
setRows(0);
setGameOver(false);
};
const drop = () => {
// Increase level when player has cleared 10 rows
if (rows > (level + 1) * 10) {
setLevel(prev => prev + 1);
// Also increase speed
setDropTime(1000 / (level + 1) + 200);
}
if (!checkCollision(player, stage, { x: 0, y: 1 })) {
updatePlayerPos({ x: 0, y: 1, collided: false });
} else {
// Game over!
if (player.pos.y < 1) {
console.log('GAME OVER!!!');
setGameOver(true);
setDropTime(null);
}
updatePlayerPos({ x: 0, y: 0, collided: true });
}
};
const dropPlayer = () => {
setDropTime(null);
drop();
};
const hardDrop = () => {
let pot = 0;
while (!checkCollision(player, stage, { x: 0, y: pot })) {
setDropTime(5);
pot += 1;
}
updatePlayerPos({ x: 0, y: pot-1, collided: true });
}
// starts the game
useInterval(() => {
drop();
}, dropTime);
const move = ({ keyCode }) => {
if (!gameOver) {
if (keyCode === 37) {
movePlayer(-1);
} else if (keyCode === 39) {
movePlayer(1);
} else if (keyCode === 40) {
dropPlayer();
} else if (keyCode === 38) {
playerRotate(stage, 1);
} else if (keyCode === 32) {
hardDrop()
}
}
};
return (
<StyledTetrisWrapper
role="button"
tabIndex="0"
onKeyDown={e => move(e)}
onKeyUp={keyUp}
>
<StyledHeader/>
<StyledTetris>
<Stage stage={stage} />
<aside>
{gameOver ? (
<Display gameOver={gameOver} text="Game Over" />
) : (
<div className='aside-container'>
<Display text={`Score: ${score}`} />
<Display text={`rows: ${rows}`} />
<Display text={`Level: ${level}`} />
</div>
)}
<StartButton callback={startGame}></StartButton>
<button className='back-btn'
type='button'
onClick={(e) => {
e.preventDefault();
window.location.href="https://play-it-games.netlify.app/";
}}>Back to Play it!</button>
</aside>
</StyledTetris>
</StyledTetrisWrapper>
);
};
export default Tetris;
I have a component that draws a sequence that it gets from my redux store:
const sequence = useSelector(state => state.sequence)
My useEffect call that does the canvas draw is being triggered whenever ‘sequence’ updates:
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(sequence)
});
}, [sequence])
But for some reason the ‘sequence’ populated by useSelector is behind my store state. If I draw sequence instead of the store state it draws the correct state:
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(store.getState().sequence)
});
}, [sequence])
I don't get why I need to use the store state here instead of the selector. What am I not understanding?
Here is the full module:
import React, { useEffect, useRef } from 'react';
import store from '../app/store'
import { useSelector } from "react-redux";
import * as PIXI from 'pixi.js'
import SongPlayer from "../player/song-player";
Math.minmax = (value, min, max) => Math.min(Math.max(value, min), max);
var pixiapp;
var container;
var parentContainer;
var renderer;
var height;
var width;
var mouseIsDown = false;
var isDraggingVertical = false
var isDraggingHorizontal = false
const dragThresholdVertical = 10
const dragThresholdHorizontal = 10
const grey = 0x404040
const darkgrey = 0x707070
const green = 0x10a010
const brightRed = 0xdb5660
const brightGreen = 0x56db60
const blue = 0x0000ff
const borderColor = 0x1e1e1e
const envelopeLineColor = 0xff0000
const backgroundColor = 0x28282b
const darkgreen = 0x107010
const red = 0xff0000
const white = 0xffffff;
const colorVelocityBar = 0x5e5f68;
const middleNote = 60;
const minNote = middleNote - 24;
const maxNote = middleNote + 24;
const borderWidth = 10;
const bottomBorderHeight = 20;
const topBorderHeight = 20;
const gapWidth = 4;
const barWidth = 50;
const numSteps = 16;
const lineThickness = 0.005;
const DragTargets = {
None: 0,
NoteBox: 1,
OctaveBox: 2,
VelocityBox: 3,
}
const DragDirections = {
None: 0,
Vertical: 1,
Horizontal: 2,
}
var dragTarget
var dragDirection
var dragStepNum = -1
var dragStartPos = { x: -1, y: -1, }
const noteBarTextStyle = new PIXI.TextStyle({
align: "center",
fill: "#000000",
fontFamily: "Helvetica",
fontSize: 12
});
const turnAKnobTextStyle = new PIXI.TextStyle({
align: "center",
fill: "#000000",
fontFamily: "Helvetica",
fontSize: 36,
});
const noteName = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
]
function getControllerValueTop(value, minValue, maxValue) {
// return bottomBorderHeight + (value - minvalue) / (maxValue - minvalue) * (height - bottomBorderHeight - topBorderHeight) / 127
return height - bottomBorderHeight - (value - minValue) / (maxValue - minValue) * (height - bottomBorderHeight - topBorderHeight);
}
function getTimeX(sequence, time) {
const maxTime = sequence.steps.length * sequence.tempo / 60 / sequence.division
// console.log(`maxTime ${maxTime} sequence.steps.length ${sequence.steps.length} sequence.tempo ${sequence.tempo} sequence.division ${sequence.division}`)
const timex = borderWidth + time / maxTime * (width - borderWidth - borderWidth)
// console.log(`time ${time} timex ${timex} borderWidth ${borderWidth} maxTime ${maxTime} width ${width}`)
return timex
}
// function getXTime(sequence, x) {
// const maxTime = sequence.steps * sequence.tempo / 60 / sequence.division
// return borderWidth + x / (width - borderWidth - borderWidth) * maxTime
// }
function getNoteTop(note) {
return height - bottomBorderHeight - (note + 12 - minNote) / (maxNote - minNote + 12) * (height - bottomBorderHeight - topBorderHeight);
}
function getNoteBottom(note) {
return getNoteTop(note - 12) + lineThickness;
// return bottomBorderHeight - lineThickness + (note - minNote) / (maxNote - minNote + 12) * (1 - bottomBorderHeight - topBorderHeight);
}
function getNoteHeight() {
return (height - bottomBorderHeight - topBorderHeight) * 12 / (maxNote - minNote + 12);
}
function getStepLeft(sequence, stepNum) {
var stride = getStepStride(sequence)
// console.log(`getStepLeft: borderWidth ${borderWidth} num steps ${sequence.steps.length} stride ${stride}`)
return borderWidth + stepNum * stride;
}
function getStepRight(sequence, stepNum) {
var stride = (width - (borderWidth * 2)) / sequence.steps.length;
var step = sequence.steps[stepNum];
return getStepLeft(sequence, stepNum) + stride * step.gateLength - lineThickness;
}
function getStepStride(sequence) {
var stride = (width - (borderWidth * 2)) / sequence.steps.length;
// console.log(`getStepStride: ${sequence.numSteps} -> ${stride}`)
return stride
}
function getStepWidth(sequence, stepNum) {
var stride = getStepStride(sequence);
var step = sequence.steps[stepNum];
return stride * step.gateLength;
}
function drawSequenceCanvas(sequence) {
const sequencePlayer = SongPlayer.getSequencePlayer(sequence._id)
// console.log(`draw: sequence player is ${JSON.stringify(sequencePlayer)}`)
const stepnum = sequencePlayer ? sequencePlayer.stepNum : -1;
// console.log(`stepnum ${stepnum}`)
if (container) {
container.destroy({ children: true })
}
container = new PIXI.Container();
parentContainer.addChild(container);
// Background
var bar = new PIXI.Graphics();
// bar.lineStyle(2, 0x00FFFF, 1);
bar.beginFill(backgroundColor, 1);
bar.drawRect(0, 0, width, height);
bar.endFill();
container.addChild(bar);
// Top border
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, 0, width, topBorderHeight, 16);
bar.endFill();
container.addChild(bar);
// Bottom border
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, height-bottomBorderHeight, width, bottomBorderHeight);
bar.endFill();
container.addChild(bar);
// Sides
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, topBorderHeight, borderWidth, height - bottomBorderHeight - topBorderHeight);
bar.drawRect(width - borderWidth, topBorderHeight, borderWidth, height - bottomBorderHeight - topBorderHeight);
bar.endFill();
container.addChild(bar);
// Velocity bars
var stepNum = 0;
var barColor = colorVelocityBar;
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
bar.beginFill(barColor, 1);
var barHeight = height - bottomBorderHeight - getControllerValueTop(step.velocity, 0, 127);
bar.drawRect(getStepLeft(sequence, stepNum), height - bottomBorderHeight - barHeight, getStepWidth(sequence, stepNum), barHeight);
bar.endFill();
container.addChild(bar);
++stepNum;
})
// Horizontal grid lines
const lineColor = borderColor;
var lines = new PIXI.Graphics();
// console.log(`minnote ${minNote} ${getNoteTop(minNote)} ${getNoteTop(minNote - 12)}`)
lines.position.set(borderWidth, getNoteTop(minNote - 12));
lines.lineStyle(2, lineColor, 1)
.moveTo(borderWidth, getNoteTop(minNote - 12))
.lineTo(width - borderWidth, getNoteTop(minNote - 12))
.moveTo(borderWidth, getNoteTop(minNote))
.lineTo(width - borderWidth, getNoteTop(minNote))
.moveTo(borderWidth, getNoteTop(minNote + 12))
.lineTo(width - borderWidth, getNoteTop(minNote + 12))
.moveTo(borderWidth, getNoteTop(minNote + 24))
.lineTo(width - borderWidth, getNoteTop(minNote + 24))
.moveTo(borderWidth, getNoteTop(minNote + 36))
.lineTo(width - borderWidth, getNoteTop(minNote + 36))
.moveTo(borderWidth, getNoteTop(minNote + 48))
.lineTo(width - borderWidth, getNoteTop(minNote + 48))
container.addChild(lines);
// Note bars
stepNum = 0;
console.log(`SequenceCanvas.draw - note bars - ${JSON.stringify(sequence.steps)}`);
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
barColor = sequencePlayer && stepNum === sequencePlayer.stepNum ? brightRed : brightGreen;
bar.beginFill(barColor, 1);
var barHeight = height - bottomBorderHeight - getControllerValueTop(step.velocity, 0, 127);
bar.drawRoundedRect(getStepLeft(sequence, stepNum), getNoteTop(step.note), getStepWidth(sequence, stepNum), getNoteHeight(), 8);
bar.endFill();
container.addChild(bar);
const noteText = new PIXI.Text(noteName[step.note % 12], noteBarTextStyle)
noteText.anchor.set(0.5);
// noteText.width = getStepStride(sequence)
noteText.x = getStepLeft(sequence, stepNum) + getStepWidth(sequence, stepNum) / 2
noteText.y = getNoteTop(step.note) + getNoteHeight() / 2
container.addChild(noteText)
++stepNum;
})
// Bottom bars
stepNum = 0;
barColor = green;
// console.log(`SequenceCanvas.Init ${JSON.stringify(sequence.steps)}`);
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
bar.beginFill(0x707481, 1);
bar.drawRoundedRect(getStepLeft(sequence, stepNum), height - bottomBorderHeight, getStepWidth(sequence, stepNum), bottomBorderHeight, 4);
bar.endFill();
container.addChild(bar);
const noteText = new PIXI.Text((step.note % 12), noteBarTextStyle)
noteText.anchor.set(0.5);
// noteText.width = getStepStride(sequence)
noteText.x = getStepLeft(sequence, stepNum) + getStepStride(sequence) / 2
noteText.y = height - bottomBorderHeight + 10
container.addChild(noteText)
++stepNum;
})
if (sequence.currentEnvelopeId != 'notes') {
DrawEnvelopes(container, sequence)
}
// // Random box for testing
// bar = new PIXI.Graphics();
// bar.lineStyle(2, 0x00FFFF, 1);
// bar.beginFill(0x650A5A, 0.25);
// bar.drawRoundedRect(height, getNoteTop(minNote), 50, getNoteHeight(minNote), 16);
// bar.endFill();
// container.addChild(bar);
}
function DrawEnvelopes(container, sequence) {
if (sequence.currentEnvelopeId) {
console.log(`DrawEnvelopes: ${sequence.currentEnvelopeId}`)
if (sequence.currentEnvelopeId == "notes") {
// don't draw envelopes
} else if (sequence.currentEnvelopeId == "new" || sequence.currentEnvelopeId == "learn") {
console.log(`draw warning`)
const bar = new PIXI.Graphics();
bar.beginFill(white, 0.5);
bar.drawRect(0, 0, width, height);
bar.endFill();
container.addChild(bar);
const turnAKnobText = new PIXI.Text("Turn a knob to map", turnAKnobTextStyle)
turnAKnobText.anchor.set(0.5);
turnAKnobText.width = width / 2
turnAKnobText.x = width / 2
turnAKnobText.y = height / 2
container.addChild(turnAKnobText)
} else {
DrawEnvelope(container, sequence, sequence.currentEnvelopeId)
}
}
}
function DrawEnvelope(container, sequence, envelopeId) {
console.log(`DrawEnvelope: ${envelopeId} ${JSON.stringify(sequence)}`)
const envelope = sequence.envelopes[envelopeId]
if (!envelope) {
return
}
console.log(`DrawEnvelope: ${envelopeId} - envelope.controller ${JSON.stringify(envelope.controller)}`)
if (envelope.controller !== "notes") {
console.log(`DrawEnvelope: ${envelope.controller}`)
if (envelope.controller == null) {
} else {
const points = envelope.points
// console.log(`DrawEnvelope: We have ${envelope.controllers.length} controllers with points ${JSON.stringify(points)}`)
// console.log(`DrawEnvelope: point 0 ${JSON.stringify(points[0])}`)
// Lines
const lineColor = envelopeLineColor;
var lines = new PIXI.Graphics();
lines.position.set(0, 0);
lines.lineStyle(1, lineColor, 1)
var y = getControllerValueTop(points[0].value, 0, 127)
lines.moveTo(0, y)
for (const [pointTime, point] of Object.entries(points)) {
var x = getTimeX(sequence, point.time)
y = getControllerValueTop(point.value, 0, 127)
// console.log(`point ${JSON.stringify(point)} draw line to time ${point.time} value ${point.value} ${x},${y}`)
lines.lineTo(x, y)
}
lines.lineTo(width, y)
// Dots
lines.fillStyle = { color: lineColor, alpha: 0.5, visible: true }
lines.beginFill(lineColor)
for (const [pointTime, point] of Object.entries(points)) {
var x = getTimeX(sequence, point.time)
y = getControllerValueTop(point.value, 0, 127)
// console.log(`point ${JSON.stringify(point)} draw line to time ${point.time} value ${point.value} ${x},${y}`)
lines.drawCircle(x, y, 4)
}
container.addChild(lines);
}
}
}
const handlePointerDown = e => {
const sequence = store.getState().sequence
console.log(`handlePointerDown ${mouseIsDown} ${JSON.stringify(e.data)}`)
mouseIsDown = true;
var x = e.data.global.x
var y = e.data.global.y
dragStartPos = { x: x, y: y, }
dragStepNum = -1
for (var stepNum = 0; stepNum < sequence.steps.length; stepNum++) {
var stepleft = getStepLeft(sequence, stepNum);
if (x > stepleft && x < getStepRight(sequence, stepNum)) {
dragStepNum = stepNum
console.log(`tapped step ${dragStepNum}`)
break
}
}
console.log(`handlePointerDown: ${x},${y} dragStepNum ${dragStepNum} target ${dragTarget}`)
console.log(`handlePointerDown: step ${JSON.stringify(sequence.steps[stepNum])}`)
if (dragStartPos.y > height - bottomBorderHeight) {
dragTarget = DragTargets.NoteBox
} else if (dragStartPos.y < getNoteBottom(sequence.steps[stepNum].note) && dragStartPos.y > getNoteTop(sequence.steps[stepNum].note)) {
dragTarget = DragTargets.OctaveBox
} else if (dragStartPos.y > bottomBorderHeight && dragStartPos.y < height - topBorderHeight) {
console.log(`drag velocity box y = ${dragStartPos.y}`)
dragTarget = DragTargets.VelocityBox
}
console.log(`handlePointerDown: step ${dragStepNum} target ${dragTarget}`)
}
const handlePointerUp = e => {
const sequence = store.getState().sequence
console.log(`handlePointerUp ${mouseIsDown}`)
mouseIsDown = false;
isDraggingVertical = false;
isDraggingHorizontal = false;
}
const handlePointerMove = e => {
const sequence = store.getState().sequence
if (mouseIsDown) {
var x = e.data.global.x
var y = e.data.global.y
if (x !== null && y !== null && x !== Infinity && y !== Infinity) {
// console.log(`drag ${JSON.stringify(e.data)}`)
if (dragTarget === DragTargets.OctaveBox) {
// Detect direction before we start editing
if (!isDraggingVertical && !isDraggingHorizontal) {
if (Math.abs(y - dragStartPos.y) > dragThresholdVertical) {
isDraggingVertical = true;
}
else if (Math.abs(x - dragStartPos.x) > dragThresholdHorizontal) {
isDraggingHorizontal = true;
}
else {
return
}
}
if (isDraggingVertical) {
console.log(`drag note: ${sequence.steps[dragStepNum].note} ${x},${y} `)
const notenum = sequence.steps[dragStepNum].note
if (y < getNoteTop(notenum)) {
console.log(`drag y ${y} < ${getNoteTop(notenum)} getNoteTop note for note ${notenum}, dragStepNum ${dragStepNum}`)
store.dispatch({type: "sequence/stepNote", payload: {stepNum: dragStepNum, note: notenum + 12}})
} else if (y > getNoteBottom(notenum)) {
console.log(`drag y ${y} > ${getNoteBottom(notenum)} getNoteBottom for note ${notenum}, dragStepNum ${dragStepNum}`)
store.dispatch({type: "sequence/stepNote", payload: {stepNum: dragStepNum, note: notenum - 12}})
}
}
else {
var stride = getStepStride(sequence);
// note.gateLength
// x = Math.minmix(x, getStepLeft(sequence, dragStepNum), getStepRight(sequence, dragStepNum))
var newGateLength = (x - getStepLeft(sequence, dragStepNum)) / getStepStride(sequence)
if (x < getStepLeft(sequence, dragStepNum)) {
newGateLength = 0;
} else if (x > getStepLeft(sequence, dragStepNum) + getStepStride(sequence)) {
newGateLength = 1.0
}
store.dispatch({
type: "sequence/stepGateLength",
payload: {stepNum: dragStepNum, gateLength: newGateLength}
})
}
}
else if (dragTarget === DragTargets.NoteBox) {
const notenum = sequence.steps[dragStepNum].note
if (y - dragStartPos.y > bottomBorderHeight) {
store.dispatch({ type: "sequence/stepNote", payload: { stepNum: dragStepNum, note: notenum - 1 } })
dragStartPos.y = y
} else if (y - dragStartPos.y < -bottomBorderHeight) {
store.dispatch({ type: "sequence/stepNote", payload: { stepNum: dragStepNum, note: notenum + 1 } })
dragStartPos.y = y
}
}
else if (dragTarget === DragTargets.VelocityBox) {
const velocity = sequence.steps[dragStepNum].velocity
y = Math.minmax(y, topBorderHeight, height - bottomBorderHeight)
const newVelocity = 127 - (127 * (y - topBorderHeight) / (height - topBorderHeight - bottomBorderHeight))
console.log(`drag velocitybox ${y} => ${newVelocity}`)
store.dispatch({ type: "sequence/stepVelocity", payload: { stepNum: dragStepNum, velocity: newVelocity } })
}
}
}
}
function SequenceCanvas(props) {
console.log(`hi from SequenceCanvas ${JSON.stringify(props)}`)
// The React way to get a dom element is by giving it a ref prop
const canvasRef = useRef(null)
const sequence = useSelector(state => state.sequence)
useEffect( () => {
// console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
width = props.width;
height = props.height;
console.log("new PIXI.Application")
pixiapp = new PIXI.Application({
view: canvasRef.current,
powerPreference: 'high-performance',
backgroundAlpha: false,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
width: width,
height: height
});
renderer = PIXI.autoDetectRenderer();
parentContainer = new PIXI.Container();
parentContainer.interactive = true
parentContainer.on('pointerdown', (e) => { handlePointerDown(e) })
parentContainer.on('pointerup', (e) => { handlePointerUp(e) })
parentContainer.on('mouseupoutside', (e) => { handlePointerUp(e) })
parentContainer.on('pointermove', (e) => { handlePointerMove(e) })
pixiapp.stage.addChild(parentContainer);
drawSequenceCanvas(sequence)
return () => {
parentContainer.removeAllListeners()
}
}, [])
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(store.getState().sequence)
});
}, [sequence])
const timerIdRef = useRef(0)
function handleInterval() {
console.log(`hi from sequenceCanvas.IntervalTimer`)
drawSequenceCanvas(sequence)
}
const startIntervalTimer = () => {
console.log(`SequenceCanvas.startIntervalTimer - timerIdRef.current === ${timerIdRef.current}`)
if (timerIdRef.current == 0) {
timerIdRef.current = setInterval(() => {
handleInterval()
})
}
}
const stopIntervalTimer = () => {
console.log(`SequenceCanvas.stopIntervalTimer`)
clearInterval(timerIdRef.current)
timerIdRef.current = 0
}
useEffect(() => {
console.log(`startIntervalTime from useEffect`)
// startIntervalTimer();
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<div>
<canvas {...props} ref={canvasRef}>
Your browser does not support the HTML canvas tag.
</canvas>
</div>
</div>
);
}
// export {SequenceCanvas, drawSequenceCanvas};
export {SequenceCanvas};
Every time that sequence is changed - you create new store subscribe on useEffect hook - more and more. You can make it work like this:
const sequence = useSelector(state => state.sequence);
const sequenceRef = useRef(sequence);
sequenceRef.current = sequence;
useEffect(() => {
return store.subscribe(() => { // <--- return will unsubscribe from store
drawSequenceCanvas(sequenceRef.current); // <---actual link
});
}, []);
or you can do it like this
const sequence = useSelector(state => state.sequence);
useEffect(() => {
return store.subscribe(() => { // <--- return will unsubscribe from store
drawSequenceCanvas(sequence);
});
}, [sequence]);
BUT - useSelector hook is already uses store subscribe - so you can do just like this:
const sequence = useSelector(state => state.sequence);
useEffect(() => {
drawSequenceCanvas(sequence);
}, [sequence]);
// OR with usePrevious hook - https://usehooks.com/usePrevious/
const sequence = useSelector(state => state.sequence);
const prevSequence = usePrevious(sequence);
if (sequence !== prevSequence) {
drawSequenceCanvas(sequence);
}
I am working on drawing app project where I am using react.js and react-konva. Now I am trying to add multiple shape selection for resizing feature. I find a codesandbox example but when I am trying to run the code on my browser adding the code in my project, I am not getting expected output that is shown in codesandbox output. Only the last element is resizing, not the previous elements. and also cannot able to add cntrl and shift key button selection feature as well.
The example is - https://codesandbox.io/s/react-konva-multiple-selection-forked-wysowd?file=/src/index.js:6016-6045
I am sharing the code below of App.js:
import "./App.css";
import { Layer, Rect, Stage, Transformer } from "react-konva";
import { useEffect, useRef, useState } from "react";
import Rectangle from "./Rectangle";
import { v4 as uuidv4 } from 'uuid';
const initialRectangle = [
{
x: 10,
y: 10,
width: 100,
height: 100,
fill: "red",
id: uuidv4(),
},
{
x: 150,
y: 150,
width: 100,
height: 100,
fill: "green",
id: uuidv4(),
},
{
x: 300,
y: 300,
width: 100,
height: 100,
fill: "blue",
id: uuidv4(),
}
];
function App() {
const selectionRect = useRef(null);
const [rectangles, setRectangles] = useState(initialRectangle);
const [nodesArray, setNodes] = useState([]);
const trRef = useRef(null);
const [selectedId, selectShape] = useState(null);
const layerRef = useRef(null);
const Konva = window.Konva;
const selection = useRef({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
const updateSelection = () => {
const node = selectionRect.current;
node.setAttrs({
visible: selection.current.visible,
x: Math.min(selection.current.x1, selection.current.x2),
y: Math.min(selection.current.y1, selection.current.y2),
width: Math.abs(selection.current.x1 - selection.current.x2),
height: Math.abs(selection.current.y1 - selection.current.y2),
fill: "rgba(0, 161, 255, 0.3)",
});
};
const onMouseDown = (e) => {
const isElement = e.target.findAncestor(".elements-container");
const isTransformer = e.target.findAncestor("Transformer");
if (isElement || isTransformer) {
return;
}
const pos = e.target.getStage().getPointerPosition();
selection.current.x1 = pos.x;
selection.current.y1 = pos.y;
selection.current.x2 = pos.x;
selection.current.y2 = pos.y;
selection.current.visible = true;
updateSelection();
};
const onMouseMove = (e) => {
if (!selection.current.visible) {
return;
}
const pos = e.target.getStage().getPointerPosition();
selection.current.x2 = pos.x;
selection.current.y2 = pos.y;
updateSelection();
};
const onMouseUp = (e) => {
if (!selection.current.visible) {
return;
}
const selBox = selectionRect.current.getClientRect();
const elements = [];
// console.log({layerRef});
layerRef.current.find(".rectangle").forEach((node) => {
const nodeBox = node.getClientRect();
if (Konva.Util.haveIntersection(selBox, nodeBox)) {
elements.push(node);
}
});
trRef.current.nodes(elements);
console.log({trRef})
selection.current.visible = false;
Konva.listenClickTap = false;
updateSelection();
};
const onClickTap = (e) => {
if (selection.current.visible) {
return;
}
let stage = e.target.getStage();
let layer = layerRef.current;
let tr = trRef.current;
if (e.target === stage) {
selectShape(null);
tr.nodes([]);
setNodes([]);
layer.batchDraw();
return;
}
if (!e.target.hasName("rectangle")) {
return;
}
const metaPressed = e.evt.ctrlKey || e.evt.shiftKey;
// console.log({nodesArray});
const isSelected = tr.nodes().indexOf(e.target) >= 0;
// console.log({isSelected})
// console.log(tr.nodes().indexOf(e.target));
// console.log(metaPressed, isSelected);
// if (!metaPressed && !isSelected) {
// selectShape(e.target.id());
// tr.nodes([e.target]);
// setNodes([e.target]);
// } else if (metaPressed && !isSelected) {
// selectShape(e.target.id());
// tr.nodes([...tr.nodes(), e.target]);
// setNodes([...nodesArray, e.target]);
// } else if (metaPressed && isSelected) {
// selectShape(null);
// const nodes = tr.nodes();
// console.log(nodes);
// const index = nodes.indexOf(e.target);
// nodes.splice(index, 1);
// setNodes(nodes);
// tr.nodes(nodes);
// }
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
tr.nodes([e.target]);
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
const nodes = tr.nodes().slice(); // use slice to have new copy of array
// remove node from array
nodes.splice(nodes.indexOf(e.target), 1);
tr.nodes(nodes);
} else if (metaPressed && !isSelected) {
// add the node into selection
const nodes = tr.nodes().concat([e.target]);
tr.nodes(nodes);
}
trRef.current.getLayer().batchDraw();
};
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onClick={onClickTap}
>
<Layer ref={layerRef}>
{rectangles.map((rect, i) => (
<Rectangle
key={i}
shapeProps={rect}
isSelected={rect.id === selectedId}
onSelect={(e) => {
// if (e.current !== undefined) {
// let temp = nodesArray;
// if (!nodesArray.includes(e.current)) temp.push(e.current);
// setNodes(temp);
// trRef.current.nodes(nodesArray);
// trRef.current.getLayer().batchDraw();
// }
selectShape(rect.id);
}}
onChange={(newAttrs) => {
const copyOfRectangles = JSON.parse(JSON.stringify(rectangles));
const currentIndex = rectangles.findIndex(item => item.id === newAttrs.id);
copyOfRectangles[currentIndex] = newAttrs;
setRectangles(copyOfRectangles);
console.log(copyOfRectangles);
}}
/>
))}
<Transformer
ref={trRef}
boundBoxFunc={(oldBox, newBox) => {
if (newBox.width < 5 || newBox.height < 5) {
return oldBox;
}
return newBox;
}}
/>
<Rect fill="blue" ref={selectionRect} />
</Layer>
</Stage>
);
}
export default App;
the below code is Rectangle component -
import React from 'react'
import { Rect } from 'react-konva';
const Rectangle = ({shapeProps, onSelect, onChange}) => {
const shapeRef = React.useRef(null);
return (
<Rect
{...shapeProps}
onClick={() => onSelect(shapeRef)}
// onDragStart={() => onChange(shapeRef)}
onDragEnd={(e) => onChange({
...shapeProps,
x: e.target.x(),
y: e.target.y(),
})}
ref={shapeRef}
onTap={() => onSelect(shapeRef)}
name="rectangle"
draggable
onTransformEnd={(e) => {
const node = shapeRef.current;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
// we will reset it back
node.scaleX(1);
node.scaleY(1);
onChange({
...shapeProps,
x: node.x(),
y: node.y(),
// set minimal value
width: Math.max(5, node.width() * scaleX),
height: node.height() * scaleY
});
}
}
/>
)
}
export default Rectangle
What is the problem I don't understand.
I faced an issue with react-p5-wrapper that it is running in background although I have switch to another route in my react app.
For example, I am currently in /game, and the console is logging "running draw", but when I switch to /about-us, it still logging, meaning it is still running the draw function
Here is my code in sandbox
App.js
import "./styles.css";
import { Route, Switch, BrowserRouter as Router, Link } from "react-router-dom";
export default function App() {
return (
<div className="App">
<Router>
<Link to="/game">Game</Link> | <Link to="/about-us">About Us</Link>
<Switch>
<Route path="/about-us" component={require("./abtus").default} />
<Route path="/game" component={require("./game").default} />
</Switch>
</Router>
</div>
);
}
game.js
import { useEffect } from "react";
import { ReactP5Wrapper, P5Instance } from "react-p5-wrapper";
// Sound
let pointSound, endSound;
let playEndSound = false;
/**
* #param {P5Instance} p
*/
const sketch = (p) => {
const MAX_SPEED = 15;
const pickDirections = () => {
return ((Math.floor(Math.random() * 3) % 2 === 0 ? 1 : -1) * (Math.floor(Math.random() * 2) + 1));
};
const randomXPos = () => {
return p.random(30, p.width - 30);
};
const ramdomImgIndex = () => {
return Math.floor(Math.random() * imgs.length);
};
const reset = () => {
score = 0;
speed = 2;
falls = [
{
y: -70,
x: randomXPos(),
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
}
];
};
const rotate_n_draw_image = (image,img_x,img_y,img_width,img_height,img_angle) => {
p.imageMode(p.CENTER);
p.translate(img_x + img_width / 2, img_y + img_width / 2);
p.rotate((Math.PI / 180) * img_angle);
p.image(image, 0, 0, img_width, img_height);
p.rotate((-Math.PI / 180) * img_angle);
p.translate(-(img_x + img_width / 2), -(img_y + img_width / 2));
p.imageMode(p.CORNER);
};
// Images
let imgs = [],
basket = { img: null, width: 150, height: 150 },
imgSize = 50;
let screen = 0,
falls = [
{
y: -70,
x: randomXPos(),
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
}
],
score = 0,
speed = 2;
const startScreen = () => {
p.background(205, 165, 142);
p.fill(255);
p.textAlign(p.CENTER);
p.text("WELCOME TO MY CATCHING GAME", p.width / 2, p.height / 2);
p.text("click to start", p.width / 2, p.height / 2 + 20);
reset();
};
const gameOn = () => {
p.background(219, 178, 157);
p.textAlign(p.LEFT);
p.text("score = " + score, 30, 20);
falls = falls.map(({ x, y, rotation, direction, imgIndex }) => {
// rotate while dropping
rotation += direction;
// dropping
y += speed;
return { x, y, rotation, direction, imgIndex };
});
falls.forEach(({ x, y, rotation, imgIndex }, i) => {
// when is lower than the border line
if (y > p.height) {
screen = 2;
playEndSound = true;
}
// when reaching the border line and is within the range
if (
y > p.height - 50 &&
x > p.mouseX - basket.width / 2 &&
x < p.mouseX + basket.width / 2
) {
// Play Sound
pointSound.currentTime = 0;
pointSound.play();
// Increase Score
score += 10;
// Increase Speed
if (speed < MAX_SPEED) {
speed += 0.1;
speed = parseFloat(speed.toFixed(2));
}
// Whether add new item into array or not
if (i === falls.length - 1 && falls.length < 3) {
falls.push({
x: randomXPos(),
y: -70 - p.height / 3,
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
});
}
falls[i].y = -70;
falls[i].x = randomXPos();
falls[i].imgIndex = ramdomImgIndex();
}
rotate_n_draw_image(imgs[imgIndex], x, y, imgSize, imgSize, rotation);
});
p.imageMode(p.CENTER);
p.image(
basket.img,
p.mouseX,
p.height - basket.height / 2,
basket.width,
basket.height
);
};
const endScreen = () => {
if (playEndSound) {
endSound.play();
playEndSound = false;
}
p.background(205, 165, 142);
p.textAlign(p.CENTER);
p.text("GAME OVER", p.width / 2, p.height / 2);
p.text("SCORE = " + score, p.width / 2, p.height / 2 + 20);
p.text("click to play again", p.width / 2, p.height / 2 + 60);
};
p.preload = () => {
// Load Images
imgs[0] = p.loadImage("https://dummyimage.com/400x400");
imgs[1] = p.loadImage("https://dummyimage.com/400x400");
imgs[2] = p.loadImage("https://dummyimage.com/401x401");
basket.img = p.loadImage("https://dummyimage.com/500x500");
};
p.setup = () => {
p.createCanvas(
window.innerWidth > 400 ? 400 : window.innerWidth,
window.innerHeight > 500 ? 500 : window.innerHeight
);
};
p.draw = () => {
console.log("running draw");
switch (screen) {
case 0:
startScreen();
break;
case 1:
gameOn();
break;
case 2:
endScreen();
break;
default:
}
};
p.mousePressed = () => {
if (screen === 0) {
screen = 1;
} else if (screen === 2) {
screen = 0;
}
};
};
const CatchingGmae = () => {
useEffect(() => {
// eslint-disable-next-line
pointSound = new Audio("/game/points.wav");
// eslint-disable-next-line
endSound = new Audio("/game/end.wav");
pointSound.volume = 0.3;
return () => {
pointSound.muted = true;
endSound.muted = true;
};
});
return (
<div className="mx-auto flex justify-center items-center">
<ReactP5Wrapper sketch={sketch} />
</div>
);
};
export default CatchingGame;
Is there anyway to stop it from running in background when user switches route?
Given your setup, I can see two ways of telling the sketch to stop when route is switched and the Game react component is not rendered anymore.
Alt 1. You can make something similar to react-p5-wrapper
documentation, reacting to props:
In CatchingGmae component:
const [lastRender, setLastRender] = useState(Date.now());
useEffect(() => {
const interval = setInterval(() => setLastRender(Date.now()), 100);
return () => {
clearInterval(interval);
};
}, []);
return (
<>
<div className="mx-auto flex justify-center items-center">
<ReactP5Wrapper sketch={sketch} lastRender={lastRender} />
In sketch:
let lastRender = 0;
p.updateWithProps = (props) => {
lastRender = props.lastRender;
};
p.draw = () => {
if (!(Date.now() > lastRender + 100)) {
console.log("running draw");
☝ The problem with the Alt 1 is that react will do calculations and re-render frequently for no reason.
Alt 2. Use a state outside of React, a very simple side-effect
for the component, for the sketch to poll on.
Add to CatchingGmae component:
useEffect(() => {
window.noLoop = false;
return () => {
window.noLoop = true;
};
}, []);
Inside p.draw:
if (window.noLoop) return p.noLoop();
☝ This works without calculations, but you might want to scope the global within your own namespace or using other state manager.