diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..739f50a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode +.idea +*.iml +.env diff --git a/README.md b/README.md index 1e7f3e9..1f91655 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,48 @@ # Algorithms-Visualizer -DSA project - - TypeScript +This is a project made for the Data Structures and Algorithms course in International University with main the purpose of visualizing some algorithms. The project is finished with our initial intentions completed. + +## Pre-requisites +This project is made with: ++ Pure HTML and CSS ++ TypeScript + +## How to run +In order to run the project you need to +download TypeScript. When you have Node.js installed, you can use npm to install TypeScript globally: ++ Install Typescript +```bash +npm install -g typescript +``` ++ Run cmd +```bash +tsc --watch +``` +*Note*: It will auto compile from TypeScript into JavaScript everytime you save the files. + +If you can't run the project, you can visit here: to see the demo. + +## Features +### Sorting Algorithms ++ Visualize the sorting process of algorithms via *Books* in a Library. The goal is to sort all the books alphabetically. ++ User can choose the Algorithm desired to visualize, the speed of the visualization, the number of books and the worst case. ++ **Algorithms supported**: Bubble Sort, Selection Sort, Insertion Sort, Quick Sort +### Path Finding Algorithms ++ Visualize the path finding process of algorithms via drawable *Maze*. ++ User is presented with a blank canvas in which the user can draw the maze, set the start and end point and visualize the path finding process. ++ User can choose the Algorithm desired to visualize, the speed of the visualization. ++ **Algorithms supported**: Depth First Search, Breadth First Search + +## Demo +![The Library](https://imgur.com/C3tMUqv.png) +*The Library* +![Path Finding](https://imgur.com/m0nlZtu.png) +*Path Finding* + +## Contacts +If you have any questions regarding the projects, you can contact any one of us: ++ Pham Vu Quang ([quang-pham-1109](https://github.com/quang-pham-1109)): phvuquang@gmail.com ++ Nguyen Manh Viet Khoi ([tp-space](https://github.com/tpSpace)): nmvkhoivcl@gmail.com ++ Le Tuan Phuc ([Darkiee12](https://github.com/Darkiee12)): kizluxury0506@gmail.com ++ Do Anh Quan ([David-Do Maker](https://github.com/DavidDo-maker)): davidquan247@gmail.com + +Fellow IUers are very welcome! diff --git a/dest/graph/code/main.js b/dest/graph/code/main.js new file mode 100644 index 0000000..c5acf88 --- /dev/null +++ b/dest/graph/code/main.js @@ -0,0 +1,385 @@ +// Project: Algorithms and Data Structures +// Author: nmvkhoi +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +// ts-check +// constants +import getAdjacencyList from '../code/pathFindingAlgorithms/utility.js'; +import { getEndNode, getSourceNode, getNodeXCoordinates, getNodeYCoordinates } from "./pathFindingAlgorithms/utility.js"; +import { getShortestDistanceBFS } from "./pathFindingAlgorithms/bfs.js"; +import { getPathDFS } from "./pathFindingAlgorithms/dfs.js"; +import createText from "./popup.js"; +const canvas = document.getElementById('canvas'); +const ctx = canvas.getContext('2d'); +const clear = document.getElementById('clear'); +const begin = document.getElementById('begin'); +const end = document.getElementById('end'); +const wall = document.getElementById('wall'); +const start = document.getElementById('start'); +const select = document.getElementById('select'); +document.querySelector('#slider_time input').addEventListener('input', function () { + delay = Number(this.value); + document.querySelector('#value_time').textContent = this.value + "ms"; +}); +const width = canvas.width; +const height = canvas.height; +const cellSize = 20; +export let delay = 10; +const rows = height / cellSize; +const cols = width / cellSize; +let matrix = []; +let isDragging = false; +let isStart = false; +let prevStart = [-1, -1]; +let isEnd = false; +let prevEnd = [-1, -1]; +let selectedAlgorithm = ''; +// console.log(rows, cols); +let adjList = []; +let startNode; +let endNode = -1; +// Functions +// draw a square with animation zoom out +export function drawSquareWithAnimation(x, y, color) { + return __awaiter(this, void 0, void 0, function* () { + const initialSize = 1; + const targetSize = cellSize; + let currentSize = initialSize; + let animationStartTime = 0; // milliseconds + function animate() { + const now = Date.now(); + const elapsedTime = now - animationStartTime; + const progress = elapsedTime / 100; // Divide by 1000 to convert milliseconds to seconds + ctx.clearRect(y * cellSize, x * cellSize, cellSize, cellSize); // Clear the previous frame + if (progress < 1) { + currentSize = initialSize + (targetSize - initialSize) * progress; + ctx.fillStyle = color; + ctx.fillRect(y * cellSize + (cellSize - currentSize) / 2, x * cellSize + (cellSize - currentSize) / 2, currentSize, currentSize); + requestAnimationFrame(animate); + } + else { + ctx.fillStyle = color; + ctx.fillRect(y * cellSize, x * cellSize, cellSize, cellSize); + } + } + animationStartTime = Date.now(); + animate(); + }); +} +// create a matrix +function createMatrix([]) { + for (let i = 0; i < rows; i++) { + matrix[i] = []; + for (let j = 0; j < cols; j++) { + matrix[i][j] = 0; + } + } +} +// clear the matrix +function clearMatrix([]) { + for (let i = 0; i < rows; i++) { + matrix[i] = []; + for (let j = 0; j < cols; j++) { + matrix[i][j] = 0; + } + } +} +// print the matrix +function printMatrix([]) { + for (let i = 0; i < rows; i++) { + let line = ''; + for (let j = 0; j < cols; j++) { + line += matrix[i][j] + ' '; + } + console.log(line); + } +} +createMatrix(matrix); +// draw the grid +function drawGrid() { + for (let i = cellSize; i < height; i += cellSize) { + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(width, i); + ctx.stroke(); + } + for (let j = cellSize; j < width; j += cellSize) { + ctx.beginPath(); + ctx.moveTo(j, 0); + ctx.lineTo(j, height); + ctx.stroke(); + } +} +// draw a border +function drawSquare(event) { + let x = event.offsetX; + let y = event.offsetY; + let i = Math.floor(y / cellSize); + let j = Math.floor(x / cellSize); + // ctx.fillStyle = '#19A7CE'; + // ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); + matrix[i][j] = 2; + // printMatrix(matrix); + drawSquareWithAnimation(i, j, '#1239C6'); +} +// clear the canvas +function clearCanvas() { + ctx.clearRect(0, 0, width, height); + drawGrid(); + clearMatrix(matrix); + prevStart = [-1, -1]; +} +// clear the path and keep the wall and start and end point +// function clearPath() { +// ctx.clearRect(0, 0, width, height); +// drawGrid(); +// for (let i in matrix) { +// for (let j in matrix[i]) { +// if (matrix[i][j] === 2) { +// ctx.fillStyle = '#1239C6'; +// ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); +// } else if (matrix[i][j] === 1) { +// ctx.fillStyle = '#43c943'; +// ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); +// } else if (matrix[i][j] === 3) { +// ctx.fillStyle = '#ff4d4d'; +// ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); +// } +// } +// } +// } +// delete a square +function deleteSquare(event) { + let x = event.offsetX; + let y = event.offsetY; + let i = Math.floor(y / cellSize); + let j = Math.floor(x / cellSize); + ctx.clearRect(j * cellSize, i * cellSize, cellSize, cellSize); + // draw the line again + ctx.beginPath(); + ctx.moveTo(j * cellSize, i * cellSize); + ctx.lineTo((j + 1) * cellSize, i * cellSize); + // draw 4 edges of the square + ctx.moveTo((j + 1) * cellSize, i * cellSize); + ctx.lineTo((j + 1) * cellSize, (i + 1) * cellSize); + ctx.moveTo((j + 1) * cellSize, (i + 1) * cellSize); + ctx.lineTo(j * cellSize, (i + 1) * cellSize); + ctx.moveTo(j * cellSize, (i + 1) * cellSize); + ctx.lineTo(j * cellSize, i * cellSize); + ctx.moveTo(j * cellSize, i * cellSize); + ctx.stroke(); + matrix[i][j] = 0; +} +function initPoint(event) { + let x = Math.floor(event.offsetY / cellSize); + let y = Math.floor(event.offsetX / cellSize); + // delete the previous start point + if (prevStart[0] !== -1 && prevStart[1] !== -1) { + ctx.clearRect(prevStart[1] * cellSize, prevStart[0] * cellSize, cellSize, cellSize); + // draw the new start point + matrix[x][y] = 1; + updateAdjacencyList(); + ctx.fillStyle = '#43c943'; + ctx.fillRect(y * cellSize, x * cellSize, cellSize, cellSize); + console.log(prevStart); + // draw the line again + ctx.beginPath(); + ctx.moveTo(prevStart[1] * cellSize, prevStart[0] * cellSize); + ctx.lineTo((prevStart[1] + 1) * cellSize, prevStart[0] * cellSize); + // draw 4 edges of the square + ctx.moveTo((prevStart[1] + 1) * cellSize, prevStart[0] * cellSize); + ctx.lineTo((prevStart[1] + 1) * cellSize, (prevStart[0] + 1) * cellSize); + ctx.moveTo((prevStart[1] + 1) * cellSize, (prevStart[0] + 1) * cellSize); + ctx.lineTo(prevStart[1] * cellSize, (prevStart[0] + 1) * cellSize); + ctx.moveTo(prevStart[1] * cellSize, (prevStart[0] + 1) * cellSize); + ctx.lineTo(prevStart[1] * cellSize, prevStart[0] * cellSize); + ctx.moveTo(prevStart[1] * cellSize, prevStart[0] * cellSize); + ctx.stroke(); + return [x, y]; + } + else { + return [0, 0]; + } +} +function setEndPoint(event) { + let x = Math.floor(event.offsetY / cellSize); + let y = Math.floor(event.offsetX / cellSize); + // delete the previous start point + if (prevEnd[0] !== -1 && prevEnd[1] !== -1) { + ctx.clearRect(prevEnd[1] * cellSize, prevEnd[0] * cellSize, cellSize, cellSize); + // draw the new start point + matrix[x][y] = 3; + updateAdjacencyList(); + ctx.fillStyle = '#ff4d4d'; + ctx.fillRect(y * cellSize, x * cellSize, cellSize, cellSize); + console.log(prevEnd); + // draw the line again + ctx.beginPath(); + ctx.moveTo(prevEnd[1] * cellSize, prevEnd[0] * cellSize); + ctx.lineTo((prevEnd[1] + 1) * cellSize, prevEnd[0] * cellSize); + // draw 4 edges of the square + ctx.moveTo((prevEnd[1] + 1) * cellSize, prevEnd[0] * cellSize); + ctx.lineTo((prevEnd[1] + 1) * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.moveTo((prevEnd[1] + 1) * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.lineTo(prevEnd[1] * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.moveTo(prevEnd[1] * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.lineTo(prevEnd[1] * cellSize, prevEnd[0] * cellSize); + ctx.moveTo(prevEnd[1] * cellSize, prevEnd[0] * cellSize); + ctx.stroke(); + return [x, y]; + } + else { + return [0, 0]; + } +} +// check if the matrix is set begin node and end node +function checkMatrix(matrix) { + let count = 0; + for (let i in matrix) { + for (let j in matrix[i]) { + if (matrix[i][j] === 1 || matrix[i][j] === 3) { + count++; + } + } + } + if (count === 2) { + return true; + } + else { + return false; + } +} +function updateAdjacencyList() { + adjList = getAdjacencyList(matrix); + startNode = getSourceNode(matrix); + endNode = getEndNode(matrix); +} +function resetAdjacencyList() { + for (let i in adjList) { + for (let j in adjList[i]) { + adjList[i][j] = 0; + } + } + startNode = -1; + endNode = -1; +} +export function initPath(node) { + drawSquareWithAnimation(getNodeXCoordinates(node), getNodeYCoordinates(node), '#FFEA00'); +} +export function initPrevPath(node) { + drawSquareWithAnimation(getNodeXCoordinates(node), getNodeYCoordinates(node), '#33A3FF'); +} +// Add event listeners +clear.addEventListener('click', () => { + clearCanvas(); + resetAdjacencyList(); +}); +begin.addEventListener('click', (event) => { + isStart = true; + console.log('begin'); +}); +wall.addEventListener('click', (event) => { + isStart = false; + isEnd = false; +}); +end.addEventListener('click', () => { + console.log(endNode); + updateAdjacencyList(); + console.log('set-end'); + isStart = false; + isEnd = true; +}); +start.addEventListener('click', () => { + if (checkMatrix(matrix)) { + updateAdjacencyList(); + // switch case + if (selectedAlgorithm !== '') { + switch (selectedAlgorithm) { + case 'bfs': { + getShortestDistanceBFS(adjList, startNode, endNode); + matrix[prevStart[0]][prevStart[1]] = 1; + break; + } + case 'dfs': { + getPathDFS(adjList, startNode, endNode); + break; + } + // case 'aStar': {getShortestPathAStar(adjList); break;} + default: { + createText("Please select an algorithm", "red"); + break; + } + } + } + else { + createText("Please select an algorithms", "red"); + } + } + else { + createText('Please set the start and end point', "red"); + } +}); +select.addEventListener('change', (event) => { + const target = event.target; + selectedAlgorithm = target.value; + console.log(selectedAlgorithm); +}); +canvas.addEventListener('mousedown', (event) => { + if (event.button === 0 && !isStart && !isEnd) { + isDragging = true; + drawSquare(event); + } + else if (event.button === 0 && isStart) { + prevStart = initPoint(event); + } + else if (event.button === 0 && isEnd === true) { + console.log('end'); + prevEnd = setEndPoint(event); + } +}); +canvas.addEventListener('mousemove', (event) => { + if (isDragging && event.button === 0) { + drawSquare(event); + } +}); +canvas.addEventListener('mouseup', () => { + isDragging = false; +}); +canvas.addEventListener('contextmenu', (event) => { + deleteSquare(event); +}); +// Run the functions once the page is loaded +(function once(arrays) { + // console.log(cellSize); + // console.log(width, height); + // console.log(rows, cols); + createMatrix(arrays); + printMatrix(arrays); + drawGrid(); +})(matrix); +function featureEnabling(bool) { + if (!bool) { + clear.setAttribute('disabled', 'true'); + begin.setAttribute('disabled', 'true'); + wall.setAttribute('disabled', 'true'); + end.setAttribute('disabled', 'true'); + start.setAttribute('disabled', 'true'); + select.setAttribute('disabled', 'true'); + } + else { + clear.removeAttribute('disabled'); + begin.removeAttribute('disabled'); + wall.removeAttribute('disabled'); + end.removeAttribute('disabled'); + start.removeAttribute('disabled'); + select.removeAttribute('disabled'); + } +} +export { matrix, cellSize, width, height, rows, cols, ctx, canvas }; diff --git a/dest/graph/code/pathFindingAlgorithms/Maze.js b/dest/graph/code/pathFindingAlgorithms/Maze.js new file mode 100644 index 0000000..0bfe462 --- /dev/null +++ b/dest/graph/code/pathFindingAlgorithms/Maze.js @@ -0,0 +1,55 @@ +export function generateMazeUsingKruskal(maze) { + const rows = maze.length; + const cols = maze[0].length; + // Create a list of all walls in the maze + const walls = []; + for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + // Add vertical walls + if (j < cols - 1) { + walls.push([i, j, i, j + 1]); + } + // Add horizontal walls + if (i < rows - 1) { + walls.push([i, j, i + 1, j]); + } + } + } + // Create a disjoint set to track the connected components + const parent = []; + for (let i = 0; i < rows * cols; i++) { + parent[i] = i; + } + // Helper function to find the parent of a set + function find(parent, i) { + if (parent[i] !== i) { + parent[i] = find(parent, parent[i]); + } + return parent[i]; + } + // Helper function to join two sets + function union(parent, i, j) { + const rootA = find(parent, i); + const rootB = find(parent, j); + parent[rootA] = rootB; + } + // Randomize the order of walls + walls.sort(() => Math.random() - 0.5); + // Process each wall and remove it if it connects two different sets + for (const wall of walls) { + const [x1, y1, x2, y2] = wall; + const indexA = x1 * cols + y1; + const indexB = x2 * cols + y2; + const rootA = find(parent, indexA); + const rootB = find(parent, indexB); + if (rootA !== rootB) { + union(parent, rootA, rootB); + maze[x1][y1] = 0; + maze[x2][y2] = 0; + } + } + // Set the beginning node (start) as 1 and the end node as 3 + maze[0][0] = 1; + maze[rows - 1][cols - 1] = 3; + return maze; +} diff --git a/dest/graph/code/pathFindingAlgorithms/bfs.js b/dest/graph/code/pathFindingAlgorithms/bfs.js new file mode 100644 index 0000000..cd19713 --- /dev/null +++ b/dest/graph/code/pathFindingAlgorithms/bfs.js @@ -0,0 +1,69 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +//BFS algorithm +import { initPath, delay } from "../main.js"; +import { initPrevPath } from "../main.js"; +import { delayRender } from "./utility.js"; +import createText from "../popup.js"; +//Helper function that uses BFS to transverse the graph +export function bfs(adj, src, dest, v, prev, dist) { + return __awaiter(this, void 0, void 0, function* () { + let queue = []; + let visited = new Array(v); + for (let i = 0; i < v; i++) { + visited[i] = false; + dist[i] = Number.MAX_VALUE; + prev[i] = -1; + } + visited[src] = true; + dist[src] = 0; + queue.push(src); + while (queue.length > 0) { + let u = queue[0]; + queue.shift(); + for (let i in adj[u]) { + if (visited[adj[u][i]] == false) { + visited[adj[u][i]] = true; + dist[adj[u][i]] = dist[u] + 1; + prev[adj[u][i]] = u; + queue.push(adj[u][i]); + initPrevPath(adj[u][i]); + yield delayRender(delay); + if (adj[u][i] == dest) { + return true; + } + } + } + } + return false; + }); +} +//Helper function to backtrack the path and print the shortest path +export function getShortestDistanceBFS(adj, src, dest) { + return __awaiter(this, void 0, void 0, function* () { + let v = adj.length; + let prev = new Array(v).fill(0); + let dist = new Array(v).fill(0); + if (!(yield bfs(adj, src, dest, v, prev, dist))) { + createText('Source and destination vertex is not connected!', "red"); + } + let path = []; + let crawl = dest; + path.push(crawl); + while (prev[crawl] != -1) { + path.push(prev[crawl]); + crawl = prev[crawl]; + } + for (let i = path.length - 1; i >= 0; i--) { + yield delayRender(4 * delay); + initPath(path[i]); + } + }); +} diff --git a/dest/graph/code/pathFindingAlgorithms/dfs.js b/dest/graph/code/pathFindingAlgorithms/dfs.js new file mode 100644 index 0000000..d9b3894 --- /dev/null +++ b/dest/graph/code/pathFindingAlgorithms/dfs.js @@ -0,0 +1,50 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { initPath, delay } from "../main.js"; +import createText from "../popup.js"; +import { delayRender } from "./utility.js"; +export function drawPath(stack) { + return __awaiter(this, void 0, void 0, function* () { + for (let i in stack) { + initPath(stack[i]); + yield delayRender(delay); + } + }); +} +export function DFS(visited, adjList, source, destination, stack) { + return __awaiter(this, void 0, void 0, function* () { + visited[source] = true; + stack.push(source); + if (source == destination) { + yield drawPath(stack); + return; + } + for (let i in adjList[source]) { + if (!visited[adjList[source][i]]) { + yield DFS(visited, adjList, adjList[source][i], destination, stack); + } + } + stack.pop(); + }); +} +export function getPathDFS(adjList, source, destination) { + return __awaiter(this, void 0, void 0, function* () { + let n = adjList.length; + let visited = new Array(n + 1); + let stack = []; + for (let i = 0; i < n + 1; i++) { + visited[i] = false; + } + yield DFS(visited, adjList, source, destination, stack); + if (stack.length === 0) { + createText("No path found!", "red"); + } + }); +} diff --git a/dest/graph/code/pathFindingAlgorithms/utility.js b/dest/graph/code/pathFindingAlgorithms/utility.js new file mode 100644 index 0000000..d0bd021 --- /dev/null +++ b/dest/graph/code/pathFindingAlgorithms/utility.js @@ -0,0 +1,120 @@ +let vertexIndex = []; +export function addEdge(adj, u, v) { + adj[u].push(v); +} +//Check if a vertex is in the grid, if it is return its index in the vertexIndex array +//If it is not in the grid, return -1 +export function findVertexIndex(vertexIndex, row, col) { + for (let i = 0; i < vertexIndex.length; i++) { + if (vertexIndex[i][0] === row && vertexIndex[i][1] === col) { + return i; + } + } + return -1; +} +export function resetVertexIndex() { + vertexIndex = []; +} +//# is wall, . is empty, A is start, B is destination +//every . and A or B are considered a vertex +export default function getAdjacencyList(graph) { + //number of vertices + let v = 0; + //adjacency list + let adj = new Array(graph.length * graph[0].length); + resetVertexIndex(); + for (let i = 0; i < graph.length; i++) { + for (let j = 0; j < graph[i].length; j++) { + //If the current element is not a wall, add it to the vertexIndex array + //and increment the number of vertices + if (graph[i][j] != 2) { + vertexIndex[v] = []; + vertexIndex[v].push(i); + vertexIndex[v].push(j); + adj[v] = []; + v++; + } + } + } + //Check for each vertex if there is a path to another vertex + //if there is, add an edge between them + for (let i = 0; i < vertexIndex.length; i++) { + let row = vertexIndex[i][0]; + let col = vertexIndex[i][1]; + //check if there is a path to the vertex above + if (findVertexIndex(vertexIndex, row - 1, col) !== -1) { + addEdge(adj, i, findVertexIndex(vertexIndex, row - 1, col)); + } + //check if there is a path to the vertex below + if (findVertexIndex(vertexIndex, row + 1, col) !== -1) { + addEdge(adj, i, findVertexIndex(vertexIndex, row + 1, col)); + } + //check if there is a path to the vertex to the left + if (findVertexIndex(vertexIndex, row, col - 1) !== -1) { + addEdge(adj, i, findVertexIndex(vertexIndex, row, col - 1)); + } + //check if there is a path to the vertex to the right + if (findVertexIndex(vertexIndex, row, col + 1) !== -1) { + addEdge(adj, i, findVertexIndex(vertexIndex, row, col + 1)); + } + } + return adj; +} +export function getSourceNode(graph) { + let v = 0; + for (let i = 0; i < graph.length; i++) { + for (let j = 0; j < graph[i].length; j++) { + if (graph[i][j] != 2) { + if (graph[i][j] == 1) { + return v; + } + v++; + } + } + } + return -1; +} +export function getEndNode(graph) { + let v = 0; + for (let i = 0; i < graph.length; i++) { + for (let j = 0; j < graph[i].length; j++) { + if (graph[i][j] != 2) { + if (graph[i][j] == 3) { + return v; + } + v++; + } + } + } + return -1; +} +export function getNodeXCoordinates(v) { + for (let i = 0; i < vertexIndex.length; i++) { + if (i === v) { + return vertexIndex[i][0]; + } + } + return -1; +} +export function getNodeYCoordinates(v) { + for (let i = 0; i < vertexIndex.length; i++) { + if (i === v) { + return vertexIndex[i][1]; + } + } + return -1; +} +export function delayRender(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +// let graph: string[][] = [ +// ['#', '#', '#', '#', '#'], +// ['#', 'A', '.', '.', '#'], +// ['#', '#', '#', '.', '#'], +// ['#', '.', '#', '.', '#'], +// ['#', '.', '.', '.', '#'], +// ['#', '#', 'B', '#', '#'] +// ]; +// let adj: number[][] = []; +// +// fromGridToList(graph, adj); diff --git a/dest/graph/code/popup.js b/dest/graph/code/popup.js new file mode 100644 index 0000000..c9a097e --- /dev/null +++ b/dest/graph/code/popup.js @@ -0,0 +1,62 @@ +const warn = document.getElementsByClassName('warning')[0]; +const FADE_IN_TIME = 1000; //ms +const FADE_OUT_TIME = 1000; +const APPEAR_TIME = 8000; +export default function createText(message, color) { + warn.textContent = message; + warn.style.color = color; + warn.style.opacity = '0'; + warn.style.top = '-50px'; + fadeIn(warn, FADE_IN_TIME); + // Fade out after 10 seconds with 2 second fade-out time + setTimeout(function () { + fadeOut(warn, FADE_OUT_TIME); + }, APPEAR_TIME); +} +function fadeIn(element, duration) { + var interval = 10; + var opacity = 0; + var targetOpacity = 1; + var delta = (interval / duration) * (targetOpacity - opacity); + var timer = setInterval(function () { + opacity += delta; + element.style.opacity = String(opacity); + if (opacity >= targetOpacity) { + clearInterval(timer); + animate(element, 'rise', FADE_IN_TIME); + } + }, interval); +} +function fadeOut(element, duration) { + var interval = 10; + var opacity = 1; + var targetOpacity = 0; + var delta = (interval / duration) * (opacity - targetOpacity); + var timer = setInterval(function () { + opacity -= delta; + element.style.opacity = String(opacity); + if (opacity <= targetOpacity) { + clearInterval(timer); + animate(element, 'fall', FADE_OUT_TIME); + } + }, interval); +} +function animate(element, direction, duration) { + var interval = 10; + var start = parseInt(element.style.top); + var end; + if (direction === 'rise') { + end = 0; + } + else { + end = -50; + } + var delta = (interval / duration) * (end - start); + var timer = setInterval(function () { + start += delta; + element.style.top = String(start) + 'px'; + if ((direction === 'rise' && start >= end) || (direction === 'fall' && start <= end)) { + clearInterval(timer); + } + }, interval); +} diff --git a/dest/Book.js b/dest/sort/code/Book.js similarity index 100% rename from dest/Book.js rename to dest/sort/code/Book.js diff --git a/dest/main.js b/dest/sort/code/main.js similarity index 53% rename from dest/main.js rename to dest/sort/code/main.js index 6014208..f3c8327 100644 --- a/dest/main.js +++ b/dest/sort/code/main.js @@ -1,12 +1,15 @@ import Book from './Book.js'; -import bubbleSort from './sorting/BubbleSort.js'; -import selectionSort from './sorting/SelectionSort.js'; -import insertionSort from './sorting/InsertionSort.js'; +import createText from './popup.js'; +import bubbleSort from './sortingAlgorithms/BubbleSort.js'; +import selectionSort from './sortingAlgorithms/SelectionSort.js'; +import insertionSort from './sortingAlgorithms/InsertionSort.js'; +import quickSort from './sortingAlgorithms/QuickSort.js'; +// import mergeSort from './sortingAlgorithms/MergeSort'; const bookshelfContainer = document.getElementById('bookshelf_container'); -const above = document.getElementById('augmented_container_above'); +const above = document.getElementById('augmented_container'); //DEFAULT const DEFAULT_NUM_BOOKS = 20; -const DEFAULT_TIMEOUT = 500; +const DEFAULT_TIMEOUT = 100; const sortingBooks = { bubble: { name: 'Bubble Sort', @@ -20,6 +23,14 @@ const sortingBooks = { name: 'Insertion Sort', algo: insertionSort, }, + quick: { + name: 'Quick Sort', + algo: quickSort, + }, + // merge:{ + // name: 'Merge Sort', + // algo: mergeSort, + // } }; let books = DEFAULT_NUM_BOOKS; let bookshelf = []; @@ -27,47 +38,40 @@ let bookshelfStack = []; let timeOut = DEFAULT_TIMEOUT; let selectedAlgo = ""; createBookshelf(); -//DOM elements -const goButton = document.getElementById('go-button'); -const randomButton = document.getElementById('random-button'); -const resetButton = document.getElementById('reset-button'); -const worstCaseButton = document.getElementById('worst-case-button'); -const sortingAlgoSelection = document.getElementById('format'); -const sliderBooks = document.querySelector('.slider_books input'); -const valueBooks = document.querySelector('.value_books'); -const sliderTime = document.querySelector('.slider_time input'); -const valueTime = document.querySelector('.value_time'); -//Add event listeners -goButton.addEventListener('click', () => { +//Add event listeners to DOM elements +document.getElementById('go-button').addEventListener('click', () => { go(); }); -randomButton.addEventListener('click', () => { +document.getElementById('random-button').addEventListener('click', () => { random(); }); -resetButton.addEventListener('click', () => { +document.getElementById('reset-button').addEventListener('click', () => { reset(); }); -worstCaseButton.addEventListener('click', () => { +document.getElementById('worst-case-button').addEventListener('click', () => { createWorstBookshelf(); }); -sliderBooks.addEventListener('input', function () { +document.querySelector('#slider_books input').addEventListener('input', function () { books = Number(this.value); - valueBooks.textContent = this.value + " books."; + document.querySelector('#value_books').textContent = this.value + " books."; createBookshelf(); }); -sliderTime.addEventListener('input', function () { +document.querySelector('#slider_time input').addEventListener('input', function () { timeOut = Number(this.value); - valueTime.textContent = this.value + "ms"; + document.querySelector('#value_time').textContent = this.value + "ms"; }); -sortingAlgoSelection.addEventListener('change', () => { - selectedAlgo = sortingAlgoSelection.value; +document.getElementById('format').addEventListener('change', function () { + selectedAlgo = this.value; }); //Helper functions function go() { const copy = [...bookshelf]; const sort = sortingBooks[selectedAlgo]; - if (sort == undefined) - alert("Please select a sorting algorithm"); + if (sort == undefined) { + createText("Please select an algorithm.", "red"); + return; + } + featureEnabling(false); const moves = sort.algo(copy); animate(moves); } @@ -112,12 +116,7 @@ function visualizeBookshelf(move) { bookshelfContainer.innerHTML = ''; above.innerHTML = ''; for (let i = 0; i < bookshelf.length; i++) { - const book = document.createElement('div'); - const bookName = document.createElement('span'); - book.classList.add('book'); - book.style.backgroundColor = bookshelf[i].color; - bookName.textContent = bookshelf[i].name; - book.appendChild(bookName); + const book = visualizeBook(bookshelf[i]); let bookAbove = document.createElement('div'); if (move && move.indices.includes(i)) { book.style.opacity = '0'; @@ -136,6 +135,8 @@ function visualizeBookshelf(move) { function animate(moves) { if (moves.length == 0) { visualizeBookshelf(); + createText("The bookshelf is sorted!", "lime"); + featureEnabling(true); return; } const move = moves.shift(); @@ -144,12 +145,40 @@ function animate(moves) { [bookshelf[i], bookshelf[j]] = [bookshelf[j], bookshelf[i]]; } visualizeBookshelf(move); + // setTimeout(function () { + // visualizeBookshelf(); + // }, timeOut); setTimeout(function () { animate(moves); }, timeOut); } +function visualizeBook(book) { + const newBook = document.createElement('div'); + newBook.classList.add('book'); + newBook.style.backgroundColor = book.color; + const newBookName = document.createElement('span'); + newBookName.textContent = book.name; + newBook.appendChild(newBookName); + return newBook; +} function displayBookshelfConsole(bookshelf) { const bookNames = bookshelf.map((book) => book.name); console.log(bookNames.join(", ")); } displayBookshelfConsole(bookshelf); +function featureEnabling(bool) { + if (!bool) { + document.getElementById('go-button').setAttribute('disabled', String(bool)); + document.getElementById('random-button').setAttribute('disabled', String(bool)); + document.getElementById('reset-button').setAttribute('disabled', String(bool)); + document.getElementById('worst-case-button').setAttribute('disabled', String(bool)); + document.getElementById('number_book_input').setAttribute('disabled', String(bool)); + } + else { + document.getElementById('go-button').removeAttribute('disabled'); + document.getElementById('random-button').removeAttribute('disabled'); + document.getElementById('reset-button').removeAttribute('disabled'); + document.getElementById('worst-case-button').removeAttribute('disabled'); + document.getElementById('number_book_input').removeAttribute('disabled'); + } +} diff --git a/dest/sort/code/popup.js b/dest/sort/code/popup.js new file mode 100644 index 0000000..c9a097e --- /dev/null +++ b/dest/sort/code/popup.js @@ -0,0 +1,62 @@ +const warn = document.getElementsByClassName('warning')[0]; +const FADE_IN_TIME = 1000; //ms +const FADE_OUT_TIME = 1000; +const APPEAR_TIME = 8000; +export default function createText(message, color) { + warn.textContent = message; + warn.style.color = color; + warn.style.opacity = '0'; + warn.style.top = '-50px'; + fadeIn(warn, FADE_IN_TIME); + // Fade out after 10 seconds with 2 second fade-out time + setTimeout(function () { + fadeOut(warn, FADE_OUT_TIME); + }, APPEAR_TIME); +} +function fadeIn(element, duration) { + var interval = 10; + var opacity = 0; + var targetOpacity = 1; + var delta = (interval / duration) * (targetOpacity - opacity); + var timer = setInterval(function () { + opacity += delta; + element.style.opacity = String(opacity); + if (opacity >= targetOpacity) { + clearInterval(timer); + animate(element, 'rise', FADE_IN_TIME); + } + }, interval); +} +function fadeOut(element, duration) { + var interval = 10; + var opacity = 1; + var targetOpacity = 0; + var delta = (interval / duration) * (opacity - targetOpacity); + var timer = setInterval(function () { + opacity -= delta; + element.style.opacity = String(opacity); + if (opacity <= targetOpacity) { + clearInterval(timer); + animate(element, 'fall', FADE_OUT_TIME); + } + }, interval); +} +function animate(element, direction, duration) { + var interval = 10; + var start = parseInt(element.style.top); + var end; + if (direction === 'rise') { + end = 0; + } + else { + end = -50; + } + var delta = (interval / duration) * (end - start); + var timer = setInterval(function () { + start += delta; + element.style.top = String(start) + 'px'; + if ((direction === 'rise' && start >= end) || (direction === 'fall' && start <= end)) { + clearInterval(timer); + } + }, interval); +} diff --git a/dest/sort/code/sortingAlgorithms/BubbleSort.js b/dest/sort/code/sortingAlgorithms/BubbleSort.js new file mode 100644 index 0000000..253be54 --- /dev/null +++ b/dest/sort/code/sortingAlgorithms/BubbleSort.js @@ -0,0 +1,22 @@ +export default function selectionSort(bookshelf) { + const moves = []; + var swapped; + do { + swapped = false; + for (let i = 0; i < bookshelf.length - 1; i++) { + moves.push({ + indices: [i, i + 1], + type: "compare" + }); + if (bookshelf[i].name > bookshelf[i + 1].name) { + swapped = true; + moves.push({ + indices: [i, i + 1], + type: "swap" + }); + [bookshelf[i], bookshelf[i + 1]] = [bookshelf[i + 1], bookshelf[i]]; + } + } + } while (swapped); + return moves; +} diff --git a/dest/sorting/InsertionSort.js b/dest/sort/code/sortingAlgorithms/InsertionSort.js similarity index 84% rename from dest/sorting/InsertionSort.js rename to dest/sort/code/sortingAlgorithms/InsertionSort.js index 367cd53..565dc57 100644 --- a/dest/sorting/InsertionSort.js +++ b/dest/sort/code/sortingAlgorithms/InsertionSort.js @@ -4,18 +4,18 @@ export default function insertionSort(bookshelf) { const current = bookshelf[i]; let j = i - 1; while (j >= 0 && bookshelf[j].name > current.name) { + bookshelf[j + 1] = bookshelf[j]; moves.push({ indices: [j, j + 1], - type: "shift" + type: "swap" }); - bookshelf[j + 1] = bookshelf[j]; j--; } + bookshelf[j + 1] = current; moves.push({ - indices: [i, j + 1], - type: "swap" + indices: [j + 1], + type: "compare" }); - bookshelf[j + 1] = current; } return moves; } diff --git a/dest/sort/code/sortingAlgorithms/MergeSort.js b/dest/sort/code/sortingAlgorithms/MergeSort.js new file mode 100644 index 0000000..3918c74 --- /dev/null +++ b/dest/sort/code/sortingAlgorithms/MergeSort.js @@ -0,0 +1 @@ +"use strict"; diff --git a/dest/sort/code/sortingAlgorithms/QuickSort.js b/dest/sort/code/sortingAlgorithms/QuickSort.js new file mode 100644 index 0000000..9b35048 --- /dev/null +++ b/dest/sort/code/sortingAlgorithms/QuickSort.js @@ -0,0 +1,47 @@ +export default function quickSort(bookshelf) { + const moves = []; + const partition = (left, right) => { + const pivotIndex = Math.floor((left + right) / 2); + const pivot = bookshelf[pivotIndex]; + let i = left; + let j = right; + while (i <= j) { + while (bookshelf[i].name < pivot.name) { + i++; + moves.push({ + indices: [i, pivotIndex], + type: "compare" + }); + } + while (bookshelf[j].name > pivot.name) { + j--; + moves.push({ + indices: [j, pivotIndex], + type: "compare" + }); + } + if (i <= j) { + if (i !== j) { + moves.push({ + indices: [i, j, pivotIndex], + type: "swap" + }); + } + [bookshelf[i], bookshelf[j]] = [bookshelf[j], bookshelf[i]]; + i++; + j--; + } + } + return i; + }; + const quickSortRecursive = (left, right) => { + if (left >= right) { + return; + } + const index = partition(left, right); + quickSortRecursive(left, index - 1); + quickSortRecursive(index, right); + }; + quickSortRecursive(0, bookshelf.length - 1); + return moves; +} diff --git a/dest/sorting/SelectionSort.js b/dest/sort/code/sortingAlgorithms/SelectionSort.js similarity index 85% rename from dest/sorting/SelectionSort.js rename to dest/sort/code/sortingAlgorithms/SelectionSort.js index 78fc72a..69f5370 100644 --- a/dest/sorting/SelectionSort.js +++ b/dest/sort/code/sortingAlgorithms/SelectionSort.js @@ -2,6 +2,10 @@ export default function selectionSort(bookshelf) { const moves = []; for (let i = 0; i < bookshelf.length; i++) { let minIndex = i; + moves.push({ + indices: [i, minIndex], + type: "compare" + }); for (let j = i + 1; j < bookshelf.length; j++) { if (bookshelf[j].name < bookshelf[minIndex].name) { minIndex = j; diff --git a/dest/sorting/BubbleSort.js b/dest/sorting/BubbleSort.js deleted file mode 100644 index 798f3bd..0000000 --- a/dest/sorting/BubbleSort.js +++ /dev/null @@ -1,21 +0,0 @@ -export default function bubbleSort(bookshelf) { - const moves = []; - do { - var swapped = false; - for (let i = 1; i < bookshelf.length; i++) { - moves.push({ - indices: [i - 1, i], - type: "compare" - }); - if (bookshelf[i - 1].name > bookshelf[i].name) { - swapped = true; - moves.push({ - indices: [i - 1, i], - type: "swap" - }); - [bookshelf[i - 1], bookshelf[i]] = [bookshelf[i], bookshelf[i - 1]]; - } - } - } while (swapped); - return moves; -} diff --git a/icons/github.png b/icons/github.png deleted file mode 100644 index 9490ffc..0000000 Binary files a/icons/github.png and /dev/null differ diff --git a/icons/go-button.png b/icons/go-button.png deleted file mode 100644 index 950e44b..0000000 Binary files a/icons/go-button.png and /dev/null differ diff --git a/icons/wooden-pattern-header.jpg b/icons/wooden-pattern-header.jpg deleted file mode 100644 index 2216b11..0000000 Binary files a/icons/wooden-pattern-header.jpg and /dev/null differ diff --git a/image/bubble_sort_bg.jpg b/image/bubble_sort_bg.jpg new file mode 100644 index 0000000..75494e9 Binary files /dev/null and b/image/bubble_sort_bg.jpg differ diff --git a/image/pathfinder.png b/image/pathfinder.png new file mode 100644 index 0000000..6ece830 Binary files /dev/null and b/image/pathfinder.png differ diff --git a/image/up-arrow.png b/image/up-arrow.png new file mode 100644 index 0000000..ef4bbb9 Binary files /dev/null and b/image/up-arrow.png differ diff --git a/index.html b/index.html index 3b721a5..03ccdf6 100644 --- a/index.html +++ b/index.html @@ -2,62 +2,143 @@ - The Library - - - - - + + The Library - Book Sorting Visualizer + + + + + + + + +
-

