Canvas onclick event react - reactjs

Hello and thank you for reading this question.
I'm struggling to deal with the onclick event on canvas with React.
I am currently building a component that has to draw bounding boxes on an image and make those boxes fire a onclick event with its coordinates and/or metadata.
I am using the following react-bounding-box component (https://www.npmjs.com/package/react-bounding-box) with a bit of customization.
The idea is that my component receive the following data :
an image
a JSON with a list of items that contains coordinates of bounding boxes and other data related to those boxes.
Once the JSON is loaded, my code iterates on the list of items and draws the bounding boxes on the image using canvas.
My component definition looks like that (I omitted useless lines of code) :
[...]
import BoundingBox from 'react-bounding-box'
[...]
export const ComicImageDrawer = (props) => {
const [boundingBoxesItems, setBoundingBoxesItems] = useState(Array<any>());
const [selectedBoxItem, setSelectedBoxItem] = useState({})
const [selectedBoxIndex, setSelectedBoxIndex] = useState<Number>(-1);
const [currentImageBoxes, setCurrentImageBoxes] = useState(Array<any>())
useEffect(() => {
[...] // loading data
}, [])
// That function is fired when a box is hovered
// param value is the index of the box
// I would like to do the same but with the `onclick` event
function onOver(param) {
[...] // don't care
}
const params = {
[...] // don't care
}
};
return (
<BoundingBox
image={currentImage}
boxes={currentImageBoxes}
options={params.options}
onSelected={onOver}
drawBox={drawBoxCustom}
drawLabel={() => {}}
/>
)
}
The redefined the component drawBox() function to add some customization. So that function definition looks like this :
function drawBoxCustom(canvas, box, color, lineWidth) {
if(!box || typeof box === 'undefined')
return null;
const ctx = canvas.getContext('2d');
const coord = box.coord ? box.coord : box;
let [x, y, width, height] = [0, 0, 0, 0]
[...] // removed useless lines of codes
ctx.lineWidth = lineWidth;
ctx.beginPath();
[...] // drawing element definition
ctx.stroke();
};
I haved tried the following stuff to make the canvas fire an onclick event but it never fires (i also tried other event like mouseover) :
// Listen for mouse moves
canvas.addEventListener('onmouseover', function (event) {
console.log('click event', event);
});
What I would like to obtain is to fire a function in my React component that looks like that. The idea is to determine which box has been clicked :
const handleCanvasClick = (event, box) => {
console.log('event', event);
console.log('box', box);
}
Any help or suggestion would be appreciated.
Thanks.

Related

d3.js Tooltip do not receive data properly

