My application, pulls data ( text, numbers) from a database and then places them in a canvas image. This is working as expected, and the data is being displayed along with a few lines of the final diagram. However the useEffect() function is not stopping and continues to display the same image, which I would not have realised had I not placed an alert.
I have seen the documentation and other questions that have discussed this problem. I understand that I need to put a variable whose value will not change. But I am simply unable to do this. I am sharing my code. Will be grateful if someone can help.
import React, { useEffect, useState, useRef } from "react";
import { useParams } from "react-router";
import './cindex.css';
export default function GetChartData() {
const params = useParams();
const [dbdata, setData] = useState('empty52');
const canvas = useRef();
let ctx = null;
let name = null
let Lalong = null
let T1 = { text: 'dummy', x: 180, y: 100 }
let T2 = { text: 'dummy', x: 180, y: 200 }
useEffect(() => {
async function getData() {
const response = await fetch(`http://localhost:5000/getchartdata/${params.id.toString()}`);
//const response = await fetch(`http://khona21.cleverapps.io/getchartdata/${params.id.toString()}`);
if (!response.ok) {
const message = `An error occurred: ${response.statusText}`;
window.alert(message);
return;
}
const dbdata = await response.json();
setData(dbdata);
}
getData();
return;
},[dbdata]);
// initialize the canvas context
useEffect(() => {
// dynamically assign the width and height to canvas
const canvasEle = canvas.current;
canvasEle.width = canvasEle.clientWidth;
canvasEle.height = canvasEle.clientHeight;
// get context of the canvas
ctx = canvasEle.getContext("2d");
//}, []);
}, [T1,T2]);
useEffect(() => {
let pid = dbdata.pid;
let spid = JSON.stringify(pid);
if (typeof spid !== 'undefined'){
name = dbdata.pid.name;
Lalong = dbdata.GLon.La
if (T1.text === 'dummy') {
T1.text = name
T2.text = Lalong.toString()
}
}
else {
//window.alert('undefined')
}
drawLine({ x: 150, y: 20, x1: 150, y1: 450 });
drawLine({ x: 300, y: 20, x1: 300, y1: 450 });
writeText(T1);
writeText(T2);
window.alert('finished drawing')
//}, []);
//}, [[]]);
}, [T1]);
const drawLine = (info, style = {}) => {
const { x, y, x1, y1 } = info;
const { color = 'black', width = 1 } = style;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x1, y1);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
}
// write a text
const writeText = (info, style = {}) => {
const { text, x, y } = info;
const { fontSize = 20, fontFamily = 'Arial', color = 'black', textAlign = 'left', textBaseline = 'top' } = style;
ctx.beginPath();
ctx.font = fontSize + 'px ' + fontFamily;
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.fillStyle = color;
ctx.fillText(text, x, y);
ctx.stroke();
}
return (
<div>
<h3>Chart Positions</h3>
<p>{JSON.stringify(dbdata)}</p>
<hr></hr>
<canvas ref={canvas}></canvas>
</div>
);
}
I have tried to ensure that that the text in T1 changes only once and hence should stop the loop. But it is not. What should I do?
the TRICK was to use [dbdata.length] instead of simply [dbdata] as the stopping condition in the useEffect() function. dbdata was being set each time as new, but dbdata.length was not changing.
import React, { useEffect, useState, useRef } from "react";
import { useParams } from "react-router";
import './cindex.css';
//let T1 = { text: 'dummy', x: 180, y: 100 }
//let T2 = { text: 'dummy', x: 180, y: 200 }
export default function GetChartData() {
const canvas = useRef();
let ctx = null;
const params = useParams();
const [dbdata, setData] = useState('empty52');
let name = null
let Lalong = null
const T1 = { text: 'dummy', x: 180, y: 100 }
const T2 = { text: 'dummy', x: 180, y: 200 }
useEffect(() => {
async function getData() {
const response = await fetch(`http://localhost:5000/getchartdata/${params.id.toString()}`);
//const response = await fetch(`http://khona21.cleverapps.io/getchartdata/${params.id.toString()}`);
if (!response.ok) {
const message = `An error occurred: ${response.statusText}`;
window.alert(message);
return;
}
const dbdata = await response.json();
setData(dbdata);
// *********************************************************************
let pid = dbdata.pid;
let spid = JSON.stringify(pid);
if (typeof spid !== 'undefined'){
name = dbdata.pid.name;
Lalong = dbdata.GLon.La
T1.text = name
T2.text = Lalong.toString()
}
else {
//window.alert('undefined')
}
// dynamically assign the width and height to canvas
const canvasEle = canvas.current;
canvasEle.width = canvasEle.clientWidth;
canvasEle.height = canvasEle.clientHeight;
// get context of the canvas
ctx = canvasEle.getContext("2d");
drawLine({ x: 150, y: 20, x1: 150, y1: 450 });
drawLine({ x: 300, y: 20, x1: 300, y1: 450 });
writeText(T1);
writeText(T2);
window.alert('image drawn')
// **********************************************************************
}
getData();
return;
},[dbdata.length]);
const drawLine = (info, style = {}) => {
const { x, y, x1, y1 } = info;
const { color = 'black', width = 1 } = style;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x1, y1);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
}
// write a text
const writeText = (info, style = {}) => {
const { text, x, y } = info;
const { fontSize = 20, fontFamily = 'Arial', color = 'black', textAlign = 'left', textBaseline = 'top' } = style;
ctx.beginPath();
ctx.font = fontSize + 'px ' + fontFamily;
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.fillStyle = color;
ctx.fillText(text, x, y);
ctx.stroke();
}
return (
<div>
<h3>Chart Positions</h3>
<p>{JSON.stringify(dbdata)}</p>
<hr></hr>
<canvas ref={canvas}></canvas>
</div>
);
}
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 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 am using React with TypeScript, I am trying to draw a rectangle shape on the canvas, the shape is drawn on the canvas but after drawing it again it went into an infinite loop. Even I am passing firstClicks, lastClicks as the second argument in useEffect.
due to running indefinitely, my app keeps crashing after some time.
here is my code:
import { useEffect, useRef, useState } from 'react';
interface ICoordinates{
x: number
y: number
}
const Canvas = ({height, width}: ICanvas) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const divRef = useRef<HTMLDivElement>(null);
let [firstClicks, setFirstClicks] = useState<ICoordinates>();
let [lastClicks, setLastClicks] = useState<ICoordinates>();
useEffect(() => {
const canvas = canvasRef.current?.getContext('2d');
const context = canvasRef.current;
let mousedown = false;
function drawRectangle(){
if(canvas){
canvas.beginPath();
if(firstClicks && lastClicks){
canvas.rect(firstClicks.x, firstClicks.y, lastClicks.x-firstClicks.x, lastClicks.y-firstClicks.y);
}
canvas.fillStyle = 'rgba(100,100,100,0.5)';
canvas.fill();
canvas.strokeStyle = "#df4b26";
canvas.lineWidth = 1;
canvas.stroke();
}
};
function redraw(){
if(context){
context.width = context.width;
}
drawRectangle();
};
if(context){
context.addEventListener("mousedown", function (e) {
setFirstClicks({
x: e.offsetX,
y: e.offsetY
})
mousedown = true;
});
context.addEventListener("mousemove", function (e) {
if (mousedown) {
setLastClicks({
x: e.offsetX,
y: e.offsetY
})
redraw();
}
});
context.addEventListener("mouseup", function (e) {
mousedown = false;
setLastClicks({
x: e.offsetX,
y: e.offsetY
})
});
context.addEventListener("mouseleave", function () {
mousedown = false;
});
}
},[firstClicks, lastClicks])
return (
<div ref={divRef}>
<canvas className='canvas' ref={canvasRef}>
</canvas>
</div>
)
}
export default Canvas
Have you tried this without having [firstClicks, lastClicks] and instead, have an empty dependency array: [].
The dependency array means that this useEffect callback will run on the mounting and unmounting of the component and any change to the variables in the dependency array.
I see that you are using setLastClicks and setFirstClicks meaning once the this is executed, the useEffect hook will detect a change in firstClicks or lastClick and run the effect again causing an infinite loop.
You can double check this by adding a console.log just before the setLastClicks and setFirstClicks to see if it is changing the state on every run of the hook's callback.
Here is a working version:
import { useEffect, useRef } from "react";
interface ICoordinates {
x: number;
y: number;
}
const Canvas = ({ height, width }: { height: number; width: number }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
console.log("Rerunning");
const canvas = canvasRef.current?.getContext("2d");
const context = canvasRef.current;
let mousedown = false;
let firstClick: ICoordinates = { x: 0, y: 0 };
let lastClick: ICoordinates = { x: 0, y: 0 };
function drawRectangle(
firstClicks: ICoordinates,
lastClicks: ICoordinates
) {
if (canvas) {
canvas.beginPath();
if (firstClicks && lastClicks) {
canvas.rect(
firstClicks.x,
firstClicks.y,
lastClicks.x - firstClicks.x,
lastClicks.y - firstClicks.y
);
}
canvas.fillStyle = "rgba(100,100,100,0.5)";
canvas.fill();
canvas.strokeStyle = "#df4b26";
canvas.lineWidth = 1;
canvas.stroke();
}
}
if (context) {
context.addEventListener("mousedown", function (e) {
firstClick = {
x: e.offsetX,
y: e.offsetY,
};
mousedown = true;
});
context.addEventListener("mousemove", function (e) {
if (mousedown) {
lastClick = {
x: e.offsetX,
y: e.offsetY,
};
drawRectangle(firstClick, lastClick);
}
});
context.addEventListener("mouseup", function (e) {
mousedown = false;
lastClick = {
x: e.offsetX,
y: e.offsetY,
};
});
context.addEventListener("mouseleave", function () {
mousedown = false;
});
}
}, []);
return (
<div ref={divRef}>
<canvas className="canvas" ref={canvasRef}></canvas>
</div>
);
};
export default Canvas;
Drawing rectangles :D
I'm trying to build a image cropping tool with React and konva. I'd like to change the opacity outside of the cropping rectangle to blur the rest of the image.
I have so far tried to set different opacities to the rectangle and the image but failed. I have looked up and there is no direct way to doing this
Here's the cropping function that I adapted to react with the help of this answer
import React, { useState, useEffect, useRef } from "react";
import { render } from "react-dom";
import { Stage, Layer, Rect, Image } from "react-konva";
import Konva from "konva";
const App = () => {
// Stage dims
let sW = 720,
sH = 720,
sX = 0,
sY = 0;
let src = "https://dummyimage.com/720x720/e85de8/fff&text=SO Rocks!";
let img = document.createElement("img");
useEffect(() => {
img.src = src;
function loadStatus() {
setloading(false);
}
img.addEventListener("load", loadStatus);
return () => {
img.removeEventListener("load", loadStatus);
};
}, [img, src]);
let scale = 1;
const [loading, setloading] = useState(true);
const [posStart, setposStart] = useState({});
const [posNow, setposNow] = useState({});
const [mode, setmode] = useState("");
/**
* Sets the state of posStart and posNow for tracking the coordinates of the cropping rectangle
* #param {Object} posIn - Coordinates of the pointer when MouseDown is fired
*/
function startDrag(posIn) {
setposStart({ x: posIn.x, y: posIn.y });
setposNow({ x: posIn.x, y: posIn.y });
}
/**
* Updates the state accordingly when the MouseMove event is fired
* #param {Object} posIn - Coordiantes of the current position of the pointer
*/
function updateDrag(posIn) {
setposNow({ x: posIn.x, y: posIn.y });
let posRect = reverse(posStart, posNow);
r2.current.x(posRect.x1);
r2.current.y(posRect.y1);
r2.current.width(posRect.x2 - posRect.x1);
r2.current.height(posRect.y2 - posRect.y1);
r2.current.visible(true);
}
/**
* Reverse coordinates if dragged left or up
* #param {Object} r1 - Coordinates of the starting position of cropping rectangle
* #param {Object} r2 - Coordinates of the current position of cropping rectangle
*/
function reverse(r1, r2) {
let r1x = r1.x,
r1y = r1.y,
r2x = r2.x,
r2y = r2.y,
d;
if (r1x > r2x) {
d = Math.abs(r1x - r2x);
r1x = r2x;
r2x = r1x + d;
}
if (r1y > r2y) {
d = Math.abs(r1y - r2y);
r1y = r2y;
r2y = r1y + d;
}
return { x1: r1x, y1: r1y, x2: r2x, y2: r2y }; // return the corrected rect.
}
/**
* Crops the image and saves it in jpeg format
* #param {Konva.Rect} r - Ref of the cropping rectangle
*/
function setCrop(r) {
let jpeg = new Konva.Image({
image: img,
x: sX,
y: sY
});
jpeg.cropX(r.x());
jpeg.cropY(r.y());
jpeg.cropWidth(r.width() * scale);
jpeg.cropHeight(r.height() * scale);
jpeg.width(r.width());
jpeg.height(r.height());
const url = jpeg.toDataURL({ mimeType: "image/jpeg", quality: "1.0" });
const a = document.createElement("a");
a.href = url;
a.download = "cropped.jpg";
a.click();
}
// Foreground rect to capture events
const r1 = useRef();
// Cropping rect
const r2 = useRef();
const image = useRef();
return (
<div className="container">
<Stage width={sW} height={sH}>
<Layer>
{!loading && (
<Image
ref={image}
{...{
image: img,
x: sX,
y: sY
}}
/>
)}
<Rect
ref={r1}
{...{
x: 0,
y: 0,
width: sW,
height: sH,
fill: "white",
opacity: 0
}}
onMouseDown={function (e) {
setmode("drawing");
startDrag({ x: e.evt.layerX, y: e.evt.layerY });
}}
onMouseMove={function (e) {
if (mode === "drawing") {
updateDrag({ x: e.evt.layerX, y: e.evt.layerY });
image.current.opacity(0.5);
r2.current.opacity(1);
}
}}
onMouseUp={function (e) {
setmode("");
r2.current.visible(false);
setCrop(r2.current);
image.current.opacity(1);
}}
/>
<Rect
ref={r2}
listening={false}
{...{
x: 0,
y: 0,
width: 0,
height: 0,
stroke: "white",
dash: [5, 5]
}}
/>
</Layer>
</Stage>
</div>
);
};
render(<App />, document.getElementById("root"));
Demo for the above code
It can be done using Konva.Group and its clip property. Add a new Konva.Image to the group and set its clipping positions and size to be the same as the cropping rectangle. Don't forget to set the listening prop of the group to false otherwise it will complicate things. Here's the final result
import { render } from "react-dom";
import React, { useLayoutEffect, useRef, useState } from "react";
import { Stage, Layer, Image, Rect, Group } from "react-konva";
/**
* Crops a portion of image in Konva stage and saves it in jpeg format
* #param {*} props - Takes no props
*/
function Cropper(props) {
// Stage dims
let sW = 720,
sH = 720,
sX = 0,
sY = 0;
let src = "https://dummyimage.com/720x720/e85de8/fff&text=SO Rocks!";
let img = new window.Image();
useLayoutEffect(() => {
img.src = src;
function loadStatus() {
// img.crossOrigin = "Anonymous";
setloading(false);
}
img.addEventListener("load", loadStatus);
return () => {
img.removeEventListener("load", loadStatus);
};
}, [img, src]);
let i = new Konva.Image({
x: 0,
y: 0,
width: 0,
height: 0
});
let scale = 1;
const [loading, setloading] = useState(true);
const [posStart, setposStart] = useState({});
const [posNow, setposNow] = useState({});
const [mode, setmode] = useState("");
/**
* Sets the state of posStart and posNow for tracking the coordinates of the cropping rectangle
* #param {Object} posIn - Coordinates of the pointer when MouseDown is fired
*/
function startDrag(posIn) {
setposStart({ x: posIn.x, y: posIn.y });
setposNow({ x: posIn.x, y: posIn.y });
}
/**
* Updates the state accordingly when the MouseMove event is fired
* #param {Object} posIn - Coordiantes of the current position of the pointer
*/
function updateDrag(posIn) {
setposNow({ x: posIn.x, y: posIn.y });
let posRect = reverse(posStart, posNow);
r2.current.x(posRect.x1);
r2.current.y(posRect.y1);
r2.current.width(posRect.x2 - posRect.x1);
r2.current.height(posRect.y2 - posRect.y1);
r2.current.visible(true);
grp.current.clip({
x: posRect.x1,
y: posRect.y1,
width: posRect.x2 - posRect.x1,
height: posRect.y2 - posRect.y1
});
grp.current.add(i);
i.image(img);
i.width(img.width);
i.height(img.height);
i.opacity(1);
}
/**
* Reverse coordinates if dragged left or up
* #param {Object} r1 - Coordinates of the starting position of cropping rectangle
* #param {Object} r2 - Coordinates of the current position of cropping rectangle
*/
function reverse(r1, r2) {
let r1x = r1.x,
r1y = r1.y,
r2x = r2.x,
r2y = r2.y,
d;
if (r1x > r2x) {
d = Math.abs(r1x - r2x);
r1x = r2x;
r2x = r1x + d;
}
if (r1y > r2y) {
d = Math.abs(r1y - r2y);
r1y = r2y;
r2y = r1y + d;
}
return { x1: r1x, y1: r1y, x2: r2x, y2: r2y }; // return the corrected rect.
}
/**
* Crops the image and saves it in jpeg format
* #param {Konva.Rect} r - Ref of the cropping rectangle
*/
function setCrop(r) {
let jpeg = new Konva.Image({
image: img,
x: sX,
y: sY
});
jpeg.cropX(r.x());
jpeg.cropY(r.y());
jpeg.cropWidth(r.width() * scale);
jpeg.cropHeight(r.height() * scale);
jpeg.width(r.width());
jpeg.height(r.height());
const url = jpeg.toDataURL({ mimeType: "image/jpeg", quality: "1.0" });
const a = document.createElement("a");
a.href = url;
a.download = "cropped.jpg";
a.click();
}
// Foreground rect to capture events
const r1 = useRef();
// Cropping rect
const r2 = useRef();
const image = useRef();
const grp = useRef();
return (
<div className="container">
<Stage width={sW} height={sH}>
<Layer>
{!loading && (
<Image
ref={image}
listening={false}
{...{
image: img,
x: sX,
y: sY
}}
/>
)}
<Rect
ref={r1}
{...{
x: 0,
y: 0,
width: sW,
height: sH,
fill: "white",
opacity: 0
}}
onMouseDown={function (e) {
setmode("drawing");
startDrag({ x: e.evt.layerX, y: e.evt.layerY });
image.current.opacity(0.2);
}}
onMouseMove={function (e) {
if (mode === "drawing") {
updateDrag({ x: e.evt.layerX, y: e.evt.layerY });
}
}}
onMouseUp={function (e) {
setmode("");
r2.current.visible(false);
setCrop(r2.current);
image.current.opacity(1);
grp.current.removeChildren(i);
}}
/>
<Group listening={false} ref={grp}></Group>
<Rect
ref={r2}
listening={false}
{...{
x: 0,
y: 0,
width: 0,
height: 0,
stroke: "white",
dash: [5, 10]
}}
/>
</Layer>
</Stage>
</div>
);
}
render(<Cropper />, document.getElementById("root"));
Thanks to #Vanquished Wombat for all the precious inputs. This is an adaptation of his answer here
Demo of the above code
I'm trying to build a space invader game from scratch using React/React-Hook & HTML5 canvas.
So far i achieved to draw my ship on the canvas but i can't figure out how to access my states in the "requestAnimationFrame" function. I did succeed to access REFs but i don't want all my vars to be refs.
So far my code looks like this :
import React from 'react';
import {makeStyles} from '#material-ui/core/styles';
const spaceInvaderStyles = makeStyles((theme) => ({
canvas: {
display: 'block',
margin: 'auto',
imageRendering: 'optimizeSpeed',
imageRendering: '-moz-crisp-edges',
imageRendering: '-webkit-optimize-contrast',
imageRendering: 'optimize-contrast',
backgroundColor: 'black',
}
}))
// GAME CONSTANTS
const GAME = {
shape: {
w:'640px',
h:'640px',
},
shipRow: '600',
shipColor: 'rgba(0,252,0)',
spritesSrc: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAADuCAYAAABh/7RrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAM4SURBVHhe7ZhRctswDAWrHCH3P6OPkNTuqB0FwxEAEqTkvt0feyYUJCRvX5xsvwr4frK/7WJ7sr9dxsf+KgeLqyG7eLNURstqNhVlSNTV0HX87j5HyXpP1NXA8QyeT97M0etbeDMtRF0NHH93cDwIi6uR8uIvXi94vo1eXwFRVwPHz8g6Oft8BURdDV3HPb9ezHAsw4xnJOpq6C5+tb8V9OzQtXSkbEZY8cPAcTVYXI1QicwuM48ZZUfU1cDxI1c77VHhPFFXQ9fxHp+tY6OdUDHPzvAg6mrg+LuD40FYXI2mF5731qfRnsjOs+d7IOpq6Doe8bPCqRFmPCNRV0PX8f31FM+x0Q6YPb8FUVeDxdXoKg2vjEaZUWYWoq4Gi6tR8sltNnxyK4TF1Wi6c7XTHhXOE3U1dB3v8dk6NtoJFfPsDA+irgaOvzs4HoTF1ej6zGt7IeuXpXpeBKKuBo4fsc5ZrIPeeY/sPHu+B6Kuhq7jET+rHcxe751/4d3TQtTV0F0868Yd6dmhZOlI+ZxxxTcfx9VgcTWapTJaVrOpKEOiroau43f3OUrWe6KuBo5n8HzyZo5e38KbaSHqauD4u4PjQVhcjZAX2R7wfKue1wNRVwPHj3gOjjo8+/oIRF0NHD9jtnMrnLYQdTVYXI2ucptRNkdW3I+oq8HiajT/y+qVSXX5ZOfZ8y+yz0DU1cDxI9aX1pmVRJ7HnvEg6mrgeIaIcxkq5tkZHkRdDRx/d3A8CIur0fTi7t5nfW5B1NXA8TOs8xWOnbHifkRdDRw/stppjxnPQ9TVYHE1QiWxuuxW3I+oq8HianSVRnX5ZOc9j3/ub//xvOSxvw1B1NXA8TOsg6sZ7ZAWRF0NHD/iOW2dG+2A7Dx7vgeiroau46N+3oWs90RdDRw/Yn25ugciz2PPeBB1NXD8iOdU1qcs3v3s119kn4moq6Hr+P6aouXYEc+30esrIOpqsLga00ukxcf3549y+9oey5+DqKvB4mosKRVbZh4ryo6oq8HiarC4GrKLN39fZn/v3p3W5wKiroau4/+bz1GIuhosrgaLq8HiarC4GiyuBourIbv4n/9Fqf1N/rU9tt/3UY/ZXED3UQAAAABJRU5ErkJggg==',
sprites: {
ship: {
x: 0,
y: 204,
w: 61,
h: 237,
}
},
player: {
initialPos: {
x: 290,
y: 580,
}
}
}
const KEYS = {
left:81,
right:68,
down: 83,
up: 90,
arrowLeft: 37,
arrowRight: 39,
arrowDown: 40,
arrowUp: 38,
}
const SpaceInvader = (props) => {
const classes = spaceInvaderStyles();
const canvasRef = React.useRef();
const [cctx, setCctx] = React.useState(null);
const [sprites, setSprites] = React.useState(null);
const [player, setPlayer] = React.useState({
pos: GAME.player.initialPos.x,
})
// keys
const [currentKey, setCurrentKey] = React.useState(null);
//time
let lastTime = 0;
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
// INIT
// context
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
setCctx(context)
// sprites
loadSprites();
}, [])
// key handler
React.useEffect(() => {
window.addEventListener('keydown', (event) => handleUserKeyPress(event,true));
window.addEventListener('keyup', (event) => handleUserKeyPress(event,false))
return () => {
window.removeEventListener('keydown', (event) => handleUserKeyPress(event,true));
window.removeEventListener('keyup', (event) => handleUserKeyPress(event,false))
};
}, [cctx, sprites, player, currentKey])
React.useEffect(() => {
if(!cctx) return
animate();
}, [cctx])
React.useEffect(() => {
if(spritesAreLoaded()){
cctx.drawImage(sprites, GAME.sprites.ship.x, GAME.sprites.ship.y, GAME.sprites.ship.w, GAME.sprites.ship.h, GAME.player.initialPos.x, GAME.player.initialPos.y , GAME.sprites.ship.w, GAME.sprites.ship.h)
}
}, [sprites])
React.useEffect(() => {
console.log(counter)
}, [counter])
// utils
const clearCanvas = () => {
cctx.clearRect(0,0, 640, 640);
}
const saveCanvas = () => {
cctx.save();
}
const drawImage = (image, sx, sy, sw, dx, dy, sh, dw, dh) => {
cctx.drawImage(image, sx, sy, sw, dx, dy, sh, dw, dh);
}
const restore = () => {
cctx.restore();
}
const loadSprites = () => {
var spritesImg = new Image();
spritesImg.src = GAME.spritesSrc;
spritesImg.onload = function() {
// sprites are loaded at this point
setSprites(spritesImg);
}
}
const spritesAreLoaded = () => {
return sprites !== null;
}
const move = (direction) => {
// cctx, sprites and all others state vars are at default value here too
clearCanvas();
saveCanvas();
drawImage(sprites, GAME.sprites.ship.x, GAME.sprites.ship.y, GAME.sprites.ship.w, GAME.sprites.ship.h, player.pos + (10 * direction), GAME.player.initialPos.y , GAME.sprites.ship.w, GAME.sprites.ship.h);
restore();
setPlayer({...player, pos: player.pos + (10 * direction)});
}
const handleUserKeyPress = React.useCallback( (event, isDown) => {
event.preventDefault();
const {key, keyCode} = event;
setCurrentKey(isDown ? keyCode : null);
}, [cctx, sprites, player, currentKey])
const updatePlayer = () => {
// currentKey is at default value here...
const direction = currentKey === KEYS.left ? -1 : currentKey === KEYS.right ? 1 : null;
if(direction !== null) move(direction)
}
const animate = (time) => {
var now = window.performance.now();
var dt = now - lastTime;
if(dt > 100) {
lastTime = now;
updatePlayer();
};
requestAnimationFrame(animate);
}
return (
<canvas
className={classes.canvas}
ref={canvasRef}
width={GAME.shape.w}
height={GAME.shape.h}
/>
)
}
export default SpaceInvader;
I'm trying to access "currentKey" in the thread function but it always return "null" (the default state value),
I found on some topic that you need to bind a context to the animate function but i don't know how to do it with a functional component (with a class component i would do a .bind(this))
I'm pretty new at HTML5 canvas so I might not be able to see the problem here.
All tips are appreciated,
Thanks in advance !
Finally after a lot of testing I did that :
React.useEffect(() => {
console.log("use Effect");
if(!cctx) return
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
})
and it works...
If anyone have a cleaner solution
EDIT 1 : After some more testing, it might not be a viable solution, it causes too many re-render