Hey I am new to react konva and wanted to create an animation in which a rectangle follows a user defined path of dots. First user defines the path by clicking on screen and initializing the destination dots and then the rectangle should follow the path accordingly
const [coords, setCoords] = useState([]); //path dots variable
const [points, setPoints] = useState([250, 250]); //rectangle x,y variable
const ref= useRef(null);
const [check, setCheck] = useState(false); //check wheather user has clicked the start tracing button
const [i, setI] = useState(0); //index state variable
const handleStart= () => {
if (check === false) {
setCheck(true);
} else {
setCheck(false);
}
};
const handleClick = (e) => {
if (check === false) {
var stage = e.target.getStage();
var newcoord = stage.getPointerPosition();
var temp = [newcoord.x, newcoord.y];
setCoords((coords) => [...coords, temp]);
}
};
useEffect(() => {
if (!check) {
return;
}
var node = ref.current;
var anim = new Konva.Animation(function (frame) {
if (frame.time / 10 >= coords[i][0]) {
alert("reached");
setPoints([coords[i][0], coords[i][1]]);
setI(i + 1);
} else {
node.x(frame.time / 10);
}
if (frame.time / 10 >= coords[i][1]) {
alert("reached");
setPoints([coords[i][0], coords[i][1]]);
setI(i + 1);
} else {
node.y(frame.time / 10);
}
}, node.getLayer());
anim?.start();
return () => anim?.stop();
}, [check, i]);
return (
<div>
<Stage
onMouseDown={(e) => handleClick(e)}
width={window.innerWidth}
height={window.innerHeight}
>
<Layer>
<Group >
<Rect
width={50}
height={50}
x={points[0]}
y={points[1]}
strokeWidth={2}
fill="black"
opacity={1}
draggable
ref={ref}
/>
</Group>
{coords.map((key, index) => (
<Circle
x={key[0]}
y={key[1]}
numPoints={1}
radius={4}
fill="black"
strokeWidth={2}
/>
))}
</Layer>
</Stage>
<Button onClick={handleStart}>Start Tracing</Button>
</div>
this is my code but it doesnt seem to work as intended.Any help is much appreciated.
PS if you have any queries plz lemme know
You can use some Konva API to work with the path and get points on it.
useEffect(() => {
if (!check) {
return;
}
var node = ref.current;
// generate path from points
let data = coords
.map(([x, y], index) => {
if (index === 0) {
return `M ${x} ${y}`;
}
return `L ${x} ${y}`;
})
.join(" ");
const firstPoint = coords[0];
data += ` L ${firstPoint[0]} ${firstPoint[1]}`;
const path = new Konva.Path({
data
});
var anim = new Konva.Animation(function (frame) {
const length = path.getLength();
const delta = ((frame.time / 2000) % 1) * length;
const point = path.getPointAtLength(delta);
if (point) {
node.position(point);
}
}, node.getLayer());
anim?.start();
return () => {
anim?.stop();
path.destroy();
};
}, [check, i]);
https://codesandbox.io/s/react-konva-follow-path-mwd2w
Related
So I'm trying to create an infinite scroll component that will display if the component is scrolling left, right, or not at all. I'm running into alot of issues using hooks. Ive tried combining this https://dirask.com/posts/React-scroll-stop-event-DkBe01 with this https://codesandbox.io/s/icy-river-wr0uk?file=/src/App.js but could not get it functioning. Lastly, any ideas why it smoothly transitions when I scroll right but it stops upon scrolling left? Thank you guys for your help, this is my first post and I appreciate any help from the community!
import React, { Component, useEffect, useRef, useReducer, useState } from 'react'
import Card from './Card'
import './skills.scss'
import myImage from './Images/editIcon.png'
function SkillsFunction() {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{disableScroll: false, scrollWidth: 0, scrollPos: 1, clonesWidth: 0}
);
const scrollContainerRefy = useRef();
function reCalc() {
let scrollPos = scrollPos;
let scrollWidth = scrollContainerRefy.current.clientWidth;
let clonesWidth = getClonesWidth();
if (scrollPos <= 0) {
scrollPos = 1;
}
setState({
scrollPos: scrollPos,
scrollWidth: scrollWidth,
clonesWidth: clonesWidth,
});
};
// function sideScroll(element,direction,speed,distance,step) {
// let scrollAmount = 0;
// var slideTimer = setInterval(() => {
// if(direction == 'left'){
// element.scrollLeft -= step;
// } else {
// element.scrollLeft += step;
// }
// scrollAmount += step;
// if(scrollAmount >= distance){
// window.clearInterval(slideTimer);
// }
// }, speed);
// };
function handleScroll(e) {
const container = e.currentTarget;
const scrollWidth = container.scrollWidth;
const clonesWidth = getClonesWidth();
let scrollPos = container.scrollLeft;
let scrollPosAdd;
container.clientWidth > clonesWidth ? scrollPosAdd = container.clientWidth : scrollPosAdd = clonesWidth;
if (!state.disableScroll) {
if (scrollPos + scrollPosAdd >= scrollWidth) {
setScroll(
// The math floor value helps smooth out the scroll jump,
// I don't know why that particular value works, but it does
// Same goes for the other setScroll call below
container, 1
// + Math.floor(scrollPosAdd/50)
);
setState({
disableScroll: true,
});
} else if (scrollPos <= 0) {
setScroll(
container, scrollWidth - clonesWidth
// - Math.floor(scrollPosAdd/50)
);
setState({
disableScroll: true,
});
}
}
setState({
scrollWidth: container.scrollWidth,
scrollPos: container.scrollLeft,
});
} ;
function getClonesWidth() {
const clones = document.getElementsByClassName('is-clone');
let clonesWidth = 0;
for (let i = 0; i < clones.length; i++) {
clonesWidth = clonesWidth + clones[i].clientWidth;
}
return clonesWidth;
};
function setScroll(element, pos) {
element.scrollLeft = pos;
setState({
scrollPos: element.scrollLeft,
});
};
function scrollNext(e) {
const container = e.currentTarget.previousSibling;
const element = container;
const direction = 'right';
const speed = 10;
const distance = 272;
const step = 10;
let scrollAmount = 0;
var slideTimer = setInterval(() => {
if(direction == 'left'){
element.scrollLeft -= step;
} else {
element.scrollLeft += step;
}
scrollAmount += step;
if(scrollAmount >= distance){
window.clearInterval(slideTimer);
}
}, speed);
};
function scrollPrev(e) {
const container = e.currentTarget.nextSibling;
const element = container;
const direction = 'left';
const speed = 10;
const distance = 272;
const step = 10;
let scrollAmount = 0;
var slideTimer = setInterval(() => {
if(direction == 'left'){
element.scrollLeft -= step;
} else {
element.scrollLeft += step;
}
scrollAmount += step;
if(scrollAmount >= distance){
window.clearInterval(slideTimer);
}
}, speed);
};
// function componentDidUpdate(prevProps, prevState) {
// };
// function componentDidMount() {
// window.addEventListener('resize', reCalc);
// };
// function componentWillUnmount() {
// window.removeEventListener('resize', reCalc);
// };
useEffect(() => {
// Update the document title using the browser API
if (state.disableScroll) {
window.setTimeout(function() {
setState({
disableScroll: false,
});
}, 40)
}
});
const [scrollDir, setScrollDir] = useState("scrolling left");
const containerRef = useRef();
useEffect(() => {
console.log("olay");
const threshold = 0;
let lastScrollX = window.scrollX;
let ticking = false;
const updateScrollDir = () => {
const scrollX = window.scrollX;
if (Math.abs(scrollX - lastScrollX) < threshold) {
ticking = false;
return;
}
setScrollDir(
scrollX > lastScrollX ? "scrolling right" : "scrolling left"
);
lastScrollX = scrollX > 0 ? scrollX : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
return(
<>
<div className="card-container">
<div className="scroll scroll-prev" onClick={scrollPrev}>
<i className="fas fa-chevron-left"></i>
</div>
<div ref={containerRef} className="scrolling-wrapper" onScroll={handleScroll}>
<Card title={'Card Number 7'} classes={""}/>
<Card title={'Card Number 1'} classes={""}/>
<Card title={'Card Number 2'}/>
<Card title={'Card Number 3'}/>
<Card title={'Card Number 4'}/>
<Card title={'Card Number 5'}/>
<Card title={'Card Number 6'} image={myImage}/>
<Card title={'Card Number 7'} classes={"is-clone is-start"}/>
<Card title={'Card Number 1'} classes={"is-clone"}/>
<Card title={'Card Number 2'} classes={"is-clone"}/>
<Card title={'Card Number 3'} classes={"is-clone"}/>
<Card title={'Card Number 4'} classes={"is-clone"}/>
<Card title={'Card Number 5'} classes={"is-clone"}/>
<Card title={'Card Number 6'} classes={"is-clone"}/>
</div>
<div className="scroll scroll-next" onClick={scrollNext}>
<i className="fas fa-chevron-right"></i>
</div>
</div>
<div>Scrolling </div>
</>
)
}
export default SkillsFunction
** Looking for matched item to different color**
I'm currently trying to get color changes of mesh if condition match else keep it default color. but somehow it keeps same color for all mesh object.
I did try to add simple logic with Boolean to toggle for color but that also doesn't work. any advice will be accepted.
function generateMatrix(
mesh: MutableRefObject<InstancedMesh>,
getItemForGenerate: GetItemForGenerateData,
userDefineditems: string[],
xRotationValue: number,
yRotationValue: number
) {
if (getItemForGenerate?.items) {
const tempObject = new THREE.Object3D();
getItemForGenerate.items.forEach((items) => {
items.entities.forEach((item, index) => {
tempObject.position.set(
item.xMapPosition
item.yMapPosition
item.zMapPosition
);
//change color of the object if userdefined item matches the any of the items provided
if (userDefineditems?.includes(item.itemName)) {
tempObject.scale.set(item.length, item.depth , item.height );
// how to change color here if user defined lcoations matches any item on the Matrix
}
tempObject.scale.set(item.length , item.depth , item.height );
tempObject.updateMatrix();
mesh.current.setMatrixAt(index, tempObject.matrix);
});
});
mesh.current.rotation.x = xRotationValue;
mesh.current.rotation.y = yRotationValue;
mesh.current.instanceMatrix.needsUpdate = true;
return mesh;
}
}
const itemMatrix = (items: itemMatrixProps) => {
let mesh = useRef();
const { xRotationValue, yRotationValue, getItemForGenerate, userDefineditems } = items;
const [positionDetailsData, setPositionDetailsData] = useState<PositionDetails>(null);
useEffect(() => {
generateMatrix(mesh, getItemForGenerate, userDefineditems, xRotationValue, yRotationValue);
}, [getItemForGenerate, userDefineditems, xRotationValue, yRotationValue]);
useEffect(() => {
setPositionDetailsData(getPostion());
}, [getItemForGenerate]);
const getPostion = () => {
let xDivisor = 1;
let yDivisor = 1;
let zDivisor = 1;
const positionDetails: PositionDetails = {
xPosition: 0,
yPosition: 0,
zPosition: 0
};
let meshCount = getItemForGenerate?.count;
({ xDivisor, yDivisor, zDivisor } = generateMaxDivisors(getItemForGenerate, xDivisor, yDivisor, zDivisor));
const = xDivisor > yDivisor ? xDivisor : yDivisor;
const yPosition = yDivisor / (yDivisor + yDivisor * 0.3);
const xPosition = / (xDivisor + yDivisor);
positionDetails.xPosition = xPosition;
positionDetails.yPosition = yPosition;
positionDetails.zPosition = 0;
return positionDetails;
};
return (
<instancedMesh
ref={mesh}
args={[null, null, getItemForGenerate?.count]}
position={[-positionDetailsData?.xPosition, -positionDetailsData?.yPosition, positionDetailsData?.zPosition]}
>
<boxGeometry attach="geometry" args={[0.9, 0.9, 0.9]} />
<meshNormalMaterial />
<meshBasicMaterial color={'#047ABC'} />
</instancedMesh>
);
}
I try with THREE.Color but didn't work.
I just asked a question earlier here: react value of a state variable different in a different function
and now I have a new problem.
having a useEffect that looks like this
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
is breaking a setInterval that looks like this:
const countDown = () => {
// let strokeCountdown = Math.floor(Math.random() * 31) + 100;
let strokeCountdown = 20
let strokeCountdownSpeedOptions = [1000, 500, 300, 200];
let strokeCountDownSpeed = strokeCountdownSpeedOptions[Math.floor(Math.random()*strokeCountdownSpeedOptions.length)];
let strokeCounter = setInterval(() => {
strokeCountdown--
setStrokeCountdown(strokeCountdown)
if (strokeCountdown === 0) {
endOfGameRound()
clearInterval(strokeCounter)
setTotalStrokeScore(strokeScore);
}
}, strokeCountDownSpeed)
}
The full component looks like this:
import React, { useEffect, useState } from 'react';
function ScoreCard() {
const [strokeScore, setStrokeScore] = useState(1);
const [totalStrokeScore, setTotalStrokeScore] = useState(1);
const [strokeCountdown, setStrokeCountdown] = useState();
const strokeCountdownDing = new Audio('/sounds/round-complete.mp3');
// make new variable, maybe?
let strokeScoreCount = 0;
const endOfGameRound = () => {
strokeCountdownDing.play();
document.getElementById('stroke-counter-button').disabled = true;
}
const addToStrokeScore = () => {
setStrokeScore(prev => prev + 1);
// prints the correct number
console.log('Score in function', strokeScore);
if (strokeCountdown === 0) {
endOfGameRound()
}
}
const subtractStrokeScore = () => {
setStrokeScore(strokeScore - 1);
}
const countDown = () => {
// let strokeCountdown = Math.floor(Math.random() * 31) + 100;
let strokeCountdown = 20
let strokeCountdownSpeedOptions = [1000, 500, 300, 200];
let strokeCountDownSpeed = strokeCountdownSpeedOptions[Math.floor(Math.random()*strokeCountdownSpeedOptions.length)];
let strokeCounter = setInterval(() => {
strokeCountdown--
setStrokeCountdown(strokeCountdown)
if (strokeCountdown === 0) {
endOfGameRound()
clearInterval(strokeCounter)
setTotalStrokeScore(strokeScore);
}
}, strokeCountDownSpeed)
}
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
return (
<div className="game__score-card">
<div className="game__speed-level">
Speed: idk
</div>
<div className="game__stroke-countdown">
Countdown: {strokeCountdown}
</div>
<p>Score: {strokeScore}</p>
<button id="stroke-counter-button" onClick={addToStrokeScore}>
{strokeCountdown === 0 ? 'Game Over' : 'Stroke'}
</button>
{/* window.location just temp for now */}
{strokeCountdown === 0
? <button onClick={() => window.location.reload(false)}>Play Again</button>
: <button disabled>Game in Progress</button>
}
<div className="game__total-score">
Total score: {totalStrokeScore}
</div>
</div>
);
}
export default ScoreCard;
When I click on the button, the timer gets erratic and goes all over the place.
All I want to do is make it so that the timer counts down smoothly, gets the clicks the user made and add it to total score.
Why is
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
Breaking everything?
I was calling countDown() everytime I clicked so I just did
useEffect(() => {
if (strokeScore === 1) {
countDown();
}
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
I'm making a Nextjs app, and on one page I create an array of numbers that is shuffled using a random number. The problem is that every time the state changes, the component gets rendered so the array gets re-shuffled. I need the original shuffle to stay consistent. Here's the relevant code:
export default function Game() {
const { query } = useRouter();
const [cardsFlipped, setCardsFlipped] = useState(
Array(query.tileNumber).fill(false)
);
let counter = query.tileNumber / 2;
let iconSet = [...Array(counter).keys()];
while (counter > 0) {
const index = Math.floor(Math.random() * iconSet.length);
tiles.push(iconSet.splice(index, 1));
counter--;
}
const tileOrder = tiles.concat(tiles).sort((a, b) => 0.5 - Math.random());
const handleFlip = (index) => {
if (cardsFlipped[index] === false) {
setCardsFlipped((prev) =>
prev.map((el, i) => {
if (i === index) {
return true;
}
return el;
})
);
setTimeout(() => {
setCardsFlipped((prev) =>
prev.map((el, i) => {
if (i === index) {
return false;
}
return el;
})
);
}, query.tileTransition * 1000);
}
};
let cards = tileOrder.map((e, i) => (
<ReactCardFlip
isFlipped={cardsFlipped[i]}
flipDirection="horizontal"
key={"card" + i + "-" + e}
>
<Card
iconSet={2}
index={50}
callback={() => {
handleFlip(i);
}}
/>
<Card iconSet={parseInt(query.icons)} index={e} callback={handleFlip} />
</ReactCardFlip>
));
return (<div>{cards}</div>);
}
I thought of converting it into a class, and having a constructor, but then I get an error that useRouter can't be used in classes.
You just need to wrap your logic in a useEffect, something like this:
export default function Test() {
const { query } = useRouter();
const [cardsFlipped, setCardsFlipped] = useState(
Array(query.tileNumber).fill(false)
);
const [tileOrder, setTileOrder] = useState([]);
useEffect(() => {
let counter = query.tileNumber / 2;
let iconSet = [...Array(counter).keys()];
while (counter > 0) {
const index = Math.floor(Math.random() * iconSet.length);
tiles.push(iconSet.splice(index, 1));
counter--;
}
setTileOrder(tiles.concat(tiles).sort((a, b) => 0.5 - Math.random()));
}, []);
const handleFlip = index => {
if (cardsFlipped[index] === false) {
setCardsFlipped(prev =>
prev.map((el, i) => {
if (i === index) {
return true;
}
return el;
})
);
setTimeout(() => {
setCardsFlipped(prev =>
prev.map((el, i) => {
if (i === index) {
return false;
}
return el;
})
);
}, query.tileTransition * 1000);
}
};
let cards = tileOrder.map((e, i) => (
<ReactCardFlip
isFlipped={cardsFlipped[i]}
flipDirection="horizontal"
key={'card' + i + '-' + e}
>
<Card
iconSet={2}
index={50}
callback={() => {
handleFlip(i);
}}
/>
<Card iconSet={parseInt(query.icons)} index={e} callback={handleFlip} />
</ReactCardFlip>
));
return <div>{cards}</div>;
}
I am trying to draw large number of nodes in React Konva based on this performance demo.
My code:
const ZoomTest = (props) => {
let width = window.innerWidth;
let height = window.innerHeight;
const [rects, setRects] = useState([])
const [node, setNode] = useState(null)
function makeRects () {
for (let x=0; x<10000; x++) {
var color = colors[colorIndex++];
if (colorIndex >= colors.length) {
colorIndex = 0;
}
let randX = Math.random() * width;
let randY = Math.random() * height;
rects.push(<Circle
id={x}
x={randX}
y={randY}
width={20}
height={20}
fill={color}
stroke={'blue'}
strokeWidth={1}
shadowBlur={0}
/>)
}
setRects(rects)
}
function getRects() {
if (rects.length === 0)
makeRects()
return rects
}
function tooltip() {
if (node === null)
return null
return <Label x={node.x} y={node.y} opacity={0.75}>
<Tag fill={'black'} pointerDirection={'down'} pointerWidth={10} pointerHeight={10} lineJoin={'round'} shadowColor={'black'}
shadowBlur={10} shadowOffsetX={10} shadowOffsetY={10} shadowOpacity={0.2}/>
<Text text={node.text} fill={'white'} fontSize={18} padding={5} />
</Label>
}
function onMouseOver(evt) {
var node = evt.target;
if (node) {
// update tooltip
var mousePos = node.getStage().getPointerPosition();
setNode({ x: mousePos.x, y: mousePos.y, text: node.id() })
console.log('node id = ', node.id())
}
}
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
draggable='false'
onMouseOver={onMouseOver}
onMouseMove={onMouseOver}
onDragMove={onMouseOver}
>
<Layer>
{getRects()}
</Layer>
<Layer>
{tooltip()}
</Layer>
</Stage>
)
}
The issue is that tooltip is very slow as compared to the demo. I think this is due to re-renders of the component on every mouse event and this is causing the lag.
Any idea how to make the react-konva performance similar to konvajs?
There is nothing special about react-konva. It is just React performance. In your render function, you have a large list of elements. Every time your state changes (on every mouseover) the react have to compare old structure with the new one. As you have many elements, it takes time to React to check it.
I think there are many ways to solve the issue. As one solution you can just move large list into another component and use React.memo to tell React that you don't need to recheck the whole list every time.
import React from "react";
import { render } from "react-dom";
import { Stage, Layer, Circle, Label, Tag, Text } from "react-konva";
var colors = ["red", "orange", "cyan", "green", "blue", "purple"];
const Rects = React.memo(({ rects }) => {
return (
<React.Fragment>
{rects.map(rect => (
<Circle
key={rect.id}
id={rect.id}
x={rect.x}
y={rect.y}
width={20}
height={20}
fill={rect.color}
stroke={"blue"}
strokeWidth={1}
shadowBlur={0}
/>
))}
</React.Fragment>
);
});
function makeRects() {
let width = window.innerWidth;
let height = window.innerHeight;
const rects = [];
let colorIndex = 0;
for (let x = 0; x < 10000; x++) {
var color = colors[colorIndex++];
if (colorIndex >= colors.length) {
colorIndex = 0;
}
let randX = Math.random() * width;
let randY = Math.random() * height;
rects.push({ id: x, x: randX, y: randY, color });
}
return rects;
}
const INITIAL = makeRects();
const App = props => {
const [rects, setRects] = React.useState(INITIAL);
const [node, setNode] = React.useState(null);
function tooltip() {
if (node === null) return null;
return (
<Label x={node.x} y={node.y} opacity={0.75}>
<Tag
fill={"black"}
pointerDirection={"down"}
pointerWidth={10}
pointerHeight={10}
lineJoin={"round"}
shadowColor={"black"}
shadowBlur={10}
shadowOffsetX={10}
shadowOffsetY={10}
shadowOpacity={0.2}
/>
<Text text={node.text} fill={"white"} fontSize={18} padding={5} />
</Label>
);
}
function onMouseOver(evt) {
var node = evt.target;
if (node) {
// update tooltip
var mousePos = node.getStage().getPointerPosition();
setNode({ x: mousePos.x, y: mousePos.y, text: node.id() });
console.log("node id = ", node.id());
}
}
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
draggable="false"
onMouseOver={onMouseOver}
onMouseMove={onMouseOver}
onDragMove={onMouseOver}
>
<Layer>
<Rects rects={rects} />
</Layer>
<Layer>{tooltip()}</Layer>
</Stage>
);
};
render(<App />, document.getElementById("root"));
https://codesandbox.io/s/react-konva-performance-on-many-elements-y680w?file=/src/index.js