├── .github └── FUNDING.yml ├── .gitignore ├── .gitpod.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── branding ├── icon.png ├── icon.psd ├── logo.png ├── logo.psd └── screenshot.png ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── icons │ ├── icon-192x192.png │ ├── icon-32x32.png │ └── icon-512x512.png ├── index.html ├── manifest.json └── robots.txt └── src ├── apis └── index.js ├── common ├── config.js ├── stylesheet │ ├── colors.scss │ ├── dimensions.scss │ ├── fonts.scss │ └── index.scss └── util.js ├── components ├── App │ ├── App.module.scss │ └── index.js ├── BaseComponent │ └── index.js ├── Button │ ├── Button.module.scss │ └── index.js ├── CodeEditor │ ├── CodeEditor.module.scss │ └── index.js ├── Divider │ ├── Divider.module.scss │ └── index.js ├── Ellipsis │ ├── Ellipsis.module.scss │ └── index.js ├── ExpandableListItem │ ├── ExpandableListItem.module.scss │ └── index.js ├── FoldableAceEditor │ └── index.js ├── Header │ ├── Header.module.scss │ └── index.js ├── ListItem │ ├── ListItem.module.scss │ └── index.js ├── Navigator │ ├── Navigator.module.scss │ └── index.js ├── Player │ ├── Player.module.scss │ └── index.js ├── ProgressBar │ ├── ProgressBar.module.scss │ └── index.js ├── ResizableContainer │ ├── ResizableContainer.module.scss │ └── index.js ├── TabContainer │ ├── TabContainer.module.scss │ └── index.js ├── ToastContainer │ ├── ToastContainer.module.scss │ └── index.js ├── VisualizationViewer │ ├── VisualizationViewer.module.scss │ └── index.js └── index.js ├── core ├── layouts │ ├── HorizontalLayout.js │ ├── Layout.js │ ├── VerticalLayout.js │ └── index.js ├── renderers │ ├── Array1DRenderer │ │ ├── Array1DRenderer.module.scss │ │ └── index.js │ ├── Array2DRenderer │ │ ├── Array2DRenderer.module.scss │ │ └── index.js │ ├── ChartRenderer │ │ ├── ChartRenderer.module.scss │ │ └── index.js │ ├── GraphRenderer │ │ ├── GraphRenderer.module.scss │ │ └── index.js │ ├── LogRenderer │ │ ├── LogRenderer.module.scss │ │ └── index.js │ ├── MarkdownRenderer │ │ ├── MarkdownRenderer.module.scss │ │ └── index.js │ ├── Renderer │ │ ├── Renderer.module.scss │ │ └── index.js │ └── index.js └── tracers │ ├── Array1DTracer.js │ ├── Array2DTracer.js │ ├── ChartTracer.js │ ├── GraphTracer.js │ ├── LogTracer.js │ ├── MarkdownTracer.js │ ├── Tracer.jsx │ └── index.js ├── files ├── algorithm-visualizer │ └── README.md ├── index.js ├── scratch-paper │ └── CONTRIBUTING.md └── skeletons │ ├── code.cpp │ ├── code.java │ └── code.js ├── index.js ├── reducers ├── current.js ├── directory.js ├── env.js ├── index.js ├── player.js └── toast.js └── stylesheet.scss /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=CFS8Y6G3E29UA 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # WebStorm settings 2 | /.idea 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: > 3 | git clone https://github.com/algorithm-visualizer/server.git && 4 | cd server && 5 | npm install && 6 | echo -e "GITHUB_CLIENT_ID=dummy\nGITHUB_CLIENT_SECRET=dummy\nAWS_ACCESS_KEY_ID=dummy\nAWS_SECRET_ACCESS_KEY=dummy" > .env.local && 7 | cd .. 8 | command: cd server && npm run watch 9 | - init: > 10 | npm install && 11 | echo 'DANGEROUSLY_DISABLE_HOST_CHECK=true' > .env.local 12 | command: npm start 13 | ports: 14 | - port: 3000 15 | onOpen: notify 16 | - port: 8080 17 | onOpen: ignore 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at parkjs814@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > #### Table of Contents 4 | > - [Running Locally](#running-locally) 5 | > - [Running in Gitpod](#running-in-gitpod) 6 | > - [Directory Structure](#directory-structure) 7 | 8 | Are you a first-timer in contributing to open source? [These guidelines](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution) from GitHub might help! 9 | 10 | ## Running Locally 11 | 12 | 1. Fork this repository. 13 | 14 | 2. Clone your forked repo to your machine. 15 | 16 | ```bash 17 | git clone https://github.com//algorithm-visualizer.git 18 | ``` 19 | 20 | 3. Choose whether to run [`server`](https://github.com/algorithm-visualizer/server) on your machine or to use the remote server. 21 | - If you choose to run the server locally as well, follow the instructions [here](https://github.com/algorithm-visualizer/server/blob/master/CONTRIBUTING.md#running-locally). 22 | 23 | - If you choose to use the remote server, **temporarily** (i.e., don't commit this change) modify `package.json` as follows: 24 | ```diff 25 | - "proxy": "http://localhost:8080", 26 | + "proxy": "https://algorithm-visualizer.org", 27 | ``` 28 | 29 | 4. Install dependencies, and run the web app. 30 | 31 | ```bash 32 | cd algorithm-visualizer 33 | 34 | npm install 35 | 36 | npm start 37 | ``` 38 | 39 | 5. Open [`http://localhost:3000/`](http://localhost:3000/) in a web browser. 40 | 41 | ## Running in Gitpod 42 | 43 | You can also run `algorithm-visualizer` in Gitpod, a free online dev environment for GitHub. 44 | 45 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/algorithm-visualizer/algorithm-visualizer) 46 | 47 | ## Directory Structure 48 | 49 | - [**branding/**](branding) contains representative image files. 50 | - [**public/**](public) contains static files to be served. 51 | - [**src/**](src) contains source code. 52 | - [**apis/**](src/apis) defines outgoing API requests. 53 | - [**common/**](src/common) contains commonly used files. 54 | - [**components/**](src/components) contains UI components. 55 | - [**core/**](src/core) processes visualization. 56 | - [**layouts/**](src/core/layouts) layout tracers. 57 | - [**renderers/**](src/core/renderers) renders visualization data. 58 | - [**tracers/**](src/core/tracers) interprets visualizing commands into visualization data. 59 | - [**files/**](src/files) contains markdown or skeleton files to be shown in the code editor. 60 | - [**reducers/**](src/reducers) contains Redux reducers. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jinseo Jason Park 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Algorithm Visualizer 2 | 3 | > Algorithm Visualizer is an interactive online platform that visualizes algorithms from code. 4 | 5 | [![GitHub contributors](https://img.shields.io/github/contributors/algorithm-visualizer/algorithm-visualizer.svg?style=flat-square)](https://github.com/algorithm-visualizer/algorithm-visualizer/graphs/contributors) 6 | [![GitHub license](https://img.shields.io/github/license/algorithm-visualizer/algorithm-visualizer.svg?style=flat-square)](https://github.com/algorithm-visualizer/algorithm-visualizer/blob/master/LICENSE) 7 | 8 | Learning an algorithm gets much easier with visualizing it. Don't get what we mean? Check it out: 9 | 10 | [**algorithm-visualizer.org**![Screenshot](https://raw.githubusercontent.com/algorithm-visualizer/algorithm-visualizer/master/branding/screenshot.png)](https://algorithm-visualizer.org/) 11 | 12 | ## Contributing 13 | 14 | We have multiple repositories under the hood that comprise the website. Take a look at the contributing guidelines in the repository you want to contribute to. 15 | 16 | - [**`algorithm-visualizer`**](https://github.com/algorithm-visualizer/algorithm-visualizer) is a web app written in React. It contains UI components and interprets commands into visualizations. Check out [the contributing guidelines](CONTRIBUTING.md). 17 | 18 | - [**`server`**](https://github.com/algorithm-visualizer/server) serves the web app and provides APIs that it needs on the fly. (e.g., GitHub sign in, compiling/running code, etc.) 19 | 20 | - [**`algorithms`**](https://github.com/algorithm-visualizer/algorithms) contains visualizations of algorithms shown on the side menu of the website. 21 | 22 | - [**`tracers.*`**](https://github.com/search?q=topic%3Avisualization-library+org%3Aalgorithm-visualizer&type=Repositories) are visualization libraries written in each supported language. They extract visualizing commands from code. 23 | -------------------------------------------------------------------------------- /branding/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/branding/icon.png -------------------------------------------------------------------------------- /branding/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/branding/icon.psd -------------------------------------------------------------------------------- /branding/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/branding/logo.png -------------------------------------------------------------------------------- /branding/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/branding/logo.psd -------------------------------------------------------------------------------- /branding/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/branding/screenshot.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@algorithm-visualizer/algorithm-visualizer", 3 | "version": "2.0.0", 4 | "title": "Algorithm Visualizer", 5 | "description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.", 6 | "scripts": { 7 | "start": "react-scripts start", 8 | "build": "react-scripts build", 9 | "test": "react-scripts test", 10 | "eject": "react-scripts eject" 11 | }, 12 | "eslintConfig": { 13 | "extends": "react-app" 14 | }, 15 | "browserslist": { 16 | "production": [ 17 | ">0.2%", 18 | "not dead", 19 | "not op_mini all" 20 | ], 21 | "development": [ 22 | "last 1 chrome version", 23 | "last 1 firefox version", 24 | "last 1 safari version" 25 | ] 26 | }, 27 | "proxy": "http://localhost:8080", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@fortawesome/fontawesome": "^1.1.8", 31 | "@fortawesome/fontawesome-free-brands": "^5.0.13", 32 | "@fortawesome/fontawesome-free-solid": "^5.0.13", 33 | "@fortawesome/fontawesome-svg-core": "^1.2.19", 34 | "@fortawesome/react-fontawesome": "0.1.4", 35 | "axios": "^0.19.0", 36 | "bluebird": "latest", 37 | "brace": "latest", 38 | "chart.js": "^2.8.0", 39 | "js-cookie": "^2.2.0", 40 | "node-sass": "^4.12.0", 41 | "query-string": "^6.7.0", 42 | "raw-loader": "^3.0.0", 43 | "react": "^16.8.6", 44 | "react-ace": "^7.0.2", 45 | "react-chartjs-2": "^2.7.6", 46 | "react-dom": "^16.8.6", 47 | "react-helmet": "^5.2.1", 48 | "react-input-autosize": "^2.2.1", 49 | "react-input-range": "^1.3.0", 50 | "react-markdown": "^4.0.8", 51 | "react-redux": "^7.0.3", 52 | "react-router": "^5.0.1", 53 | "react-router-dom": "^5.0.1", 54 | "react-router-redux": "^4.0.8", 55 | "react-scripts": "^3.0.1", 56 | "react-toastify": "^5.2.1", 57 | "redbox-react": "^1.6.0", 58 | "redux": "^4.0.1", 59 | "redux-actions": "^2.6.5", 60 | "remove-markdown": "^0.3.0", 61 | "screenfull": "^4.2.0", 62 | "sprintf-js": "^1.1.2", 63 | "uuid": "^3.3.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- 1 | icons/icon-32x32.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/public/icons/icon-32x32.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagnikghoshcr7/algorithm-visualizer/f84321cc0729578e5167e8f800fbebaa9f7285c1/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | $TITLE 27 | 28 | 29 | 30 |
31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Algorithm Visualizer", 3 | "short_name": "Algorithm Visualizer", 4 | "start_url": "/?utm_source=homescreen", 5 | "display": "fullscreen", 6 | "orientation": "landscape", 7 | "theme_color": "#393939", 8 | "background_color": "#393939", 9 | "description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.", 10 | "icons": [ 11 | { 12 | "src": "icons/icon-32x32.png", 13 | "sizes": "32x32", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "icons/icon-192x192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "icons/icon-512x512.png", 23 | "sizes": "512x512", 24 | "type": "image/png" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /scratch-paper/ 3 | -------------------------------------------------------------------------------- /src/apis/index.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import axios from 'axios'; 3 | 4 | axios.interceptors.response.use(response => response.data); 5 | 6 | const request = (url, process) => { 7 | const tokens = url.split('/'); 8 | const baseURL = /^https?:\/\//i.test(url) ? '' : '/api'; 9 | return (...args) => { 10 | const mappedURL = baseURL + tokens.map((token, i) => token.startsWith(':') ? args.shift() : token).join('/'); 11 | return Promise.resolve(process(mappedURL, args)); 12 | }; 13 | }; 14 | 15 | const GET = URL => { 16 | return request(URL, (mappedURL, args) => { 17 | const [params, cancelToken] = args; 18 | return axios.get(mappedURL, { params, cancelToken }); 19 | }); 20 | }; 21 | 22 | const DELETE = URL => { 23 | return request(URL, (mappedURL, args) => { 24 | const [params, cancelToken] = args; 25 | return axios.delete(mappedURL, { params, cancelToken }); 26 | }); 27 | }; 28 | 29 | const POST = URL => { 30 | return request(URL, (mappedURL, args) => { 31 | const [body, params, cancelToken] = args; 32 | return axios.post(mappedURL, body, { params, cancelToken }); 33 | }); 34 | }; 35 | 36 | const PUT = URL => { 37 | return request(URL, (mappedURL, args) => { 38 | const [body, params, cancelToken] = args; 39 | return axios.put(mappedURL, body, { params, cancelToken }); 40 | }); 41 | }; 42 | 43 | const PATCH = URL => { 44 | return request(URL, (mappedURL, args) => { 45 | const [body, params, cancelToken] = args; 46 | return axios.patch(mappedURL, body, { params, cancelToken }); 47 | }); 48 | }; 49 | 50 | const AlgorithmApi = { 51 | getCategories: GET('/algorithms'), 52 | getAlgorithm: GET('/algorithms/:categoryKey/:algorithmKey'), 53 | }; 54 | 55 | const VisualizationApi = { 56 | getVisualization: GET('/visualizations/:visualizationId'), 57 | }; 58 | 59 | const GitHubApi = { 60 | auth: token => Promise.resolve(axios.defaults.headers.common['Authorization'] = token && `token ${token}`), 61 | getUser: GET('https://api.github.com/user'), 62 | listGists: GET('https://api.github.com/gists'), 63 | createGist: POST('https://api.github.com/gists'), 64 | editGist: PATCH('https://api.github.com/gists/:id'), 65 | getGist: GET('https://api.github.com/gists/:id'), 66 | deleteGist: DELETE('https://api.github.com/gists/:id'), 67 | forkGist: POST('https://api.github.com/gists/:id/forks'), 68 | }; 69 | 70 | const TracerApi = { 71 | md: ({ code }) => Promise.resolve([{ 72 | key: 'markdown', 73 | method: 'MarkdownTracer', 74 | args: ['Markdown'], 75 | }, { 76 | key: 'markdown', 77 | method: 'set', 78 | args: [code], 79 | }, { 80 | key: null, 81 | method: 'setRoot', 82 | args: ['markdown'], 83 | }]), 84 | json: ({ code }) => new Promise(resolve => resolve(JSON.parse(code))), 85 | js: ({ code }, params, cancelToken) => new Promise((resolve, reject) => { 86 | const worker = new Worker('/api/tracers/js/worker'); 87 | if (cancelToken) { 88 | cancelToken.promise.then(cancel => { 89 | worker.terminate(); 90 | reject(cancel); 91 | }); 92 | } 93 | worker.onmessage = e => { 94 | worker.terminate(); 95 | resolve(e.data); 96 | }; 97 | worker.onerror = error => { 98 | worker.terminate(); 99 | reject(error); 100 | }; 101 | worker.postMessage(code); 102 | }), 103 | cpp: POST('/tracers/cpp'), 104 | java: POST('/tracers/java'), 105 | }; 106 | 107 | export { 108 | AlgorithmApi, 109 | VisualizationApi, 110 | GitHubApi, 111 | TracerApi, 112 | }; 113 | -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | import { CODE_CPP, CODE_JAVA, CODE_JS } from 'files'; 2 | 3 | const languages = [{ 4 | name: 'JavaScript', 5 | ext: 'js', 6 | mode: 'javascript', 7 | skeleton: CODE_JS, 8 | }, { 9 | name: 'C++', 10 | ext: 'cpp', 11 | mode: 'c_cpp', 12 | skeleton: CODE_CPP, 13 | }, { 14 | name: 'Java', 15 | ext: 'java', 16 | mode: 'java', 17 | skeleton: CODE_JAVA, 18 | }]; 19 | 20 | const exts = languages.map(language => language.ext); 21 | 22 | export { 23 | languages, 24 | exts, 25 | }; 26 | -------------------------------------------------------------------------------- /src/common/stylesheet/colors.scss: -------------------------------------------------------------------------------- 1 | $theme-dark: #242424; 2 | $theme-normal: #393939; 3 | $theme-light: #505050; 4 | $color-font: #bbbbbb; 5 | $color-shadow: rgba(#000000, .2); 6 | $color-overlay: rgba(#ffffff, .1); 7 | $color-alert: #f3bd58; 8 | $color-selected: #2962ff; 9 | $color-patched: #c51162; 10 | $color-highlight: #29d; 11 | $color-active: #00e676; 12 | 13 | :export { 14 | themeDark: $theme-dark; 15 | themeNormal: $theme-normal; 16 | themeLight: $theme-light; 17 | colorFont: $color-font; 18 | colorShadow: $color-shadow; 19 | colorOverlay: $color-overlay; 20 | colorAlert: $color-alert; 21 | colorSelected: $color-selected; 22 | colorPatched: $color-patched; 23 | colorHighlight: $color-highlight; 24 | colorActive: $color-active; 25 | } 26 | -------------------------------------------------------------------------------- /src/common/stylesheet/dimensions.scss: -------------------------------------------------------------------------------- 1 | $line-height: 32px; 2 | $font-size-normal: 12px; 3 | $font-size-large: 14px; 4 | 5 | :export { 6 | lineHeight: $line-height; 7 | fontSizeNormal: $font-size-normal; 8 | fontSizeLarge: $font-size-large; 9 | } -------------------------------------------------------------------------------- /src/common/stylesheet/fonts.scss: -------------------------------------------------------------------------------- 1 | $font-family-normal: 'Roboto', sans-serif; 2 | $font-family-monospace: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; -------------------------------------------------------------------------------- /src/common/stylesheet/index.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | @import "dimensions"; 3 | @import "fonts"; -------------------------------------------------------------------------------- /src/common/util.js: -------------------------------------------------------------------------------- 1 | const classes = (...arr) => arr.filter(v => v).join(' '); 2 | 3 | const distance = (a, b) => { 4 | const dx = a.x - b.x; 5 | const dy = a.y - b.y; 6 | return Math.sqrt(dx * dx + dy * dy); 7 | }; 8 | 9 | const extension = fileName => /(?:\.([^.]+))?$/.exec(fileName)[1]; 10 | 11 | const refineGist = gist => { 12 | const gistId = gist.id; 13 | const title = gist.description; 14 | delete gist.files['algorithm-visualizer']; 15 | const { login, avatar_url } = gist.owner; 16 | const files = Object.values(gist.files).map(file => ({ 17 | name: file.filename, 18 | content: file.content, 19 | contributors: [{ login, avatar_url }], 20 | })); 21 | return { login, gistId, title, files }; 22 | }; 23 | 24 | const createFile = (name, content, contributors) => ({ name, content, contributors }); 25 | 26 | const createProjectFile = (name, content) => createFile(name, content, [{ 27 | login: 'algorithm-visualizer', 28 | avatar_url: 'https://github.com/algorithm-visualizer.png', 29 | }]); 30 | 31 | const createUserFile = (name, content) => createFile(name, content, undefined); 32 | 33 | const isSaved = ({ titles, files, lastTitles, lastFiles }) => { 34 | const serialize = (titles, files) => JSON.stringify({ 35 | titles, 36 | files: files.map(({ name, content }) => ({ name, content })), 37 | }); 38 | return serialize(titles, files) === serialize(lastTitles, lastFiles); 39 | }; 40 | 41 | export { 42 | classes, 43 | distance, 44 | extension, 45 | refineGist, 46 | createFile, 47 | createProjectFile, 48 | createUserFile, 49 | isSaved, 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/App/App.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .app { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: stretch; 7 | height: 100%; 8 | background-color: $theme-normal; 9 | 10 | .header { 11 | } 12 | 13 | .workspace { 14 | flex: 1; 15 | 16 | .visualization_viewer { 17 | background-color: $theme-dark; 18 | } 19 | 20 | .editor_tab_container { 21 | } 22 | } 23 | 24 | .toast_container { 25 | position: absolute; 26 | bottom: 0; 27 | right: 0; 28 | z-index: 99; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Cookies from 'js-cookie'; 3 | import { connect } from 'react-redux'; 4 | import Promise from 'bluebird'; 5 | import { Helmet } from 'react-helmet'; 6 | import queryString from 'query-string'; 7 | import { 8 | BaseComponent, 9 | CodeEditor, 10 | Header, 11 | Navigator, 12 | ResizableContainer, 13 | TabContainer, 14 | ToastContainer, 15 | VisualizationViewer, 16 | } from 'components'; 17 | import { AlgorithmApi, GitHubApi, VisualizationApi } from 'apis'; 18 | import { actions } from 'reducers'; 19 | import { createUserFile, extension, refineGist } from 'common/util'; 20 | import { exts, languages } from 'common/config'; 21 | import { CONTRIBUTING_MD } from 'files'; 22 | import styles from './App.module.scss'; 23 | 24 | class App extends BaseComponent { 25 | constructor(props) { 26 | super(props); 27 | 28 | this.state = { 29 | workspaceVisibles: [true, true, true], 30 | workspaceWeights: [1, 2, 2], 31 | }; 32 | 33 | this.codeEditorRef = React.createRef(); 34 | 35 | this.ignoreHistoryBlock = this.ignoreHistoryBlock.bind(this); 36 | this.handleClickTitleBar = this.handleClickTitleBar.bind(this); 37 | this.loadScratchPapers = this.loadScratchPapers.bind(this); 38 | this.handleChangeWorkspaceWeights = this.handleChangeWorkspaceWeights.bind(this); 39 | } 40 | 41 | componentDidMount() { 42 | window.signIn = this.signIn.bind(this); 43 | window.signOut = this.signOut.bind(this); 44 | 45 | const { params } = this.props.match; 46 | const { search } = this.props.location; 47 | this.loadAlgorithm(params, queryString.parse(search)); 48 | 49 | const accessToken = Cookies.get('access_token'); 50 | if (accessToken) this.signIn(accessToken); 51 | 52 | AlgorithmApi.getCategories() 53 | .then(({ categories }) => this.props.setCategories(categories)) 54 | .catch(this.handleError); 55 | 56 | this.toggleHistoryBlock(true); 57 | } 58 | 59 | componentWillUnmount() { 60 | delete window.signIn; 61 | delete window.signOut; 62 | 63 | this.toggleHistoryBlock(false); 64 | } 65 | 66 | componentWillReceiveProps(nextProps) { 67 | const { params } = nextProps.match; 68 | const { search } = nextProps.location; 69 | if (params !== this.props.match.params || search !== this.props.location.search) { 70 | const { categoryKey, algorithmKey, gistId } = params; 71 | const { algorithm, scratchPaper } = nextProps.current; 72 | if (algorithm && algorithm.categoryKey === categoryKey && algorithm.algorithmKey === algorithmKey) return; 73 | if (scratchPaper && scratchPaper.gistId === gistId) return; 74 | this.loadAlgorithm(params, queryString.parse(search)); 75 | } 76 | } 77 | 78 | toggleHistoryBlock(enable = !this.unblock) { 79 | if (enable) { 80 | const warningMessage = 'Are you sure you want to discard changes?'; 81 | window.onbeforeunload = () => { 82 | const { saved } = this.props.current; 83 | if (!saved) return warningMessage; 84 | }; 85 | this.unblock = this.props.history.block((location) => { 86 | if (location.pathname === this.props.location.pathname) return; 87 | const { saved } = this.props.current; 88 | if (!saved) return warningMessage; 89 | }); 90 | } else { 91 | window.onbeforeunload = undefined; 92 | this.unblock(); 93 | this.unblock = undefined; 94 | } 95 | } 96 | 97 | ignoreHistoryBlock(process) { 98 | this.toggleHistoryBlock(false); 99 | process(); 100 | this.toggleHistoryBlock(true); 101 | } 102 | 103 | signIn(accessToken) { 104 | Cookies.set('access_token', accessToken); 105 | GitHubApi.auth(accessToken) 106 | .then(() => GitHubApi.getUser()) 107 | .then(user => { 108 | const { login, avatar_url } = user; 109 | this.props.setUser({ login, avatar_url }); 110 | }) 111 | .then(() => this.loadScratchPapers()) 112 | .catch(() => this.signOut()); 113 | } 114 | 115 | signOut() { 116 | Cookies.remove('access_token'); 117 | GitHubApi.auth(undefined) 118 | .then(() => { 119 | this.props.setUser(undefined); 120 | }) 121 | .then(() => this.props.setScratchPapers([])); 122 | } 123 | 124 | loadScratchPapers() { 125 | const per_page = 100; 126 | const paginateGists = (page = 1, scratchPapers = []) => GitHubApi.listGists({ 127 | per_page, 128 | page, 129 | timestamp: Date.now(), 130 | }).then(gists => { 131 | scratchPapers.push(...gists.filter(gist => 'algorithm-visualizer' in gist.files).map(gist => ({ 132 | key: gist.id, 133 | name: gist.description, 134 | files: Object.keys(gist.files), 135 | }))); 136 | if (gists.length < per_page) { 137 | return scratchPapers; 138 | } else { 139 | return paginateGists(page + 1, scratchPapers); 140 | } 141 | }); 142 | return paginateGists() 143 | .then(scratchPapers => this.props.setScratchPapers(scratchPapers)) 144 | .catch(this.handleError); 145 | } 146 | 147 | loadAlgorithm({ categoryKey, algorithmKey, gistId }, { visualizationId }) { 148 | const { ext } = this.props.env; 149 | const fetch = () => { 150 | if (window.__PRELOADED_ALGORITHM__) { 151 | this.props.setAlgorithm(window.__PRELOADED_ALGORITHM__); 152 | delete window.__PRELOADED_ALGORITHM__; 153 | } else if (window.__PRELOADED_ALGORITHM__ === null) { 154 | delete window.__PRELOADED_ALGORITHM__; 155 | return Promise.reject(new Error('Algorithm Not Found')); 156 | } else if (categoryKey && algorithmKey) { 157 | return AlgorithmApi.getAlgorithm(categoryKey, algorithmKey) 158 | .then(({ algorithm }) => this.props.setAlgorithm(algorithm)); 159 | } else if (gistId === 'new' && visualizationId) { 160 | return VisualizationApi.getVisualization(visualizationId) 161 | .then(content => { 162 | this.props.setScratchPaper({ 163 | login: undefined, 164 | gistId, 165 | title: 'Untitled', 166 | files: [CONTRIBUTING_MD, createUserFile('visualization.json', JSON.stringify(content))], 167 | }); 168 | }); 169 | } else if (gistId === 'new') { 170 | const language = languages.find(language => language.ext === ext); 171 | this.props.setScratchPaper({ 172 | login: undefined, 173 | gistId, 174 | title: 'Untitled', 175 | files: [CONTRIBUTING_MD, language.skeleton], 176 | }); 177 | } else if (gistId) { 178 | return GitHubApi.getGist(gistId, { timestamp: Date.now() }) 179 | .then(refineGist) 180 | .then(this.props.setScratchPaper); 181 | } else { 182 | this.props.setHome(); 183 | } 184 | return Promise.resolve(); 185 | }; 186 | fetch() 187 | .then(() => { 188 | this.selectDefaultTab(); 189 | return null; // to suppress unnecessary bluebird warning 190 | }) 191 | .catch(error => { 192 | this.handleError(error); 193 | this.props.history.push('/'); 194 | }); 195 | } 196 | 197 | selectDefaultTab() { 198 | const { ext } = this.props.env; 199 | const { files } = this.props.current; 200 | const editingFile = files.find(file => extension(file.name) === 'json') || 201 | files.find(file => extension(file.name) === ext) || 202 | files.find(file => exts.includes(extension(file.name))) || 203 | files[files.length - 1]; 204 | this.props.setEditingFile(editingFile); 205 | } 206 | 207 | handleChangeWorkspaceWeights(workspaceWeights) { 208 | this.setState({ workspaceWeights }); 209 | this.codeEditorRef.current.handleResize(); 210 | } 211 | 212 | toggleNavigatorOpened(navigatorOpened = !this.state.workspaceVisibles[0]) { 213 | const workspaceVisibles = [...this.state.workspaceVisibles]; 214 | workspaceVisibles[0] = navigatorOpened; 215 | this.setState({ workspaceVisibles }); 216 | } 217 | 218 | handleClickTitleBar() { 219 | this.toggleNavigatorOpened(); 220 | } 221 | 222 | render() { 223 | const { workspaceVisibles, workspaceWeights } = this.state; 224 | const { titles, description, saved } = this.props.current; 225 | 226 | const title = `${saved ? '' : '(Unsaved) '}${titles.join(' - ')}`; 227 | const [navigatorOpened] = workspaceVisibles; 228 | 229 | return ( 230 |
231 | 232 | {title} 233 | 234 | 235 |
238 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
248 | ); 249 | } 250 | } 251 | 252 | export default connect(({ current, env }) => ({ current, env }), actions)( 253 | App, 254 | ); 255 | -------------------------------------------------------------------------------- /src/components/BaseComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class BaseComponent extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.handleError = this.handleError.bind(this); 8 | } 9 | 10 | handleError(error) { 11 | if (error.response) { 12 | const { data, statusText } = error.response; 13 | const message = data ? typeof data === 'string' ? data : JSON.stringify(data) : statusText; 14 | console.error(message); 15 | this.props.showErrorToast(message); 16 | } else { 17 | console.error(error.message); 18 | this.props.showErrorToast(error.message); 19 | } 20 | } 21 | } 22 | 23 | export default BaseComponent; 24 | -------------------------------------------------------------------------------- /src/components/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .button { 4 | display: flex; 5 | align-items: center; 6 | cursor: pointer; 7 | padding: 0 12px; 8 | margin: 0; 9 | 10 | .icon { 11 | margin-right: 4px; 12 | 13 | &.image { 14 | width: 1.6em; 15 | height: 1.6em; 16 | background-position: center; 17 | background-size: cover; 18 | border-radius: 2px; 19 | } 20 | } 21 | 22 | &.reverse { 23 | flex-direction: row-reverse; 24 | 25 | .icon { 26 | margin-right: 0; 27 | margin-left: 4px; 28 | } 29 | } 30 | 31 | &.icon_only { 32 | .icon { 33 | margin-left: 0; 34 | margin-right: 0; 35 | } 36 | } 37 | 38 | &:hover { 39 | background-color: $color-overlay; 40 | } 41 | 42 | &.primary { 43 | &:hover { 44 | background-color: $color-shadow; 45 | 46 | &:active { 47 | box-shadow: 0px 0px 10px 3px #1a1a1a inset; 48 | } 49 | } 50 | 51 | &.active { 52 | background-color: $color-shadow; 53 | box-shadow: 0px 0px 10px 3px #1a1a1a inset; 54 | font-weight: bold; 55 | 56 | .icon { 57 | color: $color-active; 58 | } 59 | } 60 | } 61 | 62 | &.selected { 63 | background-color: $color-shadow; 64 | 65 | &:hover { 66 | color: rgba($color-font, .8); 67 | } 68 | } 69 | 70 | &.disabled { 71 | cursor: not-allowed; 72 | background-color: $color-shadow; 73 | opacity: 0.6; 74 | } 75 | 76 | &.confirming { 77 | color: $color-alert; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import faExclamationCircle from '@fortawesome/fontawesome-free-solid/faExclamationCircle'; 5 | import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner'; 6 | import { classes } from 'common/util'; 7 | import { Ellipsis } from 'components'; 8 | import styles from './Button.module.scss'; 9 | 10 | class Button extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | confirming: false, 16 | }; 17 | 18 | this.timeout = null; 19 | } 20 | 21 | componentWillUnmount() { 22 | if (this.timeout) { 23 | window.clearTimeout(this.timeout); 24 | this.timeout = undefined; 25 | } 26 | } 27 | 28 | render() { 29 | let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, inProgress, ...rest } = this.props; 30 | const { confirming } = this.state; 31 | 32 | if (confirmNeeded) { 33 | if (confirming) { 34 | className = classes(styles.confirming, className); 35 | icon = faExclamationCircle; 36 | children = Click to Confirm; 37 | const onClickOriginal = onClick; 38 | onClick = () => { 39 | if (onClickOriginal) onClickOriginal(); 40 | if (this.timeout) { 41 | window.clearTimeout(this.timeout); 42 | this.timeout = undefined; 43 | this.setState({ confirming: false }); 44 | } 45 | }; 46 | } else { 47 | to = null; 48 | href = null; 49 | onClick = () => { 50 | this.setState({ confirming: true }); 51 | this.timeout = window.setTimeout(() => { 52 | this.timeout = undefined; 53 | this.setState({ confirming: false }); 54 | }, 2000); 55 | }; 56 | } 57 | } 58 | 59 | const iconOnly = !children; 60 | const props = { 61 | className: classes(styles.button, reverse && styles.reverse, selected && styles.selected, disabled && styles.disabled, primary && styles.primary, active && styles.active, iconOnly && styles.icon_only, className), 62 | to: disabled ? null : to, 63 | href: disabled ? null : href, 64 | onClick: disabled ? null : onClick, 65 | children: [ 66 | icon && ( 67 | typeof icon === 'string' ? 68 |
: 70 | 72 | ), 73 | children, 74 | ], 75 | ...rest, 76 | }; 77 | 78 | return to ? ( 79 | 80 | ) : href ? ( 81 | 82 | ) : ( 83 |
84 | ); 85 | } 86 | } 87 | 88 | export default Button; 89 | 90 | -------------------------------------------------------------------------------- /src/components/CodeEditor/CodeEditor.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .code_editor { 4 | flex: 1; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: stretch; 8 | 9 | .ace_editor { 10 | flex: 1; 11 | width: 100% !important; 12 | height: 100% !important; 13 | min-width: 0 !important; 14 | min-height: 0 !important; 15 | 16 | .current_line_marker { 17 | background-color: rgba($color-highlight, 0.4); 18 | border: 1px solid $color-highlight; 19 | position: absolute; 20 | width: 100% !important; 21 | 22 | animation: line_highlight .1s; 23 | } 24 | 25 | @keyframes line_highlight { 26 | from { 27 | background-color: rgba($color-highlight, 0.1); 28 | } 29 | to { 30 | background-color: rgba($color-highlight, 0.4); 31 | } 32 | } 33 | } 34 | 35 | .contributors_viewer { 36 | display: flex; 37 | flex-wrap: wrap; 38 | align-items: center; 39 | padding: 4px; 40 | background-color: $theme-normal; 41 | 42 | .contributor { 43 | height: 28px; 44 | padding: 0 6px; 45 | font-weight: bold; 46 | 47 | &.label { 48 | display: flex; 49 | align-items: center; 50 | white-space: nowrap; 51 | } 52 | } 53 | 54 | .empty { 55 | display: flex; 56 | flex: 1; 57 | } 58 | 59 | .delete { 60 | height: $line-height; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/CodeEditor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt'; 3 | import faUser from '@fortawesome/fontawesome-free-solid/faUser'; 4 | import { classes, extension } from 'common/util'; 5 | import { actions } from 'reducers'; 6 | import { connect } from 'react-redux'; 7 | import { languages } from 'common/config'; 8 | import { Button, Ellipsis, FoldableAceEditor } from 'components'; 9 | import styles from './CodeEditor.module.scss'; 10 | 11 | class CodeEditor extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.aceEditorRef = React.createRef(); 16 | } 17 | 18 | handleResize() { 19 | this.aceEditorRef.current.resize(); 20 | } 21 | 22 | render() { 23 | const { className } = this.props; 24 | const { editingFile } = this.props.current; 25 | const { user } = this.props.env; 26 | const { lineIndicator } = this.props.player; 27 | 28 | if (!editingFile) return null; 29 | 30 | const fileExt = extension(editingFile.name); 31 | const language = languages.find(language => language.ext === fileExt); 32 | const mode = language ? language.mode : 33 | fileExt === 'md' ? 'markdown' : 34 | fileExt === 'json' ? 'json' : 35 | 'plain_text'; 36 | 37 | return ( 38 |
39 | this.props.modifyFile(editingFile, code)} 47 | markers={lineIndicator ? [{ 48 | startRow: lineIndicator.lineNumber, 49 | startCol: 0, 50 | endRow: lineIndicator.lineNumber, 51 | endCol: Infinity, 52 | className: styles.current_line_marker, 53 | type: 'line', 54 | inFront: true, 55 | _key: lineIndicator.cursor, 56 | }] : []} 57 | value={editingFile.content}/> 58 |
59 | Contributed by 60 | { 61 | (editingFile.contributors || [user || { login: 'guest', avatar_url: faUser }]).map(contributor => ( 62 | 66 | )) 67 | } 68 |
69 |
70 | 74 |
75 |
76 |
77 | ); 78 | } 79 | } 80 | 81 | export default connect(({ current, env, player }) => ({ current, env, player }), actions, null, { forwardRef: true })( 82 | CodeEditor, 83 | ); 84 | -------------------------------------------------------------------------------- /src/components/Divider/Divider.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .divider { 4 | position: relative; 5 | z-index: 97; 6 | 7 | &:after { 8 | position: absolute; 9 | background-color: $theme-light; 10 | content: ''; 11 | } 12 | 13 | &.horizontal { 14 | width: 7px; 15 | margin: 0 -3px; 16 | cursor: ew-resize; 17 | 18 | &:after { 19 | top: 0; 20 | bottom: 0; 21 | left: 3px; 22 | width: 1px; 23 | } 24 | } 25 | 26 | &.vertical { 27 | height: 7px; 28 | margin: -3px 0; 29 | cursor: ns-resize; 30 | 31 | &:after { 32 | left: 0; 33 | right: 0; 34 | top: 3px; 35 | height: 1px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Divider/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classes } from 'common/util'; 3 | import styles from './Divider.module.scss'; 4 | 5 | class Divider extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.handleMouseDown = this.handleMouseDown.bind(this); 10 | this.handleMouseMove = this.handleMouseMove.bind(this); 11 | this.handleMouseUp = this.handleMouseUp.bind(this); 12 | } 13 | 14 | handleMouseDown(e) { 15 | this.target = e.target; 16 | document.addEventListener('mousemove', this.handleMouseMove); 17 | document.addEventListener('mouseup', this.handleMouseUp); 18 | } 19 | 20 | handleMouseMove(e) { 21 | const { onResize } = this.props; 22 | if (onResize) onResize(this.target.parentElement, e.clientX, e.clientY); 23 | } 24 | 25 | handleMouseUp(e) { 26 | document.removeEventListener('mousemove', this.handleMouseMove); 27 | document.removeEventListener('mouseup', this.handleMouseUp); 28 | } 29 | 30 | render() { 31 | const { className, horizontal } = this.props; 32 | 33 | return ( 34 |
36 | ); 37 | } 38 | } 39 | 40 | export default Divider; 41 | -------------------------------------------------------------------------------- /src/components/Ellipsis/Ellipsis.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .ellipsis { 4 | white-space: nowrap; 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Ellipsis.module.scss'; 3 | import { classes } from 'common/util'; 4 | 5 | class Ellipsis extends React.Component { 6 | render() { 7 | const { className, children } = this.props; 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default Ellipsis; 18 | 19 | -------------------------------------------------------------------------------- /src/components/ExpandableListItem/ExpandableListItem.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .category { 4 | justify-content: space-between; 5 | 6 | .icon { 7 | margin-left: 4px; 8 | } 9 | } 10 | 11 | .expandable_list_item { 12 | background-color: $color-shadow; 13 | border-bottom: 1px solid $theme-dark; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ExpandableListItem/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 3 | import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown'; 4 | import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight'; 5 | import styles from './ExpandableListItem.module.scss'; 6 | import { ListItem } from 'components'; 7 | import { classes } from 'common/util'; 8 | 9 | class ExpandableListItem extends React.Component { 10 | render() { 11 | const { className, children, opened, ...props } = this.props; 12 | 13 | return opened ? ( 14 |
15 | 16 | 17 | 18 | {children} 19 |
20 | ) : ( 21 | 22 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | export default ExpandableListItem; 29 | 30 | -------------------------------------------------------------------------------- /src/components/FoldableAceEditor/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import AceEditor from 'react-ace'; 3 | import 'brace/mode/plain_text'; 4 | import 'brace/mode/markdown'; 5 | import 'brace/mode/json'; 6 | import 'brace/mode/javascript'; 7 | import 'brace/mode/c_cpp'; 8 | import 'brace/mode/java'; 9 | import 'brace/theme/tomorrow_night_eighties'; 10 | import 'brace/ext/searchbox'; 11 | import { actions } from 'reducers'; 12 | 13 | class FoldableAceEditor extends AceEditor { 14 | componentDidMount() { 15 | super.componentDidMount(); 16 | 17 | const { shouldBuild } = this.props.current; 18 | if (shouldBuild) this.foldTracers(); 19 | } 20 | 21 | componentDidUpdate(prevProps, prevState, snapshot) { 22 | super.componentDidUpdate(prevProps, prevState, snapshot); 23 | 24 | const { editingFile, shouldBuild } = this.props.current; 25 | if (editingFile !== prevProps.current.editingFile) { 26 | if (shouldBuild) this.foldTracers(); 27 | } 28 | } 29 | 30 | foldTracers() { 31 | const session = this.editor.getSession(); 32 | for (let row = 0; row < session.getLength(); row++) { 33 | if (!/^\s*\/\/.+{\s*$/.test(session.getLine(row))) continue; 34 | const range = session.getFoldWidgetRange(row); 35 | if (range) { 36 | session.addFold('...', range); 37 | row = range.end.row; 38 | } 39 | } 40 | } 41 | 42 | resize() { 43 | this.editor.resize(); 44 | } 45 | } 46 | 47 | export default connect(({ current }) => ({ current }), actions, null, { forwardRef: true })( 48 | FoldableAceEditor, 49 | ); 50 | -------------------------------------------------------------------------------- /src/components/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .header { 4 | display: flex; 5 | flex-direction: column; 6 | min-width: 0; 7 | 8 | .row { 9 | display: flex; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | padding: 0 16px; 13 | border-bottom: 1px solid $theme-light; 14 | 15 | .section { 16 | height: $line-height; 17 | display: flex; 18 | align-items: stretch; 19 | 20 | .title_bar { 21 | font-size: $font-size-large; 22 | font-weight: bold; 23 | min-width: 0; 24 | 25 | .nav_arrow { 26 | margin: 0 4px; 27 | } 28 | 29 | .nav_caret { 30 | margin-left: 4px; 31 | } 32 | 33 | .input_title { 34 | padding: 4px 8px; 35 | background-color: $theme-light; 36 | } 37 | } 38 | 39 | .btn_dropdown { 40 | position: relative; 41 | font-weight: bold; 42 | 43 | &:active { 44 | box-shadow: none; 45 | } 46 | 47 | .dropdown { 48 | z-index: 98; 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | display: none; 53 | flex-direction: column; 54 | align-items: stretch; 55 | box-shadow: 0 0 8px $color-shadow; 56 | background-color: $theme-light; 57 | margin-top: $line-height; 58 | } 59 | 60 | &:hover { 61 | .dropdown { 62 | display: flex; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router'; 4 | import AutosizeInput from 'react-input-autosize'; 5 | import screenfull from 'screenfull'; 6 | import Promise from 'bluebird'; 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 8 | import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight'; 9 | import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown'; 10 | import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight'; 11 | import faCodeBranch from '@fortawesome/fontawesome-free-solid/faCodeBranch'; 12 | import faExpandArrowsAlt from '@fortawesome/fontawesome-free-solid/faExpandArrowsAlt'; 13 | import faGithub from '@fortawesome/fontawesome-free-brands/faGithub'; 14 | import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt'; 15 | import faSave from '@fortawesome/fontawesome-free-solid/faSave'; 16 | import faFacebook from '@fortawesome/fontawesome-free-brands/faFacebook'; 17 | import faStar from '@fortawesome/fontawesome-free-solid/faStar'; 18 | import { GitHubApi } from 'apis'; 19 | import { classes, refineGist } from 'common/util'; 20 | import { actions } from 'reducers'; 21 | import { languages } from 'common/config'; 22 | import { BaseComponent, Button, Ellipsis, ListItem, Player } from 'components'; 23 | import styles from './Header.module.scss'; 24 | 25 | class Header extends BaseComponent { 26 | handleClickFullScreen() { 27 | if (screenfull.enabled) { 28 | if (screenfull.isFullscreen) { 29 | screenfull.exit(); 30 | } else { 31 | screenfull.request(); 32 | } 33 | } 34 | } 35 | 36 | handleChangeTitle(e) { 37 | const { value } = e.target; 38 | this.props.modifyTitle(value); 39 | } 40 | 41 | saveGist() { 42 | const { user } = this.props.env; 43 | const { scratchPaper, titles, files, lastFiles, editingFile } = this.props.current; 44 | const gist = { 45 | description: titles[titles.length - 1], 46 | files: {}, 47 | }; 48 | files.forEach(file => { 49 | gist.files[file.name] = { 50 | content: file.content, 51 | }; 52 | }); 53 | lastFiles.forEach(lastFile => { 54 | if (!(lastFile.name in gist.files)) { 55 | gist.files[lastFile.name] = null; 56 | } 57 | }); 58 | gist.files['algorithm-visualizer'] = { 59 | content: 'https://algorithm-visualizer.org/', 60 | }; 61 | const save = gist => { 62 | if (!user) return Promise.reject(new Error('Sign In Required')); 63 | if (scratchPaper && scratchPaper.login) { 64 | if (scratchPaper.login === user.login) { 65 | return GitHubApi.editGist(scratchPaper.gistId, gist); 66 | } else { 67 | return GitHubApi.forkGist(scratchPaper.gistId).then(forkedGist => GitHubApi.editGist(forkedGist.id, gist)); 68 | } 69 | } 70 | return GitHubApi.createGist(gist); 71 | }; 72 | save(gist) 73 | .then(refineGist) 74 | .then(newScratchPaper => { 75 | this.props.setScratchPaper(newScratchPaper); 76 | this.props.setEditingFile(newScratchPaper.files.find(file => file.name === editingFile.name)); 77 | if (!(scratchPaper && scratchPaper.gistId === newScratchPaper.gistId)) { 78 | this.props.history.push(`/scratch-paper/${newScratchPaper.gistId}`); 79 | } 80 | }) 81 | .then(this.props.loadScratchPapers) 82 | .catch(this.handleError); 83 | } 84 | 85 | hasPermission() { 86 | const { scratchPaper } = this.props.current; 87 | const { user } = this.props.env; 88 | if (!scratchPaper) return false; 89 | if (scratchPaper.gistId !== 'new') { 90 | if (!user) return false; 91 | if (scratchPaper.login !== user.login) return false; 92 | } 93 | return true; 94 | } 95 | 96 | deleteGist() { 97 | const { scratchPaper } = this.props.current; 98 | const { gistId } = scratchPaper; 99 | if (gistId === 'new') { 100 | this.props.ignoreHistoryBlock(() => this.props.history.push('/')); 101 | } else { 102 | GitHubApi.deleteGist(gistId) 103 | .then(() => { 104 | this.props.ignoreHistoryBlock(() => this.props.history.push('/')); 105 | }) 106 | .then(this.props.loadScratchPapers) 107 | .catch(this.handleError); 108 | } 109 | } 110 | 111 | render() { 112 | const { className, onClickTitleBar, navigatorOpened } = this.props; 113 | const { scratchPaper, titles, saved } = this.props.current; 114 | const { ext, user } = this.props.env; 115 | 116 | const permitted = this.hasPermission(); 117 | 118 | return ( 119 |
120 |
121 |
122 | 136 |
137 |
138 | 140 | { 141 | permitted && 142 | 143 | } 144 | 146 | 148 |
149 |
150 |
151 |
152 | { 153 | user ? 154 | : 160 | 163 | } 164 | 175 |
176 | 177 |
178 |
179 | ); 180 | } 181 | } 182 | 183 | export default withRouter( 184 | connect(({ current, env }) => ({ current, env }), actions)( 185 | Header, 186 | ), 187 | ); 188 | 189 | -------------------------------------------------------------------------------- /src/components/ListItem/ListItem.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .list_item { 4 | height: $line-height; 5 | 6 | .label { 7 | flex: 1; 8 | } 9 | 10 | &.indent { 11 | padding-left: 24px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ListItem/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './ListItem.module.scss'; 3 | import { classes } from 'common/util'; 4 | import { Button, Ellipsis } from 'components'; 5 | 6 | class ListItem extends React.Component { 7 | render() { 8 | const { className, children, indent, label, ...props } = this.props; 9 | 10 | return ( 11 | 15 | ); 16 | } 17 | } 18 | 19 | export default ListItem; 20 | 21 | -------------------------------------------------------------------------------- /src/components/Navigator/Navigator.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .navigator { 4 | flex: 1; 5 | display: flex; 6 | flex-direction: column; 7 | min-height: 0; 8 | 9 | .search_bar_container { 10 | height: $line-height; 11 | padding: 0 8px; 12 | display: flex; 13 | align-items: stretch; 14 | border-bottom: 1px solid $theme-light; 15 | 16 | &:focus-within { 17 | background-color: $color-overlay; 18 | } 19 | 20 | .search_icon { 21 | align-self: center; 22 | margin-right: 8px; 23 | } 24 | 25 | .search_bar { 26 | flex: 1; 27 | box-sizing: border-box; 28 | } 29 | } 30 | 31 | .algorithm_list { 32 | flex: 1; 33 | overflow-y: auto; 34 | } 35 | 36 | .footer { 37 | max-height: 30vh; 38 | border-top: 1px solid $theme-light; 39 | overflow-y: auto; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Navigator/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import faSearch from '@fortawesome/fontawesome-free-solid/faSearch'; 5 | import faCode from '@fortawesome/fontawesome-free-solid/faCode'; 6 | import faBook from '@fortawesome/fontawesome-free-solid/faBook'; 7 | import faGithub from '@fortawesome/fontawesome-free-brands/faGithub'; 8 | import { ExpandableListItem, ListItem } from 'components'; 9 | import { classes } from 'common/util'; 10 | import { actions } from 'reducers'; 11 | import styles from './Navigator.module.scss'; 12 | 13 | class Navigator extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | categoriesOpened: {}, 19 | scratchPaperOpened: true, 20 | query: '', 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | const { algorithm } = this.props.current; 26 | if (algorithm) { 27 | this.toggleCategory(algorithm.categoryKey, true); 28 | } 29 | } 30 | 31 | componentWillReceiveProps(nextProps) { 32 | const { algorithm } = nextProps.current; 33 | if (algorithm) { 34 | this.toggleCategory(algorithm.categoryKey, true); 35 | } 36 | } 37 | 38 | toggleCategory(key, categoryOpened = !this.state.categoriesOpened[key]) { 39 | const categoriesOpened = { 40 | ...this.state.categoriesOpened, 41 | [key]: categoryOpened, 42 | }; 43 | this.setState({ categoriesOpened }); 44 | } 45 | 46 | toggleScratchPaper(scratchPaperOpened = !this.state.scratchPaperOpened) { 47 | this.setState({ scratchPaperOpened }); 48 | } 49 | 50 | handleChangeQuery(e) { 51 | const { categories } = this.props.directory; 52 | const categoriesOpened = {}; 53 | const query = e.target.value; 54 | categories.forEach(category => { 55 | if (this.testQuery(category.name) || category.algorithms.find(algorithm => this.testQuery(algorithm.name))) { 56 | categoriesOpened[category.key] = true; 57 | } 58 | }); 59 | this.setState({ categoriesOpened, query }); 60 | } 61 | 62 | testQuery(value) { 63 | const { query } = this.state; 64 | const refine = string => string.replace(/-/g, ' ').replace(/[^\w ]/g, ''); 65 | const refinedQuery = refine(query); 66 | const refinedValue = refine(value); 67 | return new RegExp(`(^| )${refinedQuery}`, 'i').test(refinedValue) || 68 | new RegExp(refinedQuery, 'i').test(refinedValue.split(' ').map(v => v && v[0]).join('')); 69 | } 70 | 71 | render() { 72 | const { categoriesOpened, scratchPaperOpened, query } = this.state; 73 | const { className } = this.props; 74 | const { categories, scratchPapers } = this.props.directory; 75 | const { algorithm, scratchPaper } = this.props.current; 76 | 77 | const categoryKey = algorithm && algorithm.categoryKey; 78 | const algorithmKey = algorithm && algorithm.algorithmKey; 79 | const gistId = scratchPaper && scratchPaper.gistId; 80 | 81 | return ( 82 | 130 | ); 131 | } 132 | } 133 | 134 | export default connect(({ current, directory, env }) => ({ current, directory, env }), actions)( 135 | Navigator, 136 | ); 137 | -------------------------------------------------------------------------------- /src/components/Player/Player.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .player { 4 | .progress_bar { 5 | width: 160px; 6 | } 7 | 8 | .speed { 9 | display: flex; 10 | align-items: center; 11 | padding: 0 12px; 12 | white-space: nowrap; 13 | 14 | &:hover { 15 | background-color: $color-shadow; 16 | } 17 | 18 | .range { 19 | position: relative; 20 | height: 16px; 21 | width: 60px; 22 | margin-left: 8px; 23 | 24 | .range_label_container { 25 | display: none; 26 | } 27 | 28 | .range_track { 29 | top: 50%; 30 | height: 6px; 31 | margin-top: -3px; 32 | background-color: $theme-light; 33 | cursor: pointer; 34 | display: block; 35 | position: relative; 36 | } 37 | 38 | .range_slider { 39 | top: 0; 40 | width: 6px; 41 | height: 12px; 42 | margin-left: -3px; 43 | margin-top: -3px; 44 | appearance: none; 45 | background-color: $color-font; 46 | cursor: pointer; 47 | display: block; 48 | position: absolute; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Player/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import InputRange from 'react-input-range'; 4 | import axios from 'axios'; 5 | import faPlay from '@fortawesome/fontawesome-free-solid/faPlay'; 6 | import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft'; 7 | import faChevronRight from '@fortawesome/fontawesome-free-solid/faChevronRight'; 8 | import faPause from '@fortawesome/fontawesome-free-solid/faPause'; 9 | import faWrench from '@fortawesome/fontawesome-free-solid/faWrench'; 10 | import { classes, extension } from 'common/util'; 11 | import { TracerApi } from 'apis'; 12 | import { actions } from 'reducers'; 13 | import { BaseComponent, Button, ProgressBar } from 'components'; 14 | import styles from './Player.module.scss'; 15 | 16 | class Player extends BaseComponent { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | speed: 2, 22 | playing: false, 23 | building: false, 24 | }; 25 | 26 | this.tracerApiSource = null; 27 | 28 | this.reset(); 29 | } 30 | 31 | componentDidMount() { 32 | const { editingFile, shouldBuild } = this.props.current; 33 | if (shouldBuild) this.build(editingFile); 34 | } 35 | 36 | componentWillReceiveProps(nextProps) { 37 | const { editingFile, shouldBuild } = nextProps.current; 38 | if (editingFile !== this.props.current.editingFile) { 39 | if (shouldBuild) this.build(editingFile); 40 | } 41 | } 42 | 43 | reset(commands = []) { 44 | const chunks = [{ 45 | commands: [], 46 | lineNumber: undefined, 47 | }]; 48 | while (commands.length) { 49 | const command = commands.shift(); 50 | const { key, method, args } = command; 51 | if (key === null && method === 'delay') { 52 | const [lineNumber] = args; 53 | chunks[chunks.length - 1].lineNumber = lineNumber; 54 | chunks.push({ 55 | commands: [], 56 | lineNumber: undefined, 57 | }); 58 | } else { 59 | chunks[chunks.length - 1].commands.push(command); 60 | } 61 | } 62 | this.props.setChunks(chunks); 63 | this.props.setCursor(0); 64 | this.pause(); 65 | this.props.setLineIndicator(undefined); 66 | } 67 | 68 | build(file) { 69 | this.reset(); 70 | if (!file) return; 71 | 72 | if (this.tracerApiSource) this.tracerApiSource.cancel(); 73 | this.tracerApiSource = axios.CancelToken.source(); 74 | this.setState({ building: true }); 75 | 76 | const ext = extension(file.name); 77 | if (ext in TracerApi) { 78 | TracerApi[ext]({ code: file.content }, undefined, this.tracerApiSource.token) 79 | .then(commands => { 80 | this.tracerApiSource = null; 81 | this.setState({ building: false }); 82 | this.reset(commands); 83 | this.next(); 84 | }) 85 | .catch(error => { 86 | if (axios.isCancel(error)) return; 87 | this.tracerApiSource = null; 88 | this.setState({ building: false }); 89 | this.handleError(error); 90 | }); 91 | } else { 92 | this.setState({ building: false }); 93 | this.handleError(new Error('Language Not Supported')); 94 | } 95 | } 96 | 97 | isValidCursor(cursor) { 98 | const { chunks } = this.props.player; 99 | return 1 <= cursor && cursor <= chunks.length; 100 | } 101 | 102 | prev() { 103 | this.pause(); 104 | const cursor = this.props.player.cursor - 1; 105 | if (!this.isValidCursor(cursor)) return false; 106 | this.props.setCursor(cursor); 107 | return true; 108 | } 109 | 110 | resume(wrap = false) { 111 | this.pause(); 112 | if (this.next() || (wrap && this.props.setCursor(1))) { 113 | const interval = 4000 / Math.pow(Math.E, this.state.speed); 114 | this.timer = window.setTimeout(() => this.resume(), interval); 115 | this.setState({ playing: true }); 116 | } 117 | } 118 | 119 | pause() { 120 | if (this.timer) { 121 | window.clearTimeout(this.timer); 122 | this.timer = undefined; 123 | this.setState({ playing: false }); 124 | } 125 | } 126 | 127 | next() { 128 | this.pause(); 129 | const cursor = this.props.player.cursor + 1; 130 | if (!this.isValidCursor(cursor)) return false; 131 | this.props.setCursor(cursor); 132 | return true; 133 | } 134 | 135 | handleChangeSpeed(speed) { 136 | this.setState({ speed }); 137 | } 138 | 139 | handleChangeProgress(progress) { 140 | const { chunks } = this.props.player; 141 | const cursor = Math.max(1, Math.min(chunks.length, Math.round(progress * chunks.length))); 142 | this.pause(); 143 | this.props.setCursor(cursor); 144 | } 145 | 146 | render() { 147 | const { className } = this.props; 148 | const { editingFile } = this.props.current; 149 | const { chunks, cursor } = this.props.player; 150 | const { speed, playing, building } = this.state; 151 | 152 | return ( 153 |
154 | 158 | { 159 | playing ? ( 160 | 161 | ) : ( 162 | 163 | ) 164 | } 165 |
182 | ); 183 | } 184 | } 185 | 186 | export default connect(({ current, player }) => ({ current, player }), actions)( 187 | Player, 188 | ); 189 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .progress_bar { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | position: relative; 8 | background-color: $theme-light; 9 | cursor: pointer; 10 | pointer-events: auto; 11 | 12 | > * { 13 | pointer-events: none; 14 | } 15 | 16 | .active { 17 | position: absolute; 18 | height: 100%; 19 | left: 0; 20 | background-color: $color-active; 21 | } 22 | 23 | .label { 24 | position: absolute; 25 | color: $theme-dark; 26 | 27 | .current { 28 | font-weight: bold; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classes } from 'common/util'; 3 | import styles from './ProgressBar.module.scss'; 4 | 5 | class ProgressBar extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.handleMouseDown = this.handleMouseDown.bind(this); 10 | this.handleMouseMove = this.handleMouseMove.bind(this); 11 | this.handleMouseUp = this.handleMouseUp.bind(this); 12 | } 13 | 14 | handleMouseDown(e) { 15 | this.target = e.target; 16 | this.handleMouseMove(e); 17 | document.addEventListener('mousemove', this.handleMouseMove); 18 | document.addEventListener('mouseup', this.handleMouseUp); 19 | } 20 | 21 | handleMouseMove(e) { 22 | const { left } = this.target.getBoundingClientRect(); 23 | const { offsetWidth } = this.target; 24 | const { onChangeProgress } = this.props; 25 | const progress = (e.clientX - left) / offsetWidth; 26 | if (onChangeProgress) onChangeProgress(progress); 27 | } 28 | 29 | handleMouseUp(e) { 30 | document.removeEventListener('mousemove', this.handleMouseMove); 31 | document.removeEventListener('mouseup', this.handleMouseUp); 32 | } 33 | 34 | render() { 35 | const { className, total, current } = this.props; 36 | 37 | return ( 38 |
39 |
40 |
41 | {current} / {total} 42 |
43 |
44 | ); 45 | } 46 | } 47 | 48 | export default ProgressBar; 49 | -------------------------------------------------------------------------------- /src/components/ResizableContainer/ResizableContainer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .resizable_container { 4 | flex: 1; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: stretch; 8 | min-width: 0; 9 | min-height: 0; 10 | 11 | &.horizontal { 12 | flex-direction: row; 13 | } 14 | 15 | .wrapper { 16 | flex: 1; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: stretch; 20 | overflow: hidden; 21 | 22 | &.horizontal { 23 | flex-direction: row; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ResizableContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classes } from 'common/util'; 3 | import { Divider } from 'components'; 4 | import styles from './ResizableContainer.module.scss'; 5 | 6 | class ResizableContainer extends React.Component { 7 | handleResize(prevIndex, index, targetElement, clientX, clientY) { 8 | const { horizontal, visibles, onChangeWeights } = this.props; 9 | const weights = [...this.props.weights]; 10 | 11 | const { left, top } = targetElement.getBoundingClientRect(); 12 | const { offsetWidth, offsetHeight } = targetElement.parentElement; 13 | const position = horizontal ? clientX - left : clientY - top; 14 | const containerSize = horizontal ? offsetWidth : offsetHeight; 15 | 16 | let totalWeight = 0; 17 | let subtotalWeight = 0; 18 | weights.forEach((weight, i) => { 19 | if (visibles && !visibles[i]) return; 20 | totalWeight += weight; 21 | if (i < index) subtotalWeight += weight; 22 | }); 23 | const newWeight = position / containerSize * totalWeight; 24 | let deltaWeight = newWeight - subtotalWeight; 25 | deltaWeight = Math.max(deltaWeight, -weights[prevIndex]); 26 | deltaWeight = Math.min(deltaWeight, weights[index]); 27 | weights[prevIndex] += deltaWeight; 28 | weights[index] -= deltaWeight; 29 | onChangeWeights(weights); 30 | } 31 | 32 | render() { 33 | const { className, children, horizontal, weights, visibles } = this.props; 34 | 35 | const elements = []; 36 | let lastIndex = -1; 37 | const totalWeight = weights.filter((weight, i) => !visibles || visibles[i]) 38 | .reduce((sumWeight, weight) => sumWeight + weight, 0); 39 | children.forEach((child, i) => { 40 | if (!visibles || visibles[i]) { 41 | if (~lastIndex) { 42 | const prevIndex = lastIndex; 43 | elements.push( 44 | this.handleResize(prevIndex, i, target, dx, dy))} />, 46 | ); 47 | } 48 | elements.push( 49 |
52 | {child} 53 |
, 54 | ); 55 | lastIndex = i; 56 | } 57 | }); 58 | 59 | return ( 60 |
61 | {elements} 62 |
63 | ); 64 | } 65 | } 66 | 67 | export default ResizableContainer; 68 | -------------------------------------------------------------------------------- /src/components/TabContainer/TabContainer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .tab_container { 4 | flex: 1; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: stretch; 8 | min-width: 0; 9 | min-height: 0; 10 | position: relative; 11 | 12 | .tab_bar { 13 | display: flex; 14 | align-items: stretch; 15 | height: $line-height; 16 | overflow-x: auto; 17 | white-space: nowrap; 18 | flex-shrink: 0; 19 | 20 | .title { 21 | display: flex; 22 | align-items: center; 23 | cursor: pointer; 24 | padding: 0 12px; 25 | margin: 0; 26 | border-bottom: 1px solid $theme-light; 27 | 28 | .input_title { 29 | input { 30 | &:hover, 31 | &:focus { 32 | margin: -4px; 33 | padding: 4px; 34 | background-color: $theme-normal; 35 | } 36 | } 37 | } 38 | 39 | &.selected { 40 | border-left: 1px solid $theme-light; 41 | border-right: 1px solid $theme-light; 42 | margin: 0 -1px; 43 | border-bottom: none; 44 | background-color: $theme-dark; 45 | } 46 | 47 | &.fake { 48 | pointer-events: none; 49 | 50 | &:first-child { 51 | flex-shrink: 0; 52 | width: $line-height / 2; 53 | } 54 | 55 | &:last-child { 56 | flex: 1; 57 | } 58 | } 59 | } 60 | } 61 | 62 | .content { 63 | flex: 1; 64 | display: flex; 65 | flex-direction: column; 66 | align-items: stretch; 67 | background-color: $theme-dark; 68 | overflow: hidden; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/TabContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import AutosizeInput from 'react-input-autosize'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import faPlus from '@fortawesome/fontawesome-free-solid/faPlus'; 6 | import { classes } from 'common/util'; 7 | import { actions } from 'reducers'; 8 | import { languages } from 'common/config'; 9 | import styles from './TabContainer.module.scss'; 10 | 11 | class TabContainer extends React.Component { 12 | handleAddFile() { 13 | const { ext } = this.props.env; 14 | const { files } = this.props.current; 15 | const language = languages.find(language => language.ext === ext); 16 | const newFile = { ...language.skeleton }; 17 | let count = 0; 18 | while (files.some(file => file.name === newFile.name)) newFile.name = `code-${++count}.${ext}`; 19 | this.props.addFile(newFile); 20 | } 21 | 22 | render() { 23 | const { className, children } = this.props; 24 | const { editingFile, files } = this.props.current; 25 | 26 | return ( 27 |
28 |
29 |
30 | { 31 | files.map((file, i) => file === editingFile ? ( 32 |
this.props.setEditingFile(file)}> 34 | e.stopPropagation()} 36 | onChange={e => this.props.renameFile(file, e.target.value)}/> 37 |
38 | ) : ( 39 |
this.props.setEditingFile(file)}> 40 | {file.name} 41 |
42 | )) 43 | } 44 |
this.handleAddFile()}> 45 | 46 |
47 |
48 |
49 |
50 | {children} 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default connect(({ current, env }) => ({ current, env }), actions)( 58 | TabContainer, 59 | ); 60 | -------------------------------------------------------------------------------- /src/components/ToastContainer/ToastContainer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .toast_container { 4 | display: flex; 5 | flex-direction: column-reverse; 6 | padding: 16px; 7 | pointer-events: none; 8 | } 9 | 10 | .toast { 11 | width: 280px; 12 | border: 1px solid; 13 | border-radius: 4px; 14 | padding: 16px; 15 | margin: 8px; 16 | white-space: pre-wrap; 17 | pointer-events: auto; 18 | font-family: $font-family-monospace; 19 | 20 | &.success { 21 | border-color: rgb(0, 150, 0); 22 | background-color: rgba(0, 120, 0, .8); 23 | } 24 | 25 | &.error { 26 | border-color: rgb(150, 0, 0); 27 | background-color: rgba(120, 0, 0, .8); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ToastContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { actions } from 'reducers'; 4 | import { classes } from 'common/util'; 5 | import styles from './ToastContainer.module.scss'; 6 | 7 | class ToastContainer extends React.Component { 8 | componentWillReceiveProps(nextProps) { 9 | const newToasts = nextProps.toast.toasts.filter(toast => !this.props.toast.toasts.includes(toast)); 10 | newToasts.forEach(toast => { 11 | window.setTimeout(() => this.props.hideToast(toast.id), 3000); 12 | }); 13 | } 14 | 15 | render() { 16 | const { className } = this.props; 17 | const { toasts } = this.props.toast; 18 | 19 | return ( 20 |
21 | { 22 | toasts.map(toast => ( 23 |
24 | {toast.message} 25 |
26 | )) 27 | } 28 |
29 | ); 30 | } 31 | } 32 | 33 | export default connect(({ toast }) => ({ toast }), actions)( 34 | ToastContainer, 35 | ); 36 | 37 | -------------------------------------------------------------------------------- /src/components/VisualizationViewer/VisualizationViewer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .visualization_viewer { 4 | flex: 1; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: stretch; 8 | min-height: 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/VisualizationViewer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { BaseComponent } from 'components'; 4 | import { actions } from 'reducers'; 5 | import styles from './VisualizationViewer.module.scss'; 6 | import * as TracerClasses from 'core/tracers'; 7 | import * as LayoutClasses from 'core/layouts'; 8 | import { classes } from 'common/util'; 9 | 10 | class VisualizationViewer extends BaseComponent { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.reset(); 15 | } 16 | 17 | reset() { 18 | this.root = null; 19 | this.objects = {}; 20 | } 21 | 22 | componentDidMount() { 23 | const { chunks, cursor } = this.props.player; 24 | this.update(chunks, cursor); 25 | } 26 | 27 | componentWillReceiveProps(nextProps) { 28 | const { chunks, cursor } = nextProps.player; 29 | const { chunks: oldChunks, cursor: oldCursor } = this.props.player; 30 | if (chunks !== oldChunks || cursor !== oldCursor) { 31 | this.update(chunks, cursor, oldChunks, oldCursor); 32 | } 33 | } 34 | 35 | update(chunks, cursor, oldChunks = [], oldCursor = 0) { 36 | let applyingChunks; 37 | if (cursor > oldCursor) { 38 | applyingChunks = chunks.slice(oldCursor, cursor); 39 | } else { 40 | this.reset(); 41 | applyingChunks = chunks.slice(0, cursor); 42 | } 43 | applyingChunks.forEach(chunk => this.applyChunk(chunk)); 44 | 45 | const lastChunk = applyingChunks[applyingChunks.length - 1]; 46 | if (lastChunk && lastChunk.lineNumber !== undefined) { 47 | this.props.setLineIndicator({ lineNumber: lastChunk.lineNumber, cursor }); 48 | } else { 49 | this.props.setLineIndicator(undefined); 50 | } 51 | } 52 | 53 | applyCommand(command) { 54 | const { key, method, args } = command; 55 | try { 56 | if (key === null && method === 'setRoot') { 57 | const [root] = args; 58 | this.root = this.objects[root]; 59 | } else if (method === 'destroy') { 60 | delete this.objects[key]; 61 | } else if (method in LayoutClasses) { 62 | const [children] = args; 63 | const LayoutClass = LayoutClasses[method]; 64 | this.objects[key] = new LayoutClass(key, key => this.objects[key], children); 65 | } else if (method in TracerClasses) { 66 | const className = method; 67 | const [title = className] = args; 68 | const TracerClass = TracerClasses[className]; 69 | this.objects[key] = new TracerClass(key, key => this.objects[key], title); 70 | } else { 71 | this.objects[key][method](...args); 72 | } 73 | } catch (error) { 74 | this.handleError(error); 75 | } 76 | } 77 | 78 | applyChunk(chunk) { 79 | chunk.commands.forEach(command => this.applyCommand(command)); 80 | } 81 | 82 | render() { 83 | const { className } = this.props; 84 | 85 | return ( 86 |
87 | { 88 | this.root && this.root.render() 89 | } 90 |
91 | ); 92 | } 93 | } 94 | 95 | export default connect(({ player }) => ({ player }), actions)( 96 | VisualizationViewer, 97 | ); 98 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as App } from './App'; 2 | export { default as BaseComponent } from './BaseComponent'; 3 | export { default as Button } from './Button'; 4 | export { default as CodeEditor } from './CodeEditor'; 5 | export { default as Divider } from './Divider'; 6 | export { default as Ellipsis } from './Ellipsis'; 7 | export { default as ExpandableListItem } from './ExpandableListItem'; 8 | export { default as FoldableAceEditor } from './FoldableAceEditor'; 9 | export { default as Header } from './Header'; 10 | export { default as ListItem } from './ListItem'; 11 | export { default as Navigator } from './Navigator'; 12 | export { default as Player } from './Player'; 13 | export { default as ProgressBar } from './ProgressBar'; 14 | export { default as ResizableContainer } from './ResizableContainer'; 15 | export { default as TabContainer } from './TabContainer'; 16 | export { default as ToastContainer } from './ToastContainer'; 17 | export { default as VisualizationViewer } from './VisualizationViewer'; 18 | -------------------------------------------------------------------------------- /src/core/layouts/HorizontalLayout.js: -------------------------------------------------------------------------------- 1 | import { Layout } from 'core/layouts'; 2 | 3 | class HorizontalLayout extends Layout { 4 | } 5 | 6 | export default HorizontalLayout; 7 | -------------------------------------------------------------------------------- /src/core/layouts/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ResizableContainer } from 'components'; 3 | import { HorizontalLayout } from 'core/layouts'; 4 | 5 | class Layout { 6 | constructor(key, getObject, children) { 7 | this.key = key; 8 | this.getObject = getObject; 9 | this.children = children.map(key => this.getObject(key)); 10 | this.weights = children.map(() => 1); 11 | this.ref = React.createRef(); 12 | 13 | this.handleChangeWeights = this.handleChangeWeights.bind(this); 14 | } 15 | 16 | add(key, index = this.children.length) { 17 | const child = this.getObject(key); 18 | this.children.splice(index, 0, child); 19 | this.weights.splice(index, 0, 1); 20 | } 21 | 22 | remove(key) { 23 | const child = this.getObject(key); 24 | const index = this.children.indexOf(child); 25 | if (~index) { 26 | this.children.splice(index, 1); 27 | this.weights.splice(index, 1); 28 | } 29 | } 30 | 31 | removeAll() { 32 | this.children = []; 33 | this.weights = []; 34 | } 35 | 36 | handleChangeWeights(weights) { 37 | this.weights.splice(0, this.weights.length, ...weights); 38 | this.ref.current.forceUpdate(); 39 | } 40 | 41 | render() { 42 | const horizontal = this instanceof HorizontalLayout; 43 | 44 | return ( 45 | 47 | { 48 | this.children.map(tracer => tracer && tracer.render()) 49 | } 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default Layout; 56 | -------------------------------------------------------------------------------- /src/core/layouts/VerticalLayout.js: -------------------------------------------------------------------------------- 1 | import { Layout } from 'core/layouts'; 2 | 3 | class VerticalLayout extends Layout { 4 | } 5 | 6 | export default VerticalLayout; 7 | -------------------------------------------------------------------------------- /src/core/layouts/index.js: -------------------------------------------------------------------------------- 1 | export { default as Layout } from './Layout'; 2 | export { default as HorizontalLayout } from './HorizontalLayout'; 3 | export { default as VerticalLayout } from './VerticalLayout'; 4 | -------------------------------------------------------------------------------- /src/core/renderers/Array1DRenderer/Array1DRenderer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | -------------------------------------------------------------------------------- /src/core/renderers/Array1DRenderer/index.js: -------------------------------------------------------------------------------- 1 | import { Array2DRenderer } from 'core/renderers'; 2 | 3 | class Array1DRenderer extends Array2DRenderer { 4 | } 5 | 6 | export default Array1DRenderer; 7 | 8 | -------------------------------------------------------------------------------- /src/core/renderers/Array2DRenderer/Array2DRenderer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .array_2d { 4 | flex-shrink: 0; 5 | display: table; 6 | border-collapse: collapse; 7 | 8 | .row { 9 | display: table-row; 10 | height: 28px; 11 | 12 | .col { 13 | display: table-cell; 14 | text-align: center; 15 | min-width: 28px; 16 | background-color: $theme-normal; 17 | border: 1px solid $theme-light; 18 | padding: 0 4px; 19 | 20 | .value { 21 | font-size: 12px; 22 | } 23 | 24 | &.selected { 25 | background-color: $color-selected; 26 | } 27 | 28 | &.patched { 29 | background-color: $color-patched; 30 | } 31 | 32 | &.index { 33 | background: none; 34 | border: none; 35 | color: $theme-light; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/core/renderers/Array2DRenderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Array1DRenderer, Renderer } from 'core/renderers'; 3 | import styles from './Array2DRenderer.module.scss'; 4 | import { classes } from 'common/util'; 5 | 6 | class Array2DRenderer extends Renderer { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.togglePan(true); 11 | this.toggleZoom(true); 12 | } 13 | 14 | renderData() { 15 | const { data } = this.props.data; 16 | 17 | const isArray1D = this instanceof Array1DRenderer; 18 | let longestRow = data.reduce((longestRow, row) => longestRow.length < row.length ? row : longestRow, []); 19 | 20 | return ( 21 | 23 | 24 | 25 | { 26 | !isArray1D && 27 | 34 | )) 35 | } 36 | 37 | { 38 | data.map((row, i) => ( 39 | 40 | { 41 | !isArray1D && 42 | 45 | } 46 | { 47 | row.map((col, j) => ( 48 | 52 | )) 53 | } 54 | 55 | )) 56 | } 57 | 58 |
28 | } 29 | { 30 | longestRow.map((_, i) => ( 31 | 32 | {i} 33 |
43 | {i} 44 | 50 | {this.toString(col.value)} 51 |
59 | ); 60 | } 61 | } 62 | 63 | export default Array2DRenderer; 64 | 65 | -------------------------------------------------------------------------------- /src/core/renderers/ChartRenderer/ChartRenderer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | -------------------------------------------------------------------------------- /src/core/renderers/ChartRenderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | import { Array1DRenderer } from 'core/renderers'; 4 | import styles from './ChartRenderer.module.scss'; 5 | 6 | class ChartRenderer extends Array1DRenderer { 7 | renderData() { 8 | const { data: [row] } = this.props.data; 9 | 10 | const chartData = { 11 | labels: row.map(col => `${col.value}`), 12 | datasets: [{ 13 | backgroundColor: row.map(col => col.patched ? styles.colorPatched : col.selected ? styles.colorSelected : styles.colorFont), 14 | data: row.map(col => col.value), 15 | }], 16 | }; 17 | return ( 18 | 31 | ); 32 | } 33 | } 34 | 35 | export default ChartRenderer; 36 | 37 | -------------------------------------------------------------------------------- /src/core/renderers/GraphRenderer/GraphRenderer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .graph { 4 | flex: 1; 5 | align-self: stretch; 6 | 7 | .node { 8 | .circle { 9 | fill: $theme-light; 10 | stroke: $color-font; 11 | stroke-width: 1; 12 | } 13 | 14 | .id { 15 | fill: $color-font; 16 | alignment-baseline: central; 17 | text-anchor: middle; 18 | } 19 | 20 | .weight { 21 | fill: white; 22 | font-weight: bold; 23 | alignment-baseline: central; 24 | text-anchor: left; 25 | } 26 | 27 | &.selected { 28 | .circle { 29 | fill: $color-selected; 30 | stroke: $color-selected; 31 | } 32 | } 33 | 34 | &.visited { 35 | .circle { 36 | fill: $color-patched; 37 | stroke: $color-patched; 38 | } 39 | } 40 | } 41 | 42 | .edge { 43 | .line { 44 | stroke: $color-font; 45 | stroke-width: 2; 46 | 47 | &.directed { 48 | marker-end: url(#markerArrow); 49 | } 50 | } 51 | 52 | .weight { 53 | fill: $color-font; 54 | alignment-baseline: baseline; 55 | text-anchor: middle; 56 | } 57 | 58 | &.selected { 59 | .line { 60 | stroke: $color-selected; 61 | 62 | &.directed { 63 | marker-end: url(#markerArrowSelected); 64 | } 65 | } 66 | 67 | .weight { 68 | fill: $color-selected; 69 | } 70 | } 71 | 72 | &.visited { 73 | .line { 74 | stroke: $color-patched; 75 | 76 | &.directed { 77 | marker-end: url(#markerArrowVisited); 78 | } 79 | } 80 | 81 | .weight { 82 | fill: $color-patched; 83 | } 84 | } 85 | } 86 | 87 | .arrow { 88 | fill: $color-font; 89 | 90 | &.selected { 91 | fill: $color-selected; 92 | } 93 | 94 | &.visited { 95 | fill: $color-patched; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/core/renderers/GraphRenderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Renderer } from 'core/renderers'; 3 | import { classes, distance } from 'common/util'; 4 | import styles from './GraphRenderer.module.scss'; 5 | 6 | class GraphRenderer extends Renderer { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.elementRef = React.createRef(); 11 | this.selectedNode = null; 12 | 13 | this.togglePan(true); 14 | this.toggleZoom(true); 15 | } 16 | 17 | handleMouseDown(e) { 18 | super.handleMouseDown(e); 19 | const coords = this.computeCoords(e); 20 | const { nodes, dimensions } = this.props.data; 21 | const { nodeRadius } = dimensions; 22 | this.selectedNode = nodes.find(node => distance(coords, node) <= nodeRadius); 23 | } 24 | 25 | handleMouseMove(e) { 26 | if (this.selectedNode) { 27 | const { x, y } = this.computeCoords(e); 28 | const node = this.props.data.findNode(this.selectedNode.id); 29 | node.x = x; 30 | node.y = y; 31 | this.refresh(); 32 | } else { 33 | super.handleMouseMove(e); 34 | } 35 | } 36 | 37 | computeCoords(e) { 38 | const svg = this.elementRef.current; 39 | const s = svg.createSVGPoint(); 40 | s.x = e.clientX; 41 | s.y = e.clientY; 42 | const { x, y } = s.matrixTransform(svg.getScreenCTM().inverse()); 43 | return { x, y }; 44 | } 45 | 46 | renderData() { 47 | const { nodes, edges, isDirected, isWeighted, dimensions } = this.props.data; 48 | const { baseWidth, baseHeight, nodeRadius, arrowGap, nodeWeightGap, edgeWeightGap } = dimensions; 49 | const viewBox = [ 50 | (this.centerX - baseWidth / 2) * this.zoom, 51 | (this.centerY - baseHeight / 2) * this.zoom, 52 | baseWidth * this.zoom, 53 | baseHeight * this.zoom, 54 | ]; 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | { 69 | edges.sort((a, b) => a.visitedCount - b.visitedCount).map(edge => { 70 | const { source, target, weight, visitedCount, selectedCount } = edge; 71 | const sourceNode = this.props.data.findNode(source); 72 | const targetNode = this.props.data.findNode(target); 73 | if (!sourceNode || !targetNode) return undefined; 74 | const { x: sx, y: sy } = sourceNode; 75 | let { x: ex, y: ey } = targetNode; 76 | const mx = (sx + ex) / 2; 77 | const my = (sy + ey) / 2; 78 | const dx = ex - sx; 79 | const dy = ey - sy; 80 | const degree = Math.atan2(dy, dx) / Math.PI * 180; 81 | if (isDirected) { 82 | const length = Math.sqrt(dx * dx + dy * dy); 83 | if (length !== 0) { 84 | ex = sx + dx / length * (length - nodeRadius - arrowGap); 85 | ey = sy + dy / length * (length - nodeRadius - arrowGap); 86 | } 87 | } 88 | 89 | return ( 90 | 92 | 93 | { 94 | isWeighted && 95 | 96 | {this.toString(weight)} 98 | 99 | } 100 | 101 | ); 102 | }) 103 | } 104 | { 105 | nodes.map(node => { 106 | const { id, x, y, weight, visitedCount, selectedCount } = node; 107 | return ( 108 | 110 | 111 | {id} 112 | { 113 | isWeighted && 114 | {this.toString(weight)} 115 | } 116 | 117 | ); 118 | }) 119 | } 120 | 121 | ); 122 | } 123 | } 124 | 125 | export default GraphRenderer; 126 | 127 | -------------------------------------------------------------------------------- /src/core/renderers/LogRenderer/LogRenderer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .log { 4 | flex: 1; 5 | align-self: stretch; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: stretch; 9 | overflow-y: auto; 10 | 11 | .content { 12 | padding: 24px; 13 | font-family: $font-family-monospace; 14 | white-space: pre-wrap; 15 | line-height: 1.6; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/renderers/LogRenderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Renderer } from 'core/renderers'; 3 | import styles from './LogRenderer.module.scss'; 4 | 5 | class LogRenderer extends Renderer { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.elementRef = React.createRef(); 10 | } 11 | 12 | componentDidUpdate(prevProps, prevState, snapshot) { 13 | super.componentDidUpdate(prevProps, prevState, snapshot); 14 | const div = this.elementRef.current; 15 | div.scrollTop = div.scrollHeight; 16 | } 17 | 18 | renderData() { 19 | const { log } = this.props.data; 20 | 21 | return ( 22 |
23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default LogRenderer; 30 | 31 | -------------------------------------------------------------------------------- /src/core/renderers/MarkdownRenderer/MarkdownRenderer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .markdown { 4 | flex: 1; 5 | align-self: stretch; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: stretch; 9 | overflow-y: auto; 10 | 11 | .content { 12 | padding: 24px; 13 | font-size: $font-size-large; 14 | 15 | a { 16 | text-decoration: underline; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/renderers/MarkdownRenderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Renderer } from 'core/renderers'; 3 | import styles from './MarkdownRenderer.module.scss'; 4 | import ReactMarkdown from 'react-markdown'; 5 | 6 | class MarkdownRenderer extends Renderer { 7 | renderData() { 8 | const { markdown } = this.props.data; 9 | 10 | const heading = ({ level, children, ...rest }) => { 11 | const HeadingComponent = [ 12 | props =>

, 13 | props =>

, 14 | props =>

, 15 | props =>

, 16 | props =>

, 17 | props =>
, 18 | ][level - 1]; 19 | 20 | const idfy = text => text.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-'); 21 | 22 | const getText = children => { 23 | return React.Children.map(children, child => { 24 | if (!child) return ''; 25 | if (typeof child === 'string') return child; 26 | if ('props' in child) return getText(child.props.children); 27 | return ''; 28 | }).join(''); 29 | }; 30 | 31 | const id = idfy(getText(children)); 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | const link = ({ href, ...rest }) => { 41 | return /^#/.test(href) ? ( 42 | 43 | ) : ( 44 | 45 | ); 46 | }; 47 | 48 | const image = ({ src, ...rest }) => { 49 | let newSrc = src; 50 | let style = { maxWidth: '100%' }; 51 | const CODECOGS = 'https://latex.codecogs.com/svg.latex?'; 52 | const WIKIMEDIA_IMAGE = 'https://upload.wikimedia.org/wikipedia/'; 53 | const WIKIMEDIA_MATH = 'https://wikimedia.org/api/rest_v1/media/math/render/svg/'; 54 | if (src.startsWith(CODECOGS)) { 55 | const latex = src.substring(CODECOGS.length); 56 | newSrc = `${CODECOGS}\\color{White}${latex}`; 57 | } else if (src.startsWith(WIKIMEDIA_IMAGE)) { 58 | style.backgroundColor = 'white'; 59 | } else if (src.startsWith(WIKIMEDIA_MATH)) { 60 | style.filter = 'invert(100%)'; 61 | } 62 | return ; 63 | }; 64 | 65 | return ( 66 |
67 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default MarkdownRenderer; 75 | 76 | -------------------------------------------------------------------------------- /src/core/renderers/Renderer/Renderer.module.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | .renderer { 4 | position: relative; 5 | flex: 1; 6 | flex-direction: column; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | min-height: 0; 11 | 12 | &:first-child { 13 | border-top: none; 14 | } 15 | 16 | .title { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | background-color: $theme-light; 21 | color: $color-font; 22 | padding: 4px 6px; 23 | font-size: $font-size-large; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/core/renderers/Renderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Renderer.module.scss'; 3 | import { Ellipsis } from 'components'; 4 | import { classes } from 'common/util'; 5 | 6 | class Renderer extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.handleMouseDown = this.handleMouseDown.bind(this); 11 | this.handleMouseMove = this.handleMouseMove.bind(this); 12 | this.handleMouseUp = this.handleMouseUp.bind(this); 13 | this.handleWheel = this.handleWheel.bind(this); 14 | 15 | this._handleMouseDown = this.handleMouseDown; 16 | this._handleWheel = this.handleWheel; 17 | this.togglePan(false); 18 | this.toggleZoom(false); 19 | 20 | this.lastX = null; 21 | this.lastY = null; 22 | this.centerX = 0; 23 | this.centerY = 0; 24 | this.zoom = 1; 25 | this.zoomFactor = 1.01; 26 | this.zoomMax = 20; 27 | this.zoomMin = 1 / 20; 28 | } 29 | 30 | componentDidUpdate(prevProps, prevState, snapshot) { 31 | } 32 | 33 | togglePan(enable = !this.handleMouseDown) { 34 | this.handleMouseDown = enable ? this._handleMouseDown : undefined; 35 | } 36 | 37 | toggleZoom(enable = !this.handleWheel) { 38 | this.handleWheel = enable ? this._handleWheel : undefined; 39 | } 40 | 41 | handleMouseDown(e) { 42 | const { clientX, clientY } = e; 43 | this.lastX = clientX; 44 | this.lastY = clientY; 45 | document.addEventListener('mousemove', this.handleMouseMove); 46 | document.addEventListener('mouseup', this.handleMouseUp); 47 | } 48 | 49 | handleMouseMove(e) { 50 | const { clientX, clientY } = e; 51 | const dx = clientX - this.lastX; 52 | const dy = clientY - this.lastY; 53 | this.centerX -= dx; 54 | this.centerY -= dy; 55 | this.refresh(); 56 | this.lastX = clientX; 57 | this.lastY = clientY; 58 | } 59 | 60 | handleMouseUp(e) { 61 | document.removeEventListener('mousemove', this.handleMouseMove); 62 | document.removeEventListener('mouseup', this.handleMouseUp); 63 | } 64 | 65 | handleWheel(e) { 66 | e.preventDefault(); 67 | const { deltaY } = e; 68 | this.zoom *= Math.pow(this.zoomFactor, deltaY); 69 | this.zoom = Math.min(this.zoomMax, Math.max(this.zoomMin, this.zoom)); 70 | this.refresh(); 71 | } 72 | 73 | toString(value) { 74 | switch (typeof(value)) { 75 | case 'number': 76 | return [Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER, 0x7fffffff].includes(value) ? '∞' : 77 | [Number.NEGATIVE_INFINITY, Number.MIN_SAFE_INTEGER, -0x80000000].includes(value) ? '-∞' : 78 | Number.isInteger(value) ? value.toString() : 79 | value.toFixed(3); 80 | case 'boolean': 81 | return value ? 'T' : 'F'; 82 | default: 83 | return value; 84 | } 85 | } 86 | 87 | refresh() { 88 | this.forceUpdate(); 89 | } 90 | 91 | renderData() { 92 | return null; 93 | } 94 | 95 | render() { 96 | const { className, title } = this.props; 97 | 98 | return ( 99 |
101 | {title} 102 | { 103 | this.renderData() 104 | } 105 |
106 | ); 107 | } 108 | } 109 | 110 | export default Renderer; 111 | 112 | -------------------------------------------------------------------------------- /src/core/renderers/index.js: -------------------------------------------------------------------------------- 1 | export { default as Renderer } from './Renderer'; 2 | export { default as MarkdownRenderer } from './MarkdownRenderer'; 3 | export { default as LogRenderer } from './LogRenderer'; 4 | export { default as Array2DRenderer } from './Array2DRenderer'; 5 | export { default as Array1DRenderer } from './Array1DRenderer'; 6 | export { default as ChartRenderer } from './ChartRenderer'; 7 | export { default as GraphRenderer } from './GraphRenderer'; 8 | -------------------------------------------------------------------------------- /src/core/tracers/Array1DTracer.js: -------------------------------------------------------------------------------- 1 | import { Array2DTracer } from 'core/tracers'; 2 | import { Array1DRenderer } from 'core/renderers'; 3 | 4 | class Array1DTracer extends Array2DTracer { 5 | getRendererClass() { 6 | return Array1DRenderer; 7 | } 8 | 9 | init() { 10 | super.init(); 11 | this.chartTracer = null; 12 | } 13 | 14 | set(array1d = []) { 15 | const array2d = [array1d]; 16 | super.set(array2d); 17 | this.syncChartTracer(); 18 | } 19 | 20 | patch(x, v) { 21 | super.patch(0, x, v); 22 | } 23 | 24 | depatch(x) { 25 | super.depatch(0, x); 26 | } 27 | 28 | select(sx, ex = sx) { 29 | super.select(0, sx, 0, ex); 30 | } 31 | 32 | deselect(sx, ex = sx) { 33 | super.deselect(0, sx, 0, ex); 34 | } 35 | 36 | chart(key) { 37 | this.chartTracer = key ? this.getObject(key) : null; 38 | this.syncChartTracer(); 39 | } 40 | 41 | syncChartTracer() { 42 | if (this.chartTracer) this.chartTracer.data = this.data; 43 | } 44 | } 45 | 46 | export default Array1DTracer; 47 | -------------------------------------------------------------------------------- /src/core/tracers/Array2DTracer.js: -------------------------------------------------------------------------------- 1 | import { Tracer } from 'core/tracers'; 2 | import { Array2DRenderer } from 'core/renderers'; 3 | 4 | class Element { 5 | constructor(value) { 6 | this.value = value; 7 | this.patched = false; 8 | this.selected = false; 9 | } 10 | } 11 | 12 | class Array2DTracer extends Tracer { 13 | getRendererClass() { 14 | return Array2DRenderer; 15 | } 16 | 17 | set(array2d = []) { 18 | this.data = array2d.map(array1d => [...array1d].map(value => new Element(value))); 19 | super.set(); 20 | } 21 | 22 | patch(x, y, v = this.data[x][y].value) { 23 | if (!this.data[x][y]) this.data[x][y] = new Element(); 24 | this.data[x][y].value = v; 25 | this.data[x][y].patched = true; 26 | } 27 | 28 | depatch(x, y) { 29 | this.data[x][y].patched = false; 30 | } 31 | 32 | select(sx, sy, ex = sx, ey = sy) { 33 | for (let x = sx; x <= ex; x++) { 34 | for (let y = sy; y <= ey; y++) { 35 | this.data[x][y].selected = true; 36 | } 37 | } 38 | } 39 | 40 | selectRow(x, sy, ey) { 41 | this.select(x, sy, x, ey); 42 | } 43 | 44 | selectCol(y, sx, ex) { 45 | this.select(sx, y, ex, y); 46 | } 47 | 48 | deselect(sx, sy, ex = sx, ey = sy) { 49 | for (let x = sx; x <= ex; x++) { 50 | for (let y = sy; y <= ey; y++) { 51 | this.data[x][y].selected = false; 52 | } 53 | } 54 | } 55 | 56 | deselectRow(x, sy, ey) { 57 | this.deselect(x, sy, x, ey); 58 | } 59 | 60 | deselectCol(y, sx, ex) { 61 | this.deselect(sx, y, ex, y); 62 | } 63 | } 64 | 65 | export default Array2DTracer; 66 | -------------------------------------------------------------------------------- /src/core/tracers/ChartTracer.js: -------------------------------------------------------------------------------- 1 | import { Array1DTracer } from 'core/tracers'; 2 | import { ChartRenderer } from 'core/renderers'; 3 | 4 | class ChartTracer extends Array1DTracer { 5 | getRendererClass() { 6 | return ChartRenderer; 7 | } 8 | } 9 | 10 | export default ChartTracer; 11 | -------------------------------------------------------------------------------- /src/core/tracers/GraphTracer.js: -------------------------------------------------------------------------------- 1 | import { Tracer } from 'core/tracers'; 2 | import { distance } from 'common/util'; 3 | import { GraphRenderer } from 'core/renderers'; 4 | 5 | class GraphTracer extends Tracer { 6 | getRendererClass() { 7 | return GraphRenderer; 8 | } 9 | 10 | init() { 11 | super.init(); 12 | this.dimensions = { 13 | baseWidth: 320, 14 | baseHeight: 320, 15 | padding: 32, 16 | nodeRadius: 12, 17 | arrowGap: 4, 18 | nodeWeightGap: 4, 19 | edgeWeightGap: 4, 20 | }; 21 | this.isDirected = true; 22 | this.isWeighted = false; 23 | this.callLayout = { method: this.layoutCircle, args: [] }; 24 | this.logTracer = null; 25 | } 26 | 27 | set(array2d = []) { 28 | this.nodes = []; 29 | this.edges = []; 30 | for (let i = 0; i < array2d.length; i++) { 31 | this.addNode(i); 32 | for (let j = 0; j < array2d.length; j++) { 33 | const value = array2d[i][j]; 34 | if (value) { 35 | this.addEdge(i, j, this.isWeighted ? value : null); 36 | } 37 | } 38 | } 39 | this.layout(); 40 | super.set(); 41 | } 42 | 43 | directed(isDirected = true) { 44 | this.isDirected = isDirected; 45 | } 46 | 47 | weighted(isWeighted = true) { 48 | this.isWeighted = isWeighted; 49 | } 50 | 51 | addNode(id, weight = null, x = 0, y = 0, visitedCount = 0, selectedCount = 0) { 52 | if (this.findNode(id)) return; 53 | this.nodes.push({ id, weight, x, y, visitedCount, selectedCount }); 54 | this.layout(); 55 | } 56 | 57 | updateNode(id, weight, x, y, visitedCount, selectedCount) { 58 | const node = this.findNode(id); 59 | const update = { weight, x, y, visitedCount, selectedCount }; 60 | Object.keys(update).forEach(key => { 61 | if (update[key] === undefined) delete update[key]; 62 | }); 63 | Object.assign(node, update); 64 | } 65 | 66 | removeNode(id) { 67 | const node = this.findNode(id); 68 | if (!node) return; 69 | const index = this.nodes.indexOf(node); 70 | this.nodes.splice(index, 1); 71 | this.layout(); 72 | } 73 | 74 | addEdge(source, target, weight = null, visitedCount = 0, selectedCount = 0) { 75 | if (this.findEdge(source, target)) return; 76 | this.edges.push({ source, target, weight, visitedCount, selectedCount }); 77 | this.layout(); 78 | } 79 | 80 | updateEdge(source, target, weight, visitedCount, selectedCount) { 81 | const edge = this.findEdge(source, target); 82 | const update = { weight, visitedCount, selectedCount }; 83 | Object.keys(update).forEach(key => { 84 | if (update[key] === undefined) delete update[key]; 85 | }); 86 | Object.assign(edge, update); 87 | } 88 | 89 | removeEdge(source, target) { 90 | const edge = this.findEdge(source, target); 91 | if (!edge) return; 92 | const index = this.edges.indexOf(edge); 93 | this.edges.splice(index, 1); 94 | this.layout(); 95 | } 96 | 97 | findNode(id) { 98 | return this.nodes.find(node => node.id === id); 99 | } 100 | 101 | findEdge(source, target, isDirected = this.isDirected) { 102 | if (isDirected) { 103 | return this.edges.find(edge => edge.source === source && edge.target === target); 104 | } else { 105 | return this.edges.find(edge => 106 | (edge.source === source && edge.target === target) || 107 | (edge.source === target && edge.target === source)); 108 | } 109 | } 110 | 111 | findLinkedEdges(source, isDirected = this.isDirected) { 112 | if (isDirected) { 113 | return this.edges.filter(edge => edge.source === source); 114 | } else { 115 | return this.edges.filter(edge => edge.source === source || edge.target === source); 116 | } 117 | } 118 | 119 | findLinkedNodeIds(source, isDirected = this.isDirected) { 120 | const edges = this.findLinkedEdges(source, isDirected); 121 | return edges.map(edge => edge.source === source ? edge.target : edge.source); 122 | } 123 | 124 | findLinkedNodes(source, isDirected = this.isDirected) { 125 | const ids = this.findLinkedNodeIds(source, isDirected); 126 | return ids.map(id => this.findNode(id)); 127 | } 128 | 129 | getRect() { 130 | const { baseWidth, baseHeight, padding } = this.dimensions; 131 | const left = -baseWidth / 2 + padding; 132 | const top = -baseHeight / 2 + padding; 133 | const right = baseWidth / 2 - padding; 134 | const bottom = baseHeight / 2 - padding; 135 | const width = right - left; 136 | const height = bottom - top; 137 | return { left, top, right, bottom, width, height }; 138 | } 139 | 140 | layout() { 141 | const { method, args } = this.callLayout; 142 | method.apply(this, args); 143 | } 144 | 145 | layoutCircle() { 146 | this.callLayout = { method: this.layoutCircle, args: arguments }; 147 | const rect = this.getRect(); 148 | const unitAngle = 2 * Math.PI / this.nodes.length; 149 | let angle = -Math.PI / 2; 150 | for (const node of this.nodes) { 151 | const x = Math.cos(angle) * rect.width / 2; 152 | const y = Math.sin(angle) * rect.height / 2; 153 | node.x = x; 154 | node.y = y; 155 | angle += unitAngle; 156 | } 157 | } 158 | 159 | layoutTree(root = 0, sorted = false) { 160 | this.callLayout = { method: this.layoutTree, args: arguments }; 161 | const rect = this.getRect(); 162 | 163 | if (this.nodes.length === 1) { 164 | const [node] = this.nodes; 165 | node.x = (rect.left + rect.right) / 2; 166 | node.y = (rect.top + rect.bottom) / 2; 167 | return; 168 | } 169 | 170 | let maxDepth = 0; 171 | const leafCounts = {}; 172 | let marked = {}; 173 | const recursiveAnalyze = (id, depth) => { 174 | marked[id] = true; 175 | leafCounts[id] = 0; 176 | if (maxDepth < depth) maxDepth = depth; 177 | const linkedNodeIds = this.findLinkedNodeIds(id, false); 178 | for (const linkedNodeId of linkedNodeIds) { 179 | if (marked[linkedNodeId]) continue; 180 | leafCounts[id] += recursiveAnalyze(linkedNodeId, depth + 1); 181 | } 182 | if (leafCounts[id] === 0) leafCounts[id] = 1; 183 | return leafCounts[id]; 184 | }; 185 | recursiveAnalyze(root, 0); 186 | 187 | const hGap = rect.width / leafCounts[root]; 188 | const vGap = rect.height / maxDepth; 189 | marked = {}; 190 | const recursivePosition = (node, h, v) => { 191 | marked[node.id] = true; 192 | node.x = rect.left + (h + leafCounts[node.id] / 2) * hGap; 193 | node.y = rect.top + v * vGap; 194 | const linkedNodes = this.findLinkedNodes(node.id, false); 195 | if (sorted) linkedNodes.sort((a, b) => a.id - b.id); 196 | for (const linkedNode of linkedNodes) { 197 | if (marked[linkedNode.id]) continue; 198 | recursivePosition(linkedNode, h, v + 1); 199 | h += leafCounts[linkedNode.id]; 200 | } 201 | }; 202 | const rootNode = this.findNode(root); 203 | recursivePosition(rootNode, 0, 0); 204 | } 205 | 206 | layoutRandom() { 207 | this.callLayout = { method: this.layoutRandom, args: arguments }; 208 | const rect = this.getRect(); 209 | const placedNodes = []; 210 | for (const node of this.nodes) { 211 | do { 212 | node.x = rect.left + Math.random() * rect.width; 213 | node.y = rect.top + Math.random() * rect.height; 214 | } while (placedNodes.find(placedNode => distance(node, placedNode) < 48)); 215 | placedNodes.push(node); 216 | } 217 | } 218 | 219 | visit(target, source, weight) { 220 | this.visitOrLeave(true, target, source, weight); 221 | } 222 | 223 | leave(target, source, weight) { 224 | this.visitOrLeave(false, target, source, weight); 225 | } 226 | 227 | visitOrLeave(visit, target, source = null, weight) { 228 | const edge = this.findEdge(source, target); 229 | if (edge) edge.visitedCount += visit ? 1 : -1; 230 | const node = this.findNode(target); 231 | if (weight !== undefined) node.weight = weight; 232 | node.visitedCount += visit ? 1 : -1; 233 | if (this.logTracer) { 234 | this.logTracer.println(visit ? (source || '') + ' -> ' + target : (source || '') + ' <- ' + target); 235 | } 236 | } 237 | 238 | select(target, source) { 239 | this.selectOrDeselect(true, target, source); 240 | } 241 | 242 | deselect(target, source) { 243 | this.selectOrDeselect(false, target, source); 244 | } 245 | 246 | selectOrDeselect(select, target, source = null) { 247 | const edge = this.findEdge(source, target); 248 | if (edge) edge.selectedCount += select ? 1 : -1; 249 | const node = this.findNode(target); 250 | node.selectedCount += select ? 1 : -1; 251 | if (this.logTracer) { 252 | this.logTracer.println(select ? (source || '') + ' => ' + target : (source || '') + ' <= ' + target); 253 | } 254 | } 255 | 256 | log(key) { 257 | this.logTracer = key ? this.getObject(key) : null; 258 | } 259 | } 260 | 261 | export default GraphTracer; 262 | -------------------------------------------------------------------------------- /src/core/tracers/LogTracer.js: -------------------------------------------------------------------------------- 1 | import { sprintf } from 'sprintf-js'; 2 | import { Tracer } from 'core/tracers'; 3 | import { LogRenderer } from 'core/renderers'; 4 | 5 | class LogTracer extends Tracer { 6 | getRendererClass() { 7 | return LogRenderer; 8 | } 9 | 10 | set(log = '') { 11 | this.log = log; 12 | super.set(); 13 | } 14 | 15 | print(message) { 16 | this.log += message; 17 | } 18 | 19 | println(message) { 20 | this.print(message + '\n'); 21 | } 22 | 23 | printf(format, ...args) { 24 | this.print(sprintf(format, ...args)); 25 | } 26 | } 27 | 28 | export default LogTracer; 29 | -------------------------------------------------------------------------------- /src/core/tracers/MarkdownTracer.js: -------------------------------------------------------------------------------- 1 | import { Tracer } from 'core/tracers'; 2 | import { MarkdownRenderer } from 'core/renderers'; 3 | 4 | class MarkdownTracer extends Tracer { 5 | getRendererClass() { 6 | return MarkdownRenderer; 7 | } 8 | 9 | set(markdown = '') { 10 | this.markdown = markdown; 11 | super.set(); 12 | } 13 | } 14 | 15 | export default MarkdownTracer; 16 | -------------------------------------------------------------------------------- /src/core/tracers/Tracer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Renderer } from 'core/renderers'; 3 | 4 | class Tracer { 5 | constructor(key, getObject, title) { 6 | this.key = key; 7 | this.getObject = getObject; 8 | this.title = title; 9 | this.init(); 10 | this.reset(); 11 | } 12 | 13 | getRendererClass() { 14 | return Renderer; 15 | } 16 | 17 | init() { 18 | } 19 | 20 | render() { 21 | const RendererClass = this.getRendererClass(); 22 | return ( 23 | 24 | ); 25 | } 26 | 27 | set() { 28 | } 29 | 30 | reset() { 31 | this.set(); 32 | } 33 | } 34 | 35 | export default Tracer; 36 | -------------------------------------------------------------------------------- /src/core/tracers/index.js: -------------------------------------------------------------------------------- 1 | export { default as Tracer } from './Tracer'; 2 | export { default as MarkdownTracer } from './MarkdownTracer'; 3 | export { default as LogTracer } from './LogTracer'; 4 | export { default as Array2DTracer } from './Array2DTracer'; 5 | export { default as Array1DTracer } from './Array1DTracer'; 6 | export { default as ChartTracer } from './ChartTracer'; 7 | export { default as GraphTracer } from './GraphTracer'; 8 | -------------------------------------------------------------------------------- /src/files/algorithm-visualizer/README.md: -------------------------------------------------------------------------------- 1 | ../../../README.md -------------------------------------------------------------------------------- /src/files/index.js: -------------------------------------------------------------------------------- 1 | import { createProjectFile, createUserFile } from 'common/util'; 2 | 3 | const getName = filePath => filePath.split('/').pop(); 4 | const getContent = filePath => require('!raw-loader!./' + filePath).default; 5 | const readProjectFile = filePath => createProjectFile(getName(filePath), getContent(filePath)); 6 | const readUserFile = filePath => createUserFile(getName(filePath), getContent(filePath)); 7 | 8 | export const CODE_CPP = readUserFile('skeletons/code.cpp'); 9 | export const CODE_JAVA = readUserFile('skeletons/code.java'); 10 | export const CODE_JS = readUserFile('skeletons/code.js'); 11 | export const README_MD = readProjectFile('algorithm-visualizer/README.md'); 12 | export const CONTRIBUTING_MD = readProjectFile('scratch-paper/CONTRIBUTING.md'); 13 | -------------------------------------------------------------------------------- /src/files/scratch-paper/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ../../../CONTRIBUTING.md -------------------------------------------------------------------------------- /src/files/skeletons/code.cpp: -------------------------------------------------------------------------------- 1 | // import visualization libraries { 2 | #include "algorithm-visualizer.h" 3 | // } 4 | 5 | #include 6 | #include 7 | 8 | // define tracer variables { 9 | Array2DTracer array2dTracer = Array2DTracer("Grid"); 10 | LogTracer logTracer = LogTracer("Console"); 11 | // } 12 | 13 | // define input variables 14 | std::vector messages{ 15 | "Visualize", 16 | "your", 17 | "own", 18 | "code", 19 | "here!", 20 | }; 21 | 22 | // highlight each line of messages recursively 23 | void highlight(int line) { 24 | if (line >= messages.size()) return; 25 | std::string message = messages[line]; 26 | // visualize { 27 | logTracer.println(message); 28 | array2dTracer.selectRow(line, 0, message.size() - 1); 29 | Tracer::delay(); 30 | array2dTracer.deselectRow(line, 0, message.size() - 1); 31 | // } 32 | highlight(line + 1); 33 | } 34 | 35 | int main() { 36 | // visualize { 37 | Layout::setRoot(VerticalLayout({array2dTracer, logTracer})); 38 | array2dTracer.set(messages); 39 | Tracer::delay(); 40 | // } 41 | highlight(0); 42 | return 0; 43 | } 44 | -------------------------------------------------------------------------------- /src/files/skeletons/code.java: -------------------------------------------------------------------------------- 1 | // import visualization libraries { 2 | import org.algorithm_visualizer.*; 3 | // } 4 | 5 | class Main { 6 | // define tracer variables { 7 | Array2DTracer array2dTracer = new Array2DTracer("Grid"); 8 | LogTracer logTracer = new LogTracer("Console"); 9 | // } 10 | 11 | // define input variables 12 | String[] messages = { 13 | "Visualize", 14 | "your", 15 | "own", 16 | "code", 17 | "here!", 18 | }; 19 | 20 | // highlight each line of messages recursively 21 | void highlight(int line) { 22 | if (line >= messages.length) return; 23 | String message = messages[line]; 24 | // visualize { 25 | logTracer.println(message); 26 | array2dTracer.selectRow(line, 0, message.length() - 1); 27 | Tracer.delay(); 28 | array2dTracer.deselectRow(line, 0, message.length() - 1); 29 | // } 30 | highlight(line + 1); 31 | } 32 | 33 | Main() { 34 | // visualize { 35 | Layout.setRoot(new VerticalLayout(new Commander[]{array2dTracer, logTracer})); 36 | array2dTracer.set(messages); 37 | Tracer.delay(); 38 | // } 39 | highlight(0); 40 | } 41 | 42 | public static void main(String[] args) { 43 | new Main(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/files/skeletons/code.js: -------------------------------------------------------------------------------- 1 | // import visualization libraries { 2 | const { Array2DTracer, Layout, LogTracer, Tracer, VerticalLayout } = require('algorithm-visualizer'); 3 | // } 4 | 5 | // define tracer variables { 6 | const array2dTracer = new Array2DTracer('Grid'); 7 | const logTracer = new LogTracer('Console'); 8 | // } 9 | 10 | // define input variables 11 | const messages = [ 12 | 'Visualize', 13 | 'your', 14 | 'own', 15 | 'code', 16 | 'here!', 17 | ]; 18 | 19 | // highlight each line of messages recursively 20 | function highlight(line) { 21 | if (line >= messages.length) return; 22 | const message = messages[line]; 23 | // visualize { 24 | logTracer.println(message); 25 | array2dTracer.selectRow(line, 0, message.length - 1); 26 | Tracer.delay(); 27 | array2dTracer.deselectRow(line, 0, message.length - 1); 28 | // } 29 | highlight(line + 1); 30 | } 31 | 32 | (function main() { 33 | // visualize { 34 | Layout.setRoot(new VerticalLayout([array2dTracer, logTracer])); 35 | array2dTracer.set(messages); 36 | Tracer.delay(); 37 | // } 38 | highlight(0); 39 | })(); 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { combineReducers, createStore } from 'redux'; 4 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 5 | import { Provider } from 'react-redux'; 6 | import { routerReducer } from 'react-router-redux'; 7 | import App from 'components/App'; 8 | import * as reducers from 'reducers'; 9 | import './stylesheet.scss'; 10 | 11 | const store = createStore(combineReducers({ ...reducers, routing: routerReducer })); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | , document.getElementById('root')); 23 | -------------------------------------------------------------------------------- /src/reducers/current.js: -------------------------------------------------------------------------------- 1 | import { combineActions, createAction, handleActions } from 'redux-actions'; 2 | import { README_MD } from 'files'; 3 | import { extension, isSaved } from 'common/util'; 4 | 5 | const prefix = 'CURRENT'; 6 | 7 | const setHome = createAction(`${prefix}/SET_HOME`, () => defaultState); 8 | const setAlgorithm = createAction(`${prefix}/SET_ALGORITHM`, ({ categoryKey, categoryName, algorithmKey, algorithmName, files, description }) => ({ 9 | algorithm: { categoryKey, algorithmKey }, 10 | titles: [categoryName, algorithmName], 11 | files, 12 | description, 13 | })); 14 | const setScratchPaper = createAction(`${prefix}/SET_SCRATCH_PAPER`, ({ login, gistId, title, files }) => ({ 15 | scratchPaper: { login, gistId }, 16 | titles: ['Scratch Paper', title], 17 | files, 18 | description: homeDescription, 19 | })); 20 | const setEditingFile = createAction(`${prefix}/SET_EDITING_FILE`, file => ({ file })); 21 | const modifyTitle = createAction(`${prefix}/MODIFY_TITLE`, title => ({ title })); 22 | const addFile = createAction(`${prefix}/ADD_FILE`, file => ({ file })); 23 | const renameFile = createAction(`${prefix}/RENAME_FILE`, (file, name) => ({ file, name })); 24 | const modifyFile = createAction(`${prefix}/MODIFY_FILE`, (file, content) => ({ file, content })); 25 | const deleteFile = createAction(`${prefix}/DELETE_FILE`, file => ({ file })); 26 | 27 | export const actions = { 28 | setHome, 29 | setAlgorithm, 30 | setScratchPaper, 31 | setEditingFile, 32 | modifyTitle, 33 | addFile, 34 | modifyFile, 35 | deleteFile, 36 | renameFile, 37 | }; 38 | 39 | const homeTitles = ['Algorithm Visualizer']; 40 | const homeFiles = [README_MD]; 41 | const homeDescription = 'Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.'; 42 | const defaultState = { 43 | algorithm: { 44 | categoryKey: 'algorithm-visualizer', 45 | algorithmKey: 'home', 46 | }, 47 | scratchPaper: undefined, 48 | titles: homeTitles, 49 | files: homeFiles, 50 | lastTitles: homeTitles, 51 | lastFiles: homeFiles, 52 | description: homeDescription, 53 | editingFile: undefined, 54 | shouldBuild: true, 55 | saved: true, 56 | }; 57 | 58 | export default handleActions({ 59 | [combineActions( 60 | setHome, 61 | setAlgorithm, 62 | setScratchPaper, 63 | )]: (state, { payload }) => { 64 | const { algorithm, scratchPaper, titles, files, description } = payload; 65 | return { 66 | ...state, 67 | algorithm, 68 | scratchPaper, 69 | titles, 70 | files, 71 | lastTitles: titles, 72 | lastFiles: files, 73 | description, 74 | editingFile: undefined, 75 | shouldBuild: true, 76 | saved: true, 77 | }; 78 | }, 79 | [setEditingFile]: (state, { payload }) => { 80 | const { file } = payload; 81 | return { 82 | ...state, 83 | editingFile: file, 84 | shouldBuild: true, 85 | }; 86 | }, 87 | [modifyTitle]: (state, { payload }) => { 88 | const { title } = payload; 89 | const newState = { 90 | ...state, 91 | titles: [state.titles[0], title], 92 | }; 93 | return { 94 | ...newState, 95 | saved: isSaved(newState), 96 | }; 97 | }, 98 | [addFile]: (state, { payload }) => { 99 | const { file } = payload; 100 | const newState = { 101 | ...state, 102 | files: [...state.files, file], 103 | editingFile: file, 104 | shouldBuild: true, 105 | }; 106 | return { 107 | ...newState, 108 | saved: isSaved(newState), 109 | }; 110 | }, 111 | [combineActions( 112 | renameFile, 113 | modifyFile, 114 | )]: (state, { payload }) => { 115 | const { file, ...update } = payload; 116 | const editingFile = { ...file, ...update }; 117 | const newState = { 118 | ...state, 119 | files: state.files.map(oldFile => oldFile === file ? editingFile : oldFile), 120 | editingFile, 121 | shouldBuild: extension(editingFile.name) === 'md', 122 | }; 123 | return { 124 | ...newState, 125 | saved: isSaved(newState), 126 | }; 127 | }, 128 | [deleteFile]: (state, { payload }) => { 129 | const { file } = payload; 130 | const index = state.files.indexOf(file); 131 | const files = state.files.filter(oldFile => oldFile !== file); 132 | const editingFile = files[Math.min(index, files.length - 1)]; 133 | const newState = { 134 | ...state, 135 | files, 136 | editingFile, 137 | shouldBuild: true, 138 | }; 139 | return { 140 | ...newState, 141 | saved: isSaved(newState), 142 | }; 143 | }, 144 | }, defaultState); 145 | -------------------------------------------------------------------------------- /src/reducers/directory.js: -------------------------------------------------------------------------------- 1 | import { combineActions, createAction, handleActions } from 'redux-actions'; 2 | 3 | const prefix = 'DIRECTORY'; 4 | 5 | const setCategories = createAction(`${prefix}/SET_CATEGORIES`, categories => ({ categories })); 6 | const setScratchPapers = createAction(`${prefix}/SET_SCRATCH_PAPERS`, scratchPapers => ({ scratchPapers })); 7 | 8 | export const actions = { 9 | setCategories, 10 | setScratchPapers, 11 | }; 12 | 13 | const defaultState = { 14 | categories: [], 15 | scratchPapers: [], 16 | }; 17 | 18 | export default handleActions({ 19 | [combineActions( 20 | setCategories, 21 | setScratchPapers, 22 | )]: (state, { payload }) => ({ 23 | ...state, 24 | ...payload, 25 | }), 26 | }, defaultState); 27 | -------------------------------------------------------------------------------- /src/reducers/env.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | import { combineActions, createAction, handleActions } from 'redux-actions'; 3 | 4 | const prefix = 'ENV'; 5 | 6 | const setExt = createAction(`${prefix}/SET_EXT`, ext => { 7 | Cookies.set('ext', ext); 8 | return { ext }; 9 | }); 10 | const setUser = createAction(`${prefix}/SET_USER`, user => ({ user })); 11 | 12 | export const actions = { 13 | setExt, 14 | setUser, 15 | }; 16 | 17 | const defaultState = { 18 | ext: Cookies.get('ext') || 'js', 19 | user: undefined, 20 | }; 21 | 22 | export default handleActions({ 23 | [combineActions( 24 | setExt, 25 | setUser, 26 | )]: (state, { payload }) => ({ 27 | ...state, 28 | ...payload, 29 | }), 30 | }, defaultState); 31 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { actions as currentActions } from './current'; 2 | import { actions as directoryActions } from './directory'; 3 | import { actions as envActions } from './env'; 4 | import { actions as playerActions } from './player'; 5 | import { actions as toastActions } from './toast'; 6 | 7 | export { default as current } from './current'; 8 | export { default as directory } from './directory'; 9 | export { default as env } from './env'; 10 | export { default as player } from './player'; 11 | export { default as toast } from './toast'; 12 | 13 | export const actions = { 14 | ...currentActions, 15 | ...directoryActions, 16 | ...envActions, 17 | ...playerActions, 18 | ...toastActions, 19 | }; 20 | -------------------------------------------------------------------------------- /src/reducers/player.js: -------------------------------------------------------------------------------- 1 | import { combineActions, createAction, handleActions } from 'redux-actions'; 2 | 3 | const prefix = 'PLAYER'; 4 | 5 | const setChunks = createAction(`${prefix}/SET_CHUNKS`, chunks => ({ chunks })); 6 | const setCursor = createAction(`${prefix}/SET_CURSOR`, cursor => ({ cursor })); 7 | const setLineIndicator = createAction(`${prefix}/SET_LINE_INDICATOR`, lineIndicator => ({ lineIndicator })); 8 | 9 | export const actions = { 10 | setChunks, 11 | setCursor, 12 | setLineIndicator, 13 | }; 14 | 15 | const defaultState = { 16 | chunks: [], 17 | cursor: 0, 18 | lineIndicator: undefined, 19 | }; 20 | 21 | export default handleActions({ 22 | [combineActions( 23 | setChunks, 24 | setCursor, 25 | setLineIndicator, 26 | )]: (state, { payload }) => ({ 27 | ...state, 28 | ...payload, 29 | }), 30 | }, defaultState); 31 | -------------------------------------------------------------------------------- /src/reducers/toast.js: -------------------------------------------------------------------------------- 1 | import { combineActions, createAction, handleActions } from 'redux-actions'; 2 | import uuid from 'uuid'; 3 | 4 | const prefix = 'TOAST'; 5 | 6 | const showSuccessToast = createAction(`${prefix}/SHOW_SUCCESS_TOAST`, message => ({ type: 'success', message })); 7 | const showErrorToast = createAction(`${prefix}/SHOW_ERROR_TOAST`, message => ({ type: 'error', message })); 8 | const hideToast = createAction(`${prefix}/HIDE_TOAST`, id => ({ id })); 9 | 10 | export const actions = { 11 | showSuccessToast, 12 | showErrorToast, 13 | hideToast, 14 | }; 15 | 16 | const defaultState = { 17 | toasts: [], 18 | }; 19 | 20 | export default handleActions({ 21 | [combineActions( 22 | showSuccessToast, 23 | showErrorToast, 24 | )]: (state, { payload }) => { 25 | const id = uuid.v4(); 26 | const toast = { 27 | id, 28 | ...payload, 29 | }; 30 | const toasts = [ 31 | ...state.toasts, 32 | toast, 33 | ]; 34 | return { 35 | ...state, 36 | toasts, 37 | }; 38 | }, 39 | [hideToast]: (state, { payload }) => { 40 | const { id } = payload; 41 | const toasts = state.toasts.filter(toast => toast.id !== id); 42 | return { 43 | ...state, 44 | toasts, 45 | }; 46 | }, 47 | }, defaultState); 48 | -------------------------------------------------------------------------------- /src/stylesheet.scss: -------------------------------------------------------------------------------- 1 | @import "~common/stylesheet/index"; 2 | 3 | html, 4 | body, 5 | #root { 6 | margin: 0; 7 | padding: 0; 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | } 12 | 13 | body { 14 | font-family: $font-family-normal; 15 | -webkit-font-smoothing: subpixel-antialiased; 16 | user-select: none; 17 | color: $color-font; 18 | font-size: $font-size-normal; 19 | } 20 | 21 | a { 22 | text-decoration: none; 23 | color: inherit; 24 | } 25 | 26 | * { 27 | box-sizing: border-box; 28 | } 29 | 30 | input, 31 | select, 32 | textarea, 33 | button { 34 | color: inherit; 35 | font-family: inherit; 36 | font-size: inherit; 37 | background: none; 38 | border: none; 39 | outline: none; 40 | min-width: 0; 41 | margin: 0; 42 | padding: 0; 43 | line-height: 1.15; 44 | } 45 | --------------------------------------------------------------------------------