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
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
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
I am building my Portfolio using React three fiber, it shows the display on mobile and laptop or PC.
But when I'm trying to see on i-pad the display is blank.
I am trying to solve this problem, but I could not find any solution.
I will be happy if can give me some advice.
This is my code
import React, { useState, useRef, useEffect } from 'react';
import * as THREE from 'three';
import { extend, Canvas, useFrame } from 'react-three-fiber';
import * as meshline from 'threejs-meshline';
import Typical from 'react-typical';
// firebase
import firebase from '../firebase/firebase.utils';
// custom hook
import useAuthor from '../hooks/getAuthor';
// component
import Spinner from './Spinner';
import '../assets/styles/components/ThreeFiber.scss';
extend(meshline);
const numLines = 100;
const lines = new Array(numLines).fill();
const colors = ['#f6f6f6', '#fff8cd', '#6495ED'];
function Fatline() {
const material = useRef();
const [color] = useState(
() => colors[parseInt(colors.length * Math.random())]
);
const [ratio] = useState(() => 0.6 + 0.7 * Math.random());
const [width] = useState(() => Math.max(0.1, 0.1 * Math.random()));
// Calculate wiggly curve
const [curve] = useState(() => {
const pos = new THREE.Vector3(
30 - 60 * Math.random(),
-5,
10 - 20 * Math.random()
);
const points = new Array(30)
.fill()
.map(() =>
pos
.add(
new THREE.Vector3(
2 - Math.random() * 4,
4 - Math.random() * 2,
5 - Math.random() * 10
)
)
.clone()
);
return new THREE.CatmullRomCurve3(points).getPoints(500);
});
// Hook into the render loop and decrease the materials dash-offset
useFrame(() => {
material.current.uniforms.dashOffset.value -= 0.0005;
});
return (
<mesh>
<meshLine attach="geometry" vertices={curve} />
<meshLineMaterial
attach="material"
ref={material}
transparent
depthTest={false}
lineWidth={width}
color={color}
dashArray={0.2}
dashRatio={ratio}
/>
</mesh>
);
}
function Scene() {
let group = useRef();
let theta = 0;
// Hook into the render loop and rotate the scene a bit
useFrame(() => {
group.current.rotation.set(
0,
8 * Math.sin(THREE.Math.degToRad((theta += 0.02))),
0
);
});
return (
<group ref={group}>
{lines.map((_, index) => (
<Fatline key={index} />
))}
</group>
);
}
const ThreeFiber = React.memo(() => {
const collectionRef = firebase.firestore().collection('author');
const authorCollection = useAuthor(collectionRef);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
}, []);
return (
<div className="Hero_container">
<Canvas
camera={{ position: [0, 60, 10], fov: 150 }}
resize={{ scroll: false }}
>
<Scene />
</Canvas>
{loading ? (
authorCollection.map((author) => (
<h4 key={author.informations} className="Hero_content">
Hi! <br /> I'm {author.informations.name}, <br />
<Typical
loop={Infinity}
wrapper="b"
steps={[
'Front-end Developer with React',
1500,
'a self-taught Web Engineer',
1500,
]}
/>
</h4>
))
) : (
<Spinner />
)}
</div>
);
});
Thank you for your help.....
I wanted to create drawing app in React and to achieve that I came across something called Konvajs. Then, I started and somehow achieved that I can draw non-straight lines like this
But what I wanted to draw is straight lines like this
and rectangle. Here is my code:
import React, { Component } from "react";
import { Stage, Layer, Line, Rect, Text } from "react-konva";
export default class Canvas extends Component {
state = {
lines: [],
};
handleMouseDown = () => {
this._drawing = true;
this.setState({
lines: [...this.state.lines, []],
});
};
handleMouseUp = () => {
this._drawing = false;
};
handleMouseMove = (e) => {
if (!this._drawing) {
return;
}
const stage = this.stageRef.getStage();
const point = stage.getPointerPosition();
const { lines } = this.state;
let lastLine = lines[lines.length - 1];
lastLine = lastLine.concat([point.x, point.y]);
lines.splice(lines.length - 1, 1, lastLine);
this.setState({
lines: lines.concat(),
});
};
render() {
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onContentMousedown={this.handleMouseDown}
onContentMousemove={this.handleMouseMove}
onContentMouseup={this.handleMouseUp}
ref={(node) => {
this.stageRef = node;
}}
>
<Layer>
<Text text="Draw a thing!" />
{this.state.lines.map((line, i) => (
<Line key={i} points={line} stroke="red" />
))}
</Layer>
</Stage>
);
}
}
For straight lines replace this
lastLine = lastLine.concat([point.x, point.y]);
With this
lastLine = [lastLine[0], lastLine[1], point.x, point.y]
Basically, you should have maximum four points. The first two are created OnMouseDown and the last two should be updated as you are moving the cursor - OnMouseMove
I currently supported my use case via componentDidMount. It worked for one time render. But soon I had to re-render the component i.e. build rows and cols again and hence to re-apply the marginLeft styles (in componentDidMount). I am not sure how to do that ?
Below is the code for the component in concern. Child graph component is a pure presentational component(read stateless).
var React = require('react');
var ReactDOM = require('react-dom');
var Graph = require('./graph');
var noOfLines, difference;
var _ = require('lodash');
module.exports = React.createClass({
displayName: 'Rules graph container',
propTypes: {
data: React.PropTypes.arrayOf(React.PropTypes.object),
lineAttributes: React.PropTypes.object,
handleNext: React.PropTypes.func,
handlePrevious: React.PropTypes.func,
pagination: React.PropTypes.object
},
getRows(n, d, lineHeight){
var rows = [], columns = [];
for (var i = n; i > 0; i--){
var isLast = (i === 1);
rows.unshift(
<div key={i} className='y-line' style={{height: lineHeight, marginTop: isLast ? (lineHeight / 2) : 0}}>
<div className='amt'>{ d + d * (n - i) }</div>
</div>
);
}
return rows;
},
buildCols(leftMargins){
const {data, lineAttributes} = this.props;
var cols = [];
for ( var i = 0; i < data.length; i++){
var height, isFirst;
if (data[i].val === lineAttributes.maxValue){
height = 100;
} else {
height = (data[i].val / lineAttributes.maxValue) * 100;
}
if (i === 0){
isFirst = true;
}
const button = `column_button_${i}`, name = _.startCase(data[i].name);
cols.push(
<div key={i} className={ isFirst ? 'first active column' : 'column'} style={{ height: `${height}%`, top: `${100 - height}%`, marginRight: '100px' }}>
<div className='count'>{data[i].val}</div>
<div className='column_button' style={{marginLeft: leftMargins && -leftMargins[i]}} ref={node => this[button] = node}><span className='event_label'>{name}</span></div>
</div>
);
}
return cols;
},
render(){
const {noOfLines: n, difference: d, lineHeight} = this.props.lineAttributes;
const rows = this.getRows(n, d, lineHeight), cols = this.buildCols();
return (
<div>
{n ? (
<Graph rows={rows} graphHeightPad={lineHeight / 2} cols={cols} pagination={this.props.pagination} />
) : null}
</div>
);
},
componentDidMount(){
const leftMargins = [];
for (var i = 0; i < this.props.data.length; i++){
var button = `column_button_${i}`;
var width = this[button].offsetWidth;
leftMargins.push(width / 4);
}
const cols = this.buildCols(leftMargins);
this.setState({cols}); // Intentional re-render
}
});
i understand that you want call the same function when re-render
what about componentDidUpdate()
do(){
const leftMargins = [];
for (var i = 0; i < this.props.data.length; i++){
var button = `column_button_${i}`;
var width = this[button].offsetWidth;
leftMargins.push(width / 4);
}
const cols = this.buildCols(leftMargins);
this.setState({cols}); // Intentional re-render
}
componentDidMount(){ this.do(); }
componentDidUpdate(){ this.do(); }