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.
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 making a 3D walkable world. Implemented the keyboard movement using wasd and space and mouse movement. I have no problem with the mouse movement, but when pressing the keys on the keyboard it doesnt work. I have seen the example multiple times and it works, but for me it doesnt seem so.
I have a useKeyboardInput.js where I add the event listeners and it registers when i`m pressing the key.
import { useCallback, useEffect, useState } from "react";
export const useKeyboardInput = (keysToListen = []) => {
const getKeys = useCallback(() => {
const lowerCaseArray = [];
const hookReturn = {};
keysToListen.forEach((key) => {
const lowerCaseKey = key.toLowerCase();
lowerCaseArray.push(lowerCaseKey);
hookReturn[lowerCaseKey] = false;
});
return {
lowerCaseArray,
hookReturn,
};
}, [keysToListen]);
const [keysPressed, setPressedKeys] = useState(getKeys().hookReturn);
useEffect(() => {
const handleKeyDown = (e) => {
const lowerKey = e.key.toLowerCase();
if (getKeys().lowerCaseArray.includes(lowerKey)) {
setPressedKeys((keysPressed) => ({ ...keysPressed, [lowerKey]: true }));
}
console.log("Pressed Key is: " + e.key);
};
const handleKeyUp = (e) => {
const lowerKey = e.key.toLowerCase();
if (getKeys().lowerCaseArray.includes(lowerKey)) {
setPressedKeys((keysPressed) => ({
...keysPressed,
[lowerKey]: false,
}));
}
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, [keysToListen, getKeys]);
console.log("KeysPressed is: " + JSON.stringify(keysPressed));
return keysPressed;
};
And then i have the Player.js where i am using the hooks for keyboard and mouse.
import { useSphere } from "#react-three/cannon";
import React, { useEffect, useRef, useState } from "react";
import { useFrame, useThree } from "#react-three/fiber";
import { Vector3 } from "three";
import { useKeyboardInput } from "../Hooks/useKeyboardInput";
import { useMouseInput } from "../Hooks/useMouseInput";
import { useVariable } from "../Hooks/useVariable";
import { Bullet } from "./Bullet";
import { Raycaster } from "three";
/** Player movement constants */
const speed = 300;
const bulletSpeed = 30;
const bulletCoolDown = 300;
const jumpSpeed = 5;
const jumpCoolDown = 400;
export const Player = () => {
/** Player collider */
const [sphereRef, api] = useSphere(() => ({
mass: 100,
fixedRotation: true,
position: [0, 1, 0],
args: [0.2],
material: {
friction: 0,
},
}));
/** Bullets */
const [bullets, setBullets] = useState([]);
/** Input hooks */
const pressed = useKeyboardInput(["w", "a", "s", "d", " "]);
const pressedMouse = useMouseInput();
/** Converts the input state to ref so they can be used inside useFrame */
const input = useVariable(pressed);
const mouseInput = useVariable(pressedMouse);
/** Player movement constants */
const { camera, scene } = useThree();
/** Player state */
const state = useRef({
timeToShoot: 0,
timeTojump: 0,
vel: [0, 0, 0],
jumping: false,
});
useEffect(() => {
api.velocity.subscribe((v) => (state.current.vel = v));
}, [api]);
/** Player loop */
useFrame((_, delta) => {
/** Handles movement */
console.log("Input.current is: " + JSON.stringify(input.current));
const { w, s, a, d } = input.current;
const space = input.current[" "];
let velocity = new Vector3(0, 0, 0);
let cameraDirection = new Vector3();
camera.getWorldDirection(cameraDirection);
let forward = new Vector3();
forward.setFromMatrixColumn(camera.matrix, 0);
forward.crossVectors(camera.up, forward);
let right = new Vector3();
right.setFromMatrixColumn(camera.matrix, 0);
let [horizontal, vertical] = [0, 0];
if (w == true) {
vertical += 1;
console.log("Pressed w");
} else if (s == true) {
vertical -= 1;
console.log("Pressed s");
} else if (d == true) {
horizontal += 1;
console.log("Pressed d");
} else if (a == true) {
horizontal -= 1;
console.log("Pressed a");
}
console.log(
"Horizontal is: " + horizontal + " and vertical is: " + vertical
);
if (horizontal !== 0 && vertical !== 0) {
velocity
.add(forward.clone().multiplyScalar(speed * vertical))
.add(right.clone().multiplyScalar(speed * horizontal));
velocity.clampLength(-speed, speed);
} else if (horizontal !== 0) {
velocity.add(right.clone().multiplyScalar(speed * horizontal));
} else if (vertical !== 0) {
velocity.add(forward.clone().multiplyScalar(speed * vertical));
}
console.log("velocity is: " + JSON.stringify(velocity));
/** Updates player velocity */
api.velocity.set(
velocity.x * delta,
state.current.vel[1],
velocity.z * delta
);
/** Updates camera position */
camera.position.set(
sphereRef.current.position.x,
sphereRef.current.position.y + 1,
sphereRef.current.position.z
);
/** Handles jumping */
if (state.current.jumping && state.current.vel[1] < 0) {
/** Ground check */
const raycaster = new Raycaster(
sphereRef.current.position,
new Vector3(0, -1, 0),
0,
0.2
);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length !== 0) {
state.current.jumping = false;
}
}
if (space && !state.current.jumping) {
const now = Date.now();
if (now > state.current.timeTojump) {
state.current.timeTojump = now + jumpCoolDown;
state.current.jumping = true;
api.velocity.set(state.current.vel[0], jumpSpeed, state.current.vel[2]);
}
}
/** Handles shooting */
const bulletDirection = cameraDirection.clone().multiplyScalar(bulletSpeed);
const bulletPosition = camera.position
.clone()
.add(cameraDirection.clone().multiplyScalar(2));
if (mouseInput.current.left) {
const now = Date.now();
if (now >= state.current.timeToShoot) {
state.current.timeToShoot = now + bulletCoolDown;
setBullets((bullets) => [
...bullets,
{
id: now,
position: [bulletPosition.x, bulletPosition.y, bulletPosition.z],
forward: [bulletDirection.x, bulletDirection.y, bulletDirection.z],
},
]);
}
}
});
return (
<>
{/** Renders bullets */}
{bullets.map((bullet) => {
return (
<Bullet
key={bullet.id}
velocity={bullet.forward}
position={bullet.position}
/>
);
})}
</>
);
};
It registers the keys that are being pressed but nothing happens.
At first glance, the following snippet seems to be the source of issue.
const getKeys = useCallback(() => {
const lowerCaseArray = [];
const hookReturn = {};
You may have to move those variables outside of the callback.
both handleKeyDown and handleKeyUp calls getKeys() which creates new empty values
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>
);
}
so the problem is that when i'm trying to select an area using leaflet and click somewhere outside of selection tab it removes focus from the selection and after trying to select it doesn't work as intended meaning that i can't select properly
how do i return focus back to the tab?
appreciate any suggestions, if need to clarify something - tell me about it
here are the screenshots of how it should be and how it actually is
EDIT: here's code
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import L, { FeatureGroup, LatLngBounds, LeafletEvent, LeafletEventHandlerFn, LeafletMouseEvent, Point, PointTuple, Map } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-area-select';
import { Box, IconButton } from '#material-ui/core';
import GpsNotFixedRoundedIcon from '#material-ui/icons/GpsNotFixedRounded';
import { useStyles } from './styles';
import { disableMap, drawGrid } from '../../../utils/MapHelpers';
import { NewObs, Observation, Panorama } from '../../../state/reducers/types';
import { clearObs, saveGridSize, saveObsBounds, selectObsArea } from '../../../state/actions/inspector';
import OrientationMap from '../OrientationMap';
export type Grid = { gridCoordX: number[]; gridCoordY: number[]; gridLayer: FeatureGroup };
export let inspMap: Map;
interface AreaSelectEvent extends LeafletEvent {
bounds: LatLngBounds;
}
interface ILeafletWrapper {
handleObsForm: (showObsForm: boolean) => void;
handleIsSelectingObs: (isSelecting: boolean) => void;
handleOrientationMap: () => void;
handleGridSizeSelecting: () => void;
isSelectingObs: boolean;
newObs: NewObs;
observations: Observation[];
showOrientationMap: boolean;
isSelectingGridSize: boolean;
panorama: Panorama;
}
export const LeafletWrapper = (props: ILeafletWrapper) => {
const {
isSelectingObs,
handleObsForm,
handleIsSelectingObs,
observations,
showOrientationMap,
handleOrientationMap,
isSelectingGridSize,
handleGridSizeSelecting,
panorama,
} = props;
const { urlTemplates, originalResolution, originalZoomLevel, gridInfo } = panorama;
const [{ gridCoordX, gridCoordY, gridLayer }, setGrid] = useState<Grid>({
gridCoordX: [0],
gridCoordY: [0],
gridLayer: {},
} as Grid);
const [gridSizeBounds, setGridSizeBounds] = useState<LatLngBounds | undefined>();
const classes = useStyles();
const dispatch = useDispatch();
// initialization of Leaflet
useEffect(() => {
inspMap = new L.Map('map', { zoomControl: false, maxBoundsViscosity: 0.9 });
inspMap.setView(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel), originalZoomLevel);
// you just need to pass something as url, further work with url is rewritten
const TileLayer = L.tileLayer('url', {
// lock to only zoom level for first version of app
minZoom: originalZoomLevel,
maxZoom: originalZoomLevel,
noWrap: true,
tileSize: 8192,
});
// custom internal method to get tile's url, have to use this method due to the inability of
// generating a templated URL from a predefined URLs (they are all unique)
TileLayer.getTileUrl = ({ z, x, y }: { z: number; x: number; y: number }) => {
return urlTemplates[y + '-' + x];
};
TileLayer.addTo(inspMap);
// disable the need to use a ctrl btn to select area
inspMap.selectArea.setControlKey(false);
// draw grid if there is cached panoramas info
if (gridInfo.x) {
const bounds = L.latLngBounds(
inspMap.unproject([0, 0], originalZoomLevel),
inspMap.unproject([gridInfo.x, gridInfo.y], originalZoomLevel),
);
const gridObj = drawGrid(bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(bounds);
}
return () => {
inspMap.remove();
dispatch(clearObs());
};
}, []);
// disable interactions with leaflet while selectArea is enabled
useEffect(() => {
disableMap(isSelectingObs, inspMap);
if ((isSelectingObs || isSelectingGridSize) && !inspMap.selectArea.enabled()) {
inspMap.selectArea.enable();
inspMap.getContainer().style.cursor = 'crosshair';
inspMap.on('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
} else {
inspMap.getContainer().style.cursor = 'grab';
}
}, [isSelectingObs, isSelectingGridSize]);
// reset grid
useEffect(() => {
if (isSelectingGridSize && gridLayer?.remove) {
gridLayer.remove();
setGridSizeBounds(undefined);
}
}, [isSelectingGridSize, gridLayer]);
// draw observations
useEffect(() => {
// eslint-disable-next-line
observations.map((obs: any) => {
const index = obs.id;
if (obs.position.bounds) return;
const bounds = L.latLngBounds(
inspMap.unproject(obs.position.leftTopPoint, originalZoomLevel),
inspMap.unproject(obs.position.rightBottomPoint, originalZoomLevel),
);
const rectangle = L.rectangle(bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 }).addTo(inspMap);
dispatch(saveObsBounds({ bounds: { remove: rectangle.remove.bind(rectangle) }, index }));
});
}, [observations]);
const handleObsAreaSelected = (e: AreaSelectEvent) => {
if (isSelectingObs) {
const leftTopPoint = inspMap.project(e.bounds.getNorthWest(), originalZoomLevel);
leftTopPoint.x = Math.round(leftTopPoint.x);
leftTopPoint.y = Math.round(leftTopPoint.y);
const rightBottomPoint = inspMap.project(e.bounds.getSouthEast(), originalZoomLevel);
rightBottomPoint.x = Math.round(rightBottomPoint.x);
rightBottomPoint.y = Math.round(rightBottomPoint.y);
const bounds = L.rectangle(e.bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 });
bounds.addTo(inspMap);
dispatch(selectObsArea({ leftTopPoint, rightBottomPoint, bounds: { remove: bounds.remove.bind(bounds) } }));
handleObsForm(true);
handleIsSelectingObs(false);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
}
if (isSelectingGridSize) {
handleGridSizeSelecting();
const gridObj = drawGrid(e.bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(e.bounds);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
dispatch(saveGridSize({ x: gridObj.gridCoordX[0], y: gridObj.gridCoordY[0] }));
}
};
const panToCell = ({ latlng }: LeafletMouseEvent) => {
// user click coordinates converted to pixel relative coordinate system for original zoom level
const point: Point = inspMap.project(latlng, originalZoomLevel);
const cellCenter = [];
// find center of cell
for (let i = 0; i < gridCoordX.length; i++) {
if (point.x < gridCoordX[i]) {
cellCenter.push(gridCoordX[0] / 2);
break;
}
if (point.x > gridCoordX[i] && (point.x < gridCoordX[i + 1] || i === gridCoordX.length - 1)) {
cellCenter.push(gridCoordX[i] + gridCoordX[0] / 2);
break;
}
}
for (let i = 0; i < gridCoordY.length; i++) {
if (point.y < gridCoordY[i]) {
cellCenter.push(gridCoordY[0] / 2);
break;
}
if (point.y > gridCoordY[i] && (point.y < gridCoordY[i + 1] || i === gridCoordY.length - 1)) {
cellCenter.push(gridCoordY[i] + gridCoordY[0] / 2);
break;
}
}
// temporary locked zoom level due only one zoom level available
inspMap.flyTo(inspMap.unproject(cellCenter as PointTuple, originalZoomLevel), originalZoomLevel);
};
const panToCenter = () => inspMap.panTo(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel));
const zoomIn = () => inspMap.setZoom(inspMap.getZoom() + 1);
const zoomOut = () => inspMap.setZoom(inspMap.getZoom() - 1);
return (
<Box className={classes.root} height="100%">
<div id="map" style={{ height: '100%', width: '100%' }} />
<Box>
<IconButton className={classes.goToCenterBtn} aria-label="delete" onClick={panToCenter}>
<GpsNotFixedRoundedIcon color="inherit" />
</IconButton>
</Box>
<OrientationMap
panToCell={panToCell}
observations={observations}
showOrientationMap={showOrientationMap}
handleOrientationMap={handleOrientationMap}
handlePanToCenter={panToCenter}
zoomIn={zoomIn}
zoomOut={zoomOut}
gridSizeBounds={gridSizeBounds}
panorama={panorama}
/>
</Box>
);
};
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