Skip to content

Commit

Permalink
Merge pull request #1 from AdiletBaimyrza/adilet/canvas
Browse files Browse the repository at this point in the history
Adilet/canvas
  • Loading branch information
andrii-venher authored Nov 5, 2023
2 parents 38d6803 + 64c01a4 commit d65251b
Show file tree
Hide file tree
Showing 17 changed files with 290 additions and 108 deletions.
1 change: 0 additions & 1 deletion dijkstra-prim-visualization/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
Expand Down
36 changes: 0 additions & 36 deletions dijkstra-prim-visualization/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,3 @@
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
}
27 changes: 2 additions & 25 deletions dijkstra-prim-visualization/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import Canvas from "./components/Canvas/Canvas";

function App() {
const [count, setCount] = useState(0);

return (
<>
<div>
<a href="https://vitejs.dev" target="_blank" rel="noreferrer">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
<Canvas />
</>
);
}
Expand Down
117 changes: 117 additions & 0 deletions dijkstra-prim-visualization/src/components/Canvas/Canvas.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useState, useRef } from "react";
import classes from "./Canvas.module.css";
import Node from "./Node/Node";
import Edge from "./Edge/Edge";
import { newEdgeValid, newNodePositionValid } from "./CanvasUtils";

/**
* Canvas component for visualizing nodes and edges.
* @returns {JSX.Element} The Canvas component.
*/
const Canvas = () => {
// State variables to keep track of all nodes and edges
const [nodePoints, setNodePoints] = useState([]);
const [edges, setEdges] = useState([]);

// Object with default data to reset firstNode, when needed
const resetNodeData = {
isClicked: false,
x: null,
y: null,
};

// State variable to keep track of the first node clicked when creating an edge
const [firstNode, setFirstNode] = useState(resetNodeData);

// Reference to the canvas SVG element
const canvasRef = useRef(null);

// Handler function for when the canvas is clicked
const canvasClickHandler = (event) => {
// Reset the firstNode state variable
setFirstNode(resetNodeData);

// Calculate the coordinates of the clicked point relative to the canvas
const nodePointAbsoluteX = event.clientX;
const nodePointAbsoluteY = event.clientY;
const nodePointCanvasRelativeX =
nodePointAbsoluteX - canvasRef.current.getBoundingClientRect().left;
const nodePointCanvasRelativeY =
nodePointAbsoluteY - canvasRef.current.getBoundingClientRect().top;

// Create a new node point object
const newNodePoint = {
x: nodePointCanvasRelativeX,
y: nodePointCanvasRelativeY,
};

// Check if the new node position is valid (not overlapping with existing nodes)
if (!newNodePositionValid(newNodePoint, nodePoints, canvasRef)) {
return;
}

// Add the new node point to the list of all nodes
setNodePoints((prevNodePoints) => [...prevNodePoints, newNodePoint]);
};

// Handler function for when a node is clicked
const nodeClickHandler = (event, points) => {
event.stopPropagation();

// If no first node has been clicked yet, set the clicked node as the first node
if (firstNode.isClicked === false) {
setFirstNode({ isClicked: true, x: points.x, y: points.y });
}
// If the same node is clicked again, reset the first node
else if (firstNode.x === points.x && firstNode.y === points.y) {
console.log("first node clicked again, reset firstNode");
setFirstNode(resetNodeData);
}
// If a different node is clicked, create a new edge between the first and second nodes
else {
addEdge(firstNode, { x: points.x, y: points.y });
setFirstNode(resetNodeData);
}
};

// Function to add a new edge to the list of all edges
const addEdge = (firstNode, secondNode) => {
const newEdge = {
x1: firstNode.x,
y1: firstNode.y,
x2: secondNode.x,
y2: secondNode.y,
};

// Check if the new edge is valid (not overlapping with existing edges)
if (!newEdgeValid(newEdge, edges)) {
return;
}

// Add the new edge to the list of all edges
setEdges((prevEdges) => [...prevEdges, newEdge]);
};

// Render the canvas SVG element with all nodes and edges
return (
<svg
ref={canvasRef}
className={classes.canvas}
onClick={canvasClickHandler}
>
{edges.map((edge, index) => (
<Edge key={index} x1={edge.x1} y1={edge.y1} x2={edge.x2} y2={edge.y2} />
))}
{nodePoints.map((nodePoint, index) => (
<Node
key={index}
cx={nodePoint.x}
cy={nodePoint.y}
onNodeClick={nodeClickHandler}
/>
))}
</svg>
);
};

export default Canvas;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.canvas {
background-color: black;
cursor: crosshair;
}/*# sourceMappingURL=Canvas.module.css.map */

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.canvas {
background-color: black;
cursor: crosshair;
}
86 changes: 86 additions & 0 deletions dijkstra-prim-visualization/src/components/Canvas/CanvasUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Calculates the Euclidean distance between two points.
* @param {Object} existingNodePoint - The coordinates of an existing node.
* @param {Object} newNodePoint - The coordinates of a new node.
* @returns {boolean} - Returns true if the distance is less than or equal to 35px, false otherwise.
*/
const isTooCloseToExistingNode = (existingNodePoint, newNodePoint) => {
const x1 = existingNodePoint.x;
const y1 = existingNodePoint.y;
const x2 = newNodePoint.x;
const y2 = newNodePoint.y;

const distance = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));

