onDblClick in react-konva not working - reactjs

I'm using react-konva to render canvas. I assign onDblClick for a Group, I also assign onClick, onDragEnd to this Group. In onDragEnd handler, I have an axios POST request to server. Whenever I double click the Group, the onDragEnd event is fired. Does anybody know what is the problem here ?
Here are my code
handleMoving(){
var thisElement = this.refs[this.state.key];
this.setState({
x: thisElement.x(),
y: thisElement.y(),
width: thisElement.getWidth(),
height: thisElement.getHeight()
});
this.focus();
}
handleDoubleClick(){
console.log('dbclick');
this.focus();
const stage = this.refs[this.state.key+'_wrapper'].getParent().getParent().getParent();
const pos = this.refs[this.state.key].getAbsolutePosition();
document.getElementById('newText').addEventListener('keydown',this.handleTextChange);
document.getElementById('newTextWrapper').style.position = "absolute";
document.getElementById('newTextWrapper').style.left = pos.x+"px";
document.getElementById('newTextWrapper').style.top = pos.y+20+"px";
document.getElementById('newText').style.width = this.refs[this.state.key+'_wrapper'].getWidth()+"px";
document.getElementById('newTextWrapper').style.display = 'block';
}
syncToServer(){
axios.post('/api/element/text/update',{
uid:this.state.uid,
key:this.state.key,
content:this.state.content,
stage:{
x:this.state.x + this.refs[this.state.key].getParent().x(),
y:this.state.y + this.refs[this.state.key].getParent().y(),
width:this.state.width,
height:this.state.height,
fontSize:this.state.fontSize
}
}).then(function(response){
console.log(response.data);
});
}
render(){
return (
<Layer>
<Group onDblClick = {this.handleDoubleClick}
onClick = {this.handleClick}
onDragMove = {this.handleMoving}
onDragEnd = {this.syncToServer}
draggable = "true">
<Rect ref = {this.state.key + '_wrapper'}
x = {this.state.x}
y = {this.state.y}
width = {this.state.width}
height = {this.state.height}
visible = {false}
fill = 'lightgrey'
cornerRadius = {3}>
</Rect>
<Text ref = {this.state.key}
x = {this.state.x}
y = {this.state.y}
fontSize = {this.state.fontSize}
fontFamily = {this.state.fontFamily}
text = {this.state.content}
fill = {this.state.color}
padding = {20}
>
</Text>
</Group>
</Layer>
);
}

Try to use the node of the ref.
node.on('dblclick dbltap', () => {
console.log('you clicked me!');
});

Related

react canvas rectangle not aligned with mouse

i got a canvas in react that i want to allow users to draw rectangles, ive searched through stackoverflow and youtube videos regarding this but my rectangle cant seem to align with the crusor. everything works fine when the canvas is the only thing on the page, as in that its positioned on the top left. but when i include my other components everything does wrong in the canvas. please help, its my first time using canvas and ive only been following tutorials, trying to understand each functions but i wasnt able to find a solution.
const HomeCanvas = () => {
const canvasRef = useRef(null);
const contextRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const canvasOffSetX = useRef(null);
const canvasOffSetY = useRef(null);
const startX = useRef(null);
const startY = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
canvas.width = 500;
canvas.height = 500;
const context = canvas.getContext("2d");
context.lineCap = "round";
context.strokeStyle = "black";
context.lineWidth = 2;
contextRef.current = context;
const canvasOffSet = canvas.getBoundingClientRect();
canvasOffSetX.current = canvasOffSet.top;
canvasOffSetY.current = canvasOffSet.left;
console.log(canvasOffSet.left);
console.log(canvasOffSet.top);
}, []);
const startDrawingRectangle = ({ nativeEvent }) => {
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
startX.current = nativeEvent.clientX - canvasOffSetX.current;
startY.current = nativeEvent.clientY - canvasOffSetY.current;
setIsDrawing(true);
};
const drawRectangle = ({ nativeEvent }) => {
if (!isDrawing) {
return;
}
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
const newMouseX = nativeEvent.clientX - canvasOffSetX.current;
const newMouseY = nativeEvent.clientY - canvasOffSetY.current;
const rectWidth = newMouseX - startX.current;
const rectHeight = newMouseY - startY.current;
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
);
contextRef.current.strokeRect(
startX.current,
startY.current,
rectWidth,
rectHeight
);
};
const stopDrawingRectangle = () => {
setIsDrawing(false);
};
return (
<div className="home-canvas">
<div className="canvas-sidebar">
<div>Merge Table</div>
</div>
<div style={{ width: "100%", paddingLeft: "5%" }}>
<canvas
className="canvas-area"
ref={canvasRef}
onMouseDown={startDrawingRectangle}
onMouseUp={stopDrawingRectangle}
onMouseMove={drawRectangle}
onMouseLeave={stopDrawingRectangle}
></canvas>
</div>
</div>
);
};
what did i do wrong? here is the video of it.
https://youtu.be/ioGINfUKBgc

How to properly load images to Canvas in React

I have an array of image urls. I want to display one image at a time on canvas. I've setup useEffect to be triggered when index of current image is changed.
I've researched online and found out that images load async and i must use onload callback to draw new image on canvas (Why won't my image show on canvas?).
I've setup effect and callback like this:
const [image, setImage] = useState<HTMLImageElement>()
useEffect(() => {
const image = new Image()
image.onload = () => update
image.src = images[imageState.imageIndex]?.url
setImage(image)
}, [imageState.imageIndex, images, update])
const update = useCallback(() => {
if (!canvasRef.current) return
const ctx = canvasRef.current.getContext('2d')
if (!ctx || !image) return
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)
canvasRef.current.width = image.width
canvasRef.current.height = image.height
ctx.drawImage(image, 0, 0)
}, [image])
canvas is setup like this:
export const ImageGallery = (props: ImageGalleryProps) => {
const classes = useImageGalleryStyles()
return (
<div className={classes.outerContainer}>
<canvas
style={{
left: `${imageState.position.x}px`,
top: `${imageState.position.y}px`,
transform: `rotate(${imageState.rotate}deg) scale(${imageState.scale})`,
}}
ref={canvas.ref}
className={classes.image}
/>
</div>
)
}
Images are changed like this:
Callback never called and canvas remains blank. What is proper way to update canvas?
Ended up with this:
useEffect(() => {
const image = new Image()
image.onload = function() {
if (!canvasRef.current) return
const ctx = canvasRef.current.getContext('2d')
if (!ctx || !image) return
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)
canvasRef.current.width = image.width
canvasRef.current.height = image.height
ctx.drawImage(image, 0, 0)
coordinates.map(c => {
switch (c.coordinates.$type) {
case DefectCoordinatesType.WideSide:
c.coordinates.WideSides.map(ws => {
if (image) {
ctx.beginPath()
ctx.imageSmoothingEnabled = false
ctx.moveTo(ws[0].x * image.width, ws[0].y * image.height)
ctx.lineTo(ws[1].x * image.width, ws[1].y * image.height)
ctx.closePath()
ctx.strokeStyle = 'red'
ctx.lineWidth = 2
ctx.stroke()
}
})
}
})
}
image.src = images[imageState.imageIndex]?.url
}, [checkBoundaries, coordinates, imageState.imageIndex, images, prevIndex, zoomOut])

react konva animation to follow a path

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

React Konva - Performance of large number of nodes

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

Set styles dynamically after render - react

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(); }

Resources