I have d3 line chart with two lines full with events and made a html tooltip to show some data from these events on mousemove. Its working fine until the moment when you switch to show only one line, than the tooltip doesnt receive any data. Ive log the data and it is coming to the tooltip, but tooltip is not receiving it for some reason. Check the code bellow:
const tooltip = d3.select('.Tooltip')
.style('visibility', 'hidden')
.style('pointer-events', 'none')
function mousemove (event) {
// recover coordinate we need
const mouse = d3.pointer(event, this)
const [xm, ym] = (mouse)
const mouseWindows = d3.pointer(event, d3.select(this))
const [xmw, ymw] = (mouseWindows)
const i = d3.least(I, i => Math.hypot(xScale(X[i]) - xm, yScale(Y[i]) - ym)) // closest point
path.style('stroke-width', ([z]) => Z[i] === z ? strokeWidthHovered : strokeWidth).filter(([z]) => Z[i] === z).raise()
dot.attr('transform', `translate(${xScale(X[i])},${yScale(Y[i])})`)
// console.log(O[i]) <-- this is the data for the current selected event and its fine after
changing the line shown
tooltip
.data([O[i]])
.text(d => console.log(d)) <-- here i log what is coming from data and doesn`t return anything
.style('left', `${tooltipX(xScale(X[i]), offSetX)}px `)
.style('top', `${tipY}px`)
.style('visibility', 'visible')
.attr('class', css.tooltipEvent)
.html(d => chartConfig.options.tooltip.pointFormat(d))
When you call tooltip.data([0[i]]), d3 returns a selection of elements already associated with your data, which does not make sense for your tooltip. If you already computed the value you want to show (I assume it is O[i]), then simply set the text or the html of the tooltip:
tooltip.html(chartConfig.options.tooltip.pointFormat(O[i]);

React, NextJS: Optimizing performance for many child components that all need to re-render/update

I've got a parent component (Grid), which generates a number of vertical rows. Each row horizontally generates a number of cell components. Each Cell component vertically generates four subcells. I've simplified it like so:
// SubCell.tsx
export default function SubCell(row, column) {
return <div>{row},{column}</div>
}
// Cell.tsx
function Cell(row, column) {
const generateCell = () => {
const subcells = [];
for (let i=0; i<4; i++){
subcells.push(<SubCell row={row+i} column={column}/>)
}
}
return <div>{generateCell()}</div>;
}
// Grid.tsx
export default function Grid(){
const horizontalLabels = [...];
const verticalLabels = [...];
const generateHeaders = () => {
return verticalLabels.map((label, index) => {
<div>{label}</div>
});
}
const generateRows = () => {
return (verticalLabels.map((vLabel, columnIndex) => {
return (horizontalLabels.map((hLabel, rowIndex) => {
return (<Cell row={rowIndex*4} column={columnIndex}/>);
});
});
}
return (
<div>
{generateHeaders()}
{generateRows()}
</div>
)
}
The components are in separate files, but I've included them together for simplicity.
My goal is to select an area of subcells; meaning, I can click my mouse down on one subcell, move it over other subcells, and the area of cells that are in the range of the mousedown cell and the mouseover cell will be highlighted. Currently, I am accomplishing this through a shared state kept in context, and passed down through Grid to cells and subcells.
function SubCell(row, column){
const {currentMouseDownCell, currentMouseOverCell, setCurrentMousedownCell, setCurrentMouseOverCell} = useContext(GridContext);
... the rest
return <div
onMouseDown{setCurrentMousedownCell(row,column)}
onMouseOver={setCurrentMouseOverCell(row,column)>
{row},{column}
</div>
}
export default function Grid(){
...// everything else
return (
<GridContext>
<div>
{generateHeaders()}
{generateRows()}
</div>
</GridContext>
)
}
// GridContext.ts
export interface GridContextType {
currentMouseDownCell: number[];
currentMouseOverCell: number[];
setCurrentMouseDownCell: Dispatch<SetStateAction<number[]>>;
setCurrentMouseOverCell: Dispatch<SetStateAction<number[]>>;
}
export const GridContext = createContext<GridContextType>(
{} as GridContextType
);
export const GridContextProvider = (children) => {
const [currentMouseDownCell, setCurrentMouseDownCell] = useState([-1, -1]);
const [currentMouseOverCell, setCurrentMouseOverCell] = useState([-1, -1]);
const GridContextValue = {
currentMouseDownCell,
setCurrentMouseDownCell,
currentMouseOverCell,
setCurrentMouseOverCell,
}
return (
<GridContext.Provider value={GridContextValue}>
{children}
</GridContext.Provider>
)
}
And then for each SubCell, I'm checking for any changes to the current mousedown and mouseover subcells. If there is a change, I check if the current subcell is in between the two. If so, then I change the background to the highlighted color. If not, I change it / keep it white.
function SubCell(row, column) {
const [currentColor, setCurrentColor] = useState("white");
const {currentMouseDownCell, currentMouseOverCell, setCurrentMousedownCell, setCurrentMouseOverCell} = useContext(GridContext);
useEffect(()=>{
if row and column are in between the mousedown and mouseover cells
setCurrentColor("blue");
else setCurrentColor("white");
}, [currentMouseDownCell, currentMouseOverCell]);
return <div style={{backgroundColor: currentColor}}>{row},{column}</div>
}
This works, but with large numbers of subcells that all re-render every time a mouseover changes, it can get somewhat choppy / slow, and I'm looking for a smooth experience. Any suggestions on speed increases?
What I've tried:
Using , but it's the same performance and I need a more customizable layout.
Memo, but I don't see how I can avoid the re-render since every subcell needs to check if it should be highlighted every time.
Virtualization doesn't help, because the cells will all be visible.
Prop drilling instead of context, but that didn't make it much faster.
Most online resources try to prevent components from doing the computation to re-render, but in my case it seems unavoidable.
Edit 04/17/22:
After reading React memo keeps rendering when props have not changed and this one I'm trying to make a grid of clickable boxes where boxes know which other boxes they're adjacent to. JS/React I realized that it's better to keep the data for all cells stored at the parent level, rather than modulated individually.
I've created a 2D array, and then I've passed it down as props to the cell, and then the individual values to each subcell. This way, the parent can have the useEffect for mousedown and mouseover, do the appropriate calculations on every subcell, and then update just the relevant segments of the table. Through memoization, I can ensure that only the affected subcells are re-rendered (by checking that their old cell value is the same as the new one).

Not getting accurate mouse mouseup and mousedown coordinate

I am using react with typescript. I have a canvas and I am trying to draw a rectangle inside the canvas based on mouseup and mousedown event. I don't know what I am doing wrong here my rectangles are drawn but in different positions.
This is how my code looks like:
const Home = () => {
const divRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let paint = false;
let StartClientXCord: any;
let StartClientYCord: any;
let FinishedClientXCord: any;
let FinishedClientYCord: any;
function startPosition(e:any){
paint = true;
StartClientXCord = e.clientX;
StartClientYCord = e.clientY;
}
function finishPosition(e:any){
FinishedClientXCord = e.clientX;
FinishedClientYCord = e.clientY;
paint = false;
}
function draw(e: any){
if(!paint){
return
}
if(divRef.current){
const canvas = divRef.current.getContext('2d');
if(canvas){
canvas?.rect(StartClientXCord, StartClientYCord, FinishedClientXCord - StartClientXCord, FinishedClientYCord - StartClientYCord);
canvas.fillStyle = 'rgba(100,100,100,0.5)';
canvas?.fill();
canvas.strokeStyle = "#ddeadd";
canvas.lineWidth = 1;
canvas?.stroke();
}
}
}
if(divRef.current){
divRef.current.addEventListener("mousedown", startPosition);
divRef.current.addEventListener("mouseup", finishPosition);
divRef.current.addEventListener("mousemove", draw);
}
})
return (
<canvas className='canvas' style={{height: height, width: width}} ref={divRef}>
</canvas>
)
}
I am drawing on a different position box is drawing on a different position. Here I attached the screenshot as well
I believe it's because you are using clientX and clientY, which MDN defines as:
The clientX read-only property of the MouseEvent interface provides the horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page).
This is the absolute position on the entire page. What you want is the is the position reative to the top left corner of the canvas. You want offsetX and offsetY which MDN defines as:
The offsetX read-only property of the MouseEvent interface provides the offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node.
So give this a try:
StartClientXCord = e.offsetX;
StartClientYCord = e.offsetY;

FabricJS object is not selectable/clickable/resizable in top left corner (React + FabricJs)

I have FabricJs canvas. And I have multiple objects outside canvas. When I click on object, it appears in Canvas. I am not doing anything else with this object programmatically.
Here is part of code that add image to Canvas (it is React code):
const { state, setState } = React.useContext(Context);
const { canvas } = state;
...
const handleElementAdd = (event, image) => {
const imageSize = event.target.childNodes[0].getBoundingClientRect();
const imageUrl = `/storage/${image.src}`;
new fabric.Image.fromURL(imageUrl, img => {
var scale = 1;
if (img.width > 100) {
scale = 100/img.width;
}
var oImg = img.set({
left: state.width / 2 - img.width * scale,
top: state.height / 2 - img.height * scale,
angle: 0})
.scale(scale);
canvas.add(oImg).renderAll.bind(canvas);
canvas.setActiveObject(oImg);
});
};
...
<div
key={img.name}
onClick={(e) => handleElementAdd(e, img)}
className={buttonClass}
>
It appears, I can drag and drop, resize, select it. However, when my object is placed in top left corner of canvas, it is not clickable, resizable etc. I can select larger area and then object is selected, but it doesn't respond to any events like resize, select etc.
Not sure where to look at.
I read that if coordinates are changed programmatically object.setCoord() to object can help, but in this case I do not understand where to put it.
You have to put (call) object.setCoords() each time you have modified the object, or moved the object. So, you can call it on mouseUp event of the canvas.
You can write a function naming "moveObject" in which you get the object you are trying to move/moved/updated. And then just selectedObject.setCoords().
Then call that function inside canvas.on('selection:updated', e => {----})

How to toggle off Openlayers custom control?

I've gone ahead and built a custom control that I'm adding to my map like so:
const BoundingBox = (function (Control) {
function BoundingBox(optOptions) {
const options = optOptions || {};
const button = document.createElement('button');
button.innerHTML = '[]';
const element = document.createElement('div');
element.className = 'bounding-box ol-unselectable ol-control';
element.appendChild(button);
Control.call(this, {
element,
target: options.target,
});
button.addEventListener('click', this.handleBoundingBox.bind(this), false);
}
if (Control) BoundingBox.__proto__ = Control;
BoundingBox.prototype = Object.create(Control && Control.prototype);
BoundingBox.prototype.constructor = BoundingBox;
BoundingBox.prototype.handleBoundingBox = function handleBoundingBox() {
this.getMap().addInteraction(extent);
};
return BoundingBox;
}(Control));
Next, I added that control to my map when my map is initialized. This is working fine. Now, I'm trying to find a way to toggle off the BoundingBox control. I was thinking that I could use the .removeInteraction() method. However, I'm unsure if that's correct. Also, should that be applied in a separate function or in my BoundingBox control?
I was able to accomplish this by checking the properties of the ol/interaction/extent and setting the value of the active property to false.
extent.setProperties({ active: false });

Resources