return distance <= 35;
};

/**
* Checks if a new node is within the bounds of the canvas.
* @param {Object} newNodePoint - The coordinates of the new node.
* @param {number} nodeRadius - The radius of the node.
* @param {Object} canvasRef - A reference to the canvas element.
* @returns {boolean} - Returns true if the new node is within the bounds of the canvas, false otherwise.
*/
const isNodeInBounds = (newNodePoint, nodeRadius, canvasRef) => {
return (
newNodePoint.x - nodeRadius >= 0 &&
newNodePoint.y - nodeRadius >= 0 &&
newNodePoint.x + nodeRadius <= canvasRef.current.clientWidth &&
newNodePoint.y + nodeRadius <= canvasRef.current.clientHeight
);
};

/**
* Checks if a new node position is valid.
* @param {Object} newNodePoint - The coordinates of the new node.
* @param {Array} nodePoints - An array of existing node coordinates.
* @param {Object} canvasRef - A reference to the canvas element.
* @returns {boolean} - Returns true if the new node position is valid, false otherwise.
*/
export const newNodePositionValid = (newNodePoint, nodePoints, canvasRef) => {
const isTooClose = nodePoints.some((existingNodePoint) =>
isTooCloseToExistingNode(existingNodePoint, newNodePoint),
);

if (isTooClose) {
console.log("New node is too close to an existing node.");
return false;
}

const isOutOfBounds = !isNodeInBounds(newNodePoint, 14, canvasRef);

if (isOutOfBounds) {
console.log("New node is out of bounds.");
return false;
}

return true;
};

/**
* Checks if a new edge is valid.
* @param {Object} newEdge - The coordinates of the new edge.
* @param {Array} edges - An array of existing edge coordinates.
* @returns {boolean} - Returns true if the new edge is valid, false otherwise.
*/
export const newEdgeValid = (newEdge, edges) => {
const edgeExists = edges.some(
(edge) =>
(edge.x1 === newEdge.x1 &&
edge.y1 === newEdge.y1 &&
edge.x2 === newEdge.x2 &&
edge.y2 === newEdge.y2) ||
(edge.x1 === newEdge.x2 &&
edge.y1 === newEdge.y2 &&
edge.x2 === newEdge.x1 &&
edge.y2 === newEdge.y1),
);

if (edgeExists) {
console.log("An edge with the same coordinates already exists.");
return false;
}

return true;
};
23 changes: 23 additions & 0 deletions dijkstra-prim-visualization/src/components/Canvas/Edge/Edge.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import PropTypes from "prop-types";
import classes from "./Edge.module.css";

/**
* Renders a line connecting two points on the canvas.
* @param {number} x1 - The x-coordinate of the starting point.
* @param {number} y1 - The y-coordinate of the starting point.
* @param {number} x2 - The x-coordinate of the ending point.
* @param {number} y2 - The y-coordinate of the ending point.
*/
const Edge = ({ x1, y1, x2, y2 }) => {
return <line className={classes.line} x1={x1} y1={y1} x2={x2} y2={y2} />;
};

export default Edge;

// Validates that the required props are passed and their type is number
Edge.propTypes = {
x1: PropTypes.number.isRequired,
y1: PropTypes.number.isRequired,
x2: PropTypes.number.isRequired,
y2: PropTypes.number.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.line {
stroke: #ff0000;
stroke-width: 2;
}/*# sourceMappingURL=Edge.module.css.map */

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.line {
stroke: #ff0000;
stroke-width: 2;
}
29 changes: 29 additions & 0 deletions dijkstra-prim-visualization/src/components/Canvas/Node/Node.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import PropTypes from "prop-types";
import classes from "./Node.module.css";

/**
* A component that renders a circle representing a node on the canvas.
* @param {number} cx - The x-coordinate of the center of the circle.
* @param {number} cy - The y-coordinate of the center of the circle.
* @param {function} onNodeClick - A callback function to handle click events on the node.
*/
const Node = ({ cx, cy, onNodeClick }) => {
return (
<circle
className={classes.circle}
cx={cx}
cy={cy}
r="14" // TODO: Make the radius a prop to allow for customization
onClick={(event) => onNodeClick(event, { x: cx, y: cy })}
/>
);
};

export default Node;

// Ensures that the required props are passed and have the correct type
Node.propTypes = {
onNodeClick: PropTypes.func.isRequired,
cx: PropTypes.number.isRequired,
cy: PropTypes.number.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.circle {
stroke: #fff;
stroke-width: 1.5;
cursor: pointer;
fill: #fff;
transition: all 0.2s linear;
}/*# sourceMappingURL=Node.module.css.map */

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.circle {
stroke: #fff;
stroke-width: 1.5;
cursor: pointer;
fill: #fff;
transition: all 0.2s linear;
}
Loading

0 comments on commit d65251b

Please sign in to comment.