├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── .idea
├── .gitignore
├── algoviz.iml
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLibraryMappings.xml
├── jsLinters
│ └── eslint.xml
├── modules.xml
└── vcs.xml
├── Dockerfile
├── README.md
├── nginx.conf
├── package.json
├── playwright.config.ts
├── postcss.config.cjs
├── src
├── app.d.ts
├── app.html
├── app.postcss
├── components
│ ├── Navbar.svelte
│ ├── heuristicsPicker
│ │ ├── CustomHeuristicsPicker.svelte
│ │ └── HeuristicsPicker.svelte
│ ├── legend
│ │ ├── Legend.svelte
│ │ └── LegendItem.svelte
│ └── loader
│ │ ├── Controls.svelte
│ │ └── PlaybackControls.svelte
├── constants
│ ├── index.ts
│ └── types.ts
├── painter
│ ├── Grid.ts
│ ├── GridNode.ts
│ ├── MouseHandlers.ts
│ ├── Painter.ts
│ └── index.ts
├── routes
│ ├── +layout.svelte
│ └── +page.svelte
├── scss
│ ├── button.scss
│ ├── global.scss
│ ├── index.scss
│ ├── table.scss
│ └── theme.scss
├── store.ts
└── worker
│ └── AlgorithmMincerWorker.ts
├── static
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── browserconfig.xml
├── dot-circle-solid.svg
├── favicon.png
├── icon-48x48.png
├── logo.svg
├── mstile-150x150.png
├── play-circle-solid.svg
├── safari-pinned-tab.svg
├── site.webmanifest
└── square-full-solid.svg
├── svelte.config.js
├── tailwind.config.cjs
├── tests
└── test.ts
├── tsconfig.json
├── vite.config.js
└── yarn.lock
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@master
8 | - name: Publish to Registry
9 | uses: elgohr/Publish-Docker-Github-Action@master
10 | with:
11 | name: algoviz
12 | registry: registry.njanjo.com/v2
13 | username: ${{ secrets.DOCKER_USERNAME }}
14 | password: ${{ secrets.DOCKER_PASSWORD }}
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Node template
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | lerna-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | dist/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # TypeScript v1 declaration files
47 | typings/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Optional REPL history
59 | .node_repl_history
60 |
61 | # Output of 'npm pack'
62 | *.tgz
63 |
64 | # Yarn Integrity file
65 | .yarn-integrity
66 |
67 | # dotenv environment variables file
68 | .env
69 | .env.test
70 |
71 | # parcel-bundler cache (https://parceljs.org/)
72 | .cache
73 |
74 | # next.js build output
75 | .next
76 |
77 | # nuxt.js build output
78 | .nuxt
79 |
80 | # vuepress build output
81 | .vuepress/dist
82 |
83 | # Serverless directories
84 | .serverless/
85 |
86 | # FuseBox cache
87 | .fusebox/
88 |
89 | # DynamoDB Local files
90 | .dynamodb/
91 |
92 | ### JetBrains template
93 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
94 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
95 |
96 | # User-specific stuff
97 | .idea/**/workspace.xml
98 | .idea/**/tasks.xml
99 | .idea/**/usage.statistics.xml
100 | .idea/**/dictionaries
101 | .idea/**/shelf
102 |
103 | # Generated files
104 | .idea/**/contentModel.xml
105 |
106 | # Sensitive or high-churn files
107 | .idea/**/dataSources/
108 | .idea/**/dataSources.ids
109 | .idea/**/dataSources.local.xml
110 | .idea/**/sqlDataSources.xml
111 | .idea/**/dynamic.xml
112 | .idea/**/uiDesigner.xml
113 | .idea/**/dbnavigator.xml
114 |
115 | # Gradle
116 | .idea/**/gradle.xml
117 | .idea/**/libraries
118 |
119 | # Gradle and Maven with auto-import
120 | # When using Gradle or Maven with auto-import, you should exclude module files,
121 | # since they will be recreated, and may cause churn. Uncomment if using
122 | # auto-import.
123 | # .idea/modules.xml
124 | # .idea/*.iml
125 | # .idea/modules
126 | # *.iml
127 | # *.ipr
128 |
129 | # CMake
130 | cmake-build-*/
131 |
132 | # Mongo Explorer plugin
133 | .idea/**/mongoSettings.xml
134 |
135 | # File-based project format
136 | *.iws
137 |
138 | # IntelliJ
139 | out/
140 |
141 | # mpeltonen/sbt-idea plugin
142 | .idea_modules/
143 |
144 | # JIRA plugin
145 | atlassian-ide-plugin.xml
146 |
147 | # Cursive Clojure plugin
148 | .idea/replstate.xml
149 |
150 | # Crashlytics plugin (for Android Studio and IntelliJ)
151 | com_crashlytics_export_strings.xml
152 | crashlytics.properties
153 | crashlytics-build.properties
154 | fabric.properties
155 |
156 | # Editor-based Rest Client
157 | .idea/httpRequests
158 |
159 | # Android studio 3.1+ serialized cache file
160 | .idea/caches/build_file_checksums.ser
161 |
162 | /build/
163 | /.svelte-kit/
164 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/algoviz.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 0, "build-stage", based on Node.js, to build and compile the frontend
2 | FROM node:18 as build-stage
3 | # Setting working directory. All the path will be relative to WORKDIR
4 | WORKDIR /usr/src/app
5 |
6 | # Installing dependencies
7 | COPY package.json yarn.lock ./
8 | RUN yarn
9 |
10 | # Copying source files
11 | COPY . .
12 |
13 | # Building app
14 | RUN yarn build
15 |
16 | # Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
17 | FROM fholzer/nginx-brotli
18 |
19 | COPY --from=build-stage /usr/src/app/build/ /usr/share/nginx/html
20 | # Copy the default nginx.conf provided by tiangolo/node-frontend
21 | COPY --from=build-stage /usr/src/app/nginx.conf /etc/nginx/conf.d/default.conf
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/ssaric/algoviz/actions/workflows/publish.yml)
2 |
3 | # Algoviz
4 | A project focusing on visualzing A* pathfinding algorithm. Available at https://algoviz.njanjo.com.
5 |
6 | ## Roadmap
7 |
8 | - [x] Multiple heuristics
9 |
10 | - [x] Seekable timeline
11 |
12 | - [x] Typescript ready
13 |
14 | - [ ] Algorithm "thoughts" (algorithm should display in each field what was it trying to do)
15 |
16 | - [ ] Side-by-side maps to compare different heuristics
17 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | # Gzip Settings
2 | gzip_disable "msie6";
3 | gzip_vary on;
4 | gzip_proxied any;
5 | gzip_comp_level 6;
6 | gzip_buffers 32 16k;
7 | gzip_http_version 1.1;
8 | gzip_min_length 250;
9 | gzip_types image/jpeg image/bmp image/svg+xml text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/x-icon;
10 | # Brotli Settings
11 | brotli_comp_level 4;
12 | brotli_buffers 32 8k;
13 | brotli_min_length 100;
14 | brotli_types image/jpeg image/bmp image/svg+xml text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/x-icon;
15 |
16 | # security headers
17 | add_header X-Frame-Options "SAMEORIGIN" always;
18 | add_header X-XSS-Protection "1; mode=block" always;
19 | add_header X-Content-Type-Options "nosniff" always;
20 | add_header Referrer-Policy "no-referrer-when-downgrade" always;
21 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
22 |
23 |
24 | server {
25 | listen 80;
26 | root /usr/share/nginx/html;
27 |
28 | location / {
29 | try_files $uri /index.html =404;
30 | }
31 |
32 | location ~* \.(jpg|jpeg|png|gif|ico)$ {
33 | expires 30d;
34 | add_header Pragma public;
35 | add_header Cache-Control "public";
36 | }
37 | location ~* \.(css|js)$ {
38 | add_header Pragma public;
39 | add_header Cache-Control "public";
40 | expires 7d;
41 | }
42 |
43 | include /etc/nginx/extra-conf.d/*.conf;
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-algoviz",
3 | "version": "1.1.0",
4 | "license": "MIT",
5 | "description": "AlgoViz implemented in Svelte",
6 | "scripts": {
7 | "dev": "vite dev",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "test": "playwright test",
11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13 | "lint": "prettier --check . && eslint .",
14 | "format": "prettier --write ."
15 | },
16 | "devDependencies": {
17 | "@floating-ui/dom": "^1.0.1",
18 | "@playwright/test": "^1.25.0",
19 | "@popperjs/core": "^2.11.6",
20 | "@sveltejs/adapter-auto": "next",
21 | "@sveltejs/kit": "next",
22 | "@typescript-eslint/eslint-plugin": "^5.27.0",
23 | "@typescript-eslint/parser": "^5.27.0",
24 | "autoprefixer": "^10.4.7",
25 | "classnames": "^2.3.1",
26 | "eslint": "^8.16.0",
27 | "eslint-config-prettier": "^8.3.0",
28 | "eslint-plugin-svelte3": "^4.0.0",
29 | "flowbite-svelte": "^0.25.13",
30 | "node-sass": "^7.0.1",
31 | "postcss": "^8.4.14",
32 | "postcss-load-config": "^4.0.1",
33 | "prettier": "^2.6.2",
34 | "prettier-plugin-svelte": "^2.7.0",
35 | "svelte": "^3.44.0",
36 | "svelte-check": "^2.7.1",
37 | "svelte-heros": "^2.3.3",
38 | "svelte-preprocess": "^4.10.7",
39 | "tslib": "^2.3.1",
40 | "typescript": "^4.7.4",
41 | "vite": "^3.0.4"
42 | },
43 | "type": "module",
44 | "dependencies": {
45 | "@fortawesome/free-brands-svg-icons": "^6.1.2",
46 | "@fortawesome/free-solid-svg-icons": "^6.1.2",
47 | "@sveltejs/adapter-static": "^1.0.0-next.39",
48 | "mathjs": "^11.0.1",
49 | "svelte-awesome": "^3.0.0",
50 | "svelte-drag": "^3.1.0",
51 | "tailwindcss": "^3.1.8"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 |
3 | const config: PlaywrightTestConfig = {
4 | webServer: {
5 | command: 'npm run build && npm run preview',
6 | port: 4173
7 | }
8 | };
9 |
10 | export default config;
11 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const tailwindcss = require("tailwindcss");
2 | const autoprefixer = require("autoprefixer");
3 |
4 | const config = {
5 | plugins: [
6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
7 | tailwindcss(),
8 | //But others, like autoprefixer, need to run after,
9 | autoprefixer,
10 | ],
11 | };
12 |
13 | module.exports = config;
14 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | // and what to do when importing types
4 | declare namespace App {
5 | // interface Locals {}
6 | // interface Platform {}
7 | // interface PrivateEnv {}
8 | // interface PublicEnv {}
9 | }
10 | declare module '@fortawesome/free-solid-svg-icons/index.es' {
11 | export * from '@fortawesome/free-solid-svg-icons';
12 | }
13 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app.postcss:
--------------------------------------------------------------------------------
1 | /* Write your global styles here, in PostCSS syntax */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
--------------------------------------------------------------------------------
/src/components/Navbar.svelte:
--------------------------------------------------------------------------------
1 |
47 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Algoviz
61 |
62 |
63 |
68 |
69 |
--------------------------------------------------------------------------------
/src/components/heuristicsPicker/CustomHeuristicsPicker.svelte:
--------------------------------------------------------------------------------
1 |
11 |
24 |
25 |
26 |
27 |
Input a custom heuristics formula using the variables
28 | x
and y
29 | representing respective horizontal and vertical distances.
30 | The formula will be parsed via math.js
31 |
32 |
33 |
Apply
34 |
35 |
--------------------------------------------------------------------------------
/src/components/heuristicsPicker/HeuristicsPicker.svelte:
--------------------------------------------------------------------------------
1 |
37 |
71 |
72 |
73 |
74 |
75 | {#if $heuristics.type === Heuristics.CUSTOM}
76 |
77 | {/if}
78 |
79 |
80 |
81 |
82 | Reset Grid
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/components/legend/Legend.svelte:
--------------------------------------------------------------------------------
1 |
81 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
95 |
96 |
97 |
Discovered
98 |
99 |
100 |
101 |
Final path
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/src/components/legend/LegendItem.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 | {text}
25 |
26 |
--------------------------------------------------------------------------------
/src/components/loader/Controls.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/components/loader/PlaybackControls.svelte:
--------------------------------------------------------------------------------
1 |
163 |
164 |
182 |
199 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | import { icon } from "@fortawesome/fontawesome-svg-core";
2 | import {
3 | faPlayCircle,
4 | faDotCircle,
5 | faSquareFull,
6 | } from "@fortawesome/free-solid-svg-icons";
7 |
8 | export const elementsData = {
9 | walls: {
10 | text: "walls",
11 | icon: icon(faSquareFull).html[0],
12 | },
13 | startPosition: {
14 | text: "start position",
15 | icon: icon(faPlayCircle).html[0],
16 | },
17 | endPosition: {
18 | text: "end position",
19 | icon: icon(faDotCircle).html[0],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/src/constants/types.ts:
--------------------------------------------------------------------------------
1 | import type { GridCoordinates } from "../painter/GridNode";
2 |
3 | export const CELL_SIZE = 20;
4 |
5 | export enum MessageType {
6 | GRID_DATA,
7 | INFO_DATA,
8 | ALGORITHM_STEP,
9 | SET_HEURISTICS,
10 | }
11 |
12 | export enum AlgorithmWorkerStepType {
13 | START,
14 | END,
15 | INFO,
16 | MARK_PATH,
17 | VISIT,
18 | DISCOVER,
19 | }
20 |
21 | export enum FieldType {
22 | START,
23 | END,
24 | WALL,
25 | }
26 |
27 | export enum MouseClick {
28 | LEFT,
29 | MIDDLE,
30 | RIGHT,
31 | }
32 |
33 | export enum PlaybackDirection {
34 | FORWARD = "forward",
35 | BACKWARD = "backward",
36 | }
37 |
38 | export type WorkerGridTransferData = {
39 | columns: number;
40 | rows: number;
41 | start: Array;
42 | end: Array;
43 | walls: Array>;
44 | heuristics: HeuristicsData;
45 | };
46 |
47 | export type GridConstructorData = {
48 | columns: number;
49 | rows: number;
50 | start?: GridCoordinates;
51 | end?: GridCoordinates;
52 | walls?: Array;
53 | heuristics?: HeuristicsData;
54 | };
55 |
56 | export type GridData = {
57 | walls: Array;
58 | startPosition: Array;
59 | endPosition: Array;
60 | };
61 |
62 | export type GridPaintStroke = (direction: PlaybackDirection) => void;
63 |
64 |
65 | export enum Heuristics {
66 | EUCLIDEAN = "0",
67 | MANHATTAN = "1",
68 | CUSTOM = "2",
69 | }
70 |
71 | export type AlgorithmStep = {
72 | type: AlgorithmWorkerStepType;
73 | location?: Array;
74 | neighbours?: [number, number][];
75 | info?: string;
76 | };
77 |
78 | export type AlgorithmInfoMessage = {
79 | type: AlgorithmWorkerStepType;
80 | tileId: string;
81 | ghValues?: [number, number];
82 | info: string;
83 | parent?: string;
84 | };
85 |
86 | export type CellInfoMessage = {
87 | ghValues?: [number, number];
88 | info: string[];
89 | parent?: string;
90 | };
91 |
92 | export type HeuristicsData = {
93 | type: Heuristics.EUCLIDEAN | Heuristics.MANHATTAN;
94 | } | {
95 | type: Heuristics.CUSTOM;
96 | formula: string;
97 | }
98 |
--------------------------------------------------------------------------------
/src/painter/Grid.ts:
--------------------------------------------------------------------------------
1 | import GridNode, {GridCoordinates} from "./GridNode";
2 | import {GridConstructorData, Heuristics, HeuristicsData,} from "../constants/types";
3 | import {evaluate} from "mathjs";
4 | import MouseHandlers from "./MouseHandlers";
5 |
6 | export interface HeuristicsFunction {
7 | (nodeA: GridCoordinates, nodeB: GridCoordinates): number;
8 | }
9 |
10 | function manhattanHeuristics(nodeA: GridCoordinates, nodeB: GridCoordinates) {
11 | return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y);
12 | }
13 | function euclideanHeuristics(nodeA: GridCoordinates, nodeB: GridCoordinates) {
14 | return (nodeA.x - nodeB.x) ** 2 + (nodeA.y - nodeB.y) ** 2;
15 | }
16 |
17 |
18 | const customHeuristics =
19 | (formula: string) => (nodeA: GridCoordinates, nodeB: GridCoordinates) => {
20 | const scope = {
21 | x: nodeA.x - nodeB.x,
22 | y: nodeA.y - nodeB.y,
23 | };
24 | return evaluate(formula, scope);
25 | };
26 |
27 | export function setHeuristicsFunction(heuristicsData: HeuristicsData) {
28 | switch (heuristicsData.type) {
29 | case Heuristics.MANHATTAN:
30 | Grid.heuristicsFunction = manhattanHeuristics;
31 | break;
32 | case Heuristics.CUSTOM:
33 | Grid.heuristicsFunction = customHeuristics(heuristicsData.formula);
34 | break;
35 | default:
36 | case Heuristics.EUCLIDEAN:
37 | Grid.heuristicsFunction = euclideanHeuristics;
38 | break;
39 | }
40 | }
41 |
42 | class Grid {
43 | public columns: number;
44 | public rows: number;
45 | public start: GridNode | null = null;
46 | public end: GridNode | null = null;
47 | private walls: Map;
48 | private visited: Map;
49 | private discovered: Map;
50 | public static heuristicsFunction: HeuristicsFunction = euclideanHeuristics;
51 |
52 | constructor({ columns, rows, start, end, walls }: GridConstructorData) {
53 | this.columns = columns;
54 | this.rows = rows;
55 | this.walls = new Map();
56 | this.visited = new Map();
57 | this.discovered = new Map();
58 | walls?.forEach((w: GridCoordinates) =>
59 | this.walls.set(w.id, new GridNode(w))
60 | );
61 | if (start) {
62 | this.start = new GridNode(start);
63 | } else {
64 | this.createStartNode();
65 | }
66 |
67 | if (end) {
68 | this.end = new GridNode(end);
69 | } else {
70 | this.createEndNode();
71 | }
72 | }
73 |
74 | public get wallsAsArray(): Array<[number,number]> {
75 | return [...this.walls.values()].map(w => w.toArray());
76 | }
77 |
78 | public get startAsArray(): [number,number] | undefined{
79 | return this.start?.toArray();
80 | }
81 |
82 | public get endAsArray(): [number,number] | undefined {
83 | return this.end?.toArray();
84 | }
85 |
86 | public isGridValid(): boolean {
87 | return this.walls?.size !== 0 && this.start !== null && this.end !== null
88 | }
89 |
90 | public nearestFreeCell(
91 | columnIndex: number,
92 | rowIndex: number
93 | ): [number, number] | null {
94 | const visited = new Set();
95 | const startNode = new GridNode(new GridCoordinates(columnIndex, rowIndex));
96 | visited.add(startNode.id);
97 | const neighbourMap = this.getCellNeighbours(startNode);
98 | let nodes = [...neighbourMap.values()];
99 | while (nodes.length > 0) {
100 | const neighbour = nodes.shift();
101 | if (!neighbour) continue;
102 | if (this.isCellFree(neighbour)) return neighbour.toArray();
103 | visited.add(neighbour.id);
104 | const validNeighbours = this.getCellNeighbours(neighbour);
105 | validNeighbours.delete(neighbour.id);
106 | nodes = [...nodes, ...validNeighbours.values()];
107 | }
108 | return null;
109 | }
110 |
111 | public addWall(columnIndex: number, rowIndex: number) {
112 | this.walls.set(
113 | [columnIndex, rowIndex].toString(),
114 | new GridNode(new GridCoordinates(columnIndex, rowIndex))
115 | );
116 | }
117 |
118 | public removeWall(columnIndex: number, rowIndex: number) {
119 | this.walls.delete(
120 | [columnIndex, rowIndex].toString()
121 | );
122 | }
123 |
124 | public setStart(columnIndex: number, rowIndex: number) {
125 | this.start = new GridNode(new GridCoordinates(columnIndex, rowIndex));
126 | }
127 |
128 | public setEnd(columnIndex: number, rowIndex: number) {
129 | this.end = new GridNode(new GridCoordinates(columnIndex, rowIndex));
130 | }
131 |
132 |
133 | public updateColumns(newColumns: number) {
134 | this.columns = newColumns;
135 | if (this.start && this.start.coordinates.x + 1 > this.columns) {
136 | this.start.coordinates.x = this.columns - 1;
137 | }
138 | if (this.end && this.end.coordinates.x + 1 > this.columns) {
139 | this.end.coordinates.x = this.columns - 1;
140 | }
141 | }
142 |
143 | public updateRows(newRows: number) {
144 | this.rows = newRows;
145 | if (this.start && this.start.coordinates.y + 1 > this.rows) {
146 | this.start.coordinates.y = this.rows;
147 | }
148 | if (this.end && this.end.coordinates.y + 1 > this.rows) {
149 | this.end.coordinates.y = this.rows;
150 | }
151 | }
152 |
153 | public createStartNode() {
154 | const startNodeRow = Math.floor(this.rows / 2);
155 | const startNodeColumn = Math.floor(this.columns / 8);
156 | this.setStart(startNodeColumn, startNodeRow);
157 | }
158 |
159 | public createEndNode() {
160 | const endNodeRow = Math.floor(this.rows / 2);
161 | const endNodeColumn = Math.floor(this.columns * (7 / 8));
162 | this.setEnd(endNodeColumn, endNodeRow);
163 | }
164 |
165 | visit(node: GridNode) {
166 | this.visited.set(node.id, node);
167 | }
168 |
169 | discover(node: GridNode) {
170 | this.discovered.set(node.id, node);
171 | }
172 |
173 | isEnd(node: GridNode | GridCoordinates): boolean {
174 | return !!this.end && node.equals(this.end);
175 | }
176 |
177 | isStart(node: GridNode | GridCoordinates): boolean {
178 | return !!this.start && node.equals(this.start);
179 | }
180 |
181 | isVisited(gridLocation: GridCoordinates): boolean {
182 | return this.visited.has(gridLocation.id);
183 | }
184 |
185 | isWall(gridNode: GridNode | GridCoordinates): boolean {
186 | return this.walls.has(gridNode.id);
187 | }
188 |
189 | /** Can algorithm consider this cell for next step **/
190 | public isCellWalkable(location: GridNode | GridCoordinates): boolean {
191 | return this.isCellFree(location) && !this.isVisited(location);
192 | }
193 |
194 | /** Can you put a wall, start or end node in this cell **/
195 | public isCellFree(location: GridNode | GridCoordinates): boolean {
196 | return (
197 | this.isWithinGridBounds(location) &&
198 | !this.isWall(location) &&
199 | !this.isStart(location) &&
200 | !this.isEnd(location)
201 | );
202 | }
203 |
204 | isWithinGridBounds(location: GridNode | GridCoordinates) {
205 | return !(
206 | location.x < 0 ||
207 | location.x > this.columns - 1 ||
208 | location.y < 0 ||
209 | location.y > this.rows - 1
210 | );
211 | }
212 |
213 | getNeighboursTemplate(
214 | gridNode: GridNode,
215 | testFunction: (location: GridCoordinates | GridNode) => boolean
216 | ): Map {
217 | const up = new GridCoordinates(gridNode.x, gridNode.y - 1);
218 | const bottom = new GridCoordinates(
219 | gridNode.x,
220 | gridNode.y + 1
221 | );
222 | const right = new GridCoordinates(gridNode.x + 1, gridNode.y);
223 | const left = new GridCoordinates(gridNode.x - 1, gridNode.y);
224 | const nodes = new Map();
225 |
226 | if (testFunction(up)) nodes.set(up.id, new GridNode(up));
227 | if (testFunction(bottom)) nodes.set(bottom.id, new GridNode(bottom));
228 | if (testFunction(right)) nodes.set(right.id, new GridNode(right));
229 | if (testFunction(left)) nodes.set(left.id, new GridNode(left));
230 | return nodes;
231 | }
232 |
233 | getCellNeighbours(gridNode: GridNode): Map {
234 | return this.getNeighboursTemplate(
235 | gridNode,
236 | this.isWithinGridBounds.bind(this)
237 | );
238 | }
239 |
240 | getCellFreeNeighbours(
241 | gridNode: GridNode
242 | ): Map {
243 | return this.getNeighboursTemplate(
244 | gridNode,
245 | this.isCellFree.bind(this)
246 | );
247 | }
248 |
249 | public reset(): void {
250 | this.walls = new Map();
251 | }
252 |
253 | public getWalkableNeighbours(gridNode: GridNode) {
254 | return this.getNeighboursTemplate(
255 | gridNode,
256 | this.isCellWalkable.bind(this)
257 | );
258 | }
259 | }
260 |
261 | export default Grid;
262 |
--------------------------------------------------------------------------------
/src/painter/GridNode.ts:
--------------------------------------------------------------------------------
1 | import Grid from "./Grid";
2 |
3 | export type ArrayLikeCoordinates = [number, number];
4 |
5 | export class GridCoordinates {
6 | public x: number;
7 | public y: number;
8 | constructor(x: number, y: number) {
9 | this.x = x;
10 | this.y = y;
11 | }
12 |
13 | get id() {
14 | return this.toArray().toString();
15 | }
16 |
17 | toArray(): ArrayLikeCoordinates {
18 | return [this.x, this.y];
19 | }
20 |
21 | public equals(node: GridCoordinates): boolean {
22 | if (node instanceof GridCoordinates) {
23 | return this.x === node.x && this.y === node.y;
24 | } else if (Array.isArray(node)) {
25 | return this.x === node[0] && this.y === node[1];
26 | }
27 | return false;
28 | }
29 | }
30 |
31 | class GridNode {
32 | public readonly coordinates: GridCoordinates;
33 | public g: number;
34 | public h: number;
35 | public p: GridNode | null;
36 |
37 | constructor(gridLocation: GridCoordinates) {
38 | this.coordinates = gridLocation;
39 | this.g = 0;
40 | this.h = 0;
41 | this.p = null;
42 | }
43 |
44 | get x() {
45 | return this.coordinates.x;
46 | }
47 |
48 | get y() {
49 | return this.coordinates.y;
50 | }
51 |
52 | get id() {
53 | return this.coordinates.id;
54 | }
55 |
56 | set parent(parent: GridNode | null) {
57 | this.p = parent;
58 | }
59 |
60 | get parent(): GridNode | null {
61 | return this.p;
62 | }
63 |
64 | set gCost(g) {
65 | this.g = g;
66 | }
67 |
68 | get gCost(): number {
69 | return this.g;
70 | }
71 |
72 | set hCost(h) {
73 | this.h = h;
74 | }
75 |
76 | get hCost(): number {
77 | return this.h;
78 | }
79 |
80 | get totalCost(): number {
81 | return this.h + this.g;
82 | }
83 |
84 | toArray(): [number, number] {
85 | return this.coordinates.toArray();
86 | }
87 |
88 | equals(node: GridNode): boolean {
89 | return node.coordinates.equals(this.coordinates);
90 | }
91 |
92 | setParameters(endNode: GridNode, fromNode: GridNode) {
93 | const initialCost: number = (fromNode && fromNode.g) || 0;
94 | this.g = initialCost + 1;
95 | this.h = Grid.heuristicsFunction(this.coordinates, endNode.coordinates);
96 | this.setParent(fromNode);
97 | }
98 |
99 | setParent(fromNode: GridNode) {
100 | if (fromNode) {
101 | this.parent = fromNode;
102 | }
103 | }
104 | }
105 |
106 | export default GridNode;
107 |
--------------------------------------------------------------------------------
/src/painter/MouseHandlers.ts:
--------------------------------------------------------------------------------
1 | import { MouseClick } from "../constants/types";
2 | import type Painter from "./Painter";
3 |
4 | class MouseHandlers {
5 | private painter: Painter;
6 | private dragSelect = false;
7 | private draggingStartCell = false;
8 | private draggingEndCell = false;
9 |
10 | private mouseDownCallback = this.onMouseDown.bind(this);
11 | private mouseMoveCallback = this.onMouseMove.bind(this);
12 | private mouseUpCallback = this.onMouseUp.bind(this);
13 | private mouseclickCallback = this.onMouseClick.bind(this);
14 | private mouseOutCallback = this.onMouseOut.bind(this);
15 |
16 | constructor(painter: Painter) {
17 | this.painter = painter;
18 | }
19 | public bind() {
20 | this.painter.container.addEventListener("mousedown", this.mouseDownCallback);
21 | this.painter.container.addEventListener("click", this.mouseclickCallback);
22 | this.painter.container.addEventListener("mouseup", this.mouseUpCallback);
23 | this.painter.container.addEventListener("mousemove", this.mouseMoveCallback);
24 | }
25 |
26 | public unbind() {
27 | this.painter.container.removeEventListener("click", this.mouseclickCallback);
28 | this.painter.container.removeEventListener("mousedown", this.mouseDownCallback);
29 | this.painter.container.removeEventListener("mouseup", this.mouseUpCallback);
30 | this.painter.container.removeEventListener("mousemove", this.mouseMoveCallback);
31 | }
32 |
33 | onMouseMove(e: MouseEvent) {
34 | if (!this.dragSelect && !this.draggingStartCell && !this.draggingEndCell)
35 | return;
36 | const target = e.target as HTMLElement;
37 | if (!target.getAttribute("columnIndex")) return;
38 | this.handleMovementEvent(e.target as HTMLTableCellElement);
39 | }
40 |
41 | handleMovementEvent(targetElement: HTMLTableCellElement) {
42 | if (this.draggingStartCell) {
43 | this.painter.renderStartAtCell(targetElement);
44 | } else if (this.draggingEndCell) {
45 | this.painter.renderEndAtCell(targetElement);
46 | }
47 | if (this.dragSelect) {
48 | this.painter.addWall(targetElement);
49 | }
50 | }
51 |
52 | onMouseClick(e: MouseEvent) {
53 | if (!e.target) return;
54 | if (!(e.target instanceof HTMLTableCellElement)) return;
55 | if (!e.target.getAttribute("columnIndex")) return;
56 | if (e.button !== MouseClick.LEFT) return;
57 | this.painter.toggleWall(e.target);
58 | }
59 |
60 | onMouseDown(e: MouseEvent) {
61 | if (!(e.target instanceof HTMLTableCellElement)) return;
62 | if (e.button !== undefined && e.button !== MouseClick.LEFT) return;
63 | this.painter.container.addEventListener("mouseout", this.mouseOutCallback);
64 | this.painter.container.addEventListener("touchcancel", this.mouseOutCallback);
65 | this.painter.container.addEventListener("mouseup", this.mouseUpCallback);
66 | if (e.target.classList.contains("cell--start"))
67 | this.draggingStartCell = true;
68 | else if (e.target.classList.contains("cell--end"))
69 | this.draggingEndCell = true;
70 | else this.dragSelect = true;
71 | }
72 | onMouseUp() {
73 | this.painter.container.removeEventListener("mouseout", this.mouseOutCallback);
74 | this.dragSelect = false;
75 | this.draggingStartCell = false;
76 | this.draggingEndCell = false;
77 | }
78 | onMouseOut(e: MouseEvent | TouchEvent) {
79 | const from = e.target;
80 | if (!from) {
81 | this.dragSelect = false;
82 | }
83 | }
84 | }
85 |
86 | export default MouseHandlers;
87 |
--------------------------------------------------------------------------------
/src/painter/Painter.ts:
--------------------------------------------------------------------------------
1 | import Grid from "./Grid";
2 | import {
3 | AlgorithmStep,
4 | AlgorithmWorkerStepType,
5 | CELL_SIZE,
6 | PlaybackDirection, GridPaintStroke, MessageType
7 | } from "../constants/types";
8 | import MouseHandlers from "./MouseHandlers";
9 | import {get, writable, Writable} from "svelte/store";
10 | import {currentStep, heuristics, interval, removeInterval, steps} from "../store";
11 | import { clamp } from ".";
12 |
13 | const TABLE_ID = "table";
14 | const STEP_SIZE = 10;
15 |
16 |
17 |
18 | class Painter {
19 | public grid: Grid;
20 | public container: HTMLDivElement;
21 | public algorithmWorker: Worker | undefined = undefined;
22 | private mouseHandlers = new MouseHandlers(this);
23 |
24 | constructor() {
25 | const root = document.getElementById("root") as HTMLDivElement;
26 | this.container = root;
27 | const rootBbox = root.getBoundingClientRect();
28 | const viewportWidth = rootBbox.width;
29 | const viewportHeight = rootBbox.height;
30 | const initialColumns = Math.floor(viewportWidth / CELL_SIZE);
31 | const initialRows = Math.floor(viewportHeight / CELL_SIZE);
32 | this.grid = new Grid({
33 | columns: initialColumns,
34 | rows: initialRows,
35 | });
36 | this.renderGrid();
37 | this.updateCellNumberListener();
38 | this.renderStarAndEndNodes();
39 | heuristics.subscribe(() => this.resetVisualization());
40 | }
41 | public async loadWorker () {
42 | const AlgorithmWorker = await import('../worker/AlgorithmMincerWorker?worker');
43 | this.algorithmWorker = new AlgorithmWorker.default();
44 | this.algorithmWorker.onmessage = (e: MessageEvent) => {
45 | const data = e.data[1];
46 | switch (data.type) {
47 | case AlgorithmWorkerStepType.START:
48 | this.resetVisualization();
49 | return;
50 | case AlgorithmWorkerStepType.INFO:
51 | break;
52 | default:
53 | this.addVisualizationStep(data);
54 |
55 | }
56 | };
57 | };
58 |
59 | public bindEventHandlers() {
60 | this.mouseHandlers.bind();
61 | }
62 |
63 | public unbindEventHandlers() {
64 | this.mouseHandlers.unbind();
65 | }
66 |
67 |
68 | private startProcessingData = () => {
69 | this.algorithmWorker?.postMessage([MessageType.GRID_DATA, {
70 | columns: this.grid.columns,
71 | rows: this.grid.rows,
72 | walls: this.grid.wallsAsArray,
73 | start: this.grid.startAsArray,
74 | end: this.grid.endAsArray,
75 | heuristics: get(heuristics),
76 | }]);
77 | };
78 |
79 |
80 |
81 | public startVisualizingSteps = () => {
82 | const totalNumberOfSteps = get(steps).length;
83 | if (!this.canProcessData()) {
84 | // showMissingDataToast();
85 | } else if (totalNumberOfSteps === 0) {
86 | this.startProcessingData();
87 | } else {
88 | this.startPlaying();
89 | }
90 | };
91 |
92 | private canProcessData() {
93 | return this.grid.isGridValid();
94 | }
95 |
96 | updateCellNumberListener() {
97 | const resizeObserver = new ResizeObserver((entries) => {
98 | for (const entry of entries) {
99 | const newColumns = Math.floor(entry.contentRect.width / CELL_SIZE);
100 | const newRows = Math.floor(entry.contentRect.height / CELL_SIZE);
101 | this.grid.updateColumns(newColumns);
102 | this.grid.updateRows(newRows);
103 | this.renderGrid();
104 | this.renderStarAndEndNodes();
105 | }
106 | });
107 | resizeObserver.observe(this.container);
108 | }
109 |
110 | createTableIfNotExist(): HTMLTableElement {
111 | const existingTable = this.getTableElement();
112 | if (!existingTable) {
113 | const table = document.createElement(TABLE_ID);
114 | table.id = "table";
115 | table.className = "table";
116 | this.container.appendChild(table);
117 | return table;
118 | }
119 | return existingTable;
120 | }
121 |
122 | createRowIfNotExist(
123 | table: HTMLTableElement,
124 | rowIndex: number
125 | ): HTMLTableRowElement {
126 | const existingRow = this.getRowElement(table, rowIndex);
127 | if (!existingRow) {
128 | const tableRow = document.createElement("tr");
129 | tableRow.setAttribute("rowIndex", `${rowIndex}`);
130 | table.appendChild(tableRow);
131 | return tableRow;
132 | }
133 | return existingRow;
134 | }
135 |
136 | createColumnIfNotExist(
137 | row: HTMLTableRowElement,
138 | rowIndex: number,
139 | columnIndex: number
140 | ): HTMLTableCellElement {
141 | const existingCell = this.getColumnElement(row, columnIndex, rowIndex);
142 | if (!existingCell) {
143 | const tableCell = document.createElement("td");
144 | tableCell.setAttribute("columnIndex", `${columnIndex}`);
145 | tableCell.setAttribute("rowIndex", `${rowIndex}`);
146 | tableCell.className = "cell";
147 | row.appendChild(tableCell);
148 | return tableCell;
149 | }
150 | return existingCell;
151 | }
152 |
153 | getTableElement(): HTMLTableElement {
154 | return document.getElementById(TABLE_ID) as HTMLTableElement;
155 | }
156 |
157 | getCell(columnIndex: number, rowIndex: number): HTMLTableCellElement | null {
158 | return document.querySelector(
159 | `td[rowIndex="${rowIndex}"][columnIndex="${columnIndex}"]`
160 | );
161 | }
162 |
163 | getRowElement(
164 | table: HTMLTableElement,
165 | rowIndex: number
166 | ): HTMLTableRowElement | null {
167 | return table.querySelector(
168 | `tr[rowIndex="${rowIndex}"]`
169 | ) as HTMLTableRowElement | null;
170 | }
171 |
172 | getColumnElement(
173 | row: HTMLTableRowElement,
174 | columnIndex: number,
175 | rowIndex: number,
176 | ): HTMLTableCellElement | null {
177 | return row.querySelector(
178 | `td[rowIndex="${rowIndex}"][columnIndex="${columnIndex}"]`
179 | ) as HTMLTableCellElement | null;
180 | }
181 |
182 | getRenderedRows(table: HTMLTableElement): HTMLTableRowElement[] {
183 | return [...table.querySelectorAll("tr")];
184 | }
185 |
186 | getRenderedColumns(table: HTMLTableRowElement): HTMLTableCellElement[] {
187 | return [...table.querySelectorAll("td")];
188 | }
189 |
190 | highlightElement(element: Element) {
191 | element.classList.add('cell--highlighted');
192 | setTimeout(() => element.classList.remove('cell--highlighted'), 200);
193 | }
194 |
195 | stepForward(element: Element, type: AlgorithmWorkerStepType) {
196 | this.highlightElement(element);
197 | switch (type) {
198 | case AlgorithmWorkerStepType.VISIT: {
199 | return element.classList.add('cell--visited');
200 | }
201 | case AlgorithmWorkerStepType.DISCOVER: {
202 | return element.classList.add('cell--discovered');
203 | }
204 | case AlgorithmWorkerStepType.MARK_PATH: {
205 | return element.classList.add('cell--path');
206 | }
207 | }
208 | }
209 |
210 | stepBackward(element: Element, type: AlgorithmWorkerStepType) {
211 | this.highlightElement(element);
212 | switch (type) {
213 | case AlgorithmWorkerStepType.VISIT: {
214 | return element.classList.remove('cell--visited');
215 | }
216 | case AlgorithmWorkerStepType.DISCOVER: {
217 | return element.classList.remove('cell--discovered');
218 | }
219 | case AlgorithmWorkerStepType.MARK_PATH: {
220 | return element.classList.remove('cell--path');
221 | }
222 | }
223 | }
224 |
225 | createGridPaintMove(element: Element, type: AlgorithmWorkerStepType, direction: PlaybackDirection) {
226 | switch (direction) {
227 | case PlaybackDirection.FORWARD:
228 | return this.stepForward(element, type);
229 | case PlaybackDirection.BACKWARD:
230 | return this.stepBackward(element, type);
231 | }
232 | }
233 |
234 | private queueForwardSteps(nrOfSteps = STEP_SIZE) {
235 | const start = get(currentStep);
236 | for (let i = start; i < start + nrOfSteps; i++) {
237 | setTimeout(() => get(steps)[i](PlaybackDirection.FORWARD), 0);
238 | }
239 | }
240 |
241 | private queueBackwardsSteps(nrOfSteps = STEP_SIZE) {
242 | const start = get(currentStep);
243 | for (let i = start; i > start + nrOfSteps; i--) {
244 | setTimeout(() => {
245 | if (!get(steps)[i]) debugger;
246 | get(steps)[i](PlaybackDirection.BACKWARD);
247 | }, 0);
248 | }
249 | }
250 |
251 | public skipBackward = () => {
252 | const nrOfSteps = clamp(-STEP_SIZE, -get(currentStep), 0)
253 | this.queueBackwardsSteps(nrOfSteps);
254 | currentStep.update(value => value + nrOfSteps);
255 | };
256 |
257 | public skipForward = () => {
258 | const nrOfSteps = clamp(STEP_SIZE, 0, (get(steps).length - 1) - get(currentStep))
259 | this.queueForwardSteps(nrOfSteps);
260 | currentStep.update(value => value + nrOfSteps);
261 | };
262 |
263 | onManualLoaderChange = (e: CustomEvent) => {
264 | removeInterval();
265 | const value = parseInt(e.detail.target.value, 10);
266 | const nrOfSteps = value - get(currentStep);
267 | const direction = nrOfSteps > 0 ? PlaybackDirection.FORWARD : PlaybackDirection.BACKWARD;
268 | if (direction === PlaybackDirection.FORWARD) this.queueForwardSteps(nrOfSteps)
269 | else if (direction === PlaybackDirection.BACKWARD) this.queueBackwardsSteps(nrOfSteps);
270 | currentStep.set(value);
271 | };
272 |
273 | public stopPlaying (){
274 | const _interval = get(interval);
275 | if (_interval === null) return;
276 | clearInterval(_interval);
277 | interval.set(null);
278 | };
279 |
280 | public startPlaying() {
281 | interval.set(window.setInterval(() => {
282 | if (get(currentStep) >= get(steps).length - 1) {
283 | removeInterval();
284 | return;
285 | }
286 | const stepToExecute = get(steps)[get(currentStep)];
287 | currentStep.update(value => value + 1);
288 | stepToExecute(PlaybackDirection.FORWARD);
289 | }, 20));
290 | }
291 |
292 | public addVisualizationStep(step: AlgorithmStep) {
293 | const { location, type } = step;
294 | if (!location) return;
295 | const element = this.getCell(location[0], location[1]);
296 | if (!element) return;
297 | steps.update(s => [...s, direction => this.createGridPaintMove(element, type, direction)]);
298 | if (!get(interval)) this.startVisualizingSteps();
299 | }
300 |
301 | public resetVisualization() {
302 | steps.set([]);
303 | currentStep.set(0);
304 | const cells = this.container.querySelectorAll('td.cell--visited, td.cell--discovered, td.cell--path');
305 | [...cells].forEach(e => {
306 | e.classList.remove('cell--visited');
307 | e.classList.remove('cell--discovered');
308 | e.classList.remove('cell--path');
309 | });
310 | }
311 |
312 | public resetGrid = () => {
313 | this.resetVisualization();
314 | this.grid.reset();
315 | const cells = this.container.querySelectorAll('td.cell--wall');
316 | [...cells].forEach(e => {
317 | e.classList.remove('cell--wall');
318 | });
319 | }
320 |
321 | adjustStartCell(
322 | targetElement: HTMLTableCellElement,
323 | columnIndex: number,
324 | rowIndex: number
325 | ): void {
326 | if (!targetElement.classList.contains("cell--start")) return;
327 | const newLocation = this.grid.nearestFreeCell(columnIndex, rowIndex);
328 | if (!newLocation) return;
329 | const [newColumnIndex, newRowIndex] = newLocation;
330 | const newStartCell = this.getCell(newColumnIndex, newRowIndex);
331 | if (!newStartCell) return;
332 | this.renderStartAtCell(newStartCell);
333 | }
334 |
335 |
336 | adjustEndCell(
337 | targetElement: HTMLTableCellElement,
338 | columnIndex: number,
339 | rowIndex: number
340 | ): void {
341 | if (!targetElement.classList.contains("cell--end")) return;
342 | const newLocation = this.grid.nearestFreeCell(columnIndex, rowIndex);
343 | if (!newLocation) return;
344 | const [newColumnIndex, newRowIndex] = newLocation;
345 | const newEndCell = this.getCell(newColumnIndex, newRowIndex);
346 | if (!newEndCell) return;
347 | this.renderEndAtCell(newEndCell);
348 | }
349 |
350 | addWall(targetElement: HTMLTableCellElement) {
351 | if (get(steps).length > 0) {
352 | this.resetVisualization();
353 | }
354 | const columnIndex = parseInt(targetElement.getAttribute("columnIndex") as string);
355 | const rowIndex = parseInt(targetElement.getAttribute("rowIndex") as string);
356 | this.adjustStartCell(targetElement, columnIndex, rowIndex);
357 | this.adjustEndCell(targetElement, columnIndex, rowIndex);
358 | this.grid.addWall(columnIndex, rowIndex);
359 | targetElement.classList.add("cell--wall");
360 | }
361 |
362 | renderStartAtCell(targetElement: HTMLTableCellElement) {
363 | if (!this.grid.start) return;
364 | const startCell = this.getCell(
365 | this.grid.start.coordinates.x,
366 | this.grid.start.coordinates.y
367 | );
368 | startCell?.classList.remove("cell--start");
369 | const columnIndex = parseInt(targetElement.getAttribute("columnIndex") as string);
370 | const rowIndex = parseInt(targetElement.getAttribute("rowIndex") as string);
371 | this.grid.setStart(columnIndex, rowIndex);
372 | targetElement.classList.add("cell--start");
373 | if (get(steps).length > 0) this.resetVisualization();
374 | }
375 |
376 | renderEndAtCell(targetElement: HTMLTableCellElement) {
377 | if (!this.grid.end) return;
378 | const endCell = this.getCell(
379 | this.grid.end.coordinates.x,
380 | this.grid.end.coordinates.y
381 | );
382 | endCell?.classList.remove("cell--end");
383 | const columnIndex = parseInt(targetElement.getAttribute("columnIndex") as string);
384 | const rowIndex = parseInt(targetElement.getAttribute("rowIndex") as string);
385 | this.grid.setEnd(columnIndex, rowIndex);
386 | targetElement.classList.add("cell--end");
387 | if (get(steps).length > 0) this.resetVisualization();
388 | }
389 |
390 | toggleWall(targetElement: HTMLTableCellElement) {
391 | const columnIndex = parseInt(targetElement.getAttribute("columnIndex") as string);
392 | const rowIndex = parseInt(targetElement.getAttribute("rowIndex") as string);
393 | if (targetElement.classList.contains("cell--wall")) {
394 | this.grid.removeWall(columnIndex, rowIndex);
395 | targetElement.classList.remove("cell--wall");
396 | } else {
397 | this.grid.addWall(columnIndex, rowIndex);
398 | targetElement.classList.add("cell--wall");
399 | }
400 | }
401 |
402 | renderStarAndEndNodes() {
403 | if (!this.grid.start) return;
404 | const startCell = this.getCell(
405 | this.grid.start.coordinates.x,
406 | this.grid.start.coordinates.y
407 | );
408 | if (!this.grid.end) return;
409 | const endCell = this.getCell(
410 | this.grid.end.coordinates.x,
411 | this.grid.end.coordinates.y
412 | );
413 | startCell?.classList.add("cell--start");
414 | endCell?.classList.add("cell--end");
415 | }
416 |
417 | removeCellsOutOfViewPort(table: HTMLTableElement) {
418 | const existingRows = this.getRenderedRows(table);
419 | if (existingRows.length > this.grid.rows) {
420 | const rowsToRemove = existingRows.slice(this.grid.rows);
421 | rowsToRemove.forEach((r) => r.remove());
422 | }
423 | }
424 |
425 | renderNewCells(table: HTMLTableElement) {
426 | for (let i = 0; i < this.grid.rows; i++) {
427 | const tableRow = this.createRowIfNotExist(table, i);
428 | const existingColumns = this.getRenderedColumns(tableRow);
429 | if (existingColumns.length > this.grid.columns) {
430 | const columnsToRemove = existingColumns.slice(this.grid.columns);
431 | columnsToRemove.forEach((r) => r.remove());
432 | }
433 | for (let j = 0; j < this.grid.columns; j++) {
434 | this.createColumnIfNotExist(tableRow, i, j);
435 | }
436 | }
437 | }
438 |
439 | renderGrid() {
440 | const table = this.createTableIfNotExist();
441 | this.removeCellsOutOfViewPort(table);
442 | this.renderNewCells(table);
443 | }
444 | }
445 |
446 | export default Painter;
447 |
--------------------------------------------------------------------------------
/src/painter/index.ts:
--------------------------------------------------------------------------------
1 | import type { ArrayLikeCoordinates } from "./GridNode";
2 |
3 | export function debounce(func: any, wait: number, immediate?: false) {
4 | let timeout: number | null;
5 | return function () {
6 | // @ts-ignore
7 | const context: any = this;
8 | const args = arguments;
9 | const later = function () {
10 | timeout = null;
11 | if (!immediate) func.apply(context, args);
12 | };
13 | const callNow = immediate && !timeout;
14 | timeout !== null && clearTimeout(timeout);
15 | timeout = window.setTimeout(later, wait);
16 | if (callNow) func.apply(context, args);
17 | };
18 | }
19 |
20 | export function isLocationValid(
21 | location: ArrayLikeCoordinates,
22 | width: number,
23 | height: number
24 | ) {
25 | return !(
26 | location[0] < 0 ||
27 | location[0] > height - 1 ||
28 | location[1] < 0 ||
29 | location[1] > width - 1
30 | );
31 | }
32 |
33 | export function clamp(number: number, min: number, max: number): number {
34 | return Math.max(min, Math.min(number, max));
35 | }
36 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
61 |
62 |
63 |
64 |
77 |
78 |
--------------------------------------------------------------------------------
/src/scss/button.scss:
--------------------------------------------------------------------------------
1 | @import "./theme";
2 | /**
3 | * 1. Change the font styles in all browsers.
4 | * 2. Remove the margin in Firefox and Safari.
5 | */
6 |
7 | button,
8 | input,
9 | optgroup,
10 | select,
11 | textarea {
12 | font-family: inherit; /* 1 */
13 | font-size: 100%; /* 1 */
14 | line-height: 1.15; /* 1 */
15 | margin: 0; /* 2 */
16 | }
17 |
18 | /**
19 | * Show the overflow in IE.
20 | * 1. Show the overflow in Edge.
21 | */
22 |
23 | button,
24 | input { /* 1 */
25 | overflow: visible;
26 | }
27 |
28 | /**
29 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
30 | * 1. Remove the inheritance of text transform in Firefox.
31 | */
32 |
33 | button,
34 | select { /* 1 */
35 | text-transform: none;
36 | }
37 |
38 | /**
39 | * Correct the inability to style clickable types in iOS and Safari.
40 | */
41 |
42 | button,
43 | [type="button"],
44 | [type="reset"],
45 | [type="submit"] {
46 | -webkit-appearance: button;
47 | }
48 |
49 |
50 | %btn {
51 | position: relative;
52 | margin: 0;
53 | padding: 5px 12px;
54 | height: 60px;
55 | outline: none;
56 | text-decoration: none;
57 | display: flex;
58 | justify-content: center;
59 | align-items: center;
60 | cursor: pointer;
61 | text-transform: uppercase;
62 | background-color: transparent;
63 | border: none;
64 | font-weight: 400;
65 | font-size: 20px;
66 | font-family: inherit;
67 | z-index: 0;
68 | overflow: hidden;
69 | transition: all 0.2s cubic-bezier(0.02, 0.01, 0.47, 1);
70 | }
71 |
72 | .btn__icon {
73 | color: white;
74 | font-size: 30px;
75 | width: 40px;
76 | height: 40px;
77 | display: flex;
78 | align-items: center;
79 | justify-content: center;
80 | transition: all 0.2s cubic-bezier(0.02, 0.01, 0.47, 1);
81 | }
82 |
83 | .btn__text {
84 | color: white;
85 | font-size: 16px;
86 | position: relative;
87 | text-transform: uppercase;
88 | transition: all 0.2s cubic-bezier(0.02, 0.01, 0.47, 1);
89 | }
90 |
91 | .btn-primary {
92 | @extend %btn;
93 |
94 | &:hover {
95 | .btn__icon {
96 | color: $color-primary30;
97 | }
98 |
99 | .btn__text {
100 | color: $color-primary30;
101 | }
102 | }
103 | }
104 |
105 |
--------------------------------------------------------------------------------
/src/scss/global.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background: $color-neutral70;
3 | position: fixed;
4 | font-family: Source Sans Pro, sans-serif;
5 | top: 0;
6 | left: 0;
7 | padding: 0;
8 | margin: 0;
9 | right: 0;
10 | bottom: 0;
11 | display: flex;
12 | box-sizing: border-box;
13 | touch-action: manipulation;
14 | div {
15 | width: 100%;
16 | flex-grow: 0;
17 | display: flex;
18 | }
19 | }
20 |
21 | *, *:before, *:after {
22 | box-sizing: inherit;
23 | }
24 |
25 | .subtitle-1, .subtitle-2, .body-1, .body-2, .button, .caption,
26 | h1, h2, h3, h4, h5, h6 {
27 | color: $color-neutral5;
28 | margin: 0;
29 | }
30 |
31 | h1 {
32 | font-size: $font-size-h1;
33 | font-weight: normal;
34 | }
35 |
36 | h2 {
37 | font-size: $font-size-h2;
38 | font-weight: normal;
39 | }
40 |
41 | h3 {
42 | font-size: $font-size-h3;
43 | font-weight: 500;
44 | }
45 |
46 | h4 {
47 | font-size: $font-size-h4;
48 | font-weight: 500;
49 | }
50 |
51 | h5 {
52 | font-size: $font-size-h5;
53 | font-weight: normal;
54 | }
55 |
56 | h6 {
57 | font-size: $font-size-h6;
58 | font-weight: 500;
59 | }
60 |
61 | .subtitle-1 {
62 | font-size: $font-size-subtitle1;
63 | }
64 |
65 | .subtitle-2 {
66 | font-size: $font-size-subtitle2;
67 | }
68 |
69 | .body-1 {
70 | font-size: $font-size-body1;
71 | }
72 |
73 | .body-2 {
74 | font-size: $font-size-body2;
75 | }
76 |
77 | .button {
78 | font-size: $font-size-button;
79 | }
80 |
81 | .caption {
82 | font-size: $font-size-caption;
83 | }
84 |
--------------------------------------------------------------------------------
/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | @import './theme';
2 | @import './global';
3 | @import './button';
4 | @import './table';
5 |
6 |
--------------------------------------------------------------------------------
/src/scss/table.scss:
--------------------------------------------------------------------------------
1 | .cell {
2 | height: 20px;
3 | width: 20px;
4 | cursor: pointer;
5 | border: 1px solid $color-neutral10;
6 | transition: background-color 0.01s;
7 | position: relative;
8 | }
9 |
10 |
11 | .cell--wall {
12 | background-color: $color-neutral40;
13 | }
14 |
15 | .cell--start {
16 | cursor: grab;
17 | background-size: 15px 15px;
18 | background-repeat: no-repeat;
19 | background-position: center;
20 | background-image: url("/play-circle-solid.svg");
21 | }
22 |
23 | .cell--end {
24 | cursor: grab;
25 | background-size: 15px 15px;
26 | background-repeat: no-repeat;
27 | background-position: center;
28 | background-image: url("/dot-circle-solid.svg");
29 | }
30 |
31 | .cell--discovered {
32 | background-color: $color-primary20;
33 | }
34 |
35 | .cell--visited {
36 | background-color: $color-primary40;
37 | }
38 |
39 | .cell--path {
40 | background-color: $color-secondary50;
41 | }
42 |
43 | .cell--highlighted {
44 | border: 2px solid blue;
45 | }
46 | .cell--visited {
47 | &.gradient-0 {
48 | background-color: #0067E5;
49 | }
50 |
51 | &.gradient-10 {
52 | background-color: #0B6FDC;
53 | }
54 |
55 | &.gradient-20 {
56 | background-color: #1677D3;
57 | }
58 |
59 | &.gradient-30 {
60 | background-color: #2180CA;
61 | }
62 |
63 | &.gradient-40 {
64 | background-color: #2C88C1;
65 | }
66 |
67 | &.gradient-50 {
68 | background-color: #3890B8;
69 | }
70 |
71 | &.gradient-60 {
72 | background-color: #4399AF;
73 | }
74 |
75 | &.gradient-70 {
76 | background-color: #4EA1A6;
77 | }
78 |
79 | &.gradient-80 {
80 | background-color: #59A99D;
81 | }
82 |
83 | &.gradient-90 {
84 | background-color: #65B294;
85 | }
86 | }
87 |
88 |
89 | .table {
90 | border-spacing: 0;
91 | border-collapse: unset;
92 | width: 100%;
93 | height: calc(100% - 240px);
94 | user-select: none;
95 | td:not(.cell--start):not(.cell--end):hover.cell:after {
96 | content: "";
97 | height: 14px;
98 | width: 14px;
99 | display: block;
100 | background-size: 15px 15px;
101 | background-repeat: no-repeat;
102 | background-image: url("/square-full-solid.svg");
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/scss/theme.scss:
--------------------------------------------------------------------------------
1 | $color-primary10: #C4D3CE;
2 | $color-primary20: #65B294;
3 | $color-primary30: #3E906B;
4 | $color-primary40: #185032;
5 | $color-secondary50: #C6755F;
6 | $color-neutral5: #E9EBF0;
7 | $color-neutral10: #C5C7CB;
8 | $color-neutral20: #A0A2A6;
9 | $color-neutral30: #7C7E81;
10 | $color-neutral40: #5F6163;
11 | $color-neutral50: #424445;
12 | $color-neutral60: #242627;
13 | $color-neutral70: #070909;
14 | $font-size-h1: 96px;
15 | $font-size-h2: 72px;
16 | $font-size-h3: 48px;
17 | $font-size-h4: 34px;
18 | $font-size-h5: 24px;
19 | $font-size-h6: 20px;
20 | $font-size-subtitle1: 16px;
21 | $font-size-subtitle2: 14px;
22 | $font-size-body1: 16px;
23 | $font-size-body2: 14px;
24 | $font-size-button: 14px;
25 | $font-size-caption: 12px;
26 | $breakpoint-large: 1264px;
27 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import {GridPaintStroke, Heuristics} from "./constants/types";
2 | import {get, Writable, writable} from 'svelte/store';
3 |
4 |
5 | const heuristics = writable<{ type: Heuristics, formula?: string}>({
6 | type: Heuristics.EUCLIDEAN,
7 | });
8 |
9 | const steps: Writable> = writable([]);
10 | const currentStep = writable(0);
11 | const interval = writable(null);
12 |
13 | function removeInterval() {
14 | const currentInterval = get(interval);
15 | currentInterval && clearInterval(currentInterval);
16 | interval.set(null);
17 | }
18 |
19 |
20 | export {
21 | heuristics,
22 | steps,
23 | currentStep,
24 | interval,
25 | removeInterval,
26 | }
27 |
--------------------------------------------------------------------------------
/src/worker/AlgorithmMincerWorker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import Grid, {setHeuristicsFunction} from "../painter/Grid";
3 | import {
4 | AlgorithmInfoMessage,
5 | AlgorithmStep,
6 | AlgorithmWorkerStepType,
7 | GridConstructorData,
8 | Heuristics,
9 | HeuristicsData,
10 | MessageType,
11 | WorkerGridTransferData,
12 | } from "../constants/types";
13 | import GridNode, {GridCoordinates} from "../painter/GridNode";
14 |
15 | const ctx: Worker = self as any;
16 |
17 | function processNeighbours(
18 | node: GridNode,
19 | neighbours: GridNode[],
20 | open: GridNode[],
21 | end: GridNode,
22 | grid: Grid
23 | ) {
24 | neighbours.forEach((neighbour) => {
25 | neighbour.setParameters(end, node);
26 | const existingNode = open.find((n) => n.id === neighbour.id);
27 | if (existingNode && neighbour.g > existingNode.g) {
28 | sendAlgorithmInfoMessage(
29 | createInfoStep(
30 | neighbour.id,
31 | `This node was previously discovered and alternative path to it proves to be better`
32 | )
33 | );
34 | return;
35 | } else if (existingNode) {
36 | sendAlgorithmInfoMessage(
37 | createInfoStep(
38 | neighbour.id,
39 | `This node was previously discovered and but better path was found`
40 | )
41 | );
42 | open.splice(open.indexOf(existingNode), 1);
43 | }
44 | sendAlgorithmInfoMessage(
45 | createInfoStep(
46 | neighbour.id,
47 | `Discovered a new node and sending it to open stack`,
48 | [neighbour.g, neighbour.h],
49 | node.id
50 | )
51 | );
52 | grid.discover(neighbour);
53 | sendAlgorithmStep(createDiscoverStep(neighbour));
54 | open.push(neighbour);
55 | });
56 | }
57 |
58 | function markPath(endNode: GridNode) {
59 | let backtrackNode = endNode;
60 | while (backtrackNode.parent != undefined) {
61 | sendAlgorithmStep(createMarkPathStep(backtrackNode));
62 | backtrackNode = backtrackNode.parent;
63 | }
64 | sendAlgorithmStep(createMarkPathStep(backtrackNode));
65 | }
66 |
67 | function sendAlgorithmStep(step: AlgorithmStep) {
68 | ctx.postMessage([MessageType.ALGORITHM_STEP, step]);
69 | }
70 |
71 | function sendAlgorithmInfoMessage(step: AlgorithmInfoMessage) {
72 | ctx.postMessage([MessageType.INFO_DATA, step]);
73 | }
74 |
75 | function createStartStep() {
76 | return {
77 | type: AlgorithmWorkerStepType.START,
78 | };
79 | }
80 |
81 | function createInfoStep(
82 | tileId: string,
83 | info: string,
84 | ghValues?: [number, number],
85 | parent?: string
86 | ): AlgorithmInfoMessage {
87 | return {
88 | type: AlgorithmWorkerStepType.INFO,
89 | info,
90 | ghValues,
91 | tileId,
92 | parent,
93 | };
94 | }
95 |
96 | function createMarkPathStep(node: GridNode): AlgorithmStep {
97 | return {
98 | type: AlgorithmWorkerStepType.MARK_PATH,
99 | location: node.toArray(),
100 | };
101 | }
102 |
103 | function createEndStep(node: GridNode): AlgorithmStep {
104 | return {
105 | type: AlgorithmWorkerStepType.END,
106 | location: node.toArray(),
107 | };
108 | }
109 |
110 | function createVisitStep(
111 | node: GridNode,
112 | neighbours: [number, number][]
113 | ): AlgorithmStep {
114 | return {
115 | type: AlgorithmWorkerStepType.VISIT,
116 | neighbours,
117 | location: node.toArray(),
118 | };
119 | }
120 |
121 | function createDiscoverStep(node: GridNode): AlgorithmStep {
122 | return {
123 | type: AlgorithmWorkerStepType.DISCOVER,
124 | location: node.toArray(),
125 | };
126 | }
127 |
128 | function sortOpenNodes(open: GridNode[]) {
129 | open.sort((a, b) => {
130 | const costDiff = b.totalCost - a.totalCost;
131 | if (costDiff === 0) {
132 | return b.hCost - a.hCost;
133 | }
134 | return costDiff;
135 | });
136 | }
137 |
138 | function process(grid: Grid) {
139 | if (!grid.start || !grid.end) return;
140 | const startNode = grid.start;
141 | const endNode = grid.end;
142 | const open = [startNode];
143 | sendAlgorithmStep(createStartStep());
144 | while (open.length > 0) {
145 | const currentNode: GridNode | null | undefined = open.pop();
146 | if (!currentNode) return;
147 | grid.visit(currentNode);
148 | if (grid.isEnd(currentNode)) {
149 | sendAlgorithmStep(createEndStep(currentNode));
150 | markPath(currentNode);
151 | return;
152 | }
153 | const neighbours: GridNode[] = [
154 | ...grid.getWalkableNeighbours(currentNode).values(),
155 | ];
156 | sendAlgorithmStep(
157 | createVisitStep(
158 | currentNode,
159 | neighbours.map((n) => n.toArray())
160 | )
161 | );
162 |
163 | processNeighbours(currentNode, neighbours, open, endNode, grid);
164 | sortOpenNodes(open);
165 | }
166 | }
167 |
168 | function parseWorkerGridTransferData(
169 | gridData: WorkerGridTransferData
170 | ): GridConstructorData {
171 | return {
172 | start: new GridCoordinates(gridData.start[0], gridData.start[1]),
173 | end: new GridCoordinates(gridData.end[0], gridData.end[1]),
174 | walls: gridData.walls.map((w) => new GridCoordinates(w[0], w[1])),
175 | columns: gridData.columns,
176 | rows: gridData.rows,
177 | heuristics: gridData.heuristics,
178 | };
179 | }
180 |
181 | let grid: Grid;
182 | onmessage = function (e) {
183 | switch (e.data[0]) {
184 | case MessageType.GRID_DATA: {
185 | const gridData = e.data[1];
186 | grid = new Grid(parseWorkerGridTransferData(gridData));
187 | setHeuristicsFunction(gridData.heuristics);
188 | process(grid);
189 | break;
190 | }
191 | case MessageType.SET_HEURISTICS: {
192 | const message = e.data[1];
193 | setHeuristicsFunction(message);
194 | break;
195 | }
196 | }
197 | };
198 |
199 | export {}
200 |
--------------------------------------------------------------------------------
/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssaric/algoviz/21fcd4910102ccc6b2e7aa7edbcbd9af2ef48c91/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssaric/algoviz/21fcd4910102ccc6b2e7aa7edbcbd9af2ef48c91/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssaric/algoviz/21fcd4910102ccc6b2e7aa7edbcbd9af2ef48c91/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/static/dot-circle-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssaric/algoviz/21fcd4910102ccc6b2e7aa7edbcbd9af2ef48c91/static/favicon.png
--------------------------------------------------------------------------------
/static/icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssaric/algoviz/21fcd4910102ccc6b2e7aa7edbcbd9af2ef48c91/static/icon-48x48.png
--------------------------------------------------------------------------------
/static/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/static/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssaric/algoviz/21fcd4910102ccc6b2e7aa7edbcbd9af2ef48c91/static/mstile-150x150.png
--------------------------------------------------------------------------------
/static/play-circle-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
18 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/static/square-full-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-static';
2 | import preprocess from "svelte-preprocess";
3 | import tailwind from "tailwindcss";
4 | import autoprefixer from "autoprefixer";
5 | const dev = process.env.NODE_ENV === 'development';
6 | /** @type {import('@sveltejs/kit').Config} */
7 | const config = {
8 | // Consult https://github.com/sveltejs/svelte-preprocess
9 | // for more information about preprocessors
10 | preprocess: [
11 | preprocess({
12 | sourceMap: !dev,
13 | postcss: {
14 | plugins: [tailwind(), autoprefixer()],
15 | },
16 | }),
17 | ],
18 | kit: {
19 | prerender: {
20 | enabled: true
21 | },
22 | adapter: adapter({
23 | pages: 'build',
24 | assets: 'build',
25 | fallback: 'index.html',
26 | trailingSlash: 'always',
27 | precompress: true,
28 | }),
29 | },
30 | };
31 |
32 | export default config;
33 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | content: [
3 | "./src/**/*.{html,js,svelte,ts}",
4 | "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
5 | ],
6 |
7 | theme: {
8 | extend: {},
9 | },
10 |
11 | plugins: [require("flowbite/plugin")],
12 | darkMode: "class",
13 | };
14 |
15 | module.exports = config;
16 |
--------------------------------------------------------------------------------
/tests/test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test('index page has expected h1', async ({ page }) => {
4 | await page.goto('/');
5 | expect(await page.textContent('h1')).toBe('Welcome to SvelteKit');
6 | });
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "preserveValueImports": false,
11 | "isolatedModules": false,
12 | "sourceMap": true,
13 | "strict": true
14 | },
15 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
16 | //
17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18 | // from the referenced tsconfig.json - TypeScript does not merge them in
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 |
3 | /** @type {import('vite').UserConfig} */
4 | const config = {
5 | plugins: [sveltekit()]
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------