The Library

+ +

+ Book Sorting
Visualizer +

+
+
-
-
-
-
- -
-
20 books
+ + + + + + + + + + + + + Up Arrow + Back to top + + +
+
+ +
+
Đỗ Anh Quân
+
Phạm Vũ Quang
+
Nguyễn Mạnh Việt Khôi
+
Lê Tuấn Phúc
- - - - -
- -
-
-
-
-
- - - - \ No newline at end of file diff --git a/main.css b/main.css new file mode 100644 index 0000000..8d395c1 --- /dev/null +++ b/main.css @@ -0,0 +1,554 @@ +body { + font-family: sans-serif; + margin: 0; + padding: 0; +} + +.logo { + position: absolute; + top: 90px; + left: 60px; + font-size: 40px; + font-weight: bold; + color: #fff; +} + +header { + position: relative; + text-align: center; + padding: 20px 0; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 100px; + height: 100vh; + background-image: url("./image/bubble_sort_bg.jpg"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +header h1 { + margin: 0; + font-size: 100px; + opacity: 0; + animation: fade-in 2s forwards; + text-align: center; + white-space: nowrap; + font-family: 'sans-serif'; + +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.vertical-line1:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; +} + +.vertical-line1:after { + content: ''; + position: absolute; + top: 10%; + left: 50%; + width: 2px; + height: 26%; + background: #fff; + z-index: 2; + transform-origin: top; + transform: scaleY(0); + animation: animate1 1.5s forwards; + animation-fill-mode: forwards; +} + +@keyframes animate1 { + 0% { + transform: translateY(-50%) scaleY(0); + transform-origin: top; + } + 100% { + transform: translateY(0) scaleY(1); + transform-origin: top; + } +} + +.vertical-line2:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; +} + +.vertical-line2:after { + content: ''; + position: absolute; + top: 65%; + left: 50%; + width: 2px; + height: 25%; + background: #fff; + z-index: 2; + transform-origin: top; + transform: scaleY(0); + animation: animate2 1.5s forwards; + animation-delay: 1s; + animation-fill-mode: forwards; +} + +@keyframes animate2 { + 0% { + transform: translateY(-20%) scaleY(0); + transform-origin: top; + } + 100% { + transform: translateY(0) scaleY(1); + transform-origin: top; + } +} + +nav { + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: rgba(0, 0, 0, 0.8); + height: 50px; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Source Sans Pro' +} + +nav a { + color: rgb(255, 255, 255); + text-decoration: none; + margin: 0 20px; + font-size: 25px; + transition: transform 0.3s; + font-weight: bold; +} + +nav a:hover { + transform: scale(1.1); +} + + +section { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 100px; + color: #000; + text-align: center; + cursor: pointer; +} + +.sort-info { + display: flex; + align-items: flex-start; + gap: 50px; + flex-direction: column; + justify-content: flex-start; + max-width: 800px; + margin: 0 auto; + padding: 20px; + position: relative; +} + + +#bubble-sort .sort-info::before { + content: ''; + position: absolute; + top: -15%; + left: -75%; + width: 253%; + height: 3px; + background-color: #000; + transform-origin: left; + transform: scaleX(0); + animation: animate-line 1s forwards; + animation-delay: 1s; +} + +@keyframes animate-line { + 0% { + transform: scaleX(0); + } + 100% { + transform: scaleX(1); + } +} + + +#selection-sort .sort-info::before { + content: ''; + position: absolute; + top: -10%; + left: -75%; + width: 253%; + height: 3px; + background-color: #fff; + transform-origin: left; + transform: scaleX(0); + animation: animate-line 1s forwards; + animation-delay: 1s; +} + +@keyframes animate-line { + 0% { + transform: scaleX(0); + } + 100% { + transform: scaleX(1); + } +} + + +#quick-sort .sort-info::before { + content: ''; + position: absolute; + top: -5%; + left: -75%; + width: 253%; + height: 3px; + background-color: #000; + transform-origin: left; + transform: scaleX(0); + animation: animate-line 1s forwards; + animation-delay: 1s; +} + +@keyframes animate-line { + 0% { + transform: scaleX(0); + } + 100% { + transform: scaleX(1); + } +} + + +#insertion-sort .sort-info::before { + content: ''; + position: absolute; + top: -15%; + left: -75%; + width: 253%; + height: 3px; + background-color: #fff; + transform-origin: left; + transform: scaleX(0); + animation: animate-line 1s forwards; + animation-delay: 1s; +} + +@keyframes animate-line { + 0% { + transform: scaleX(0); + } + 100% { + transform: scaleX(1); + } +} + +#pathfinder .sort-info::before { + content: ''; + position: absolute; + top: -20%; + left: -75%; + width: 253%; + height: 3px; + background-color: #000; + transform-origin: left; + transform: scaleX(0); + animation: animate-line 1s forwards; + animation-delay: 1s; +} + +@keyframes animate-line { + 0% { + transform: scaleX(0); + } + 100% { + transform: scaleX(1); + } +} + + +.sort-name { + font-size: 65px; + font-weight: bold; + font-family: 'Sources Sans Pro'; + text-align: left; + margin-left: -55%; +} + +.sort-definition { + font-size: 25px; + line-height: 1.4; + flex-grow: 1; + margin-left: -67%; + color: #000; + text-align: left; + width:500px; +} + +.fade-out{ + opacity: 0; + transition: opacity 0.5s ease-in; +} + +.fade-out.appear{ + opacity: 1; +} + +.try-link { + font-size: 24px; + background-color: black; + color: white; + padding: 12px 36px; + border-radius: 5px; + transition: transform 0.3s; + position: relative; + overflow: hidden; + display: inline-block; + letter-spacing: 2px; + border-radius: 40px; + background: linear-gradient(90deg, #000); + font-weight: bold; + align-self: flex-start; + margin-left: -52%; + margin-top: 30px; +} + +.try-link::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0; + height: 0; + background-color: rgba(255, 255, 255, 0.3); + border-radius: 50%; + opacity: 0; + transition: width 0.4s ease-out, height 0.4s ease-out, opacity 0.4s ease-out; +} + +.try-link:hover::after { + width: 300px; + height: 300px; + opacity: 1; +} + +#bubble-sort { + background-color: white; +} +#bubble-sort .sort-name { + color: black; +} +#bubble-sort .sort-definition { + color: black; +} + + +#selection-sort { + background-color: black; +} +#selection-sort .sort-name { + color: white; +} +#selection-sort .sort-definition { + color: white; +} +#selection-sort .try-link{ + background-color: white; + color: black; +} +#selection-sort .try-link::after{ + background-color: rgba(255, 255, 255, 0.635); +} + + +#quick-sort { + background-color: white; +} +#quick-sort .sort-name { + color: black; +} +#quick-sort .sort-definition { + color: black; +} + + +#insertion-sort { + background-color: black; +} +#insertion-sort .sort-name { + color: white; +} +#insertion-sort .sort-definition { + color: white; +} +#insertion-sort .try-link{ + background-color: white; + color: black; +} +#insertion-sort .try-link::after{ + background-color: rgba(255, 255, 255, 0.635); +} + + +#pathfinder { + background-color: white; +} +#pathfinder .sort-name { + color: black; +} +#pathfinder .sort-definition { + color: black; +} + + +#back-to-top { + position: fixed; + bottom: 30px; + right: 30px; + display: none; + width: 40px; + height: 40px; + background-color: #333; + border-radius: 50%; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.3s; +} + +#back-to-top img { + width: 40px; + height: 40px; + display: block; +} + +#back-to-top:hover { + opacity: 0.8; +} +#back-to-top .tooltip { + position: absolute; + bottom: 50px; + left: 50%; + transform: translateX(-60%); + background-color: #ffffff; + color: black; + padding: 0px 10px; + font-size: 16px; + border-radius: 4px; + opacity: 0; + transition: opacity 0.3s; + white-space: nowrap; +} + +#back-to-top:hover .tooltip { + opacity: 1.3; +} + +#information { + background-color: #f9f9f9; + display: flex; + align-items: center; + justify-content: center; + height: 30vh; +} + +.information-content { + display: flex; + align-items: center; + justify-content: space-between; + width: 60%; + max-width: 1000px; + margin: 0 auto; + padding: 20px; +} + +.information-logo { + font-size: 40px; + font-weight: bold; + color: #000; + font-family: 'Proxima Nova'; +} + +.contact-info { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.team-member { + margin-bottom: 20px; + font-size: 25px; + color: #000; +} + +.hidden{ + opacity: 0; + transform: translateY(20px); + transition: opacity 1.5s, transform 1.5s; +} +.show{ + opacity: 1; + transform: translateY(0); +} + +.video-container { + position: absolute; + top: 50%; + right: 10%; + transform: translateY(-50%); + width: 550px; + height: auto; +} + +.video-container video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-container { + position: absolute; + top: 50%; + right: 10%; + transform: translateY(-50%); + width: 550px; + height: auto; +} + +.image-container img { + width: 100%; + height: 100%; + object-fit: cover; +} + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..314c99c --- /dev/null +++ b/main.js @@ -0,0 +1,70 @@ +document.addEventListener("DOMContentLoaded", function() { + const fadeInElements = document.querySelectorAll(".fade-in"); + + function checkVisibility() { + for (const element of fadeInElements) { + if (isElementInViewport(element)) { + element.classList.add("show"); + } else { + element.classList.remove("show"); + } + } + } + + function isElementInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } + + window.addEventListener('scroll', function() { + var backToTopButton = document.getElementById('back-to-top'); + if (window.scrollY > 200) { + backToTopButton.style.display = 'block'; + } else { + backToTopButton.style.display = 'none'; + } + }); + + document.getElementById('back-to-top').addEventListener('click', function(e) { + e.preventDefault(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('show'); + const line = entry.target.querySelector('.horizontal-line'); + setTimeout(() => { + line.classList.add('animate-line'); + }, 2000); // Adjust the delay as needed + } else { + entry.target.classList.remove('show'); + } + }); + }); + + const hiddenElements = document.querySelectorAll('.hidden'); + hiddenElements.forEach((el) => observer.observe(el)); + + // Call checkVisibility on page load + checkVisibility(); + + // Recheck visibility on scroll + window.addEventListener('scroll', checkVisibility); +}); + + + + + + + + + + diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index acecb8c..59c0dee 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,5 +1,5 @@ { - "name": "typescript-pain", + "name": "the-library", "version": "1.0.0", "lockfileVersion": 3, "requires": true, diff --git a/package-lock.json b/package-lock.json index d8839d5..b7afebe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "typescript-pain", + "name": "the-library", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "typescript-pain", + "name": "the-library", "version": "1.0.0", "license": "ISC", "devDependencies": { diff --git a/package.json b/package.json index 5b24c9e..eed0262 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "typescript-pain", + "name": "the-library", "version": "1.0.0", "description": "", "main": "main.js", diff --git a/src/graph/code/.hintrc b/src/graph/code/.hintrc new file mode 100644 index 0000000..2fa1a2e --- /dev/null +++ b/src/graph/code/.hintrc @@ -0,0 +1,13 @@ +{ + "extends": [ + "development" + ], + "hints": { + "axe/forms": [ + "default", + { + "select-name": "off" + } + ] + } +} \ No newline at end of file diff --git a/src/graph/code/main.ts b/src/graph/code/main.ts new file mode 100644 index 0000000..3d7a872 --- /dev/null +++ b/src/graph/code/main.ts @@ -0,0 +1,400 @@ +// Project: Algorithms and Data Structures +// Author: nmvkhoi + +// ts-check +// constants + +import getAdjacencyList from '../code/pathFindingAlgorithms/utility.js'; +import {getEndNode, getSourceNode, getNodeXCoordinates, getNodeYCoordinates} from "./pathFindingAlgorithms/utility.js"; +import {getShortestDistanceBFS} from "./pathFindingAlgorithms/bfs.js"; +import {getPathDFS} from "./pathFindingAlgorithms/dfs.js"; +import createText from "./popup.js"; + +const canvas = document.getElementById('canvas') as HTMLCanvasElement; +const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; +const clear = document.getElementById('clear') as HTMLButtonElement; +const begin = document.getElementById('begin') as HTMLButtonElement; +const end = document.getElementById('end') as HTMLButtonElement; +const wall = document.getElementById('wall') as HTMLButtonElement; +const start = document.getElementById('start') as HTMLButtonElement; +const select = document.getElementById('select') as HTMLSelectElement; + +document.querySelector('#slider_time input')!.addEventListener('input', function(this: HTMLInputElement) { + delay = Number(this.value); + document.querySelector('#value_time')!.textContent = this.value +"ms"; +}); + +const width = canvas.width; +const height = canvas.height; +const cellSize = 20; +export let delay:number = 10; + +const rows = height / cellSize; +const cols = width / cellSize; + +let matrix: number[][] = []; +let isDragging: boolean = false; +let isStart = false; +let prevStart = [-1, -1]; +let isEnd = false; +let prevEnd = [-1, -1]; +let selectedAlgorithm = ''; +// console.log(rows, cols); + +let adjList: number[][] = []; +let startNode: number; +let endNode: number = -1; + +// Functions +// draw a square with animation zoom out +export async function drawSquareWithAnimation(x: number, y: number, color: string) { + const initialSize = 1; + const targetSize = cellSize; + + let currentSize = initialSize; + let animationStartTime= 0; // milliseconds + + function animate() { + const now = Date.now(); + const elapsedTime = now - animationStartTime; + const progress = elapsedTime / 100; // Divide by 1000 to convert milliseconds to seconds + + ctx.clearRect(y * cellSize, x * cellSize, cellSize, cellSize); // Clear the previous frame + + if (progress < 1) { + currentSize = initialSize + (targetSize - initialSize) * progress; + + ctx.fillStyle = color; + ctx.fillRect( + y * cellSize + (cellSize - currentSize) / 2, + x * cellSize + (cellSize - currentSize) / 2, + currentSize, + currentSize + ); + + requestAnimationFrame(animate); + } else { + ctx.fillStyle = color; + ctx.fillRect(y * cellSize, x * cellSize, cellSize, cellSize); + } + } + + animationStartTime = Date.now(); + animate(); +} +// create a matrix +function createMatrix([]: number[][]) { + for (let i = 0; i < rows; i++) { + matrix[i] = []; + for (let j = 0; j < cols; j++) { + matrix[i][j] = 0; + } + } +} +// clear the matrix +function clearMatrix([]: number[][]) { + for (let i = 0; i < rows; i++) { + matrix[i] = []; + for (let j = 0; j < cols; j++) { + matrix[i][j] = 0; + } + } +} +// print the matrix +function printMatrix([]: number[][]) { + for (let i = 0; i < rows; i++) { + let line = ''; + for (let j = 0; j < cols; j++) { + line += matrix[i][j] + ' '; + } + console.log(line); + } +} +createMatrix(matrix); + +// draw the grid +function drawGrid() { + for (let i = cellSize; i < height; i += cellSize) { + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(width, i); + ctx.stroke(); + } + for (let j = cellSize; j < width; j += cellSize) { + ctx.beginPath(); + ctx.moveTo(j, 0); + ctx.lineTo(j, height); + ctx.stroke(); + } +} +// draw a border +function drawSquare(event: MouseEvent) { + let x = event.offsetX; + let y = event.offsetY; + let i = Math.floor(y / cellSize); + let j = Math.floor(x / cellSize); + // ctx.fillStyle = '#19A7CE'; + // ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); + matrix[i][j] = 2; + // printMatrix(matrix); + drawSquareWithAnimation(i, j, '#1239C6'); +} +// clear the canvas +function clearCanvas() { + ctx.clearRect(0, 0, width, height); + drawGrid(); + clearMatrix(matrix); + prevStart = [-1, -1]; +} +// clear the path and keep the wall and start and end point +// function clearPath() { +// ctx.clearRect(0, 0, width, height); +// drawGrid(); +// for (let i in matrix) { +// for (let j in matrix[i]) { +// if (matrix[i][j] === 2) { +// ctx.fillStyle = '#1239C6'; +// ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); +// } else if (matrix[i][j] === 1) { +// ctx.fillStyle = '#43c943'; +// ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); +// } else if (matrix[i][j] === 3) { +// ctx.fillStyle = '#ff4d4d'; +// ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize); +// } +// } +// } +// } +// delete a square +function deleteSquare(event: MouseEvent) { + let x = event.offsetX; + let y = event.offsetY; + let i = Math.floor(y / cellSize); + let j = Math.floor(x / cellSize); + ctx.clearRect(j * cellSize, i * cellSize, cellSize, cellSize); + // draw the line again + ctx.beginPath(); + ctx.moveTo(j * cellSize, i * cellSize); + ctx.lineTo((j + 1) * cellSize, i * cellSize); + // draw 4 edges of the square + ctx.moveTo((j + 1) * cellSize, i * cellSize); + ctx.lineTo((j + 1) * cellSize, (i + 1) * cellSize); + ctx.moveTo((j + 1) * cellSize, (i + 1) * cellSize); + ctx.lineTo(j * cellSize, (i + 1) * cellSize); + ctx.moveTo(j * cellSize, (i + 1) * cellSize); + ctx.lineTo(j * cellSize, i * cellSize); + ctx.moveTo(j * cellSize, i * cellSize); + ctx.stroke(); + matrix[i][j] = 0; +} +function initPoint(event: MouseEvent) { + let x = Math.floor(event.offsetY / cellSize); + let y = Math.floor(event.offsetX / cellSize); + // delete the previous start point + if (prevStart[0] !== -1 && prevStart[1] !== -1) { + ctx.clearRect(prevStart[1] * cellSize, prevStart[0] * cellSize, cellSize, cellSize); + // draw the new start point + matrix[x][y] = 1; + updateAdjacencyList(); + + ctx.fillStyle = '#43c943'; + ctx.fillRect(y * cellSize, x * cellSize, cellSize, cellSize); + console.log(prevStart); + // draw the line again + ctx.beginPath(); + ctx.moveTo(prevStart[1] * cellSize, prevStart[0] * cellSize); + ctx.lineTo((prevStart[1] + 1) * cellSize, prevStart[0] * cellSize); + // draw 4 edges of the square + ctx.moveTo((prevStart[1] + 1) * cellSize, prevStart[0] * cellSize); + ctx.lineTo((prevStart[1] + 1) * cellSize, (prevStart[0] + 1) * cellSize); + ctx.moveTo((prevStart[1] + 1) * cellSize, (prevStart[0] + 1) * cellSize); + ctx.lineTo(prevStart[1] * cellSize, (prevStart[0] + 1) * cellSize); + ctx.moveTo(prevStart[1] * cellSize, (prevStart[0] + 1) * cellSize); + ctx.lineTo(prevStart[1] * cellSize, prevStart[0] * cellSize); + ctx.moveTo(prevStart[1] * cellSize, prevStart[0] * cellSize); + ctx.stroke(); + return [x, y]; + } else { + return [0,0]; + } +} +function setEndPoint(event: MouseEvent) { + let x = Math.floor(event.offsetY / cellSize); + let y = Math.floor(event.offsetX / cellSize); + + // delete the previous start point + if (prevEnd[0] !== -1 && prevEnd[1] !== -1) { + ctx.clearRect(prevEnd[1] * cellSize, prevEnd[0] * cellSize, cellSize, cellSize); + // draw the new start point + matrix[x][y] = 3; + updateAdjacencyList(); + ctx.fillStyle = '#ff4d4d'; + ctx.fillRect(y * cellSize, x * cellSize, cellSize, cellSize); + console.log(prevEnd); + // draw the line again + ctx.beginPath(); + ctx.moveTo(prevEnd[1] * cellSize, prevEnd[0] * cellSize); + ctx.lineTo((prevEnd[1] + 1) * cellSize, prevEnd[0] * cellSize); + // draw 4 edges of the square + ctx.moveTo((prevEnd[1] + 1) * cellSize, prevEnd[0] * cellSize); + ctx.lineTo((prevEnd[1] + 1) * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.moveTo((prevEnd[1] + 1) * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.lineTo(prevEnd[1] * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.moveTo(prevEnd[1] * cellSize, (prevEnd[0] + 1) * cellSize); + ctx.lineTo(prevEnd[1] * cellSize, prevEnd[0] * cellSize); + ctx.moveTo(prevEnd[1] * cellSize, prevEnd[0] * cellSize); + ctx.stroke(); + return [x, y]; + } else { + return [0,0]; + } +} +// check if the matrix is set begin node and end node +function checkMatrix(matrix: number[][]): boolean { + let count = 0; + for (let i in matrix) { + for (let j in matrix[i]) { + if (matrix[i][j] === 1 || matrix[i][j] === 3) { + count++; + } + } + } + if (count === 2) { + return true; + } else { + return false; + } +} + +function updateAdjacencyList() { + adjList = getAdjacencyList(matrix); + startNode = getSourceNode(matrix); + endNode = getEndNode(matrix); +} + +function resetAdjacencyList() { + for (let i in adjList) { + for (let j in adjList[i]) { + adjList[i][j] = 0; + } + } + startNode = -1; + endNode = -1; +} + +export function initPath(node : number){ + drawSquareWithAnimation(getNodeXCoordinates(node), getNodeYCoordinates(node), '#FFEA00'); +} +export function initPrevPath(node : number){ + drawSquareWithAnimation(getNodeXCoordinates(node), getNodeYCoordinates(node), '#33A3FF'); +} + +// Add event listeners +clear.addEventListener('click', ()=>{ + clearCanvas() + resetAdjacencyList(); +}); +begin.addEventListener('click', (event)=>{ + isStart = true; + console.log('begin'); +}); +wall.addEventListener('click', (event)=>{ + isStart = false; + isEnd = false; +}); + +end.addEventListener('click', ()=>{ + console.log(endNode); + updateAdjacencyList(); + console.log('set-end'); + isStart = false; + isEnd = true; +}); +start.addEventListener('click', ()=>{ + if (checkMatrix(matrix)) { + updateAdjacencyList(); + // switch case + if (selectedAlgorithm !== '') { + switch (selectedAlgorithm) { + case 'bfs': { + getShortestDistanceBFS(adjList, startNode, endNode);matrix[prevStart[0]][prevStart[1]] = 1; + break; + } + case 'dfs': { + getPathDFS(adjList, startNode, endNode); + break; + } + // case 'aStar': {getShortestPathAStar(adjList); break;} + default: { + createText("Please select an algorithm","red"); + break; + } + } + } else { + createText("Please select an algorithms","red"); + } + } else { + createText('Please set the start and end point',"red"); + } +}); +select.addEventListener('change', (event)=>{ + const target = event.target as HTMLSelectElement; + selectedAlgorithm = target.value; + console.log(selectedAlgorithm); +}); +canvas.addEventListener('mousedown', (event)=>{ + if (event.button === 0 && !isStart && !isEnd) { + isDragging = true; + drawSquare(event); + } else if (event.button === 0 && isStart) { + prevStart = initPoint(event); + } else if (event.button === 0 && isEnd === true) { + console.log('end'); + prevEnd = setEndPoint(event); + } +}); +canvas.addEventListener('mousemove', (event)=>{ + if(isDragging && event.button === 0){ + drawSquare(event) ; + } +}) +canvas.addEventListener('mouseup', ()=>{ + isDragging = false; +}); +canvas.addEventListener('contextmenu', (event)=>{ + deleteSquare(event); +}); + + +// Run the functions once the page is loaded +( + function once(arrays: number[][]) { + // console.log(cellSize); + // console.log(width, height); + // console.log(rows, cols); + createMatrix(arrays); + printMatrix(arrays); + drawGrid(); + } +)(matrix); + +function featureEnabling(bool:boolean):void{ + if(!bool){ + clear.setAttribute('disabled','true'); + begin.setAttribute('disabled','true'); + wall.setAttribute('disabled','true'); + end.setAttribute('disabled','true'); + start.setAttribute('disabled','true'); + select.setAttribute('disabled','true'); + }else{ + clear.removeAttribute('disabled'); + begin.removeAttribute('disabled'); + wall.removeAttribute('disabled'); + end.removeAttribute('disabled'); + start.removeAttribute('disabled'); + select.removeAttribute('disabled'); + } +} + + +export {matrix, cellSize, width, height, rows, cols, ctx, canvas}; diff --git a/src/graph/code/pathFindingAlgorithms/Maze.ts b/src/graph/code/pathFindingAlgorithms/Maze.ts new file mode 100644 index 0000000..23c06ce --- /dev/null +++ b/src/graph/code/pathFindingAlgorithms/Maze.ts @@ -0,0 +1,65 @@ +export function generateMazeUsingKruskal(maze: number[][]): number[][] { + const rows = maze.length; + const cols = maze[0].length; + + // Create a list of all walls in the maze + const walls: [number, number, number, number][] = []; + for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + // Add vertical walls + if (j < cols - 1) { + walls.push([i, j, i, j + 1]); + } + // Add horizontal walls + if (i < rows - 1) { + walls.push([i, j, i + 1, j]); + } + } + } + + // Create a disjoint set to track the connected components + const parent: number[] = []; + for (let i = 0; i < rows * cols; i++) { + parent[i] = i; + } + + // Helper function to find the parent of a set + function find(parent: number[], i: number): number { + if (parent[i] !== i) { + parent[i] = find(parent, parent[i]); + } + return parent[i]; + } + + // Helper function to join two sets + function union(parent: number[], i: number, j: number): void { + const rootA = find(parent, i); + const rootB = find(parent, j); + parent[rootA] = rootB; + } + + // Randomize the order of walls + walls.sort(() => Math.random() - 0.5); + + // Process each wall and remove it if it connects two different sets + for (const wall of walls) { + const [x1, y1, x2, y2] = wall; + const indexA = x1 * cols + y1; + const indexB = x2 * cols + y2; + const rootA = find(parent, indexA); + const rootB = find(parent, indexB); + + if (rootA !== rootB) { + union(parent, rootA, rootB); + maze[x1][y1] = 0; + maze[x2][y2] = 0; + } + } + + // Set the beginning node (start) as 1 and the end node as 3 + maze[0][0] = 1; + maze[rows - 1][cols - 1] = 3; + + return maze; +} + \ No newline at end of file diff --git a/src/graph/code/pathFindingAlgorithms/bfs.ts b/src/graph/code/pathFindingAlgorithms/bfs.ts new file mode 100644 index 0000000..749857d --- /dev/null +++ b/src/graph/code/pathFindingAlgorithms/bfs.ts @@ -0,0 +1,69 @@ +//BFS algorithm +import {initPath,delay} from "../main.js"; +import {initPrevPath} from "../main.js"; +import {delayRender} from "./utility.js"; +import createText from "../popup.js"; + + +//Helper function that uses BFS to transverse the graph +export async function bfs(adj: number[][], + src: number, + dest: number, + v: number, + prev: number[], + dist: number[]){ + let queue: number[] = []; + let visited = new Array(v); + + for (let i = 0; i < v; i++){ + visited[i] = false; + dist[i] = Number.MAX_VALUE; + prev[i] = -1; + } + + visited[src] = true; + dist[src] = 0; + queue.push(src); + + while(queue.length > 0){ + let u = queue[0]; + queue.shift(); + for (let i in adj[u]){ + if (visited[adj[u][i]] == false){ + visited[adj[u][i]] = true; + dist[adj[u][i]] = dist[u] + 1; + prev[adj[u][i]] = u; + queue.push(adj[u][i]); + initPrevPath(adj[u][i]); + await delayRender(delay); + if (adj[u][i] == dest){ + return true; + } + } + } + } + return false; +} +//Helper function to backtrack the path and print the shortest path +export async function getShortestDistanceBFS(adj: number[][], src: number, dest: number){ + let v = adj.length; + let prev = new Array(v).fill(0); + let dist = new Array(v).fill(0); + + if (!await bfs(adj, src, dest, v, prev, dist)){ + createText('Source and destination vertex is not connected!',"red"); + } + + let path = []; + let crawl = dest; + + path.push(crawl); + while (prev[crawl] != -1) { + path.push(prev[crawl]); + crawl = prev[crawl]; + } + for (let i = path.length - 1; i >= 0; i--){ + await delayRender(4*delay); + initPath(path[i]); + } +} diff --git a/src/graph/code/pathFindingAlgorithms/dfs.ts b/src/graph/code/pathFindingAlgorithms/dfs.ts new file mode 100644 index 0000000..74ea892 --- /dev/null +++ b/src/graph/code/pathFindingAlgorithms/dfs.ts @@ -0,0 +1,49 @@ +import {initPath,delay} from "../main.js"; +import {initPrevPath} from "../main.js"; +import createText from "../popup.js"; +import {delayRender} from "./utility.js"; + +export async function drawPath(stack: number[]) { + for(let i in stack) { + initPath(stack[i]); + await delayRender(delay); + } +} + +export async function DFS(visited: boolean[], + adjList: number[][], + source: number, + destination: number, + stack: number[]) { + visited[source] = true; + stack.push(source); + + if (source == destination) { + await drawPath(stack); + return; + } + + for (let i in adjList[source]) { + if (!visited[adjList[source][i]]) { + await DFS(visited, adjList, adjList[source][i], destination, stack); + } + } + stack.pop(); +} + +export async function getPathDFS(adjList: number[][], source: number, destination: number) { + let n = adjList.length; + let visited: boolean[] = new Array(n + 1); + let stack: number[] = []; + + for (let i = 0; i < n + 1; i++) { + visited[i] = false; + } + + await DFS(visited, adjList, source, destination, stack); + + if (stack.length === 0) { + createText("No path found!","red"); + } +} + diff --git a/src/graph/code/pathFindingAlgorithms/dijkstra.js b/src/graph/code/pathFindingAlgorithms/dijkstra.js new file mode 100644 index 0000000..d33e07b --- /dev/null +++ b/src/graph/code/pathFindingAlgorithms/dijkstra.js @@ -0,0 +1,72 @@ +let V = 5; + +function minDistance(dist, sptSet) { + let min = Number.MAX_VALUE; + let min_index = -1; + + for (let v = 0; v < V; v++) { + if (sptSet[v] == false && dist[v] <= min) { + min = dist[v]; + min_index = v; + } + } + return min_index; +} + +function printSolution(dist) { + console.log("Vertex \t\t Distance from Source"); + for (let i = 0; i < V; i++) { + console.log(i + " \t\t " + dist[i]); + } +} +function printShortestPath(prev, src, des){ + let path = new Set; + let crawl = des; + + path.add(crawl); + while(prev[crawl] != src){ + path.add(prev[crawl]); + crawl = prev[crawl]; + } + path.add(src); + + path = Array.from(path).reverse(); + console.log("Shortest path from " + src + " to " + des + " is: ") + console.log(path); +} + +function dijkstra(graph, src){ + let dist = new Array(V); + let sptSet = new Array(V); + let prev = new Array(V); + + for (let i = 0; i < V; i++){ + dist[i] = Number.MAX_VALUE; + sptSet[i] = false; + } + + dist[src] = 0; + + for (let count = 0; count < V; count++){ + let u = minDistance(dist, sptSet); + sptSet[u] = true; + for (let v = 0; v < V; v++){ + if (!sptSet[v] && graph[u][v] != 0 && + dist[u] != Number.MAX_VALUE && + dist[u] + graph[u][v] < dist[v]){ + dist[v] = dist[u] + graph[u][v]; + prev[v] = u; + } + } + } + printSolution(dist); + printShortestPath(prev, src, 4); +} +let graph = [ + [0, 6, 0, 1, 0], + [6, 0, 5, 2, 2], + [0, 5, 0, 0, 5], + [1, 2, 0, 0, 1], + [0, 2, 5, 1, 0] +] +dijkstra(graph, 0); diff --git a/src/graph/code/pathFindingAlgorithms/utility.ts b/src/graph/code/pathFindingAlgorithms/utility.ts new file mode 100644 index 0000000..26b2f67 --- /dev/null +++ b/src/graph/code/pathFindingAlgorithms/utility.ts @@ -0,0 +1,132 @@ +let vertexIndex: number[][] = []; + +export function addEdge(adj: number[][], u: number, v: number){ + adj[u].push(v); +} + +//Check if a vertex is in the grid, if it is return its index in the vertexIndex array +//If it is not in the grid, return -1 +export function findVertexIndex (vertexIndex: number[][], row: number, col: number){ + for (let i = 0; i < vertexIndex.length; i++){ + if (vertexIndex[i][0] === row && vertexIndex[i][1] === col){ + return i; + } + } + return -1; +} + +export function resetVertexIndex(){ + vertexIndex = []; +} + +//# is wall, . is empty, A is start, B is destination +//every . and A or B are considered a vertex +export default function getAdjacencyList (graph: number[][]){ + //number of vertices + let v: number = 0; + + //adjacency list + let adj: number[][] = new Array(graph.length * graph[0].length); + + resetVertexIndex(); + + for (let i = 0; i < graph.length; i++){ + for (let j = 0; j < graph[i].length; j++){ + //If the current element is not a wall, add it to the vertexIndex array + //and increment the number of vertices + if (graph[i][j] != 2){ + vertexIndex[v] = []; + vertexIndex[v].push(i); + vertexIndex[v].push(j); + adj[v] = []; + v++; + } + } + } + + //Check for each vertex if there is a path to another vertex + //if there is, add an edge between them + for (let i = 0; i < vertexIndex.length; i++){ + let row: number = vertexIndex[i][0]; + let col: number = vertexIndex[i][1]; + //check if there is a path to the vertex above + if (findVertexIndex(vertexIndex, row - 1, col) !== -1){ + addEdge(adj, i, findVertexIndex(vertexIndex, row - 1, col)); + } + //check if there is a path to the vertex below + if (findVertexIndex(vertexIndex, row + 1, col) !== -1){ + addEdge(adj, i, findVertexIndex(vertexIndex, row + 1, col)); + } + //check if there is a path to the vertex to the left + if (findVertexIndex(vertexIndex, row, col - 1) !== -1){ + addEdge(adj, i, findVertexIndex(vertexIndex, row, col - 1)); + } + //check if there is a path to the vertex to the right + if (findVertexIndex(vertexIndex, row, col + 1) !== -1){ + addEdge(adj, i, findVertexIndex(vertexIndex, row, col + 1)); + } + } + return adj; +} +export function getSourceNode (graph: number[][]): number{ + let v: number = 0; + for (let i = 0; i < graph.length; i++){ + for (let j = 0; j < graph[i].length; j++){ + if (graph[i][j] != 2){ + if (graph[i][j] == 1){ + return v; + } + v++; + } + } + } + return -1; +} + +export function getEndNode(graph: number[][]): number{ + let v: number = 0; + for (let i = 0; i < graph.length; i++){ + for (let j = 0; j < graph[i].length; j++){ + if (graph[i][j] != 2){ + if (graph[i][j] == 3){ + return v; + } + v++; + } + } + } + return -1; +} +export function getNodeXCoordinates(v: number): number{ + for (let i = 0; i < vertexIndex.length; i++){ + if (i === v){ + return vertexIndex[i][0]; + } + } + return -1; +} +export function getNodeYCoordinates(v: number): number{ + for (let i = 0; i < vertexIndex.length; i++){ + if (i === v){ + return vertexIndex[i][1]; + } + } + return -1; +} + +export function delayRender(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + + +// let graph: string[][] = [ +// ['#', '#', '#', '#', '#'], +// ['#', 'A', '.', '.', '#'], +// ['#', '#', '#', '.', '#'], +// ['#', '.', '#', '.', '#'], +// ['#', '.', '.', '.', '#'], +// ['#', '#', 'B', '#', '#'] +// ]; +// let adj: number[][] = []; +// +// fromGridToList(graph, adj); \ No newline at end of file diff --git a/src/graph/code/popup.ts b/src/graph/code/popup.ts new file mode 100644 index 0000000..c87795e --- /dev/null +++ b/src/graph/code/popup.ts @@ -0,0 +1,76 @@ +const warn = document.getElementsByClassName('warning')[0] as HTMLDivElement; +const FADE_IN_TIME = 1000; //ms +const FADE_OUT_TIME = 1000; +const APPEAR_TIME = 8000; + + +export default function createText(message: string, color: string) { + warn.textContent = message; + warn.style.color = color; + warn.style.opacity = '0'; + warn.style.top = '-50px'; + + fadeIn(warn, FADE_IN_TIME); + + // Fade out after 10 seconds with 2 second fade-out time + setTimeout(function() { + fadeOut(warn, FADE_OUT_TIME); + }, APPEAR_TIME); +} + +function fadeIn(element: HTMLElement, duration: number) { + var interval = 10; + var opacity = 0; + var targetOpacity = 1; + var delta = (interval / duration) * (targetOpacity - opacity); + + var timer = setInterval(function() { + opacity += delta; + element.style.opacity = String(opacity); + + if (opacity >= targetOpacity) { + clearInterval(timer); + animate(element, 'rise', FADE_IN_TIME); + } + }, interval); +} + +function fadeOut(element: HTMLElement, duration: number) { + var interval = 10; + var opacity = 1; + var targetOpacity = 0; + var delta = (interval / duration) * (opacity - targetOpacity); + + var timer = setInterval(function() { + opacity -= delta; + element.style.opacity = String(opacity); + + if (opacity <= targetOpacity) { + clearInterval(timer); + animate(element, 'fall', FADE_OUT_TIME); + } + }, interval); +} + +function animate(element: HTMLElement, direction: string, duration: number) { + var interval = 10; + var start = parseInt(element.style.top); + var end:number; + + if (direction === 'rise') { + end = 0; + } else { + end = -50; + } + + var delta = (interval / duration) * (end - start); + + var timer = setInterval(function() { + start += delta; + element.style.top = String(start) + 'px'; + + if ((direction === 'rise' && start >= end) || (direction === 'fall' && start <= end)) { + clearInterval(timer); + } + }, interval); +} diff --git a/src/graph/css-style/icons/begin.png b/src/graph/css-style/icons/begin.png new file mode 100644 index 0000000..582a765 Binary files /dev/null and b/src/graph/css-style/icons/begin.png differ diff --git a/src/graph/css-style/icons/end.png b/src/graph/css-style/icons/end.png new file mode 100644 index 0000000..43893db Binary files /dev/null and b/src/graph/css-style/icons/end.png differ diff --git a/src/graph/css-style/icons/eraser.png b/src/graph/css-style/icons/eraser.png new file mode 100644 index 0000000..3aaed5c Binary files /dev/null and b/src/graph/css-style/icons/eraser.png differ diff --git a/src/graph/css-style/icons/start.png b/src/graph/css-style/icons/start.png new file mode 100644 index 0000000..4f582f7 Binary files /dev/null and b/src/graph/css-style/icons/start.png differ diff --git a/src/graph/css-style/icons/wall.png b/src/graph/css-style/icons/wall.png new file mode 100644 index 0000000..5e6c975 Binary files /dev/null and b/src/graph/css-style/icons/wall.png differ diff --git a/src/graph/css-style/styles.css b/src/graph/css-style/styles.css new file mode 100644 index 0000000..313e9d6 --- /dev/null +++ b/src/graph/css-style/styles.css @@ -0,0 +1,222 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} +body { + height: 100%; + width: 100%; + font-family: "Gill Sans", sans-serif; + background-color: #e3f2fd; +} +html { + height: 100%; + width: 100%; + font-size: 16px; +} +header{ + background: #5a909c; + width: 100%; + height: 10%; + display: flex; + justify-content: space-around; + align-items: center; +} + +main{ + height: 90%; + width: 100%; +} + +.algorithms { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; +} +#h1 { + text-decoration: none; + cursor: pointer; +} +.algorithms > a { + padding: 10px; + transition: 0.3s; + text-decoration: none; + font-family: "Gill Sans", sans-serif; + font-weight: bold; + color: #19A7CE; + font-size: 1.5rem; + background-color: #F9FBE7; + border-radius: 5px; +} +a:hover { + color: black; + text-decoration: none; + cursor: pointer; +} +h1 { + font-family: "Gill Sans", sans-serif; + color: #F9FBE7; + font-size: 2.5rem; + font-weight: bold; +} + + +.control-panel{ + position: relative; + margin-top: 5px; + height: 20%; + margin-left: 1vh; + display: flex; + gap: 10px; + align-items: center; + justify-content: center; +} + +.box{ + max-width: 300px; + height: 120px; + background: white; + padding: 20px 20px 30px 30px; + border-radius: 5px; + box-shadow: 0 0 5px rgba(0,0,0,0.5); + align-items: center; + } + +.box .content{ + font-size: 20px; + font-weight: 300; + color: #5a909c;; + display: flex; + justify-content: center; + } + +.box .slider{ + height: 40px; + width: 200px; + display: flex; + align-items: center; + margin-right: 15px; + } + +.box .slider input{ + height: 10px; + width: 100%; + -webkit-appearance: none; + outline: none; + background: #f2f2f2; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2); + } + +.box .value{ + width: 100px; + text-align: center; +} + +.control-panel .right .lower{ + display: flex; + justify-content: center; + align-items: center; + margin-top: 10px; +} + +select { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; + outline: 0; + border: 0; + background: #ffffff; + background-image: none; + padding: 20px 20px 30px 30px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + flex: 1; + padding: 0 .5em; + cursor: pointer; + display: flex; + align-items: center; + font-size: 1.4rem; + font-weight: bold; + color: #5a909c; +} + +select::-ms-expand { + display: none; +} + +.select { + position: relative; + display: flex; + width: 17em; + height: 3.5em; + line-height: 3; + background: #D3D3D3; + overflow: hidden; + border-radius: .25em; +} +.select::after { + content: '\25BC'; + position: absolute; + top: -12px; + right: 0; + padding: 1em 1em; + background: #D3D3D3; + cursor:pointer; + pointer-events:none; + transition:.25s all ease; +} +.select:hover::after { +color: #7ce8ff; +} + +button { + background: #5a909c; + width: 50px; + height: 50px; + padding: 10px 15px; + border: none; + border-radius: 3px; + font-size: 16px; + color: #fff; + cursor: pointer; + text-transform: uppercase; + transition: background-color 0.5s ease; + display: flex; + justify-content: center; + align-items: center; + margin: 2px; + } + button:hover { + cursor: pointer; + box-shadow: 0 0 10px #737b7e; + opacity: 0.8; + } + + button img{ + width: 40px; + height: 40px; + } + +.container { + display: flex; + justify-content: center; + align-items: center; + + height: 80%; + width: 100%; +} +#canvas { + border : 1px solid black; +} + +#upper{ + display: flex; +} + +.warning{ + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/graph/css-style/tooltip.css b/src/graph/css-style/tooltip.css new file mode 100644 index 0000000..c3004bb --- /dev/null +++ b/src/graph/css-style/tooltip.css @@ -0,0 +1,44 @@ +[data-tooltip] { + position: relative; + z-index: 2; + cursor: pointer; +} + +/* Hide the tooltip content by default */ +[data-tooltip]:before, +[data-tooltip]:after { + visibility: hidden; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: progid: DXImageTransform.Microsoft.Alpha(Opacity=0); + opacity: 0; + pointer-events: none; + z-index: 2; +} + +/* Position tooltip above the element */ +[data-tooltip]:before { + position: absolute; + bottom: 100%; + padding: 7px; + width: 60px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #ffffff; + background-color: hsla(0, 0%, 100%, 0.9); + color: #5a909c;; + content: attr(data-tooltip); + text-align: center; + font-size: 14px; + line-height: 1.2; + z-index: 2; +} + +/* Show tooltip content on hover */ +[data-tooltip]:hover:before, +[data-tooltip]:hover:after { + visibility: visible; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; + filter: progid: DXImageTransform.Microsoft.Alpha(Opacity=100); + opacity: 1; +} \ No newline at end of file diff --git a/src/graph/index.html b/src/graph/index.html new file mode 100644 index 0000000..3c09e01 --- /dev/null +++ b/src/graph/index.html @@ -0,0 +1,72 @@ + + + + + + Algorithms Visualizer + + + + + +
+

Algorithms Visualizer

+
+ Sorting + Graph +
+
+
+
+
+
+
+ +
+
+ +
100ms
+
+
+
+
+
+
+ +
+
+
+ + + + + +
+
+ +
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/src/Book.ts b/src/sort/code/Book.ts similarity index 100% rename from src/Book.ts rename to src/sort/code/Book.ts diff --git a/src/main.ts b/src/sort/code/main.ts similarity index 52% rename from src/main.ts rename to src/sort/code/main.ts index b307b8f..3c8a0b1 100644 --- a/src/main.ts +++ b/src/sort/code/main.ts @@ -1,13 +1,16 @@ -import Book from './Book'; -import bubbleSort from './sorting/BubbleSort'; -import selectionSort from './sorting/SelectionSort'; -import insertionSort from './sorting/InsertionSort'; +import Book from './Book.js'; +import createText from './popup.js'; +import bubbleSort from './sortingAlgorithms/BubbleSort.js'; +import selectionSort from './sortingAlgorithms/SelectionSort.js'; +import insertionSort from './sortingAlgorithms/InsertionSort.js'; +import quickSort from './sortingAlgorithms/QuickSort.js'; +// import mergeSort from './sortingAlgorithms/MergeSort'; const bookshelfContainer = document.getElementById('bookshelf_container')!; -const above = document.getElementById('augmented_container_above')!; +const above = document.getElementById('augmented_container')!; //DEFAULT const DEFAULT_NUM_BOOKS = 20; -const DEFAULT_TIMEOUT = 500; +const DEFAULT_TIMEOUT = 100; type SortingAlgo = { name: string; @@ -27,6 +30,14 @@ const sortingBooks: Record = { name: 'Insertion Sort', algo: insertionSort, }, + quick: { + name: 'Quick Sort', + algo: quickSort, + }, + // merge:{ + // name: 'Merge Sort', + // algo: mergeSort, + // } }; let books: number = DEFAULT_NUM_BOOKS; @@ -36,56 +47,49 @@ let timeOut: number = DEFAULT_TIMEOUT; let selectedAlgo: string = "" createBookshelf(); -//DOM elements -const goButton = document.getElementById('go-button')!; -const randomButton = document.getElementById('random-button')!; -const resetButton = document.getElementById('reset-button')!; -const worstCaseButton = document.getElementById('worst-case-button')!; -const sortingAlgoSelection = document.getElementById('format') as HTMLSelectElement; -const sliderBooks = document.querySelector('.slider_books input') as HTMLInputElement; -const valueBooks = document.querySelector('.value_books') as HTMLDivElement; -const sliderTime = document.querySelector('.slider_time input') as HTMLInputElement; -const valueTime = document.querySelector('.value_time') as HTMLDivElement; - -//Add event listeners - -goButton.addEventListener('click', () => { +//Add event listeners to DOM elements +document.getElementById('go-button')!.addEventListener('click', () => { go(); }); -randomButton.addEventListener('click', () => { +document.getElementById('random-button')!.addEventListener('click', () => { random(); }); -resetButton.addEventListener('click', () => { +document.getElementById('reset-button')!.addEventListener('click', () => { reset() }); -worstCaseButton.addEventListener('click', () => { +document.getElementById('worst-case-button')!.addEventListener('click', () => { createWorstBookshelf(); }); -sliderBooks.addEventListener('input', function() { +document.querySelector('#slider_books input')!.addEventListener('input', function(this: HTMLInputElement) { books = Number(this.value); - valueBooks.textContent = this.value +" books."; + document.querySelector('#value_books')!.textContent = this.value +" books."; createBookshelf(); }); -sliderTime.addEventListener('input', function() { +document.querySelector('#slider_time input')!.addEventListener('input', function(this: HTMLInputElement) { timeOut = Number(this.value); - valueTime.textContent = this.value +"ms"; + document.querySelector('#value_time')!.textContent = this.value +"ms"; }); -sortingAlgoSelection.addEventListener('change', () => { - selectedAlgo = sortingAlgoSelection.value; +document.getElementById('format')!.addEventListener('change', function(this: HTMLInputElement) { + selectedAlgo = this.value; }); + //Helper functions function go(){ const copy = [...bookshelf]; const sort = sortingBooks[selectedAlgo]; - if (sort==undefined) alert("Please select a sorting algorithm"); + if (sort==undefined){ + createText("Please select an algorithm.", "red"); + return; + } + featureEnabling(false); const moves = sort.algo(copy); animate(moves); } @@ -139,12 +143,7 @@ function visualizeBookshelf(move?: { indices: number[]; type: string }) { bookshelfContainer.innerHTML = ''; above.innerHTML = ''; for (let i = 0; i < bookshelf.length; i++) { - const book = document.createElement('div'); - const bookName = document.createElement('span'); - book.classList.add('book'); - book.style.backgroundColor = bookshelf[i].color; - bookName.textContent = bookshelf[i].name; - book.appendChild(bookName); + const book = visualizeBook(bookshelf[i]); let bookAbove = document.createElement('div'); if (move && move.indices.includes(i)) { book.style.opacity = '0'; @@ -163,6 +162,8 @@ function visualizeBookshelf(move?: { indices: number[]; type: string }) { function animate(moves: Move[]) { if (moves.length == 0) { visualizeBookshelf(); + createText("The bookshelf is sorted!", "lime"); + featureEnabling(true); return; } const move = moves.shift()!; @@ -172,14 +173,42 @@ function animate(moves: Move[]) { [bookshelf[i], bookshelf[j]] = [bookshelf[j], bookshelf[i]]; } visualizeBookshelf(move); + // setTimeout(function () { + // visualizeBookshelf(); + // }, timeOut); setTimeout(function () { animate(moves); }, timeOut); } +function visualizeBook(book: Book){ + const newBook = document.createElement('div'); + newBook.classList.add('book'); + newBook.style.backgroundColor = book.color; + const newBookName = document.createElement('span'); + newBookName.textContent = book.name; + newBook.appendChild(newBookName); + return newBook; +} + function displayBookshelfConsole(bookshelf: Book[]) { const bookNames = bookshelf.map((book) => book.name); console.log(bookNames.join(", ")); } displayBookshelfConsole(bookshelf); - + +function featureEnabling(bool:boolean):void{ + if (!bool){ + (document.getElementById('go-button') as HTMLButtonElement).setAttribute('disabled', String(bool)); + (document.getElementById('random-button') as HTMLButtonElement).setAttribute('disabled', String(bool)); + (document.getElementById('reset-button') as HTMLButtonElement).setAttribute('disabled', String(bool)); + (document.getElementById('worst-case-button') as HTMLButtonElement).setAttribute('disabled', String(bool)); + (document.getElementById('number_book_input') as HTMLInputElement).setAttribute('disabled', String(bool)); + } else { + (document.getElementById('go-button') as HTMLButtonElement).removeAttribute('disabled'); + (document.getElementById('random-button') as HTMLButtonElement).removeAttribute('disabled'); + (document.getElementById('reset-button') as HTMLButtonElement).removeAttribute('disabled'); + (document.getElementById('worst-case-button') as HTMLButtonElement).removeAttribute('disabled'); + (document.getElementById('number_book_input') as HTMLInputElement).removeAttribute('disabled'); + } +} diff --git a/src/sort/code/popup.ts b/src/sort/code/popup.ts new file mode 100644 index 0000000..c87795e --- /dev/null +++ b/src/sort/code/popup.ts @@ -0,0 +1,76 @@ +const warn = document.getElementsByClassName('warning')[0] as HTMLDivElement; +const FADE_IN_TIME = 1000; //ms +const FADE_OUT_TIME = 1000; +const APPEAR_TIME = 8000; + + +export default function createText(message: string, color: string) { + warn.textContent = message; + warn.style.color = color; + warn.style.opacity = '0'; + warn.style.top = '-50px'; + + fadeIn(warn, FADE_IN_TIME); + + // Fade out after 10 seconds with 2 second fade-out time + setTimeout(function() { + fadeOut(warn, FADE_OUT_TIME); + }, APPEAR_TIME); +} + +function fadeIn(element: HTMLElement, duration: number) { + var interval = 10; + var opacity = 0; + var targetOpacity = 1; + var delta = (interval / duration) * (targetOpacity - opacity); + + var timer = setInterval(function() { + opacity += delta; + element.style.opacity = String(opacity); + + if (opacity >= targetOpacity) { + clearInterval(timer); + animate(element, 'rise', FADE_IN_TIME); + } + }, interval); +} + +function fadeOut(element: HTMLElement, duration: number) { + var interval = 10; + var opacity = 1; + var targetOpacity = 0; + var delta = (interval / duration) * (opacity - targetOpacity); + + var timer = setInterval(function() { + opacity -= delta; + element.style.opacity = String(opacity); + + if (opacity <= targetOpacity) { + clearInterval(timer); + animate(element, 'fall', FADE_OUT_TIME); + } + }, interval); +} + +function animate(element: HTMLElement, direction: string, duration: number) { + var interval = 10; + var start = parseInt(element.style.top); + var end:number; + + if (direction === 'rise') { + end = 0; + } else { + end = -50; + } + + var delta = (interval / duration) * (end - start); + + var timer = setInterval(function() { + start += delta; + element.style.top = String(start) + 'px'; + + if ((direction === 'rise' && start >= end) || (direction === 'fall' && start <= end)) { + clearInterval(timer); + } + }, interval); +} diff --git a/src/sort/code/sortingAlgorithms/BubbleSort.ts b/src/sort/code/sortingAlgorithms/BubbleSort.ts new file mode 100644 index 0000000..bb92b62 --- /dev/null +++ b/src/sort/code/sortingAlgorithms/BubbleSort.ts @@ -0,0 +1,30 @@ +import Book from '../Book'; + +type Move = { + indices: number[], + type: string +} + +export default function selectionSort(bookshelf: Book[]) { + const moves: Move[] = []; + var swapped:boolean; + do { + swapped = false; + for (let i = 0; i < bookshelf.length - 1; i++) { + moves.push({ + indices: [i, i+1], + type: "compare" + }); + if (bookshelf[i].name > bookshelf[i+1].name) { + swapped = true; + moves.push({ + indices: [i, i+1], + type: "swap" + }); + [bookshelf[i], bookshelf[i+1]] = [bookshelf[i+1], bookshelf[i]]; + } + } + } while (swapped); + + return moves; +} diff --git a/src/sorting/InsertionSort.ts b/src/sort/code/sortingAlgorithms/InsertionSort.ts similarity index 76% rename from src/sorting/InsertionSort.ts rename to src/sort/code/sortingAlgorithms/InsertionSort.ts index 182a242..f0bf32c 100644 --- a/src/sorting/InsertionSort.ts +++ b/src/sort/code/sortingAlgorithms/InsertionSort.ts @@ -5,24 +5,26 @@ type Move = { type: string } -export default function insertionSort(bookshelf: Book[]) { +export default function insertionSort(bookshelf: Book[]): Move[] { const moves: Move[] = []; + for (let i = 1; i < bookshelf.length; i++) { const current = bookshelf[i]; let j = i - 1; while (j >= 0 && bookshelf[j].name > current.name) { + bookshelf[j + 1] = bookshelf[j]; moves.push({ indices: [j, j + 1], - type: "shift" + type: "swap" }); - bookshelf[j + 1] = bookshelf[j]; j--; } + bookshelf[j + 1] = current; moves.push({ - indices: [i, j + 1], - type: "swap" + indices: [j + 1], + type: "compare" }); - bookshelf[j + 1] = current; } + return moves; -} +} \ No newline at end of file diff --git a/src/sort/code/sortingAlgorithms/MergeSort.ts b/src/sort/code/sortingAlgorithms/MergeSort.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/sort/code/sortingAlgorithms/QuickSort.ts b/src/sort/code/sortingAlgorithms/QuickSort.ts new file mode 100644 index 0000000..cae62f9 --- /dev/null +++ b/src/sort/code/sortingAlgorithms/QuickSort.ts @@ -0,0 +1,60 @@ +import Book from '../Book'; + +type Move = { + indices: number[], + type: string +}; + +export default function quickSort(bookshelf: Book[]): Move[] { + const moves: Move[] = []; + + const partition = (left: number, right: number) => { + const pivotIndex = Math.floor((left + right) / 2); + const pivot = bookshelf[pivotIndex]; + + let i = left; + let j = right; + + while (i <= j) { + while (bookshelf[i].name < pivot.name) { + i++; + moves.push({ + indices: [i, pivotIndex], + type: "compare" + }); + } + while (bookshelf[j].name > pivot.name) { + j--; + moves.push({ + indices: [j, pivotIndex], + type: "compare" + }); + } + if (i <= j) { + if (i !== j) { + moves.push({ + indices: [i, j, pivotIndex], + type: "swap" + }); + } + [bookshelf[i], bookshelf[j]] = [bookshelf[j], bookshelf[i]]; + i++; + j--; + } + } + return i; + }; + + const quickSortRecursive = (left: number, right: number) => { + if (left >= right) { + return; + } + const index = partition(left, right); + quickSortRecursive(left, index - 1); + quickSortRecursive(index, right); + }; + + quickSortRecursive(0, bookshelf.length - 1); + + return moves; +} diff --git a/src/sorting/SelectionSort.ts b/src/sort/code/sortingAlgorithms/SelectionSort.ts similarity index 88% rename from src/sorting/SelectionSort.ts rename to src/sort/code/sortingAlgorithms/SelectionSort.ts index b8ca720..5ee9482 100644 --- a/src/sorting/SelectionSort.ts +++ b/src/sort/code/sortingAlgorithms/SelectionSort.ts @@ -9,6 +9,10 @@ export default function selectionSort(bookshelf: Book[]) { const moves: Move[] = []; for (let i = 0; i < bookshelf.length; i++) { let minIndex = i; + moves.push({ + indices: [i, minIndex], + type: "compare" + }); for (let j = i + 1; j < bookshelf.length; j++) { if (bookshelf[j].name < bookshelf[minIndex].name) { minIndex = j; diff --git a/src/sort/css-style/background.css b/src/sort/css-style/background.css new file mode 100644 index 0000000..868c5fe --- /dev/null +++ b/src/sort/css-style/background.css @@ -0,0 +1,98 @@ +@import url('https://fonts.googleapis.com/css2?family=Lilita+One'); +* { + font-family: "Gill Sans", sans-serif; +} +body{ + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 0; + height: 100vh; + background-color: #ffffff; +} + +header{ + background: #5a909c; + + font-family: "Gill Sans", sans-serif; + color: white; + + height: 10%; + width: 100%; + + display: flex; + justify-content: space-around; + align-items: center; + + margin-bottom: 10px; +} +h1 { + font-size: 40px; + font-weight: bold; + font-family: "Gill Sans", sans-serif; +} +#h1 { + text-decoration: none; + cursor: pointer; + color: #F9FBE7; +} +main{ + height: 85vh; + width: 100%; + position: relative; + align-items: center; + justify-content: center; +} +.algorithms{ + height: 100%; + gap: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} +.algorithms > a { + text-decoration: none; + font-family: "Gill Sans", sans-serif; + font-weight: bold; + padding: 10px; + transition: 0.3s; + color: #19A7CE; + font-size: 25px; + padding: 10px; + background-color: #F9FBE7; + border-radius: 5px; +} +.algorithms > a:hover { + color: black; + text-decoration: none; + cursor: pointer; +} +footer{ + background: linear-gradient(to bottom, #F0ECEC, #BFD7EA); + height: 5vh; + display: flex; + justify-content: left; + align-items: left; +} + +.button_container { + position: relative; + margin-top: 5px; + height: 10vh; + margin-left: 1vh; + display: flex; + gap: 10px; + align-items: center; + justify-content: center; +} + +#bookshelf_container, #augmented_container{ + height: 25vh; + width: 100%; + display: flex; + align-items: flex-end; + justify-content: center; +} \ No newline at end of file diff --git a/style/canvas.css b/src/sort/css-style/canvas.css similarity index 84% rename from style/canvas.css rename to src/sort/css-style/canvas.css index 8ec0293..b860899 100644 --- a/style/canvas.css +++ b/src/sort/css-style/canvas.css @@ -1,14 +1,17 @@ .book { width: 30px; height: 90%; - border: 2px solid black; border-radius: 5px 5px 0px 0px; margin: 1px; display: flex; + flex-direction: column; justify-content: center; align-items: center; font-size: 16px; font-weight: bold; - color: black; text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white; } + +.canvas{ + width: 100%; +} \ No newline at end of file diff --git a/style/control.css b/src/sort/css-style/control.css similarity index 71% rename from style/control.css rename to src/sort/css-style/control.css index 51d0d3d..fd3d6d5 100644 --- a/style/control.css +++ b/src/sort/css-style/control.css @@ -1,9 +1,16 @@ @import url('https://fonts.googleapis.com/css2?family=Lilita+One'); + + +body{ + background: #E3F2FD; +} *{ - font-family: "Lilita One", serif; + font-family: "Gill Sans", sans-serif; + font-weight: bold; } + button { - background: linear-gradient(to bottom right, #5BCEFA, #F5A9B8); + background: #5a909c; width: 50px; height: 50px; padding: 10px 15px; @@ -17,6 +24,12 @@ button { display: flex; justify-content: center; align-items: center; + margin: 2px; +} +button:hover { + cursor: pointer; + box-shadow: 0 0 10px #737b7e; + opacity: 0.8; } button img{ @@ -24,36 +37,46 @@ button img{ height: 40px; } -button:hover { - opacity: 0.8; + +.button-box{ + position: relative; + margin-right: 2px; + z-index: 1; +} + +.button-box #below-buttons{ + display: flex; + justify-content: space-between; + margin-top: 3px; } .box{ + max-width: 300px; + height: 55px; background: white; padding: 20px 20px 30px 30px; border-radius: 5px; box-shadow: 0 0 5px rgba(0,0,0,0.5); - display: flex; align-items: center; } -.box .slider_books{ - height: 40px; - width: 100px; +.box .content{ + font-size: 20px; + font-weight: 300; + color: #5a909c;; display: flex; - align-items: center; - margin-right: 15px; + justify-content: center; } -.box .slider_time{ +.box .slider{ height: 40px; - width: 100px; + width: 200px; display: flex; align-items: center; margin-right: 15px; } -.box .slider_books input{ +.box .slider input{ height: 10px; width: 100%; -webkit-appearance: none; @@ -62,65 +85,40 @@ button:hover { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2); } -.box .slider_time input{ - height: 10px; - width: 100%; - -webkit-appearance: none; - outline: none; - background: #f2f2f2; - box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2); -} - -.box .value_books{ - font-size: 20px; - font-weight: 300; - font-family: 'Lilita One', serif; - color: #3498db; - width: 55px; - text-align: center; -} - -.box .value_time{ - font-size: 20px; - font-weight: 300; - font-family: 'Lilita One', serif; - color: #3498db; - width: 55px; +.box .value{ + width: 100px; text-align: center; } -body{ - background: #E3F2FD; -} - select { -webkit-appearance: none; -moz-appearance: none; -ms-appearance: none; appearance: none; outline: 0; - border: 0!important; + border: 0; background: #ffffff; background-image: none; padding: 20px 20px 30px 30px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); flex: 1; padding: 0 .5em; - color: #3498db; cursor: pointer; - font-size: 1em; display: flex; align-items: center; + font-size: 20px; + font-weight: bold; + color: #5a909c; } - select::-ms-expand { display: none; } + .select { position: relative; display: flex; - width: 13em; + width: 16em; height: 3em; line-height: 3; background: #D3D3D3; @@ -139,5 +137,18 @@ select::-ms-expand { transition:.25s all ease; } .select:hover::after { - color: #3498db; + color: #7ce8ff; +} + +label{ + max-width: 100px; +} + +.warning{ + align-items: center; + display: flex; + justify-content: center; + font-size: 20px; + margin-top: 10px; + height: 30px; } \ No newline at end of file diff --git a/src/sort/css-style/icons/go-button.png b/src/sort/css-style/icons/go-button.png new file mode 100644 index 0000000..4f582f7 Binary files /dev/null and b/src/sort/css-style/icons/go-button.png differ diff --git a/icons/random-button.png b/src/sort/css-style/icons/random-button.png similarity index 100% rename from icons/random-button.png rename to src/sort/css-style/icons/random-button.png diff --git a/icons/reset-button.png b/src/sort/css-style/icons/reset-button.png similarity index 100% rename from icons/reset-button.png rename to src/sort/css-style/icons/reset-button.png diff --git a/icons/worst-case-button.png b/src/sort/css-style/icons/worst-case-button.png similarity index 100% rename from icons/worst-case-button.png rename to src/sort/css-style/icons/worst-case-button.png diff --git a/src/sort/css-style/tooltip.css b/src/sort/css-style/tooltip.css new file mode 100644 index 0000000..8861eb3 --- /dev/null +++ b/src/sort/css-style/tooltip.css @@ -0,0 +1,44 @@ +[data-tooltip] { + position: relative; + z-index: 2; + cursor: pointer; +} + +/* Hide the tooltip content by default */ +[data-tooltip]:before, +[data-tooltip]:after { + visibility: hidden; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: progid: DXImageTransform.Microsoft.Alpha(Opacity=0); + opacity: 0; + pointer-events: none; + z-index: 2; +} + +/* Position tooltip above the element */ +[data-tooltip]:before { + position: absolute; + bottom: -100%; + padding: 7px; + width: 60px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #ffffff; + background-color: hsla(0, 0%, 100%, 0.9); + color: #5a909c;; + content: attr(data-tooltip); + text-align: center; + font-size: 14px; + line-height: 1.2; + z-index: 2; +} + +/* Show tooltip content on hover */ +[data-tooltip]:hover:before, +[data-tooltip]:hover:after { + visibility: visible; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; + filter: progid: DXImageTransform.Microsoft.Alpha(Opacity=100); + opacity: 1; +} \ No newline at end of file diff --git a/src/sort/index.html b/src/sort/index.html new file mode 100644 index 0000000..c77a4ad --- /dev/null +++ b/src/sort/index.html @@ -0,0 +1,80 @@ + + + + + + Algorithms Visualizer + + + + + + + +
+

Algorithms Visualizer

+
+ Sorting + Graph +
+
+
+
+
+
+ +
+
+ +
20 books.
+
+
+
+
+ +
+
+ +
100ms
+
+
+
+
+
+ +
+
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/sorting/BubbleSort.ts b/src/sorting/BubbleSort.ts deleted file mode 100644 index 65bea0b..0000000 --- a/src/sorting/BubbleSort.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Book from '../Book'; - -type Move = { - indices: number[], - type: string -} - -export default function bubbleSort(bookshelf: Book[]) { - const moves:Move[] = []; - do{ - var swapped:boolean = false; - for (let i=1; i bookshelf[i].name) { - swapped = true; - moves.push({ - indices: [i-1,i], - type: "swap" - }); - [bookshelf[i-1], bookshelf[i]] = [bookshelf[i], bookshelf[i-1]]; - } - } - } while(swapped); - return moves; -} diff --git a/style/background.css b/style/background.css deleted file mode 100644 index 19a9f00..0000000 --- a/style/background.css +++ /dev/null @@ -1,64 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Lilita+One'); - -body{ - position: relative; - display: flex; - flex-direction: column; - align-items: left; - justify-content: center; - margin: 0; - height: 100vh; - background-color: #ffffff; -} - -header{ - background: linear-gradient(to right, #FF0000, #FF7F00, #FFFF00, #00FF00, #0000FF, #4B0082, #8F00FF); - height: 10vh; - font-family: 'Lilita One', serif; - color: white; - -webkit-text-stroke: 1px #000000; - opacity: 0.8; - position: relative; - height: 10vh; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - margin: 0; - padding: 0; - z-index: 1; -} -main{ - height: 85vh; -} - -footer{ - background: linear-gradient(to bottom, #F0ECEC, #BFD7EA); - height: 5vh; - display: flex; - justify-content: left; - align-items: left; -} - -body{ - position: relative; - display: flex; - align-items: center; - flex-direction: column; -} - -.button_container { - position: relative; - margin-top: 5px; - height: 10vh; - margin-left: 1vh; - display: flex; - gap: 10px; -} - -#bookshelf_container, #augmented_container_above{ - height: 25vh; - display: flex; - align-items: flex-end; - justify-content: center; - } diff --git a/video/BubbleSort.mp4 b/video/BubbleSort.mp4 new file mode 100644 index 0000000..1fd6413 Binary files /dev/null and b/video/BubbleSort.mp4 differ diff --git a/video/InsertionSort.mp4 b/video/InsertionSort.mp4 new file mode 100644 index 0000000..3b067d3 Binary files /dev/null and b/video/InsertionSort.mp4 differ diff --git a/video/QuickSort.mp4 b/video/QuickSort.mp4 new file mode 100644 index 0000000..9150677 Binary files /dev/null and b/video/QuickSort.mp4 differ diff --git a/video/SelectionSort.mp4 b/video/SelectionSort.mp4 new file mode 100644 index 0000000..f72aac6 Binary files /dev/null and b/video/SelectionSort.mp4 differ