├── .babelrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── algorithms ├── .babelrc ├── bfs.js ├── binary-search.js └── quicksort.js ├── binary-search.gif ├── components ├── __fixtures__ │ ├── illustrations │ │ ├── binary-search │ │ │ ├── beginning.js │ │ │ ├── comparing.js │ │ │ ├── found.js │ │ │ └── intro.js │ │ ├── quicksort │ │ │ ├── intro.js │ │ │ ├── intro │ │ │ │ └── blank.js │ │ │ └── outro │ │ │ │ └── blank.js │ │ ├── raw-data │ │ │ └── first-frame-wip.js │ │ └── shared │ │ │ ├── emoji-block │ │ │ ├── blank.js │ │ │ ├── empty.js │ │ │ └── glow.js │ │ │ ├── emoji-icon │ │ │ ├── bear.js │ │ │ ├── cat.js │ │ │ ├── dog.js │ │ │ ├── lion.js │ │ │ ├── no-entry.js │ │ │ ├── panda.js │ │ │ └── snail.js │ │ │ ├── label │ │ │ └── pivot.js │ │ │ └── number-var │ │ │ └── max-5.js │ ├── menu │ │ └── binary-search.js │ ├── page │ │ ├── bfs.js │ │ ├── binary-search.js │ │ └── quicksort.js │ ├── playback-controls │ │ ├── finished.js │ │ ├── playing.js │ │ └── stopped.js │ ├── player │ │ ├── binary-search-mid-way.js │ │ ├── binary-search.js │ │ └── quicksort.js │ ├── source-code │ │ ├── bfs.js │ │ ├── binary-search.js │ │ └── quicksort.js │ └── stack-entry │ │ ├── binary-search-beginning.js │ │ ├── binary-search-finished.js │ │ ├── binary-search-first-assign.js │ │ ├── binary-search-intro.js │ │ ├── binary-search-last-check.js │ │ └── binary-search-returning.js ├── illustrations │ ├── binary-search │ │ ├── binary-search.js │ │ ├── comparison.js │ │ ├── high.js │ │ ├── intro.js │ │ ├── item.js │ │ ├── list.js │ │ ├── low.js │ │ └── mid.js │ ├── quicksort │ │ ├── intro.js │ │ ├── outro.js │ │ └── quicksort.js │ ├── raw-data.js │ └── shared │ │ ├── emoji-block.js │ │ ├── emoji-icon.js │ │ ├── label.js │ │ └── number-var.js ├── menu.js ├── page.js ├── playback-controls.js ├── player.js ├── source-code.js └── stack-entry.js ├── cosmos ├── __snapshots__ │ └── cosmos.test.js.snap ├── cosmos.config.js ├── cosmos.proxies.js ├── cosmos.test.js └── proxies │ ├── bg-color-proxy.js │ ├── context-proxy.js │ ├── frame-proxy.js │ ├── global-css-proxy.js │ ├── global-style-proxy.js │ └── layout-proxy.js ├── frame ├── __tests__ │ └── quicksort.js ├── base.js ├── binary-search.js ├── quicksort.js └── raw-data.js ├── jest.setup.js ├── layout ├── base.js ├── bfs.js ├── binary-search.js ├── quicksort.js └── raw-data.js ├── next.config.js ├── package.json ├── pages ├── bfs.js ├── binary-search.js ├── index.js └── quicksort.js ├── static └── FiraCode-Light.woff ├── utils ├── __tests__ │ ├── stack-recursive.js │ └── stack-single.js ├── cache.js ├── names.js ├── offset-steps.js ├── pure-layout-component.js ├── stack.js ├── transition.js └── wobble.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "modules": "commonjs" }], "next/babel"], 3 | "plugins": ["babel-plugin-inline-react-svg"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .export 4 | cosmos-export 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | -------------------------------------------------------------------------------- /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, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | 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 hello@ovidiu.ch. 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 [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See [How to contribute.](README.md#how-to-contribute) 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What's up? 2 | 3 | REPLACE: Describe the issue or value proposition 4 | 5 | ### Mkay, tell me more... 6 | 7 | REPLACE: Do you have a solution in mind or is there anything else we should know? 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ovidiu Cherecheș 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 | # Illustrated Algorithms 2 | Algorithm → AST → CSS (3 x JavaScript) 3 | 4 | [![Binary search](binary-search.gif)](https://illustrated-algorithms.now.sh/) 5 | 6 | Inspired by [Grokking Algorithms](https://www.manning.com/books/grokking-algorithms) and [python-execution-trace](https://github.com/mihneadb/python-execution-trace), this project aims to reveal the mechanics behind algorithms via interactive visualizations of their execution. 7 | 8 | Visual representations of variables and operations augment the control flow, alongside actual source code. You can fast forward and rewind the execution to closely observe how an algorithm works. 9 | 10 | ## Disclaimer ✌️ 11 | 12 | Edge cases and optimizations are beyond the scope of this project. The featured implementations are chosen for their simplicity and do not promise to work for data sets different from the illustrated ones. Please rely on other resources for learning algorithms in depth, from Wikipedia to other [visualization](https://visualgo.net/) [projects](https://www.youtube.com/watch?v=ywWBy6J5gz8). Also see community-driven [Footnotes](#footnotes). Thanks. 13 | 14 | ## Principles 15 | 16 | - The same code that is displayed next to the illustration is also decorated using [babel-plugin-trace-execution](https://github.com/skidding/babel-plugin-trace-execution) and executed to record the context at every step. Literally the same source file. 17 | - Going back and forth between function execution (and call stack when algorithm uses recursion) is effortless. So is pausing and resuming. 18 | - Visualizations are easy to follow, fun to play with and simple enough to fit inside the screen of any modern phone. 19 | 20 | ## Work in progress 21 | 22 | - Follow [@skidding](https://twitter.com/skidding) for updates 23 | - Check out gifs attached to [Releases](https://github.com/skidding/illustrated-algorithms/releases) to see project evolution 24 | - See [How to contribute](#how-to-contribute) below 25 | 26 | ## Dynamic styles 27 | 28 | This project uses [styled-jsx](https://github.com/zeit/styled-jsx), but takes the idea of *CSS-in-JS* even further. Sizing, positioning and transition offsets are computed by JS, all before elements hit the DOM. This provides complete control over layout (e.g. font scaling relative to container width, rounded to a multiplier of 2) and animation (e.g. pausing in the middle of a transition and rewinding). It's a wild concept that hopefully gets mainstream someday. 29 | 30 | ## How to contribute 31 | 32 | Consider the following actions if you want to advance this project: 33 | 34 | - Find and/or fix bugs 35 | - Add tests to [babel-plugin-trace-execution](https://github.com/skidding/babel-plugin-trace-execution) 36 | - Improve rendering perf (already decent, but not ideal due to [how styles are applied](#dynamic-styles)) 37 | - Propose algorithms to add (that can fit in a func <=25 lines of ES6) 38 | - Create elegant illustrations (sketches/wireframes do) – **Hello graphic designers and people who draw!** 39 | 40 | Before submitting a PR, make sure to: 41 | - Briefly describe the value of your contribution 42 | - Stay in line with the project's mission (i.e. to make algorithms easy, see above sections) 43 | - Test code before committing it via `npm run test` 44 | - Thoroughly test the visual experience you're creating (e.g. algorithms must fit nicely on the screen) 45 | 46 | ## Development 47 | 48 | ```bash 49 | npm i 50 | # Start Next.js server (localhost:3000) 51 | npm run dev 52 | # Run tests 53 | npm test 54 | # Start React Cosmos playground (localhost:8989) 55 | npm run cosmos 56 | ``` 57 | 58 | ## Footnotes 59 | 60 | While this project doesn't focus on algorithm implementation specifics, here's a list of valuable insights brought up by the community which serves to complement the visuals. 61 | 62 | #### Binary Search 63 | 64 | - [#21](https://github.com/skidding/illustrated-algorithms/issues/21) Calculating `mid` can be improved to avoid overflow when list is sufficiently large enough ([@mhaji](https://github.com/mhaji)) 65 | 66 | #### Quicksort 67 | 68 | - [#19](https://github.com/skidding/illustrated-algorithms/issues/19) Extending implementation to support duplicates ([@ACollectionOfAtoms](https://github.com/ACollectionOfAtoms)) 69 | 70 | --- 71 | 72 | Please note that this project is released with a [Contributor Code of Conduct.](CODE_OF_CONDUCT.md) By participating in this project you agree to abide by its terms. 73 | -------------------------------------------------------------------------------- /algorithms/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.babelrc", 3 | "plugins": [ 4 | "trace-execution" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /algorithms/bfs.js: -------------------------------------------------------------------------------- 1 | function isSeller(name) { 2 | return name.split('').pop() === 'm'; 3 | } 4 | 5 | export default function bfs(graph, name) { 6 | const queue = [...graph[name]]; 7 | const searched = new Set(); 8 | 9 | while (queue.length > 0) { 10 | const person = queue.shift(); 11 | 12 | if (!searched.has(person)) { 13 | if (isSeller(person)) { 14 | return person; 15 | } 16 | 17 | queue.push(...graph[person]); 18 | searched.add(person); 19 | } 20 | } 21 | 22 | return false; 23 | } 24 | -------------------------------------------------------------------------------- /algorithms/binary-search.js: -------------------------------------------------------------------------------- 1 | export default function binarySearch(list, item) { 2 | let low = 0; 3 | let high = list.length - 1; 4 | 5 | while (low <= high) { 6 | const mid = Math.round((low + high) / 2); 7 | const guess = list[mid]; 8 | 9 | if (guess === item) { 10 | return mid; 11 | } 12 | if (guess > item) { 13 | high = mid - 1; 14 | } else { 15 | low = mid + 1; 16 | } 17 | } 18 | 19 | return null; 20 | } 21 | -------------------------------------------------------------------------------- /algorithms/quicksort.js: -------------------------------------------------------------------------------- 1 | function random(list) { 2 | return list[Math.round(Math.random() * (list.length - 1))]; 3 | } 4 | 5 | export default function quicksort(list) { 6 | if (list.length < 2) { 7 | return list; 8 | } 9 | 10 | const pivot = random(list); 11 | const less = list.filter(i => i < pivot); 12 | const greater = list.filter(i => i > pivot); 13 | 14 | return [ 15 | ...quicksort(less), 16 | pivot, 17 | ...quicksort(greater) 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /binary-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovidiuch/illustrated-algorithms/4d71a66b36e4b48fb09fa64161e995a10cfc9a42/binary-search.gif -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/binary-search/beginning.js: -------------------------------------------------------------------------------- 1 | import BinarySearch from '../../../illustrations/binary-search/binary-search'; 2 | 3 | export default { 4 | component: BinarySearch, 5 | layoutFor: 'binarySearch', 6 | 7 | frameFrom: { 8 | prevStep: { 9 | bindings: { 10 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 11 | item: 'panda' 12 | }, 13 | intro: true 14 | }, 15 | nextStep: { 16 | bindings: { 17 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 18 | item: 'panda' 19 | }, 20 | highlight: { 21 | start: 9, 22 | end: 33 23 | } 24 | }, 25 | stepProgress: 0.5 26 | }, 27 | 28 | props: { 29 | actions: {} 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/binary-search/comparing.js: -------------------------------------------------------------------------------- 1 | import BinarySearch from '../../../illustrations/binary-search/binary-search'; 2 | 3 | export default { 4 | component: BinarySearch, 5 | layoutFor: 'binarySearch', 6 | 7 | frameFrom: { 8 | prevStep: { 9 | bindings: { 10 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 11 | item: 'panda', 12 | low: 0, 13 | mid: 3, 14 | high: 5, 15 | guess: 'lion' 16 | } 17 | }, 18 | nextStep: { 19 | bindings: { 20 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 21 | item: 'panda', 22 | low: 0, 23 | mid: 3, 24 | high: 5, 25 | guess: 'lion' 26 | }, 27 | compared: ['guess', 'item'] 28 | }, 29 | stepProgress: 0.633 30 | }, 31 | 32 | props: { 33 | actions: {} 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/binary-search/found.js: -------------------------------------------------------------------------------- 1 | import BinarySearch from '../../../illustrations/binary-search/binary-search'; 2 | 3 | export default { 4 | component: BinarySearch, 5 | layoutFor: 'binarySearch', 6 | 7 | frameFrom: { 8 | prevStep: { 9 | bindings: { 10 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 11 | item: 'panda', 12 | low: 3, 13 | mid: 3, 14 | high: 3, 15 | guess: 'panda' 16 | } 17 | }, 18 | nextStep: { 19 | bindings: { 20 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 21 | item: 'panda', 22 | low: 3, 23 | mid: 3, 24 | high: 3, 25 | guess: 'panda' 26 | }, 27 | returnValue: 4 28 | }, 29 | stepProgress: 1 30 | }, 31 | 32 | props: { 33 | actions: {} 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/binary-search/intro.js: -------------------------------------------------------------------------------- 1 | import BinarySearch from '../../../illustrations/binary-search/binary-search'; 2 | 3 | export default { 4 | component: BinarySearch, 5 | layoutFor: 'binarySearch', 6 | 7 | frameFrom: { 8 | prevStep: { 9 | bindings: { 10 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 11 | item: 'panda' 12 | }, 13 | intro: true 14 | }, 15 | nextStep: { 16 | bindings: { 17 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 18 | item: 'panda' 19 | }, 20 | intro: true 21 | }, 22 | stepProgress: 0 23 | }, 24 | 25 | props: { 26 | actions: { 27 | generateSteps: steps => console.log('steps', steps) 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/quicksort/intro.js: -------------------------------------------------------------------------------- 1 | import Quicksort from '../../../illustrations/quicksort/quicksort'; 2 | 3 | export default { 4 | component: Quicksort, 5 | layoutFor: 'quicksort', 6 | 7 | frameFrom: { 8 | prevStep: { 9 | bindings: { 10 | list: ['cherries', 'kiwi', 'grapes', 'avocado', 'pineapple', 'peach'] 11 | }, 12 | intro: true 13 | }, 14 | nextStep: { 15 | bindings: { 16 | list: ['cherries', 'kiwi', 'grapes', 'avocado', 'pineapple', 'peach'] 17 | } 18 | }, 19 | stepProgress: 0 20 | }, 21 | 22 | props: { 23 | actions: { 24 | shuffleInput: () => console.log('shuffle input'), 25 | generateSteps: cb => { 26 | console.log('generate steps'); 27 | cb(); 28 | }, 29 | play: () => console.log('play') 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/quicksort/intro/blank.js: -------------------------------------------------------------------------------- 1 | import Intro from '../../../../illustrations/quicksort/intro'; 2 | 3 | export default { 4 | component: Intro, 5 | layoutFor: 'quicksort', 6 | 7 | props: { 8 | frame: { 9 | intro: { 10 | titleFontSize: 38, 11 | titleLineHeight: 44, 12 | btnTop: 103.625, 13 | btnFontSize: 28, 14 | btnSvgSize: 32, 15 | opacity: 1 16 | } 17 | }, 18 | onShuffle: () => console.log('shuffle'), 19 | onStart: () => console.log('start') 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/quicksort/outro/blank.js: -------------------------------------------------------------------------------- 1 | import Outro from '../../../../illustrations/quicksort/outro'; 2 | 3 | export default { 4 | component: Outro, 5 | layoutFor: 'quicksort', 6 | 7 | props: { 8 | frame: { 9 | outro: { 10 | titleFontSize: 46, 11 | titleLineHeight: 52, 12 | titleTop: 16, 13 | subtextFontSize: 34, 14 | subtextTop: 249, 15 | opacity: 1 16 | } 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/raw-data/first-frame-wip.js: -------------------------------------------------------------------------------- 1 | import RawData from '../../../illustrations/raw-data'; 2 | 3 | export default { 4 | component: RawData, 5 | layoutFor: 'bfs', 6 | 7 | frameFrom: { 8 | prevStep: { 9 | bindings: { 10 | graph: { 11 | you: [ 12 | 'alice', 13 | 'bob', 14 | 'claire' 15 | ], 16 | bob: [ 17 | 'anuj', 18 | 'peggy' 19 | ], 20 | alice: [ 21 | 'peggy' 22 | ], 23 | claire: [ 24 | 'thom', 25 | 'jonny' 26 | ], 27 | anuj: [], 28 | peggy: [], 29 | thom: [], 30 | jonny: [] 31 | }, 32 | name: 'you' 33 | }, 34 | intro: true, 35 | }, 36 | nextStep: { 37 | bindings: { 38 | graph: { 39 | you: [ 40 | 'alice', 41 | 'bob', 42 | 'claire' 43 | ], 44 | bob: [ 45 | 'anuj', 46 | 'peggy' 47 | ], 48 | alice: [ 49 | 'peggy' 50 | ], 51 | claire: [ 52 | 'thom', 53 | 'jonny' 54 | ], 55 | anuj: [], 56 | peggy: [], 57 | thom: [], 58 | jonny: [] 59 | }, 60 | name: 'you' 61 | } 62 | }, 63 | stepProgress: 0, 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-block/blank.js: -------------------------------------------------------------------------------- 1 | import EmojiBlock from '../../../../illustrations/shared/emoji-block'; 2 | 3 | export default { 4 | component: EmojiBlock, 5 | layoutFor: 'binarySearch', 6 | 7 | props: { 8 | name: 'panda', 9 | glow: 0 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-block/empty.js: -------------------------------------------------------------------------------- 1 | import EmojiBlock from '../../../../illustrations/shared/emoji-block'; 2 | 3 | export default { 4 | component: EmojiBlock, 5 | layoutFor: 'quicksort' 6 | }; 7 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-block/glow.js: -------------------------------------------------------------------------------- 1 | import EmojiBlock from '../../../../illustrations/shared/emoji-block'; 2 | 3 | export default { 4 | component: EmojiBlock, 5 | layoutFor: 'binarySearch', 6 | 7 | props: { 8 | name: 'panda', 9 | glow: 0.8 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-icon/bear.js: -------------------------------------------------------------------------------- 1 | import EmojiIcon from '../../../../illustrations/shared/emoji-icon'; 2 | 3 | export default { 4 | component: EmojiIcon, 5 | 6 | props: { 7 | name: 'bear', 8 | width: 100, 9 | height: 100 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-icon/cat.js: -------------------------------------------------------------------------------- 1 | import EmojiIcon from '../../../../illustrations/shared/emoji-icon'; 2 | 3 | export default { 4 | component: EmojiIcon, 5 | 6 | props: { 7 | name: 'cat', 8 | width: 100, 9 | height: 100 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-icon/dog.js: -------------------------------------------------------------------------------- 1 | import EmojiIcon from '../../../../illustrations/shared/emoji-icon'; 2 | 3 | export default { 4 | component: EmojiIcon, 5 | 6 | props: { 7 | name: 'dog', 8 | width: 100, 9 | height: 100 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-icon/lion.js: -------------------------------------------------------------------------------- 1 | import EmojiIcon from '../../../../illustrations/shared/emoji-icon'; 2 | 3 | export default { 4 | component: EmojiIcon, 5 | 6 | props: { 7 | name: 'lion', 8 | width: 100, 9 | height: 100 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-icon/no-entry.js: -------------------------------------------------------------------------------- 1 | import EmojiIcon from '../../../../illustrations/shared/emoji-icon'; 2 | 3 | export default { 4 | component: EmojiIcon, 5 | 6 | props: { 7 | name: 'no entry', 8 | width: 100, 9 | height: 100 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-icon/panda.js: -------------------------------------------------------------------------------- 1 | import EmojiIcon from '../../../../illustrations/shared/emoji-icon'; 2 | 3 | export default { 4 | component: EmojiIcon, 5 | 6 | props: { 7 | name: 'panda', 8 | width: 100, 9 | height: 100 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/emoji-icon/snail.js: -------------------------------------------------------------------------------- 1 | import EmojiIcon from '../../../../illustrations/shared/emoji-icon'; 2 | 3 | export default { 4 | component: EmojiIcon, 5 | 6 | props: { 7 | name: 'snail', 8 | width: 100, 9 | height: 100 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/label/pivot.js: -------------------------------------------------------------------------------- 1 | import Label from '../../../../illustrations/shared/label'; 2 | 3 | export default { 4 | component: Label, 5 | layoutFor: 'quicksort', 6 | 7 | props: { 8 | text: 'pivot' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /components/__fixtures__/illustrations/shared/number-var/max-5.js: -------------------------------------------------------------------------------- 1 | import NumberVar from '../../../../illustrations/shared/number-var'; 2 | 3 | export default { 4 | component: NumberVar, 5 | layoutFor: 'binarySearch', 6 | 7 | props: { 8 | value: 5, 9 | label: 'max' 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/menu/binary-search.js: -------------------------------------------------------------------------------- 1 | import Menu from '../../menu'; 2 | 3 | export default { 4 | component: Menu, 5 | layoutFor: 'binarySearch', 6 | 7 | props: { 8 | currentPath: '/binary-search' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /components/__fixtures__/page/bfs.js: -------------------------------------------------------------------------------- 1 | import bfs from '../../../algorithms/bfs'; 2 | import RawData from '../../../components/illustrations/raw-data'; 3 | import computeBfsLayout from '../../../layout/bfs'; 4 | import computeRawDataFrame from '../../../frame/raw-data'; 5 | import Page from '../../page'; 6 | 7 | const graph = { 8 | you: ['alice', 'bob', 'claire'], 9 | bob: ['anuj', 'peggy'], 10 | alice: ['peggy'], 11 | claire: ['thom', 'jonny'], 12 | anuj: [], 13 | peggy: [], 14 | thom: [], 15 | jonny: [] 16 | }; 17 | const name = 'you'; 18 | const { steps } = bfs(graph, name); 19 | 20 | export default { 21 | component: Page, 22 | 23 | props: { 24 | currentPath: '/bfs', 25 | algorithm: bfs, 26 | illustration: RawData, 27 | computeLayout: computeBfsLayout, 28 | computeFrame: computeRawDataFrame, 29 | steps: [ 30 | { 31 | intro: true, 32 | bindings: { 33 | graph, 34 | name 35 | } 36 | }, 37 | ...steps 38 | ], 39 | actions: {} 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /components/__fixtures__/page/binary-search.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import computeBinarySearchLayout from '../../../layout/binary-search'; 4 | import computeBinarySearchFrame from '../../../frame/binary-search'; 5 | import Page from '../../page'; 6 | 7 | const list = ['bear', 'cat', 'dog', 'lion', 'panda', 'snail']; 8 | 9 | export default { 10 | component: Page, 11 | 12 | props: { 13 | currentPath: '/binary-search', 14 | algorithm: binarySearch, 15 | illustration: BinarySearch, 16 | computeLayout: computeBinarySearchLayout, 17 | computeFrame: computeBinarySearchFrame, 18 | steps: [ 19 | { 20 | bindings: { list }, 21 | intro: true 22 | } 23 | ], 24 | actions: { 25 | generateSteps: item => { 26 | console.log('generate steps', item); 27 | } 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /components/__fixtures__/page/quicksort.js: -------------------------------------------------------------------------------- 1 | import quicksort from '../../../algorithms/quicksort'; 2 | import Quicksort from '../../../components/illustrations/quicksort/quicksort'; 3 | import computeQuicksortLayout from '../../../layout/quicksort'; 4 | import computeQuicksortFrame from '../../../frame/quicksort'; 5 | import Page from '../../page'; 6 | 7 | const list = ['cherries', 'kiwi', 'grapes', 'avocado', 'pineapple', 'peach']; 8 | const { steps } = quicksort(list); 9 | 10 | export default { 11 | component: Page, 12 | layoutFor: 'quicksort', 13 | 14 | props: { 15 | currentPath: '/quicksort', 16 | algorithm: quicksort, 17 | illustration: Quicksort, 18 | computeLayout: computeQuicksortLayout, 19 | computeFrame: computeQuicksortFrame, 20 | steps, 21 | actions: {} 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /components/__fixtures__/playback-controls/finished.js: -------------------------------------------------------------------------------- 1 | import PlaybackControls from '../../playback-controls'; 2 | 3 | export default { 4 | component: PlaybackControls, 5 | layoutFor: 'binarySearch', 6 | 7 | props: { 8 | isPlaying: false, 9 | pos: 1229, 10 | maxPos: 1229, 11 | onPlay: () => console.log('play'), 12 | onPause: () => console.log('pause'), 13 | onScrollTo: to => console.log('scroll', to) 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /components/__fixtures__/playback-controls/playing.js: -------------------------------------------------------------------------------- 1 | import PlaybackControls from '../../playback-controls'; 2 | 3 | export default { 4 | component: PlaybackControls, 5 | layoutFor: 'binarySearch', 6 | 7 | props: { 8 | isPlaying: true, 9 | pos: 300, 10 | maxPos: 1229, 11 | onPlay: () => console.log('play'), 12 | onPause: () => console.log('pause'), 13 | onScrollTo: to => console.log('scroll', to) 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /components/__fixtures__/playback-controls/stopped.js: -------------------------------------------------------------------------------- 1 | import PlaybackControls from '../../playback-controls'; 2 | 3 | export default { 4 | component: PlaybackControls, 5 | layoutFor: 'binarySearch', 6 | 7 | props: { 8 | isPlaying: false, 9 | pos: 0, 10 | maxPos: 1229, 11 | onPlay: () => console.log('play'), 12 | onPause: () => console.log('pause'), 13 | onScrollTo: to => console.log('scroll', to) 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /components/__fixtures__/player/binary-search-mid-way.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import computeBinarySearchFrame from '../../../frame/binary-search'; 4 | import Player from '../../player'; 5 | 6 | const list = ['bear', 'cat', 'dog', 'lion', 'panda', 'snail']; 7 | const { steps } = binarySearch(list, 'dog'); 8 | 9 | export default { 10 | component: Player, 11 | layoutFor: 'binarySearch', 12 | 13 | props: { 14 | computeFrame: computeBinarySearchFrame, 15 | algorithm: binarySearch, 16 | illustration: BinarySearch, 17 | steps: [ 18 | { 19 | intro: true, 20 | bindings: { 21 | list 22 | } 23 | }, 24 | ...steps 25 | ], 26 | actions: { 27 | generateSteps: item => { 28 | console.log('generate steps', item); 29 | } 30 | } 31 | }, 32 | 33 | state: { 34 | pos: 425.27999999999804, 35 | isPlaying: false 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /components/__fixtures__/player/binary-search.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import computeBinarySearchFrame from '../../../frame/binary-search'; 4 | import Player from '../../player'; 5 | 6 | const list = ['bear', 'cat', 'dog', 'lion', 'panda', 'snail']; 7 | 8 | export default { 9 | component: Player, 10 | layoutFor: 'binarySearch', 11 | 12 | props: { 13 | computeFrame: computeBinarySearchFrame, 14 | algorithm: binarySearch, 15 | illustration: BinarySearch, 16 | steps: [ 17 | { 18 | intro: true, 19 | bindings: { 20 | list 21 | } 22 | } 23 | ], 24 | actions: { 25 | generateSteps: item => { 26 | console.log('generate steps', item); 27 | } 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /components/__fixtures__/player/quicksort.js: -------------------------------------------------------------------------------- 1 | import offsetSteps from '../../../utils/offset-steps'; 2 | import quicksort from '../../../algorithms/quicksort'; 3 | import Quicksort from '../../../components/illustrations/quicksort/quicksort'; 4 | import computeQuicksortFrame from '../../../frame/quicksort'; 5 | import Player from '../../player'; 6 | 7 | export default { 8 | component: Player, 9 | layoutFor: 'quicksort', 10 | 11 | props: { 12 | computeFrame: computeQuicksortFrame, 13 | algorithm: quicksort, 14 | illustration: Quicksort, 15 | steps: [ 16 | { 17 | intro: true, 18 | bindings: { 19 | list: ['cherries', 'kiwi', 'grapes', 'avocado', 'pineapple', 'peach'] 20 | } 21 | }, 22 | ...offsetSteps( 23 | [ 24 | { 25 | highlight: { 26 | start: 9, 27 | end: 24 28 | }, 29 | bindings: { 30 | list: [ 31 | 'cherries', 32 | 'kiwi', 33 | 'grapes', 34 | 'avocado', 35 | 'pineapple', 36 | 'peach' 37 | ] 38 | } 39 | }, 40 | { 41 | highlight: { 42 | start: 33, 43 | end: 48 44 | }, 45 | bindings: { 46 | list: [ 47 | 'cherries', 48 | 'kiwi', 49 | 'grapes', 50 | 'avocado', 51 | 'pineapple', 52 | 'peach' 53 | ] 54 | }, 55 | compared: ['list.length', '2'] 56 | }, 57 | { 58 | highlight: { 59 | start: 76, 60 | end: 103 61 | }, 62 | bindings: { 63 | list: [ 64 | 'cherries', 65 | 'kiwi', 66 | 'grapes', 67 | 'avocado', 68 | 'pineapple', 69 | 'peach' 70 | ], 71 | pivot: 'grapes' 72 | } 73 | }, 74 | { 75 | highlight: { 76 | start: 106, 77 | end: 147 78 | }, 79 | bindings: { 80 | list: [ 81 | 'cherries', 82 | 'kiwi', 83 | 'grapes', 84 | 'avocado', 85 | 'pineapple', 86 | 'peach' 87 | ], 88 | pivot: 'grapes', 89 | less: ['cherries', 'avocado'] 90 | } 91 | }, 92 | { 93 | highlight: { 94 | start: 150, 95 | end: 194 96 | }, 97 | bindings: { 98 | list: [ 99 | 'cherries', 100 | 'kiwi', 101 | 'grapes', 102 | 'avocado', 103 | 'pineapple', 104 | 'peach' 105 | ], 106 | pivot: 'grapes', 107 | less: ['cherries', 'avocado'], 108 | greater: ['kiwi', 'pineapple', 'peach'] 109 | } 110 | }, 111 | { 112 | highlight: { 113 | start: 214, 114 | end: 229 115 | }, 116 | bindings: { 117 | list: [ 118 | 'cherries', 119 | 'kiwi', 120 | 'grapes', 121 | 'avocado', 122 | 'pineapple', 123 | 'peach' 124 | ], 125 | pivot: 'grapes', 126 | less: ['cherries', 'avocado'], 127 | greater: ['kiwi', 'pineapple', 'peach'] 128 | }, 129 | beforeChildCall: true 130 | }, 131 | { 132 | parentStepId: 5, 133 | highlight: { 134 | start: 9, 135 | end: 24 136 | }, 137 | bindings: { 138 | list: ['cherries', 'avocado'] 139 | } 140 | }, 141 | { 142 | parentStepId: 5, 143 | highlight: { 144 | start: 33, 145 | end: 48 146 | }, 147 | bindings: { 148 | list: ['cherries', 'avocado'] 149 | }, 150 | compared: ['list.length', '2'] 151 | }, 152 | { 153 | parentStepId: 5, 154 | highlight: { 155 | start: 76, 156 | end: 103 157 | }, 158 | bindings: { 159 | list: ['cherries', 'avocado'], 160 | pivot: 'cherries' 161 | } 162 | }, 163 | { 164 | parentStepId: 5, 165 | highlight: { 166 | start: 106, 167 | end: 147 168 | }, 169 | bindings: { 170 | list: ['cherries', 'avocado'], 171 | pivot: 'cherries', 172 | less: ['avocado'] 173 | } 174 | }, 175 | { 176 | parentStepId: 5, 177 | highlight: { 178 | start: 150, 179 | end: 194 180 | }, 181 | bindings: { 182 | list: ['cherries', 'avocado'], 183 | pivot: 'cherries', 184 | less: ['avocado'], 185 | greater: [] 186 | } 187 | }, 188 | { 189 | parentStepId: 5, 190 | highlight: { 191 | start: 214, 192 | end: 229 193 | }, 194 | bindings: { 195 | list: ['cherries', 'avocado'], 196 | pivot: 'cherries', 197 | less: ['avocado'], 198 | greater: [] 199 | }, 200 | beforeChildCall: true 201 | }, 202 | { 203 | parentStepId: 11, 204 | highlight: { 205 | start: 9, 206 | end: 24 207 | }, 208 | bindings: { 209 | list: ['avocado'] 210 | } 211 | }, 212 | { 213 | parentStepId: 11, 214 | highlight: { 215 | start: 33, 216 | end: 48 217 | }, 218 | bindings: { 219 | list: ['avocado'] 220 | }, 221 | compared: ['list.length', '2'] 222 | }, 223 | { 224 | parentStepId: 11, 225 | highlight: { 226 | start: 56, 227 | end: 68 228 | }, 229 | bindings: { 230 | list: ['avocado'] 231 | }, 232 | returnValue: ['avocado'] 233 | }, 234 | { 235 | parentStepId: 5, 236 | highlight: { 237 | start: 214, 238 | end: 229 239 | }, 240 | bindings: { 241 | list: ['cherries', 'avocado'], 242 | pivot: 'cherries', 243 | less: ['avocado'], 244 | greater: [] 245 | }, 246 | afterChildCall: true 247 | }, 248 | { 249 | parentStepId: 5, 250 | highlight: { 251 | start: 249, 252 | end: 267 253 | }, 254 | bindings: { 255 | list: ['cherries', 'avocado'], 256 | pivot: 'cherries', 257 | less: ['avocado'], 258 | greater: [] 259 | }, 260 | beforeChildCall: true 261 | }, 262 | { 263 | parentStepId: 16, 264 | highlight: { 265 | start: 9, 266 | end: 24 267 | }, 268 | bindings: { 269 | list: [] 270 | } 271 | }, 272 | { 273 | parentStepId: 16, 274 | highlight: { 275 | start: 33, 276 | end: 48 277 | }, 278 | bindings: { 279 | list: [] 280 | }, 281 | compared: ['list.length', '2'] 282 | }, 283 | { 284 | parentStepId: 16, 285 | highlight: { 286 | start: 56, 287 | end: 68 288 | }, 289 | bindings: { 290 | list: [] 291 | }, 292 | returnValue: [] 293 | }, 294 | { 295 | parentStepId: 5, 296 | highlight: { 297 | start: 249, 298 | end: 267 299 | }, 300 | bindings: { 301 | list: ['cherries', 'avocado'], 302 | pivot: 'cherries', 303 | less: ['avocado'], 304 | greater: [] 305 | }, 306 | afterChildCall: true 307 | }, 308 | { 309 | parentStepId: 5, 310 | highlight: { 311 | start: 198, 312 | end: 272 313 | }, 314 | bindings: { 315 | list: ['cherries', 'avocado'], 316 | pivot: 'cherries', 317 | less: ['avocado'], 318 | greater: [] 319 | }, 320 | returnValue: ['avocado', 'cherries'] 321 | }, 322 | { 323 | highlight: { 324 | start: 214, 325 | end: 229 326 | }, 327 | bindings: { 328 | list: [ 329 | 'cherries', 330 | 'kiwi', 331 | 'grapes', 332 | 'avocado', 333 | 'pineapple', 334 | 'peach' 335 | ], 336 | pivot: 'grapes', 337 | less: ['cherries', 'avocado'], 338 | greater: ['kiwi', 'pineapple', 'peach'] 339 | }, 340 | afterChildCall: true 341 | }, 342 | { 343 | highlight: { 344 | start: 249, 345 | end: 267 346 | }, 347 | bindings: { 348 | list: [ 349 | 'cherries', 350 | 'kiwi', 351 | 'grapes', 352 | 'avocado', 353 | 'pineapple', 354 | 'peach' 355 | ], 356 | pivot: 'grapes', 357 | less: ['cherries', 'avocado'], 358 | greater: ['kiwi', 'pineapple', 'peach'] 359 | }, 360 | beforeChildCall: true 361 | }, 362 | { 363 | parentStepId: 23, 364 | highlight: { 365 | start: 9, 366 | end: 24 367 | }, 368 | bindings: { 369 | list: ['kiwi', 'pineapple', 'peach'] 370 | } 371 | }, 372 | { 373 | parentStepId: 23, 374 | highlight: { 375 | start: 33, 376 | end: 48 377 | }, 378 | bindings: { 379 | list: ['kiwi', 'pineapple', 'peach'] 380 | }, 381 | compared: ['list.length', '2'] 382 | }, 383 | { 384 | parentStepId: 23, 385 | highlight: { 386 | start: 76, 387 | end: 103 388 | }, 389 | bindings: { 390 | list: ['kiwi', 'pineapple', 'peach'], 391 | pivot: 'kiwi' 392 | } 393 | }, 394 | { 395 | parentStepId: 23, 396 | highlight: { 397 | start: 106, 398 | end: 147 399 | }, 400 | bindings: { 401 | list: ['kiwi', 'pineapple', 'peach'], 402 | pivot: 'kiwi', 403 | less: [] 404 | } 405 | }, 406 | { 407 | parentStepId: 23, 408 | highlight: { 409 | start: 150, 410 | end: 194 411 | }, 412 | bindings: { 413 | list: ['kiwi', 'pineapple', 'peach'], 414 | pivot: 'kiwi', 415 | less: [], 416 | greater: ['pineapple', 'peach'] 417 | } 418 | }, 419 | { 420 | parentStepId: 23, 421 | highlight: { 422 | start: 214, 423 | end: 229 424 | }, 425 | bindings: { 426 | list: ['kiwi', 'pineapple', 'peach'], 427 | pivot: 'kiwi', 428 | less: [], 429 | greater: ['pineapple', 'peach'] 430 | }, 431 | beforeChildCall: true 432 | }, 433 | { 434 | parentStepId: 29, 435 | highlight: { 436 | start: 9, 437 | end: 24 438 | }, 439 | bindings: { 440 | list: [] 441 | } 442 | }, 443 | { 444 | parentStepId: 29, 445 | highlight: { 446 | start: 33, 447 | end: 48 448 | }, 449 | bindings: { 450 | list: [] 451 | }, 452 | compared: ['list.length', '2'] 453 | }, 454 | { 455 | parentStepId: 29, 456 | highlight: { 457 | start: 56, 458 | end: 68 459 | }, 460 | bindings: { 461 | list: [] 462 | }, 463 | returnValue: [] 464 | }, 465 | { 466 | parentStepId: 23, 467 | highlight: { 468 | start: 214, 469 | end: 229 470 | }, 471 | bindings: { 472 | list: ['kiwi', 'pineapple', 'peach'], 473 | pivot: 'kiwi', 474 | less: [], 475 | greater: ['pineapple', 'peach'] 476 | }, 477 | afterChildCall: true 478 | }, 479 | { 480 | parentStepId: 23, 481 | highlight: { 482 | start: 249, 483 | end: 267 484 | }, 485 | bindings: { 486 | list: ['kiwi', 'pineapple', 'peach'], 487 | pivot: 'kiwi', 488 | less: [], 489 | greater: ['pineapple', 'peach'] 490 | }, 491 | beforeChildCall: true 492 | }, 493 | { 494 | parentStepId: 34, 495 | highlight: { 496 | start: 9, 497 | end: 24 498 | }, 499 | bindings: { 500 | list: ['pineapple', 'peach'] 501 | } 502 | }, 503 | { 504 | parentStepId: 34, 505 | highlight: { 506 | start: 33, 507 | end: 48 508 | }, 509 | bindings: { 510 | list: ['pineapple', 'peach'] 511 | }, 512 | compared: ['list.length', '2'] 513 | }, 514 | { 515 | parentStepId: 34, 516 | highlight: { 517 | start: 76, 518 | end: 103 519 | }, 520 | bindings: { 521 | list: ['pineapple', 'peach'], 522 | pivot: 'peach' 523 | } 524 | }, 525 | { 526 | parentStepId: 34, 527 | highlight: { 528 | start: 106, 529 | end: 147 530 | }, 531 | bindings: { 532 | list: ['pineapple', 'peach'], 533 | pivot: 'peach', 534 | less: [] 535 | } 536 | }, 537 | { 538 | parentStepId: 34, 539 | highlight: { 540 | start: 150, 541 | end: 194 542 | }, 543 | bindings: { 544 | list: ['pineapple', 'peach'], 545 | pivot: 'peach', 546 | less: [], 547 | greater: ['pineapple'] 548 | } 549 | }, 550 | { 551 | parentStepId: 34, 552 | highlight: { 553 | start: 214, 554 | end: 229 555 | }, 556 | bindings: { 557 | list: ['pineapple', 'peach'], 558 | pivot: 'peach', 559 | less: [], 560 | greater: ['pineapple'] 561 | }, 562 | beforeChildCall: true 563 | }, 564 | { 565 | parentStepId: 40, 566 | highlight: { 567 | start: 9, 568 | end: 24 569 | }, 570 | bindings: { 571 | list: [] 572 | } 573 | }, 574 | { 575 | parentStepId: 40, 576 | highlight: { 577 | start: 33, 578 | end: 48 579 | }, 580 | bindings: { 581 | list: [] 582 | }, 583 | compared: ['list.length', '2'] 584 | }, 585 | { 586 | parentStepId: 40, 587 | highlight: { 588 | start: 56, 589 | end: 68 590 | }, 591 | bindings: { 592 | list: [] 593 | }, 594 | returnValue: [] 595 | }, 596 | { 597 | parentStepId: 34, 598 | highlight: { 599 | start: 214, 600 | end: 229 601 | }, 602 | bindings: { 603 | list: ['pineapple', 'peach'], 604 | pivot: 'peach', 605 | less: [], 606 | greater: ['pineapple'] 607 | }, 608 | afterChildCall: true 609 | }, 610 | { 611 | parentStepId: 34, 612 | highlight: { 613 | start: 249, 614 | end: 267 615 | }, 616 | bindings: { 617 | list: ['pineapple', 'peach'], 618 | pivot: 'peach', 619 | less: [], 620 | greater: ['pineapple'] 621 | }, 622 | beforeChildCall: true 623 | }, 624 | { 625 | parentStepId: 45, 626 | highlight: { 627 | start: 9, 628 | end: 24 629 | }, 630 | bindings: { 631 | list: ['pineapple'] 632 | } 633 | }, 634 | { 635 | parentStepId: 45, 636 | highlight: { 637 | start: 33, 638 | end: 48 639 | }, 640 | bindings: { 641 | list: ['pineapple'] 642 | }, 643 | compared: ['list.length', '2'] 644 | }, 645 | { 646 | parentStepId: 45, 647 | highlight: { 648 | start: 56, 649 | end: 68 650 | }, 651 | bindings: { 652 | list: ['pineapple'] 653 | }, 654 | returnValue: ['pineapple'] 655 | }, 656 | { 657 | parentStepId: 34, 658 | highlight: { 659 | start: 249, 660 | end: 267 661 | }, 662 | bindings: { 663 | list: ['pineapple', 'peach'], 664 | pivot: 'peach', 665 | less: [], 666 | greater: ['pineapple'] 667 | }, 668 | afterChildCall: true 669 | }, 670 | { 671 | parentStepId: 34, 672 | highlight: { 673 | start: 198, 674 | end: 272 675 | }, 676 | bindings: { 677 | list: ['pineapple', 'peach'], 678 | pivot: 'peach', 679 | less: [], 680 | greater: ['pineapple'] 681 | }, 682 | returnValue: ['peach', 'pineapple'] 683 | }, 684 | { 685 | parentStepId: 23, 686 | highlight: { 687 | start: 249, 688 | end: 267 689 | }, 690 | bindings: { 691 | list: ['kiwi', 'pineapple', 'peach'], 692 | pivot: 'kiwi', 693 | less: [], 694 | greater: ['pineapple', 'peach'] 695 | }, 696 | afterChildCall: true 697 | }, 698 | { 699 | parentStepId: 23, 700 | highlight: { 701 | start: 198, 702 | end: 272 703 | }, 704 | bindings: { 705 | list: ['kiwi', 'pineapple', 'peach'], 706 | pivot: 'kiwi', 707 | less: [], 708 | greater: ['pineapple', 'peach'] 709 | }, 710 | returnValue: ['kiwi', 'peach', 'pineapple'] 711 | }, 712 | { 713 | highlight: { 714 | start: 249, 715 | end: 267 716 | }, 717 | bindings: { 718 | list: [ 719 | 'cherries', 720 | 'kiwi', 721 | 'grapes', 722 | 'avocado', 723 | 'pineapple', 724 | 'peach' 725 | ], 726 | pivot: 'grapes', 727 | less: ['cherries', 'avocado'], 728 | greater: ['kiwi', 'pineapple', 'peach'] 729 | }, 730 | afterChildCall: true 731 | }, 732 | { 733 | highlight: { 734 | start: 198, 735 | end: 272 736 | }, 737 | bindings: { 738 | list: [ 739 | 'cherries', 740 | 'kiwi', 741 | 'grapes', 742 | 'avocado', 743 | 'pineapple', 744 | 'peach' 745 | ], 746 | pivot: 'grapes', 747 | less: ['cherries', 'avocado'], 748 | greater: ['kiwi', 'pineapple', 'peach'] 749 | }, 750 | returnValue: [ 751 | 'avocado', 752 | 'cherries', 753 | 'grapes', 754 | 'kiwi', 755 | 'peach', 756 | 'pineapple' 757 | ] 758 | } 759 | ], 760 | 1 761 | ) 762 | ], 763 | actions: { 764 | shuffleInput: () => console.log('shuffle input'), 765 | generateSteps: cb => { 766 | console.log('generate steps'); 767 | cb(); 768 | } 769 | } 770 | } 771 | }; 772 | -------------------------------------------------------------------------------- /components/__fixtures__/source-code/bfs.js: -------------------------------------------------------------------------------- 1 | import bfs from '../../../algorithms/bfs'; 2 | import SourceCode from '../../source-code'; 3 | 4 | export default { 5 | component: SourceCode, 6 | layoutFor: 'bfs', 7 | 8 | props: { 9 | def: bfs.code 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/source-code/binary-search.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import SourceCode from '../../source-code'; 3 | 4 | export default { 5 | component: SourceCode, 6 | layoutFor: 'binarySearch', 7 | 8 | props: { 9 | def: binarySearch.code 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/source-code/quicksort.js: -------------------------------------------------------------------------------- 1 | import quicksort from '../../../algorithms/quicksort'; 2 | import SourceCode from '../../source-code'; 3 | 4 | export default { 5 | component: SourceCode, 6 | layoutFor: 'quicksort', 7 | 8 | props: { 9 | def: quicksort.code 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /components/__fixtures__/stack-entry/binary-search-beginning.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import StackEntry from '../../stack-entry'; 4 | 5 | const { code } = binarySearch; 6 | 7 | export default { 8 | component: StackEntry, 9 | layoutFor: 'binarySearch', 10 | 11 | frameFrom: { 12 | prevStep: { 13 | bindings: { 14 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 15 | item: 'panda' 16 | }, 17 | intro: true 18 | }, 19 | nextStep: { 20 | bindings: { 21 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 22 | item: 'panda' 23 | }, 24 | highlight: { 25 | start: 9, 26 | end: 33 27 | } 28 | }, 29 | stepProgress: 0.5 30 | }, 31 | 32 | props: { 33 | code, 34 | illustration: BinarySearch, 35 | actions: { 36 | generateSteps: steps => console.log('steps', steps) 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /components/__fixtures__/stack-entry/binary-search-finished.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import StackEntry from '../../stack-entry'; 4 | 5 | const { code } = binarySearch; 6 | 7 | export default { 8 | component: StackEntry, 9 | layoutFor: 'binarySearch', 10 | 11 | frameFrom: { 12 | prevStep: { 13 | bindings: { 14 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 15 | item: 'cat', 16 | low: 0, 17 | high: 2, 18 | mid: 1, 19 | guess: 'cat' 20 | }, 21 | highlight: { 22 | start: 214, 23 | end: 225 24 | }, 25 | returnValue: 1 26 | }, 27 | nextStep: { 28 | bindings: { 29 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 30 | item: 'cat', 31 | low: 0, 32 | high: 2, 33 | mid: 1, 34 | guess: 'cat' 35 | }, 36 | highlight: { 37 | start: 214, 38 | end: 225 39 | }, 40 | returnValue: 1 41 | }, 42 | stepProgress: 0 43 | }, 44 | 45 | props: { 46 | code, 47 | illustration: BinarySearch, 48 | actions: { 49 | generateSteps: steps => console.log('steps', steps) 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /components/__fixtures__/stack-entry/binary-search-first-assign.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import StackEntry from '../../stack-entry'; 4 | 5 | const { code } = binarySearch; 6 | 7 | export default { 8 | component: StackEntry, 9 | layoutFor: 'binarySearch', 10 | 11 | frameFrom: { 12 | prevStep: { 13 | bindings: { 14 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 15 | item: 'panda' 16 | }, 17 | highlight: { 18 | start: 9, 19 | end: 33 20 | } 21 | }, 22 | nextStep: { 23 | bindings: { 24 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 25 | item: 'panda', 26 | low: 0 27 | }, 28 | highlight: { 29 | start: 38, 30 | end: 50 31 | } 32 | }, 33 | stepProgress: 0.5 34 | }, 35 | 36 | props: { 37 | code, 38 | illustration: BinarySearch, 39 | actions: { 40 | generateSteps: steps => console.log('steps', steps) 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /components/__fixtures__/stack-entry/binary-search-intro.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import StackEntry from '../../stack-entry'; 4 | 5 | const { code } = binarySearch; 6 | 7 | export default { 8 | component: StackEntry, 9 | layoutFor: 'binarySearch', 10 | 11 | frameFrom: { 12 | prevStep: { 13 | bindings: { 14 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 15 | item: 'panda' 16 | }, 17 | intro: true 18 | }, 19 | nextStep: { 20 | bindings: { 21 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 22 | item: 'panda' 23 | }, 24 | intro: true 25 | }, 26 | stepProgress: 0 27 | }, 28 | 29 | props: { 30 | code, 31 | illustration: BinarySearch, 32 | actions: { 33 | generateSteps: steps => console.log('steps', steps) 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /components/__fixtures__/stack-entry/binary-search-last-check.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import StackEntry from '../../stack-entry'; 4 | 5 | const { code } = binarySearch; 6 | 7 | export default { 8 | component: StackEntry, 9 | layoutFor: 'binarySearch', 10 | 11 | frameFrom: { 12 | prevStep: { 13 | bindings: { 14 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 15 | item: 'cat', 16 | low: 0, 17 | high: 2, 18 | mid: 1, 19 | guess: 'cat' 20 | }, 21 | highlight: { 22 | start: 156, 23 | end: 180 24 | } 25 | }, 26 | nextStep: { 27 | bindings: { 28 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 29 | item: 'cat', 30 | low: 0, 31 | high: 2, 32 | mid: 1, 33 | guess: 'cat' 34 | }, 35 | highlight: { 36 | start: 190, 37 | end: 204 38 | }, 39 | compared: ['guess', 'item'] 40 | }, 41 | stepProgress: 0.65 42 | }, 43 | 44 | props: { 45 | code, 46 | illustration: BinarySearch, 47 | actions: { 48 | generateSteps: steps => console.log('steps', steps) 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /components/__fixtures__/stack-entry/binary-search-returning.js: -------------------------------------------------------------------------------- 1 | import binarySearch from '../../../algorithms/binary-search'; 2 | import BinarySearch from '../../../components/illustrations/binary-search/binary-search'; 3 | import StackEntry from '../../stack-entry'; 4 | 5 | const { code } = binarySearch; 6 | 7 | export default { 8 | component: StackEntry, 9 | layoutFor: 'binarySearch', 10 | 11 | frameFrom: { 12 | prevStep: { 13 | bindings: { 14 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 15 | item: 'cat', 16 | low: 0, 17 | high: 2, 18 | mid: 1, 19 | guess: 'cat' 20 | }, 21 | highlight: { 22 | start: 190, 23 | end: 204 24 | }, 25 | compared: ['guess', 'item'] 26 | }, 27 | nextStep: { 28 | bindings: { 29 | list: ['bear', 'cat', 'dog', 'lion', 'panda', 'snail'], 30 | item: 'cat', 31 | low: 0, 32 | high: 2, 33 | mid: 1, 34 | guess: 'cat' 35 | }, 36 | highlight: { 37 | start: 214, 38 | end: 225 39 | }, 40 | returnValue: 1 41 | }, 42 | stepProgress: 0.65 43 | }, 44 | 45 | props: { 46 | code, 47 | illustration: BinarySearch, 48 | actions: { 49 | generateSteps: steps => console.log('steps', steps) 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/binary-search.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import PureLayoutComponent from '../../../utils/pure-layout-component'; 4 | import List from './list'; 5 | import Item from './item'; 6 | import Low from './low'; 7 | import High from './high'; 8 | import Mid from './mid'; 9 | import Comparison from './comparison'; 10 | import Intro from './intro'; 11 | 12 | class BinarySearch extends PureLayoutComponent { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.handleSelect = this.handleSelect.bind(this); 17 | } 18 | 19 | handleSelect(item) { 20 | this.props.actions.generateSteps(item, () => { 21 | this.props.actions.play(); 22 | }); 23 | } 24 | 25 | render() { 26 | const { props } = this; 27 | const { 28 | innerWidth, 29 | illustrationHeight, 30 | } = this.context.layout; 31 | 32 | return ( 33 |
40 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | 57 |
58 | ); 59 | } 60 | } 61 | 62 | BinarySearch.propTypes = { 63 | actions: PropTypes.object.isRequired, 64 | }; 65 | 66 | BinarySearch.contextTypes = { 67 | layout: PropTypes.object, 68 | }; 69 | 70 | export default BinarySearch; 71 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/comparison.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | export default function Comparison({ frame }, { layout }) { 5 | const { 6 | value, 7 | opacity, 8 | } = frame.comparison; 9 | const { 10 | blockLabelFontSize, 11 | numberVarHeight, 12 | comparison, 13 | } = layout; 14 | const { 15 | top, 16 | left, 17 | } = comparison; 18 | 19 | return ( 20 |
32 | {value} 33 | 44 |
45 | ); 46 | } 47 | 48 | Comparison.propTypes = { 49 | frame: PropTypes.object.isRequired, 50 | }; 51 | 52 | Comparison.contextTypes = { 53 | layout: PropTypes.object, 54 | }; 55 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/high.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import NumberVar from '../shared/number-var'; 4 | 5 | export default function High({ frame }) { 6 | const { 7 | value, 8 | top, 9 | left, 10 | opacity, 11 | rotation, 12 | } = frame.high; 13 | 14 | if (value === undefined) { 15 | return null; 16 | } 17 | 18 | return ( 19 |
29 | 33 | 40 |
41 | ); 42 | } 43 | 44 | High.propTypes = { 45 | frame: PropTypes.object.isRequired, 46 | }; 47 | 48 | High.contextTypes = { 49 | layout: PropTypes.object, 50 | }; 51 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/intro.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | export default function Intro({ frame }, { layout }) { 5 | const { 6 | illustrationHeight, 7 | } = layout; 8 | const { 9 | opacity, 10 | } = frame.intro; 11 | 12 | return ( 13 |
19 |

25 | Find the position of a value
inside a sorted list 26 |

27 |

33 | press on one of the animals to begin 34 |

35 | 56 |
57 | ); 58 | } 59 | 60 | Intro.propTypes = { 61 | frame: PropTypes.object.isRequired, 62 | }; 63 | 64 | Intro.contextTypes = { 65 | layout: PropTypes.object, 66 | }; 67 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/item.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import EmojiBlock from '../shared/emoji-block'; 4 | 5 | export default function Item({ frame }, { layout }) { 6 | const { 7 | value, 8 | opacity, 9 | rotation, 10 | } = frame.item; 11 | const { 12 | top, 13 | left, 14 | } = layout.item; 15 | 16 | if (!value) { 17 | return null; 18 | } 19 | 20 | return ( 21 |
31 | 32 | 39 |
40 | ); 41 | } 42 | 43 | Item.propTypes = { 44 | frame: PropTypes.object.isRequired, 45 | }; 46 | 47 | Item.contextTypes = { 48 | layout: PropTypes.object, 49 | }; 50 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/list.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import PureLayoutComponent from '../../../utils/pure-layout-component'; 5 | import EmojiBlock from '../shared/emoji-block'; 6 | 7 | class ListItem extends PureLayoutComponent { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.handleClick = this.handleClick.bind(this); 12 | } 13 | 14 | handleClick() { 15 | const { 16 | name, 17 | isSelectable, 18 | onSelect, 19 | } = this.props; 20 | 21 | if (isSelectable) { 22 | onSelect(name); 23 | } 24 | } 25 | 26 | render() { 27 | const { 28 | name, 29 | glow, 30 | } = this.props; 31 | 32 | return ( 33 |
34 | 38 |
39 | ); 40 | } 41 | } 42 | 43 | ListItem.propTypes = { 44 | name: PropTypes.string.isRequired, 45 | glow: PropTypes.number.isRequired, 46 | isSelectable: PropTypes.bool.isRequired, 47 | onSelect: PropTypes.func.isRequired, 48 | }; 49 | 50 | ListItem.contextTypes = { 51 | layout: PropTypes.object, 52 | }; 53 | 54 | class List extends PureLayoutComponent { 55 | render() { 56 | const { 57 | frame, 58 | onSelect, 59 | } = this.props; 60 | const { 61 | items, 62 | isSelectable, 63 | } = frame.list; 64 | const { layout } = this.context; 65 | const { 66 | listTop, 67 | } = layout; 68 | return ( 69 |
75 | {items.map(({ 76 | name, 77 | isGuess, 78 | left, 79 | opacity, 80 | rotation, 81 | glow, 82 | }) => { 83 | return ( 84 |
99 | 105 |
106 | ); 107 | })} 108 | 121 |
122 | ); 123 | } 124 | } 125 | 126 | List.propTypes = { 127 | frame: PropTypes.object.isRequired, 128 | onSelect: PropTypes.func.isRequired, 129 | }; 130 | 131 | List.contextTypes = { 132 | layout: PropTypes.object, 133 | }; 134 | 135 | export default List; 136 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/low.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import NumberVar from '../shared/number-var'; 4 | 5 | export default function Low({ frame }) { 6 | const { 7 | value, 8 | top, 9 | left, 10 | opacity, 11 | rotation, 12 | } = frame.low; 13 | 14 | if (value === undefined) { 15 | return null; 16 | } 17 | 18 | return ( 19 |
29 | 33 | 40 |
41 | ); 42 | } 43 | 44 | Low.propTypes = { 45 | frame: PropTypes.object.isRequired, 46 | }; 47 | 48 | Low.contextTypes = { 49 | layout: PropTypes.object, 50 | }; 51 | -------------------------------------------------------------------------------- /components/illustrations/binary-search/mid.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import NumberVar from '../shared/number-var'; 4 | 5 | export default function Mid({ frame }) { 6 | const { 7 | value, 8 | top, 9 | left, 10 | opacity, 11 | } = frame.mid; 12 | 13 | if (value === undefined) { 14 | return null; 15 | } 16 | 17 | return ( 18 |
25 | 29 | 36 |
37 | ); 38 | } 39 | 40 | Mid.propTypes = { 41 | frame: PropTypes.object.isRequired, 42 | }; 43 | 44 | Mid.contextTypes = { 45 | layout: PropTypes.object, 46 | }; 47 | -------------------------------------------------------------------------------- /components/illustrations/quicksort/intro.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import PureLayoutComponent from '../../../utils/pure-layout-component'; 5 | 6 | class Intro extends PureLayoutComponent { 7 | render() { 8 | const { 9 | frame, 10 | } = this.props; 11 | const { 12 | padding, 13 | borderWidth, 14 | innerWidth, 15 | } = this.context.layout; 16 | const { 17 | opacity, 18 | titleFontSize, 19 | titleLineHeight, 20 | btnTop, 21 | btnFontSize, 22 | btnSvgSize, 23 | areControlsEnabled, 24 | } = frame.intro; 25 | 26 | return ( 27 |
33 |

Place the elements of a list
in alphabetical order 40 |

41 |
53 | 61 | 62 | 63 | shuffle 70 | 71 |
72 |
84 | 92 | 93 | 94 | start 101 | 102 |
103 | 144 |
145 | ); 146 | } 147 | } 148 | 149 | Intro.propTypes = { 150 | frame: PropTypes.object.isRequired, 151 | onShuffle: PropTypes.func.isRequired, 152 | onStart: PropTypes.func.isRequired, 153 | }; 154 | 155 | Intro.contextTypes = { 156 | layout: PropTypes.object, 157 | }; 158 | 159 | export default Intro; 160 | -------------------------------------------------------------------------------- /components/illustrations/quicksort/outro.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import PureLayoutComponent from '../../../utils/pure-layout-component'; 4 | 5 | class Outro extends PureLayoutComponent { 6 | render() { 7 | const { 8 | frame, 9 | } = this.props; 10 | const { 11 | opacity, 12 | titleFontSize, 13 | titleLineHeight, 14 | titleTop, 15 | subtextFontSize, 16 | subtextTop, 17 | } = frame.outro; 18 | 19 | return ( 20 |
26 |

Sorted by name – phew! 34 |

35 |

rewind & scroll for close examination 42 |

43 | 64 |
65 | ); 66 | } 67 | } 68 | 69 | Outro.propTypes = { 70 | frame: PropTypes.object.isRequired, 71 | }; 72 | 73 | Outro.contextTypes = { 74 | layout: PropTypes.object, 75 | }; 76 | 77 | export default Outro; 78 | -------------------------------------------------------------------------------- /components/illustrations/quicksort/quicksort.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import PureLayoutComponent from '../../../utils/pure-layout-component'; 4 | import EmojiBlock from '../shared/emoji-block'; 5 | import Label from '../shared/label'; 6 | import Intro from '../quicksort/intro'; 7 | import Outro from '../quicksort/outro'; 8 | 9 | const BASE_ROTATIONS = { 10 | cherries: 0.5, 11 | kiwi: 1.4, 12 | grapes: -2.9, 13 | avocado: 1.9, 14 | peach: -0.8, 15 | pineapple: -2.35 16 | }; 17 | 18 | class Quicksort extends PureLayoutComponent { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.handleShuffle = this.handleShuffle.bind(this); 23 | this.handleStart = this.handleStart.bind(this); 24 | } 25 | 26 | handleShuffle() { 27 | this.props.actions.shuffleInput(); 28 | } 29 | 30 | handleStart() { 31 | this.props.actions.generateSteps(() => { 32 | this.props.actions.play(); 33 | }); 34 | } 35 | 36 | render() { 37 | const { 38 | frame, 39 | } = this.props; 40 | const { 41 | layout 42 | } = this.context; 43 | const { 44 | innerWidth, 45 | labelTopPosition, 46 | illustrationHeight, 47 | itemGroupTopPosition, 48 | listCenterTopPosition, 49 | } = layout; 50 | const { 51 | pivot, 52 | less, 53 | greater, 54 | lessEmpty, 55 | greaterEmpty, 56 | listEmptyOpacity, 57 | itemPositions, 58 | bindings, 59 | } = frame; 60 | const { 61 | list, 62 | } = bindings; 63 | 64 | return ( 65 |
72 |
82 |
84 |
94 |
96 |
106 |
108 |
119 | 122 |
123 |
134 | 137 |
138 |
148 | 151 |
152 |
153 | {list.map(name => { 154 | const { 155 | left, 156 | top, 157 | rotation, 158 | index, 159 | glow 160 | } = itemPositions[name]; 161 | const baseRotation = BASE_ROTATIONS[name]; 162 | const relRotation = baseRotation + rotation; 163 | 164 | return ( 165 |
176 | 180 |
181 | ); 182 | })} 183 | 188 | 191 |
192 | 209 |
210 | ); 211 | } 212 | } 213 | 214 | Quicksort.propTypes = { 215 | actions: PropTypes.object.isRequired, 216 | }; 217 | 218 | Quicksort.contextTypes = { 219 | layout: PropTypes.object, 220 | }; 221 | 222 | export default Quicksort; 223 | -------------------------------------------------------------------------------- /components/illustrations/raw-data.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import PureLayoutComponent from '../../utils/pure-layout-component'; 5 | 6 | class RawData extends PureLayoutComponent { 7 | render() { 8 | const { 9 | frame, 10 | } = this.props; 11 | const { 12 | layout 13 | } = this.context; 14 | const { 15 | bindings, 16 | returnValue, 17 | isFirstStep, 18 | } = frame; 19 | const { 20 | padding, 21 | fontSize, 22 | lineHeight, 23 | illustrationHeight, 24 | } = layout; 25 | 26 | return ( 27 |
36 |
43 | {isFirstStep ? ( 44 |
52 |

Work in progress: No visualisation. Context displayed as JSON.

53 |

54 | See binary search or quicksort for complete examples.{' '} 55 | Stay tuned for updates. 56 |

57 |
58 | ) : null} 59 | {Object.keys(bindings).filter(key => bindings[key] !== undefined).map(key => 60 | ( 61 |
 65 |                 {key} = {JSON.stringify(bindings[key])}
 66 |               
67 | ) 68 | )} 69 | {returnValue !== undefined && ( 70 |
 74 |               {JSON.stringify(returnValue)}
 75 |             
76 | )} 77 |
78 | 114 |
115 | ); 116 | } 117 | } 118 | 119 | RawData.propTypes = { 120 | frame: PropTypes.object.isRequired, 121 | }; 122 | 123 | RawData.contextTypes = { 124 | layout: PropTypes.object, 125 | }; 126 | 127 | export default RawData; 128 | -------------------------------------------------------------------------------- /components/illustrations/shared/emoji-block.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import PureLayoutComponent from '../../../utils/pure-layout-component'; 4 | import EmojiIcon from './emoji-icon'; 5 | 6 | const { round } = Math; 7 | 8 | class EmojiBlock extends PureLayoutComponent { 9 | render() { 10 | const { 11 | name, 12 | glow, 13 | } = this.props; 14 | const { 15 | layout 16 | } = this.context; 17 | const { 18 | color, 19 | borderWidth, 20 | blockWidth, 21 | blockHeight, 22 | blockLabelFontSize, 23 | blockLabelHeight, 24 | } = layout; 25 | const iconSize = round(blockWidth * 0.8); 26 | const isEmpty = !name; 27 | 28 | return ( 29 |
38 |
44 |
53 | 58 |
59 |
67 | {name ? name : 'empty'} 68 |
69 | 109 |
110 | ); 111 | } 112 | } 113 | 114 | EmojiBlock.propTypes = { 115 | name: PropTypes.string, 116 | glow: PropTypes.number, 117 | }; 118 | 119 | EmojiBlock.defaultProps = { 120 | name: '', 121 | glow: 0, 122 | }; 123 | 124 | EmojiBlock.contextTypes = { 125 | layout: PropTypes.object, 126 | }; 127 | 128 | export default EmojiBlock; 129 | -------------------------------------------------------------------------------- /components/illustrations/shared/emoji-icon.js: -------------------------------------------------------------------------------- 1 | import Bear from 'emojione/assets/svg/1f43b.svg'; 2 | import Cat from 'emojione/assets/svg/1f431.svg'; 3 | import Dog from 'emojione/assets/svg/1f436.svg'; 4 | import Lion from 'emojione/assets/svg/1f981.svg'; 5 | import Panda from 'emojione/assets/svg/1f43c.svg'; 6 | import Snail from 'emojione/assets/svg/1f40c.svg'; 7 | import Cherries from 'emojione/assets/svg/1f352.svg'; 8 | import Kiwi from 'emojione/assets/svg/1f95d.svg'; 9 | import Grapes from 'emojione/assets/svg/1f347.svg'; 10 | import Avocado from 'emojione/assets/svg/1f951.svg'; 11 | import Peach from 'emojione/assets/svg/1f351.svg'; 12 | import Pineapple from 'emojione/assets/svg/1f34d.svg'; 13 | import NoEntry from 'emojione/assets/svg/1f6ab.svg'; 14 | import PropTypes from 'prop-types'; 15 | import React from 'react'; 16 | 17 | const emojis = { 18 | bear: Bear, 19 | cat: Cat, 20 | dog: Dog, 21 | lion: Lion, 22 | panda: Panda, 23 | snail: Snail, 24 | 25 | cherries: Cherries, 26 | kiwi: Kiwi, 27 | grapes: Grapes, 28 | avocado: Avocado, 29 | peach: Peach, 30 | pineapple: Pineapple, 31 | 'no entry': NoEntry, 32 | }; 33 | 34 | export default class EmojiIcon extends React.PureComponent { 35 | render() { 36 | const { name, width, height } = this.props; 37 | return ( 38 |
39 | {emojis[name] ? React.createElement(emojis[name], { width, height }) : null} 40 |
41 | ); 42 | } 43 | } 44 | 45 | EmojiIcon.propTypes = { 46 | name: PropTypes.string.isRequired, 47 | width: PropTypes.number.isRequired, 48 | height: PropTypes.number.isRequired, 49 | }; 50 | -------------------------------------------------------------------------------- /components/illustrations/shared/label.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const Label = ({ 5 | text, 6 | }, { 7 | layout, 8 | }) => { 9 | const { 10 | padding, 11 | labelWidth, 12 | labelHeight, 13 | labelFontSize, 14 | } = layout; 15 | 16 | return ( 17 |
26 |
32 | {text} 33 |
34 | 49 |
50 | ); 51 | }; 52 | 53 | Label.propTypes = { 54 | text: PropTypes.string.isRequired, 55 | }; 56 | 57 | Label.contextTypes = { 58 | layout: PropTypes.object, 59 | }; 60 | 61 | export default Label; 62 | -------------------------------------------------------------------------------- /components/illustrations/shared/number-var.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const { round } = Math; 5 | 6 | const PX_RATIOS = { 7 | VALUE_PER_ITEM_WIDTH: 16 / 48, 8 | }; 9 | 10 | const NumberVar = ({ 11 | value, 12 | label, 13 | }, { 14 | layout, 15 | }) => { 16 | const { 17 | blockLabelFontSize, 18 | numberVarWidth, 19 | numberVarHeight, 20 | } = layout; 21 | const valueWidth = round(numberVarWidth * PX_RATIOS.VALUE_PER_ITEM_WIDTH); 22 | const labelWidth = numberVarWidth - valueWidth; 23 | 24 | return ( 25 |
32 |
41 | {label} 42 |
43 |
52 | {value} 53 |
54 | 71 |
72 | ); 73 | }; 74 | 75 | NumberVar.propTypes = { 76 | value: PropTypes.number.isRequired, 77 | label: PropTypes.string.isRequired, 78 | }; 79 | 80 | NumberVar.contextTypes = { 81 | layout: PropTypes.object, 82 | }; 83 | 84 | export default NumberVar; 85 | -------------------------------------------------------------------------------- /components/menu.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import getAlgoName from '../utils/names'; 5 | import PureLayoutComponent from '../utils/pure-layout-component'; 6 | 7 | const LINKS = [ 8 | '/binary-search', 9 | '/quicksort', 10 | '/bfs' 11 | ]; 12 | 13 | class Menu extends PureLayoutComponent { 14 | render() { 15 | const { 16 | currentPath, 17 | } = this.props; 18 | const { 19 | layout, 20 | } = this.context; 21 | const { 22 | headerHeight, 23 | headerLinkFontSize, 24 | headerLinkMargin, 25 | } = layout; 26 | const svgSize = headerHeight * 0.5; 27 | 28 | return ( 29 |
36 | {LINKS.map(linkPath => { 37 | const label = getAlgoName(linkPath); 38 | return ( 39 | 46 | {linkPath === currentPath ? 47 | {label} : 48 | {label}} 49 | 50 | ); 51 | })} 52 | 59 | 66 | 67 | 68 | About 69 | 70 | 91 |
92 | ); 93 | } 94 | } 95 | 96 | Menu.propTypes = { 97 | currentPath: PropTypes.string, 98 | }; 99 | 100 | Menu.contextTypes = { 101 | layout: PropTypes.object, 102 | }; 103 | 104 | export default Menu; 105 | -------------------------------------------------------------------------------- /components/page.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* global window, document */ 3 | 4 | import React from 'react'; 5 | import Head from 'next/head'; 6 | import debounce from 'lodash.debounce'; 7 | import getAlgoName from '../utils/names'; 8 | import Menu from './menu'; 9 | import Player from './player'; 10 | 11 | const getWindowSize = () => ({ 12 | width: document.body.clientWidth || window.innerWidth, // Fallback for jsdom 13 | height: window.innerHeight, 14 | }); 15 | 16 | const createLayout = (props, state) => { 17 | const { 18 | algorithm, 19 | computeLayout, 20 | } = props; 21 | const { width, height } = state; 22 | 23 | return computeLayout({ 24 | width, 25 | height, 26 | code: algorithm.code, 27 | }); 28 | }; 29 | 30 | class Page extends React.Component { 31 | constructor(props) { 32 | super(props); 33 | 34 | this.handleResize = debounce(this.handleResize.bind(this), 300); 35 | 36 | this.state = { 37 | renderedOnClient: false, 38 | // IPhone 6 portrait resolution 39 | width: 375, 40 | height: 667, 41 | }; 42 | 43 | this.layout = createLayout(props, this.state); 44 | } 45 | 46 | componentDidMount() { 47 | window.addEventListener('resize', this.handleResize); 48 | 49 | this.setState({ 50 | renderedOnClient: true, 51 | ...getWindowSize(), 52 | }); 53 | } 54 | 55 | componentWillUpdate(nextProps, nextState) { 56 | this.layout = createLayout(nextProps, nextState); 57 | } 58 | 59 | componentWillUnmount() { 60 | window.removeEventListener('resize', this.handleResize); 61 | } 62 | 63 | handleResize() { 64 | this.setState(getWindowSize()); 65 | } 66 | 67 | getChildContext() { 68 | return { 69 | layout: this.layout, 70 | }; 71 | } 72 | 73 | render() { 74 | const { 75 | currentPath, 76 | algorithm, 77 | illustration, 78 | computeFrame, 79 | steps, 80 | actions, 81 | } = this.props; 82 | const { 83 | renderedOnClient, 84 | } = this.state; 85 | const { 86 | color, 87 | } = this.layout; 88 | 89 | return ( 90 |
91 | 92 | {`Illustrated ${getAlgoName(currentPath)} algorithm`} 93 | 94 | 95 | 112 | 118 | 119 |
120 |
121 | 122 |
123 |
124 | 131 |
132 | 147 |
148 |
149 | ); 150 | } 151 | } 152 | 153 | Page.propTypes = { 154 | currentPath: PropTypes.string.isRequired, 155 | algorithm: PropTypes.func.isRequired, 156 | illustration: PropTypes.func.isRequired, 157 | // ESLint plugin bug 158 | // eslint-disable-next-line react/no-unused-prop-types 159 | computeLayout: PropTypes.func.isRequired, 160 | computeFrame: PropTypes.func.isRequired, 161 | steps: PropTypes.array.isRequired, 162 | actions: PropTypes.object.isRequired, 163 | }; 164 | 165 | Page.childContextTypes = { 166 | layout: PropTypes.object, 167 | }; 168 | 169 | export default Page; 170 | -------------------------------------------------------------------------------- /components/playback-controls.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* global window */ 3 | 4 | import React from 'react'; 5 | import PureLayoutComponent from '../utils/pure-layout-component'; 6 | 7 | const { round, min, max } = Math; 8 | 9 | const SvgButton = ({ 10 | svgPath, 11 | onClick, 12 | }, { 13 | layout 14 | }) => { 15 | const { 16 | footerHeight, 17 | footerButtonIconSize, 18 | } = layout; 19 | return ( 20 |
29 | 34 | 35 | 36 | 52 |
53 | ); 54 | }; 55 | 56 | SvgButton.propTypes = { 57 | svgPath: PropTypes.string.isRequired, 58 | onClick: PropTypes.func.isRequired, 59 | }; 60 | 61 | SvgButton.contextTypes = { 62 | layout: PropTypes.object, 63 | }; 64 | 65 | class PlaybackControls extends PureLayoutComponent { 66 | constructor(props) { 67 | super(props); 68 | 69 | this.handleSliderPress = this.handleSliderPress.bind(this); 70 | this.handlePointerMove = this.handlePointerMove.bind(this); 71 | this.handleRelease = this.handleRelease.bind(this); 72 | this.handleReplay = this.handleReplay.bind(this); 73 | } 74 | 75 | componentWillUnmount() { 76 | this.removeWindowHandlers(); 77 | } 78 | 79 | handleSliderPress(e) { 80 | this.setPositionFromPointerEvent(e); 81 | this.props.onPause(); 82 | this.addWindowHandlers(); 83 | } 84 | 85 | handlePointerMove(e) { 86 | // Disable text selection 87 | e.preventDefault(); 88 | this.setPositionFromPointerEvent(e); 89 | } 90 | 91 | handleRelease() { 92 | this.removeWindowHandlers(); 93 | } 94 | 95 | handleReplay() { 96 | this.props.onScrollTo(0); 97 | } 98 | 99 | addWindowHandlers() { 100 | window.addEventListener('mousemove', this.handlePointerMove); 101 | window.addEventListener('touchmove', this.handlePointerMove); 102 | window.addEventListener('mouseup', this.handleRelease); 103 | window.addEventListener('touchend', this.handleRelease); 104 | } 105 | 106 | removeWindowHandlers() { 107 | window.removeEventListener('mousemove', this.handlePointerMove); 108 | window.removeEventListener('touchmove', this.handlePointerMove); 109 | window.removeEventListener('mouseup', this.handleRelease); 110 | window.removeEventListener('touchend', this.handleRelease); 111 | } 112 | 113 | setPositionFromPointerEvent(e) { 114 | const { maxPos } = this.props; 115 | const { left } = this.sliderNode.getBoundingClientRect(); 116 | const pointerX = e.changedTouches ? e.changedTouches[0].clientX : e.clientX; 117 | const x = pointerX - left; 118 | const pos = round(max(0, min(1, x / this.sliderNode.offsetWidth)) * maxPos); 119 | this.props.onScrollTo(pos); 120 | } 121 | 122 | render() { 123 | const { 124 | onPlay, 125 | onPause, 126 | isPlaying, 127 | pos, 128 | maxPos, 129 | } = this.props; 130 | const { 131 | layout 132 | } = this.context; 133 | const { 134 | footerHeight, 135 | footerHintFontSize, 136 | } = layout; 137 | 138 | return ( 139 |
140 | {pos >= maxPos ? ( 141 | 145 | ) : isPlaying ? ( 146 | 150 | ) : ( 151 | 155 | )} 156 |
{ 158 | this.sliderNode = node; 159 | }} 160 | className="slider" 161 | style={{ left: footerHeight }} 162 | onMouseDown={this.handleSliderPress} 163 | onTouchStart={this.handleSliderPress} 164 | > 165 |
169 |
176 | drag to rewind 177 |
178 |
179 | 214 |
215 | ); 216 | } 217 | } 218 | 219 | PlaybackControls.propTypes = { 220 | pos: PropTypes.number.isRequired, 221 | isPlaying: PropTypes.bool.isRequired, 222 | onPlay: PropTypes.func.isRequired, 223 | onPause: PropTypes.func.isRequired, 224 | onScrollTo: PropTypes.func.isRequired, 225 | }; 226 | 227 | PlaybackControls.contextTypes = { 228 | layout: PropTypes.object, 229 | }; 230 | 231 | export default PlaybackControls; 232 | -------------------------------------------------------------------------------- /components/player.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import raf from 'raf'; 4 | import range from 'lodash.range'; 5 | import getStack from '../utils/stack'; 6 | import StackEntry from './stack-entry'; 7 | import PlaybackControls from './playback-controls'; 8 | 9 | const { round, floor, min } = Math; 10 | 11 | const FPS = 60; 12 | const TIME_PER_FRAME = 1000 / FPS; 13 | const DELAY_TIME = 0.5; 14 | const TRANSITION_TIME = 0.5; 15 | 16 | const getFramesPerTime = time => FPS * time; 17 | 18 | const FRAMES_PER_TRANSITION = getFramesPerTime(TRANSITION_TIME); 19 | const FRAMES_PER_DELAY = getFramesPerTime(DELAY_TIME); 20 | const FRAMES_PER_POS = FRAMES_PER_TRANSITION + FRAMES_PER_DELAY; 21 | 22 | const getMaxPos = steps => (steps - 1) * FRAMES_PER_POS; 23 | 24 | let _frames; 25 | 26 | const computeAllFrames = (layout, steps, computeFrame) => { 27 | return steps.reduce((prev, next, stepIndex) => { 28 | const stack = getStack(steps, stepIndex); 29 | const transFrameNum = round(FPS * TRANSITION_TIME); 30 | const delayFrameNum = round(FPS * DELAY_TIME); 31 | const transFrames = range(transFrameNum).map(frame => 32 | computeFrame(layout, stack, frame / (transFrameNum - 1))); 33 | const lastFrame = transFrames[transFrames.length - 1]; 34 | const delayFrames = range(delayFrameNum).map(() => lastFrame); 35 | 36 | return [ 37 | ...prev, 38 | ...transFrames, 39 | ...delayFrames, 40 | ]; 41 | }, []); 42 | }; 43 | 44 | class Player extends React.Component { 45 | constructor(props, context) { 46 | super(props, context); 47 | 48 | this.handleScrollTo = this.handleScrollTo.bind(this); 49 | this.handlePlay = this.handlePlay.bind(this); 50 | this.handlePause = this.handlePause.bind(this); 51 | this.onFrame = this.onFrame.bind(this); 52 | 53 | this.state = { 54 | pos: 0, 55 | isPlaying: false, 56 | }; 57 | 58 | const { 59 | computeFrame, 60 | steps, 61 | actions, 62 | } = props; 63 | 64 | // Assumption: props.actions never change 65 | // Creating object here instead of render func to prevent invalidating shallow 66 | // prop comparison in children components 67 | this.actions = { 68 | ...actions, 69 | play: this.handlePlay, 70 | pause: this.handlePause, 71 | }; 72 | 73 | _frames = computeAllFrames(context.layout, steps, computeFrame); 74 | } 75 | 76 | componentWillReceiveProps(nextProps, nextContext) { 77 | const { 78 | steps, 79 | computeFrame, 80 | } = nextProps; 81 | 82 | _frames = computeAllFrames(nextContext.layout, steps, computeFrame); 83 | } 84 | 85 | componentWillUnmount() { 86 | this.cancelAnimation(); 87 | } 88 | 89 | handleScrollTo(pos) { 90 | this.setState({ 91 | pos, 92 | }); 93 | } 94 | 95 | handlePlay() { 96 | this.setState({ 97 | isPlaying: true, 98 | }, this.scheduleAnimation); 99 | } 100 | 101 | handlePause() { 102 | this.setState({ 103 | isPlaying: false, 104 | }, this.cancelAnimation); 105 | } 106 | 107 | scheduleAnimation() { 108 | this.cancelAnimation(); 109 | this.prevTime = Date.now(); 110 | this.requestFrame(); 111 | } 112 | 113 | requestFrame() { 114 | this.animationHandle = raf(this.onFrame); 115 | } 116 | 117 | cancelAnimation() { 118 | raf.cancel(this.animationHandle); 119 | } 120 | 121 | onFrame() { 122 | const timeNow = Date.now(); 123 | const frames = (timeNow - this.prevTime) / TIME_PER_FRAME; 124 | this.prevTime = timeNow; 125 | 126 | const { 127 | steps, 128 | } = this.props; 129 | const { 130 | pos, 131 | } = this.state; 132 | const maxPos = getMaxPos(steps.length); 133 | 134 | if (pos < maxPos) { 135 | const newPos = min(maxPos, pos + frames); 136 | 137 | this.setState({ 138 | pos: newPos, 139 | }, this.requestFrame); 140 | } else { 141 | this.setState({ 142 | isPlaying: false, 143 | }); 144 | } 145 | } 146 | 147 | render() { 148 | const { 149 | algorithm, 150 | illustration, 151 | steps, 152 | } = this.props; 153 | const { 154 | pos, 155 | isPlaying, 156 | } = this.state; 157 | const { 158 | layout 159 | } = this.context; 160 | const { 161 | color, 162 | headerHeight, 163 | footerHeight, 164 | } = layout; 165 | 166 | const frame = _frames[floor(pos)]; 167 | const { 168 | stack, 169 | entryHeight, 170 | entries, 171 | } = frame; 172 | 173 | return ( 174 |
175 |
183 | {entries.map(entry => { 184 | const { 185 | entryId, 186 | opacity, 187 | } = entry; 188 | 189 | return ( 190 |
198 | 204 |
205 | ); 206 | })} 207 |
208 | {steps.length > 1 && ( 209 |
216 | 224 |
225 | )} 226 | 247 |
248 | ); 249 | } 250 | } 251 | 252 | Player.propTypes = { 253 | algorithm: PropTypes.func.isRequired, 254 | illustration: PropTypes.func.isRequired, 255 | computeFrame: PropTypes.func.isRequired, 256 | steps: PropTypes.array.isRequired, 257 | actions: PropTypes.object.isRequired, 258 | }; 259 | 260 | Player.contextTypes = { 261 | layout: PropTypes.object, 262 | }; 263 | 264 | export default Player; 265 | -------------------------------------------------------------------------------- /components/source-code.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import PureLayoutComponent from '../utils/pure-layout-component'; 4 | 5 | const { min, max } = Math; 6 | 7 | const renderLineNum = num => ( 8 | 9 | {num < 10 && ' '}{`${num}. `} 10 | 16 | 17 | ); 18 | 19 | const renderLine = (fnLine, lineStart, highlight, num) => { 20 | if (highlight) { 21 | const { start, end } = highlight; 22 | const lineLen = fnLine.length; 23 | const lineEnd = lineStart + lineLen; 24 | const isRangeInLine = start < lineEnd && end > lineStart; 25 | 26 | if (isRangeInLine) { 27 | const relStart = max(0, start - lineStart); 28 | const relEnd = min(end - lineStart, lineLen); 29 | 30 | return ( 31 |
32 | {renderLineNum(num)} 33 | 34 | {fnLine.slice(0, relStart)} 35 | 36 | {fnLine.slice(relStart, relEnd)} 37 | 38 | {fnLine.slice(relEnd)} 39 | 40 | 50 |
51 | ); 52 | } 53 | } 54 | 55 | return ( 56 |
57 | {renderLineNum(num)} 58 | {fnLine} 59 |
60 | ); 61 | }; 62 | 63 | class SourceCode extends PureLayoutComponent { 64 | render() { 65 | const { 66 | def, 67 | highlight, 68 | } = this.props; 69 | const { 70 | layout, 71 | } = this.context; 72 | const { 73 | padding, 74 | codeFontSize, 75 | codeLineHeight, 76 | } = layout; 77 | let lineStart = 0; 78 | 79 | return ( 80 |
 87 |         {def.split('\n').map((fnLine, num) => {
 88 |           const lineEl = renderLine(fnLine, lineStart, highlight, num);
 89 |           lineStart += fnLine.length + 1; // Account for newlines removed
 90 | 
 91 |           return lineEl;
 92 |         })}
 93 |         
102 |       
103 | ); 104 | } 105 | } 106 | 107 | SourceCode.propTypes = { 108 | def: PropTypes.string.isRequired, 109 | highlight: PropTypes.shape({ 110 | start: PropTypes.number.isRequired, 111 | end: PropTypes.number.isRequired, 112 | }), 113 | }; 114 | 115 | SourceCode.contextTypes = { 116 | layout: PropTypes.object, 117 | }; 118 | 119 | export default SourceCode; 120 | -------------------------------------------------------------------------------- /components/stack-entry.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { createElement } from 'react'; 3 | import PureLayoutComponent from '../utils/pure-layout-component'; 4 | import SourceCode from './source-code'; 5 | 6 | const { max, round } = Math; 7 | 8 | class StackEntry extends PureLayoutComponent { 9 | render() { 10 | const { 11 | illustration, 12 | code, 13 | frame, 14 | actions, 15 | } = this.props; 16 | const { 17 | highlight, 18 | } = frame; 19 | const { 20 | layout 21 | } = this.context; 22 | const { 23 | landscape, 24 | sideWidth, 25 | illustrationHeight, 26 | codeHeight, 27 | } = layout; 28 | 29 | const illustrationStyle = { 30 | width: sideWidth, 31 | height: illustrationHeight, 32 | }; 33 | const codeStyle = { 34 | width: sideWidth, 35 | height: codeHeight, 36 | }; 37 | 38 | if (landscape) { 39 | Object.assign(illustrationStyle, { 40 | display: 'table-cell', 41 | paddingTop: max(0, round((codeHeight - illustrationHeight) / 2)), 42 | verticalAlign: 'top' 43 | }); 44 | 45 | Object.assign(codeStyle, { 46 | display: 'table-cell', 47 | paddingTop: max(0, round((illustrationHeight - codeHeight) / 2)), 48 | verticalAlign: 'top' 49 | }); 50 | } 51 | 52 | return ( 53 |
54 |
58 | {createElement(illustration, { 59 | frame, 60 | actions, 61 | })} 62 |
63 |
67 | 71 |
72 | 78 |
79 | ); 80 | } 81 | } 82 | 83 | StackEntry.propTypes = { 84 | code: PropTypes.string.isRequired, 85 | illustration: PropTypes.func.isRequired, 86 | frame: PropTypes.object.isRequired, 87 | actions: PropTypes.object.isRequired, 88 | }; 89 | 90 | StackEntry.contextTypes = { 91 | layout: PropTypes.object, 92 | }; 93 | 94 | export default StackEntry; 95 | -------------------------------------------------------------------------------- /cosmos/cosmos.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootPath: '..', 3 | proxiesPath: './cosmos/cosmos.proxies', 4 | publicPath: 'static', 5 | publicUrl: '/static/' 6 | }; 7 | -------------------------------------------------------------------------------- /cosmos/cosmos.proxies.js: -------------------------------------------------------------------------------- 1 | import LayoutProxy from './proxies/layout-proxy'; 2 | import FrameProxy from './proxies/frame-proxy'; 3 | import ContextProxy from './proxies/context-proxy'; 4 | import GlobalStyleProxy from './proxies/global-style-proxy'; 5 | import BgColorProxy from './proxies/bg-color-proxy'; 6 | 7 | export default [ 8 | LayoutProxy, 9 | FrameProxy, 10 | ContextProxy, 11 | GlobalStyleProxy, 12 | BgColorProxy 13 | ]; 14 | -------------------------------------------------------------------------------- /cosmos/cosmos.test.js: -------------------------------------------------------------------------------- 1 | import runTests from 'react-cosmos-telescope'; 2 | 3 | runTests({ 4 | cosmosConfigPath: require.resolve('./cosmos.config.js') 5 | }); 6 | -------------------------------------------------------------------------------- /cosmos/proxies/bg-color-proxy.js: -------------------------------------------------------------------------------- 1 | import createGlobalCssProxy from './global-css-proxy'; 2 | 3 | export default createGlobalCssProxy({ 4 | getCss: ({ layout }) => { 5 | if (!layout) { 6 | return ''; 7 | } 8 | 9 | return `body { 10 | background: ${layout.color}; 11 | }`; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /cosmos/proxies/context-proxy.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import createContextProxy from 'react-cosmos-context-proxy'; 3 | 4 | export default createContextProxy({ 5 | childContextTypes: { 6 | layout: PropTypes.object 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /cosmos/proxies/frame-proxy.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import getStack from '../../utils/stack'; 4 | import computeBinarySearchFrame from '../../frame/binary-search'; 5 | import computeQuicksortFrame from '../../frame/quicksort'; 6 | import computeRawDataFrame from '../../frame/raw-data'; 7 | 8 | const frameComputers = { 9 | binarySearch: computeBinarySearchFrame, 10 | quicksort: computeQuicksortFrame, 11 | bfs: computeRawDataFrame 12 | }; 13 | 14 | class FrameProxy extends React.Component { 15 | render() { 16 | const { nextProxy, fixture, layout } = this.props; 17 | const { layoutFor, frameFrom } = fixture; 18 | 19 | if (!frameFrom) { 20 | return React.createElement(nextProxy.value, { 21 | ...this.props, 22 | nextProxy: nextProxy.next() 23 | }); 24 | } 25 | 26 | const { prevStep, nextStep, stepProgress } = frameFrom; 27 | const stack = getStack([prevStep, nextStep], 0); 28 | const computer = frameComputers[layoutFor]; 29 | const frame = computer(layout, stack, stepProgress).entries[0].frame; 30 | 31 | return React.createElement(nextProxy.value, { 32 | ...this.props, 33 | nextProxy: nextProxy.next(), 34 | fixture: { 35 | ...fixture, 36 | props: { 37 | ...fixture.props, 38 | frame 39 | } 40 | } 41 | }); 42 | } 43 | } 44 | 45 | FrameProxy.propTypes = { 46 | nextProxy: PropTypes.shape({ 47 | value: PropTypes.func, 48 | next: PropTypes.func 49 | }).isRequired, 50 | fixture: PropTypes.object.isRequired, 51 | layout: PropTypes.object 52 | }; 53 | 54 | FrameProxy.defaultProps = { 55 | layout: {} 56 | }; 57 | 58 | export default FrameProxy; 59 | -------------------------------------------------------------------------------- /cosmos/proxies/global-css-proxy.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* eslint-env browser */ 3 | 4 | import React from 'react'; 5 | 6 | // TODO: Make this an official React Cosmos proxy 7 | export default ({ 8 | getCss, 9 | }) => { 10 | class GlobalCSSProxy extends React.Component { 11 | componentDidMount() { 12 | const css = getCss(this.props); 13 | if (css) { 14 | const node = document.createElement('style'); 15 | node.appendChild(document.createTextNode(css)); 16 | document.head.appendChild(node); 17 | this.globalStyleNode = node; 18 | } 19 | } 20 | 21 | componentWillUnmount() { 22 | if (this.globalStyleNode) { 23 | document.head.removeChild(this.globalStyleNode); 24 | } 25 | } 26 | 27 | render() { 28 | const { 29 | nextProxy, 30 | } = this.props; 31 | 32 | return React.createElement(nextProxy.value, { 33 | ...this.props, 34 | nextProxy: nextProxy.next(), 35 | }); 36 | } 37 | } 38 | 39 | GlobalCSSProxy.propTypes = { 40 | nextProxy: PropTypes.shape({ 41 | value: PropTypes.func, 42 | next: PropTypes.func, 43 | }).isRequired, 44 | }; 45 | 46 | return GlobalCSSProxy; 47 | }; 48 | -------------------------------------------------------------------------------- /cosmos/proxies/global-style-proxy.js: -------------------------------------------------------------------------------- 1 | import createGlobalCssProxy from './global-css-proxy'; 2 | 3 | export default createGlobalCssProxy({ 4 | getCss: () => 5 | `body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 9 | } 10 | @font-face { 11 | font-family: 'FiraCode-Light'; 12 | src: url('/static/FiraCode-Light.woff'); 13 | } 14 | pre, 15 | .code { 16 | font-family: 'FiraCode-Light'; 17 | }` 18 | }); 19 | -------------------------------------------------------------------------------- /cosmos/proxies/layout-proxy.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import binarySearch from '../../algorithms/binary-search'; 5 | import quicksort from '../../algorithms/quicksort'; 6 | import bfs from '../../algorithms/bfs'; 7 | import computeBinaryBindingLayout from '../../layout/binary-search'; 8 | import computeQuicksortLayout from '../../layout/quicksort'; 9 | import computeBfsLayout from '../../layout/bfs'; 10 | 11 | const layoutRefs = { 12 | binarySearch: { 13 | code: binarySearch.code, 14 | computeLayout: computeBinaryBindingLayout 15 | }, 16 | quicksort: { 17 | code: quicksort.code, 18 | computeLayout: computeQuicksortLayout 19 | }, 20 | bfs: { 21 | code: bfs.code, 22 | computeLayout: computeBfsLayout 23 | } 24 | }; 25 | 26 | class LayoutProxy extends React.Component { 27 | render() { 28 | const { nextProxy, fixture } = this.props; 29 | const { layoutFor } = fixture; 30 | 31 | if (!layoutFor) { 32 | return React.createElement(nextProxy.value, { 33 | ...this.props, 34 | nextProxy: nextProxy.next() 35 | }); 36 | } 37 | 38 | const { code, computeLayout } = layoutRefs[layoutFor]; 39 | const layout = computeLayout({ 40 | width: 1200, 41 | height: 600, 42 | code 43 | }); 44 | 45 | return React.createElement(nextProxy.value, { 46 | ...this.props, 47 | nextProxy: nextProxy.next(), 48 | fixture: { 49 | ...fixture, 50 | context: { 51 | layout 52 | } 53 | }, 54 | // Let other proxies make use of the layout instance as well 55 | layout 56 | }); 57 | } 58 | } 59 | 60 | LayoutProxy.propTypes = { 61 | nextProxy: PropTypes.shape({ 62 | value: PropTypes.func, 63 | next: PropTypes.func 64 | }).isRequired, 65 | fixture: PropTypes.object.isRequired 66 | }; 67 | 68 | export default LayoutProxy; 69 | -------------------------------------------------------------------------------- /frame/__tests__/quicksort.js: -------------------------------------------------------------------------------- 1 | import getStack from '../../utils/stack'; 2 | import getQuicksortLayout from '../../layout/quicksort'; 3 | import computeQuicksortFrame from '../quicksort'; 4 | 5 | const layout = getQuicksortLayout({ 6 | color: 'coral', 7 | width: 600, 8 | height: 400, 9 | code: '', 10 | }); 11 | 12 | const steps = [ 13 | { 14 | intro: true, 15 | bindings: { 16 | list: [ 17 | 'cherries', 18 | 'kiwi', 19 | 'grapes', 20 | 'avocado', 21 | 'pineapple', 22 | 'peach' 23 | ] 24 | } 25 | }, 26 | { 27 | highlight: { 28 | start: 9, 29 | end: 24 30 | }, 31 | bindings: { 32 | list: [ 33 | 'cherries', 34 | 'kiwi', 35 | 'grapes', 36 | 'avocado', 37 | 'pineapple', 38 | 'peach' 39 | ] 40 | } 41 | }, 42 | { 43 | highlight: { 44 | start: 33, 45 | end: 48 46 | }, 47 | bindings: { 48 | list: [ 49 | 'cherries', 50 | 'kiwi', 51 | 'grapes', 52 | 'avocado', 53 | 'pineapple', 54 | 'peach' 55 | ] 56 | }, 57 | compared: [ 58 | 'list.length', 59 | '2' 60 | ] 61 | }, 62 | { 63 | highlight: { 64 | start: 76, 65 | end: 103 66 | }, 67 | bindings: { 68 | list: [ 69 | 'cherries', 70 | 'kiwi', 71 | 'grapes', 72 | 'avocado', 73 | 'pineapple', 74 | 'peach' 75 | ], 76 | pivot: 'pineapple' 77 | } 78 | }, 79 | { 80 | highlight: { 81 | start: 106, 82 | end: 147 83 | }, 84 | bindings: { 85 | list: [ 86 | 'cherries', 87 | 'kiwi', 88 | 'grapes', 89 | 'avocado', 90 | 'pineapple', 91 | 'peach' 92 | ], 93 | pivot: 'pineapple', 94 | less: [ 95 | 'cherries', 96 | 'kiwi', 97 | 'grapes', 98 | 'avocado', 99 | 'peach' 100 | ] 101 | } 102 | }, 103 | { 104 | highlight: { 105 | start: 150, 106 | end: 194 107 | }, 108 | bindings: { 109 | list: [ 110 | 'cherries', 111 | 'kiwi', 112 | 'grapes', 113 | 'avocado', 114 | 'pineapple', 115 | 'peach' 116 | ], 117 | pivot: 'pineapple', 118 | less: [ 119 | 'cherries', 120 | 'kiwi', 121 | 'grapes', 122 | 'avocado', 123 | 'peach' 124 | ], 125 | greater: [] 126 | } 127 | }, 128 | { 129 | highlight: { 130 | start: 214, 131 | end: 229 132 | }, 133 | bindings: { 134 | list: [ 135 | 'cherries', 136 | 'kiwi', 137 | 'grapes', 138 | 'avocado', 139 | 'pineapple', 140 | 'peach' 141 | ], 142 | pivot: 'pineapple', 143 | less: [ 144 | 'cherries', 145 | 'kiwi', 146 | 'grapes', 147 | 'avocado', 148 | 'peach' 149 | ], 150 | greater: [] 151 | }, 152 | beforeChildCall: true 153 | }, 154 | { 155 | parentStepId: 6, 156 | highlight: { 157 | start: 9, 158 | end: 24 159 | }, 160 | bindings: { 161 | list: [ 162 | 'cherries', 163 | 'kiwi', 164 | 'grapes', 165 | 'avocado', 166 | 'peach' 167 | ] 168 | } 169 | }, 170 | { 171 | parentStepId: 6, 172 | highlight: { 173 | start: 33, 174 | end: 48 175 | }, 176 | bindings: { 177 | list: [ 178 | 'cherries', 179 | 'kiwi', 180 | 'grapes', 181 | 'avocado', 182 | 'peach' 183 | ] 184 | }, 185 | compared: [ 186 | 'list.length', 187 | '2' 188 | ] 189 | } 190 | ]; 191 | 192 | const stack1 = getStack(steps, 6); 193 | const stack2 = getStack(steps, 8); 194 | 195 | test('frame entries are reused', () => { 196 | const frame1 = computeQuicksortFrame(layout, stack1, 0); 197 | const frame2 = computeQuicksortFrame(layout, stack2, 0); 198 | 199 | expect(frame1.entries[1].frame).toBe(frame2.entries[1].frame); 200 | }); 201 | -------------------------------------------------------------------------------- /frame/base.js: -------------------------------------------------------------------------------- 1 | import { 2 | transitionValue, 3 | } from '../utils/transition'; 4 | 5 | const { round, max } = Math; 6 | 7 | export const getStackEntryHeight = layout => { 8 | const { 9 | landscape, 10 | illustrationHeight, 11 | codeHeight, 12 | } = layout; 13 | 14 | return landscape ? max(illustrationHeight, codeHeight) : illustrationHeight + codeHeight; 15 | }; 16 | 17 | export const getContentHeight = (layout, stackLength) => { 18 | return getStackEntryHeight(layout) * stackLength; 19 | }; 20 | 21 | export const getContentTopOffset = (layout, stackLength) => { 22 | const { 23 | availableContentHeight, 24 | } = layout; 25 | const contentHeight = getContentHeight(layout, stackLength); 26 | 27 | return max(0, round((availableContentHeight - contentHeight) / 2)); 28 | }; 29 | 30 | export const getListItemLeftPosition = (layout, itemIndex) => { 31 | const { 32 | borderWidth, 33 | blockWidth, 34 | } = layout; 35 | 36 | return (blockWidth - borderWidth) * itemIndex; 37 | }; 38 | 39 | const getOpacityForStackDepth = level => { 40 | return level > 0 ? 0.5 : 1; 41 | }; 42 | 43 | const getTopStackEntryOpacity = (stack, stepProgress) => { 44 | const { isAddingToStack, isRemovingFromStack } = stack; 45 | 46 | if (isAddingToStack) { 47 | return transitionValue(0, 1, stepProgress); 48 | } 49 | 50 | if (isRemovingFromStack) { 51 | return transitionValue(1, 0, stepProgress); 52 | } 53 | 54 | return 1; 55 | }; 56 | 57 | const getTransOpacityForStackEntry = (stack, entryIndex, stepProgress) => { 58 | const { isAddingToStack, isRemovingFromStack } = stack; 59 | 60 | if (entryIndex === 0) { 61 | return getTopStackEntryOpacity(stack, stepProgress); 62 | } 63 | 64 | if (isAddingToStack) { 65 | return transitionValue( 66 | getOpacityForStackDepth(entryIndex - 1), 67 | getOpacityForStackDepth(entryIndex), 68 | stepProgress, 69 | ); 70 | } 71 | 72 | if (isRemovingFromStack) { 73 | return transitionValue( 74 | getOpacityForStackDepth(entryIndex), 75 | getOpacityForStackDepth(entryIndex - 1), 76 | stepProgress, 77 | ); 78 | } 79 | 80 | return getOpacityForStackDepth(entryIndex); 81 | }; 82 | 83 | const getStackTopPosition = (layout, stack, stepProgress) => { 84 | const { entries, isAddingToStack, isRemovingFromStack } = stack; 85 | const { length } = entries; 86 | const entryHeight = getStackEntryHeight(layout); 87 | 88 | if (isAddingToStack) { 89 | return transitionValue( 90 | getContentTopOffset(layout, length - 1) - entryHeight, 91 | getContentTopOffset(layout, length), 92 | stepProgress 93 | ); 94 | } 95 | 96 | if (isRemovingFromStack) { 97 | return transitionValue( 98 | getContentTopOffset(layout, length), 99 | getContentTopOffset(layout, length - 1) - entryHeight, 100 | stepProgress 101 | ); 102 | } 103 | 104 | return getContentTopOffset(layout, length); 105 | }; 106 | 107 | export default (layout, stack, stepProgress) => { 108 | const { entries } = stack; 109 | const { length } = entries; 110 | 111 | const top = getStackTopPosition(layout, stack, stepProgress); 112 | const height = getContentHeight(layout, length); 113 | const entryHeight = getStackEntryHeight(layout); 114 | 115 | return { 116 | stack: { 117 | top, 118 | height, 119 | }, 120 | entryHeight, 121 | entries: entries.map(({ nextStep }, i) => { 122 | const { 123 | parentStepId, 124 | highlight, 125 | bindings, 126 | returnValue, 127 | } = nextStep; 128 | 129 | return { 130 | // Tieing stack entry elements to their parent step id will preserve 131 | // their DOM nodes when other entries are added to or removed from stack 132 | entryId: parentStepId || 0, 133 | opacity: getTransOpacityForStackEntry(stack, i, stepProgress), 134 | frame: { 135 | highlight, 136 | bindings, 137 | returnValue, 138 | } 139 | }; 140 | }), 141 | }; 142 | }; 143 | -------------------------------------------------------------------------------- /frame/binary-search.js: -------------------------------------------------------------------------------- 1 | import { 2 | transitionValue, 3 | transitionValues, 4 | getBindingValue, 5 | } from '../utils/transition'; 6 | import getWobbleRotation from '../utils/wobble'; 7 | import { getListItemLeftPosition } from '../layout/base'; 8 | import { getNumberVarTopPosition } from '../layout/binary-search'; 9 | import computeBaseFrame from '../frame/base'; 10 | 11 | const getIntroOpacity = step => step.intro ? 1 : 0; 12 | 13 | const LIST_BASE_ROTATIONS = [-0.9, -0.4, 1.4, 0.5, -1.35, 1]; 14 | 15 | const getListItemOpacity = (index, step) => { 16 | const { 17 | low, 18 | high, 19 | } = step.bindings; 20 | 21 | const isIncluded = ( 22 | (low === undefined || high === undefined) || 23 | (index >= low && index <= high) 24 | ); 25 | 26 | return isIncluded ? 1 : 0.2; 27 | }; 28 | 29 | const getListItemGlow = (name, step) => step.bindings.guess === name ? 0.4 : 0; 30 | 31 | const ITEM_BASE_ROTATION = -1.5; 32 | 33 | const getItemOpacity = step => !step.intro && step.bindings.item !== undefined ? 1 : 0; 34 | 35 | const varTopPos = { 36 | low: 0, 37 | high: 1, 38 | mid: 2, 39 | }; 40 | 41 | const getVarProps = (step, layout, binding) => { 42 | const value = step.bindings[binding]; 43 | const isPresent = value !== undefined; 44 | 45 | if (!isPresent) { 46 | return { opacity: 0 }; 47 | } 48 | 49 | return { 50 | top: getNumberVarTopPosition(layout, varTopPos[binding]), 51 | left: getListItemLeftPosition(layout, value), 52 | opacity: 1, 53 | }; 54 | }; 55 | 56 | const getComparisonOpacity = step => { 57 | const { 58 | compared, 59 | returnValue, 60 | } = step; 61 | 62 | if (returnValue !== undefined) { 63 | return 1; 64 | } 65 | 66 | if (!compared || compared.indexOf('guess') === -1) { 67 | return 0; 68 | } 69 | 70 | return 1; 71 | }; 72 | 73 | export default (layout, stack, stepProgress) => { 74 | const baseFrame = computeBaseFrame(layout, stack, stepProgress); 75 | const entries = stack.entries.map(({ prevStep, nextStep }, i) => { 76 | const { 77 | bindings, 78 | compared, 79 | returnValue, 80 | } = nextStep; 81 | const { 82 | list, 83 | item, 84 | mid, 85 | guess, 86 | } = bindings; 87 | const baseEntry = baseFrame.entries[i]; 88 | return { 89 | ...baseEntry, 90 | frame: { 91 | ...baseEntry.frame, 92 | intro: { 93 | opacity: transitionValue( 94 | getIntroOpacity(prevStep, layout), 95 | getIntroOpacity(nextStep, layout), 96 | stepProgress, 97 | ) 98 | }, 99 | list: { 100 | items: list.map((name, index) => { 101 | const isGuess = compared && compared.indexOf('guess') !== -1 && index === mid; 102 | 103 | return { 104 | name, 105 | isGuess, 106 | left: getListItemLeftPosition(layout, index), 107 | opacity: transitionValue( 108 | getListItemOpacity(index, prevStep), 109 | getListItemOpacity(index, nextStep), 110 | stepProgress, 111 | ), 112 | rotation: LIST_BASE_ROTATIONS[index] + (isGuess ? getWobbleRotation(stepProgress) : 0), 113 | glow: transitionValue( 114 | getListItemGlow(name, prevStep), 115 | getListItemGlow(name, nextStep), 116 | stepProgress, 117 | ), 118 | }; 119 | }), 120 | isSelectable: Boolean(prevStep.intro && stepProgress === 0), 121 | }, 122 | item: { 123 | value: getBindingValue(prevStep, nextStep, 'item'), 124 | opacity: transitionValue( 125 | getItemOpacity(prevStep), 126 | getItemOpacity(nextStep), 127 | stepProgress, 128 | ), 129 | rotation: ITEM_BASE_ROTATION + ( 130 | compared && compared.indexOf('item') !== -1 ? 131 | getWobbleRotation(stepProgress) : 0 132 | ) 133 | }, 134 | low: { 135 | value: getBindingValue(prevStep, nextStep, 'low'), 136 | ...transitionValues( 137 | getVarProps(prevStep, layout, 'low'), 138 | getVarProps(nextStep, layout, 'low'), 139 | stepProgress, 140 | ), 141 | rotation: ( 142 | compared && compared.indexOf('low') !== -1 ? 143 | getWobbleRotation(stepProgress) : 0 144 | ), 145 | }, 146 | high: { 147 | value: getBindingValue(prevStep, nextStep, 'high'), 148 | ...transitionValues( 149 | getVarProps(prevStep, layout, 'high'), 150 | getVarProps(nextStep, layout, 'high'), 151 | stepProgress, 152 | ), 153 | rotation: ( 154 | compared && compared.indexOf('high') !== -1 ? 155 | getWobbleRotation(stepProgress) : 0 156 | ), 157 | }, 158 | mid: { 159 | value: getBindingValue(prevStep, nextStep, 'mid'), 160 | ...transitionValues( 161 | getVarProps(prevStep, layout, 'mid'), 162 | getVarProps(nextStep, layout, 'mid'), 163 | stepProgress, 164 | ), 165 | }, 166 | comparison: { 167 | value: returnValue !== undefined || guess === item ? '=' : ( 168 | guess > item ? '>' : '<' 169 | ), 170 | opacity: transitionValue( 171 | getComparisonOpacity(prevStep, layout), 172 | getComparisonOpacity(nextStep, layout), 173 | stepProgress, 174 | ) 175 | } 176 | } 177 | }; 178 | }); 179 | 180 | return { 181 | ...baseFrame, 182 | entries, 183 | }; 184 | }; 185 | -------------------------------------------------------------------------------- /frame/quicksort.js: -------------------------------------------------------------------------------- 1 | import { 2 | transitionValue, 3 | transitionValues, 4 | } from '../utils/transition'; 5 | import { retrieveFromCache, addToCache } from '../utils/cache'; 6 | import { getListItemLeftPosition } from '../layout/base'; 7 | import computeBaseFrame from '../frame/base'; 8 | 9 | const createBinaryBindingGetter = binding => 10 | step => step && step.bindings[binding] !== undefined && step.returnValue === undefined ? 1 : 0; 11 | 12 | const createTransitionGetter = (bindingGetter, transitioner) => 13 | (prevStep, nextStep, stepProgress, ...args) => 14 | transitioner( 15 | bindingGetter(prevStep, ...args), 16 | bindingGetter(nextStep, ...args), stepProgress); 17 | 18 | const getTransPivotOpacity = createTransitionGetter( 19 | createBinaryBindingGetter('pivot'), transitionValue); 20 | const getTransLessOpacity = createTransitionGetter( 21 | createBinaryBindingGetter('less'), transitionValue); 22 | const getTransGreaterOpacity = createTransitionGetter( 23 | createBinaryBindingGetter('greater'), transitionValue); 24 | 25 | const getLabelScaleForOpacity = opacity => 0.9 + (opacity * 0.1); 26 | 27 | const getSubListItemLeftPosition = (layout, itemIndex, itemNum) => { 28 | const { 29 | blockNum, 30 | blockWidth, 31 | borderWidth, 32 | } = layout; 33 | 34 | return ( 35 | getListItemLeftPosition(layout, itemIndex) + 36 | ((blockNum - itemNum) * (blockWidth - borderWidth) / 2) 37 | ); 38 | }; 39 | 40 | const isHighlightAt = ({ highlight }, at) => { 41 | return highlight && highlight.start === at; 42 | }; 43 | 44 | // XXX: This breaks if source changes! 45 | const startCharOfLessCall = 214; 46 | const startCharOfGresterCall = 249; 47 | 48 | const isCallingLess = step => isHighlightAt(step, startCharOfLessCall); 49 | const isCallingGreater = step => isHighlightAt(step, startCharOfGresterCall); 50 | 51 | const hasLessResult = ({ highlight, afterChildCall }) => ( 52 | highlight.start > startCharOfLessCall || 53 | (highlight.start === startCharOfLessCall && afterChildCall) 54 | ); 55 | 56 | const hasGreaterResult = ({ highlight, afterChildCall }) => ( 57 | highlight.start > startCharOfGresterCall || 58 | (highlight.start === startCharOfGresterCall && afterChildCall) 59 | ); 60 | 61 | const getLessGlow = step => isCallingLess(step) ? 0.8 : 0; 62 | const getGreaterGlow = step => isCallingGreater(step) ? 0.8 : 0; 63 | 64 | const getTransLessGlow = createTransitionGetter(getLessGlow, transitionValue); 65 | const getTransGreaterGlow = createTransitionGetter(getGreaterGlow, transitionValue); 66 | 67 | const getGroupItemProps = (layout, baseLeftPosition, group, item) => { 68 | const { 69 | blockWidth, 70 | itemGroupTopPosition 71 | } = layout; 72 | 73 | const index = group.indexOf(item); 74 | const center = (group.length / 2) - 0.5; 75 | const distanceFromCenter = index - center; 76 | const rotation = -distanceFromCenter * 10; 77 | 78 | return { 79 | left: baseLeftPosition + (distanceFromCenter * (blockWidth / 3)), 80 | top: itemGroupTopPosition, 81 | rotation, 82 | }; 83 | }; 84 | 85 | const getItemProps = (step, layout, name) => { 86 | const { 87 | bindings, 88 | returnValue, 89 | } = step; 90 | const { 91 | itemGroupTopPosition, 92 | listBottomTopPosition, 93 | listCenterTopPosition, 94 | } = layout; 95 | 96 | const { 97 | list, 98 | pivot, 99 | less, 100 | greater, 101 | } = bindings; 102 | const index = list.indexOf(name); 103 | 104 | if (returnValue !== undefined) { 105 | const sortedList = list.concat().sort(); 106 | const sortedIndex = sortedList.indexOf(name); 107 | 108 | return { 109 | left: getSubListItemLeftPosition(layout, sortedIndex, list.length), 110 | top: listCenterTopPosition, 111 | rotation: 0, 112 | index: sortedIndex, 113 | glow: 0 114 | }; 115 | } else if (name === pivot) { 116 | return { 117 | left: getListItemLeftPosition(layout, 2.5), 118 | top: itemGroupTopPosition, 119 | rotation: 0, 120 | index, 121 | glow: 0 122 | }; 123 | } else if (less && less.includes(name)) { 124 | const subList = hasLessResult(step) ? less.concat().sort() : less; 125 | const subIndex = subList.indexOf(name); 126 | 127 | return { 128 | ...getGroupItemProps(layout, getListItemLeftPosition(layout, 0.5), subList, name), 129 | index: subIndex, 130 | glow: getLessGlow(step), 131 | }; 132 | } else if (greater && greater.includes(name)) { 133 | const subList = hasGreaterResult(step) ? greater.concat().sort() : greater; 134 | const subIndex = subList.indexOf(name); 135 | 136 | return { 137 | ...getGroupItemProps(layout, getListItemLeftPosition(layout, 4.5), subList, name), 138 | index: subIndex, 139 | glow: getGreaterGlow(step), 140 | }; 141 | } 142 | 143 | return { 144 | left: getSubListItemLeftPosition(layout, index, list.length), 145 | top: list.length > 1 ? listBottomTopPosition : listCenterTopPosition, 146 | rotation: 0, 147 | glow: 0, 148 | index, 149 | }; 150 | }; 151 | 152 | const getTransItemProps = createTransitionGetter(getItemProps, transitionValues); 153 | 154 | const getIntroOpacity = step => step.intro ? 1 : 0; 155 | const getTransIntroOpacity = createTransitionGetter(getIntroOpacity, transitionValue); 156 | 157 | const isFinalStep = (step, { blockNum }) => step.bindings.list.length === blockNum && step.returnValue !== undefined; 158 | 159 | const getOutroOpacity = (step, layout) => isFinalStep(step, layout) ? 1 : 0; 160 | const getTransOutroOpacity = createTransitionGetter(getOutroOpacity, transitionValue); 161 | 162 | const isLessEmpty = ({ bindings }) => bindings.less && bindings.less.length === 0; 163 | const isGreaterEmpty = ({ bindings }) => bindings.greater && bindings.greater.length === 0; 164 | const isListEmpty = ({ bindings }) => bindings.list && bindings.list.length === 0; 165 | 166 | const computeEntryFrame = ({ 167 | baseFrame, 168 | layout, 169 | prevStep, 170 | nextStep, 171 | stepProgress 172 | }) => { 173 | const { 174 | padding, 175 | illustrationHeight, 176 | getRelSize, 177 | } = layout; 178 | 179 | const titleFontSize = getRelSize(24, 2); 180 | const titleLineHeight = getRelSize(28, 2); 181 | 182 | const pivotOpacity = getTransPivotOpacity(prevStep, nextStep, stepProgress); 183 | const lessOpacity = getTransLessOpacity(prevStep, nextStep, stepProgress); 184 | const greaterOpacity = getTransGreaterOpacity(prevStep, nextStep, stepProgress); 185 | 186 | const pivot = { 187 | left: getListItemLeftPosition(layout, 2.5), 188 | opacity: pivotOpacity, 189 | scale: getLabelScaleForOpacity(pivotOpacity), 190 | }; 191 | const less = { 192 | left: getListItemLeftPosition(layout, 0.5), 193 | opacity: lessOpacity, 194 | scale: getLabelScaleForOpacity(lessOpacity), 195 | }; 196 | const greater = { 197 | left: getListItemLeftPosition(layout, 4.5), 198 | opacity: greaterOpacity, 199 | scale: getLabelScaleForOpacity(greaterOpacity), 200 | }; 201 | 202 | const lessEmpty = { 203 | opacity: isLessEmpty(nextStep) ? lessOpacity : 0, 204 | glow: getTransLessGlow(prevStep, nextStep, stepProgress), 205 | }; 206 | const greaterEmpty = { 207 | opacity: isGreaterEmpty(nextStep) ? greaterOpacity : 0, 208 | glow: getTransGreaterGlow(prevStep, nextStep, stepProgress), 209 | }; 210 | const listEmptyOpacity = isListEmpty(nextStep) ? 1 : 0; 211 | 212 | const itemPositions = nextStep.bindings.list.reduce((positions, name) => { 213 | positions[name] = getTransItemProps(prevStep, nextStep, stepProgress, layout, name); 214 | return positions; 215 | }, {}); 216 | 217 | return { 218 | ...baseFrame, 219 | intro: { 220 | titleFontSize, 221 | titleLineHeight, 222 | btnTop: (titleLineHeight * 2) + getRelSize(10), 223 | btnFontSize: getRelSize(18, 2), 224 | btnSvgSize: getRelSize(20, 2), 225 | opacity: getTransIntroOpacity(prevStep, nextStep, stepProgress), 226 | areControlsEnabled: stepProgress === 0, 227 | }, 228 | outro: { 229 | titleFontSize, 230 | titleLineHeight, 231 | titleTop: padding * 2, 232 | subtextFontSize: getRelSize(18, 2), 233 | subtextTop: illustrationHeight * 0.75, 234 | opacity: getTransOutroOpacity(prevStep, nextStep, stepProgress, layout), 235 | }, 236 | pivot, 237 | less, 238 | greater, 239 | lessEmpty, 240 | greaterEmpty, 241 | listEmptyOpacity, 242 | itemPositions, 243 | }; 244 | }; 245 | 246 | // Memoizing entry frames speeds up the initial calculation of frames, but also 247 | // helps StackEntry components avoid useless renders because identical entry 248 | // frames will also share identity 249 | // TODO: Flush this when leaving viz 250 | const _cache = new Map(); 251 | 252 | export default (layout, stack, stepProgress) => { 253 | const baseFrame = computeBaseFrame(layout, stack, stepProgress); 254 | const entries = stack.entries.map(({ prevStep, nextStep }, i) => { 255 | const baseEntry = baseFrame.entries[i]; 256 | const entryStepProgress = nextStep === prevStep ? 0 : stepProgress; 257 | const cacheFields = [layout, prevStep, nextStep, entryStepProgress]; 258 | let entryFrame = retrieveFromCache(_cache, ...cacheFields); 259 | 260 | if (!entryFrame) { 261 | entryFrame = computeEntryFrame({ 262 | baseFrame: baseEntry.frame, 263 | layout, 264 | prevStep, 265 | nextStep, 266 | stepProgress: entryStepProgress, 267 | }); 268 | addToCache(_cache, entryFrame, ...cacheFields); 269 | } 270 | 271 | return { 272 | ...baseEntry, 273 | frame: entryFrame, 274 | }; 275 | }); 276 | 277 | return { 278 | ...baseFrame, 279 | entries, 280 | }; 281 | }; 282 | -------------------------------------------------------------------------------- /frame/raw-data.js: -------------------------------------------------------------------------------- 1 | import computeBaseFrame from '../frame/base'; 2 | 3 | export default (layout, stack, stepProgress) => { 4 | const baseFrame = computeBaseFrame(layout, stack, stepProgress); 5 | const entries = stack.entries.map(({ prevStep, nextStep }, i) => { 6 | const baseEntry = baseFrame.entries[i]; 7 | return { 8 | ...baseEntry, 9 | frame: { 10 | ...baseEntry.frame, 11 | isFirstStep: prevStep.intro 12 | } 13 | }; 14 | }); 15 | 16 | return { 17 | ...baseFrame, 18 | entries, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = cb => setTimeout(cb, 0); 2 | -------------------------------------------------------------------------------- /layout/base.js: -------------------------------------------------------------------------------- 1 | const { floor, round, max } = Math; 2 | 3 | const IPHONE6_LANDSCAPE_WIDTH = 667; 4 | 5 | // Values are relative to a base width of 320px 6 | const FRAME_OF_REFERENCE = 320; 7 | const PADDING = 4; 8 | const BORDER_WIDTH = 1; 9 | const CODE_FONT_SIZE = 10; 10 | const CODE_LINE_HEIGHT = 12; 11 | 12 | const roundToMultiOf = (value, multiOf) => 13 | multiOf === undefined ? value : multiOf * round(value / multiOf); 14 | 15 | export const getStackEntryHeight = layout => { 16 | const { 17 | landscape, 18 | illustrationHeight, 19 | codeHeight, 20 | } = layout; 21 | 22 | return landscape ? max(illustrationHeight, codeHeight) : illustrationHeight + codeHeight; 23 | }; 24 | 25 | export const getContentHeight = (layout, stackLength) => { 26 | return getStackEntryHeight(layout) * stackLength; 27 | }; 28 | 29 | export const getContentTopOffset = (layout, stackLength) => { 30 | const { 31 | availableContentHeight, 32 | } = layout; 33 | const contentHeight = getContentHeight(layout, stackLength); 34 | 35 | return max(0, round((availableContentHeight - contentHeight) / 2)); 36 | }; 37 | 38 | export const getListItemLeftPosition = (layout, itemIndex) => { 39 | const { 40 | borderWidth, 41 | blockWidth, 42 | } = layout; 43 | 44 | return (blockWidth - borderWidth) * itemIndex; 45 | }; 46 | 47 | export default init => { 48 | const { 49 | width, 50 | height, 51 | code 52 | } = init; 53 | 54 | const color = '#fff'; 55 | 56 | const landscape = width >= IPHONE6_LANDSCAPE_WIDTH && width >= height; 57 | const sideWidth = landscape ? floor(width / 2) : width; 58 | 59 | const getRelSize = (baseValue, multiOf) => 60 | roundToMultiOf(baseValue / FRAME_OF_REFERENCE * sideWidth, multiOf); 61 | 62 | const headerHeight = getRelSize(32, 2); 63 | const footerHeight = getRelSize(40, 2); 64 | 65 | const padding = getRelSize(PADDING, 2); 66 | const borderWidth = getRelSize(BORDER_WIDTH, 1); 67 | 68 | const codeLineHeight = getRelSize(CODE_LINE_HEIGHT, 2); 69 | 70 | const blockNum = 6; 71 | const blockWidth = floor((sideWidth - (padding * 2)) / blockNum); 72 | const blockLabelHeight = getRelSize(16, 2); 73 | 74 | return { 75 | ...init, 76 | color, 77 | 78 | landscape, 79 | sideWidth, 80 | getRelSize, 81 | 82 | headerHeight, 83 | headerLinkFontSize: getRelSize(12, 2), 84 | headerLinkMargin: getRelSize(10), 85 | footerHeight, 86 | footerButtonIconSize: getRelSize(32, 2), 87 | footerHintFontSize: getRelSize(16, 2), 88 | availableContentHeight: height - headerHeight - footerHeight, 89 | 90 | padding, 91 | borderWidth, 92 | 93 | codeFontSize: getRelSize(CODE_FONT_SIZE, 2), 94 | codeLineHeight, 95 | codeHeight: (codeLineHeight * code.split('\n').length) + (padding * 2), 96 | 97 | blockNum, 98 | blockWidth, 99 | blockLabelFontSize: getRelSize(10, 2), 100 | blockLabelHeight, 101 | blockHeight: blockWidth + blockLabelHeight, 102 | 103 | numberVarWidth: blockWidth, 104 | numberVarHeight: getRelSize(24, 2), 105 | 106 | labelWidth: blockWidth, 107 | labelHeight: getRelSize(24, 2), 108 | labelFontSize: getRelSize(10, 2), 109 | 110 | // YO: Calculate this and override in subclasses 111 | illustrationHeight: 0, 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /layout/bfs.js: -------------------------------------------------------------------------------- 1 | import computeRawDataLayout from './raw-data'; 2 | 3 | export default init => { 4 | const base = computeRawDataLayout(init); 5 | const { 6 | getRelSize, 7 | } = base; 8 | 9 | return { 10 | ...base, 11 | color: '#80D8FF', 12 | 13 | // Hardcoded to fit raw data from Bfs 14 | illustrationHeight: getRelSize(164, 1), 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /layout/binary-search.js: -------------------------------------------------------------------------------- 1 | import computeBaseLayout, { getListItemLeftPosition } from './base'; 2 | 3 | export const getNumberVarTopPosition = (layout, level) => { 4 | const { 5 | padding, 6 | numberVarHeight, 7 | } = layout; 8 | 9 | return (numberVarHeight * level) + (padding * (level + 1)) + padding; 10 | }; 11 | 12 | export default init => { 13 | const base = computeBaseLayout(init); 14 | const { 15 | padding, 16 | blockNum, 17 | borderWidth, 18 | blockWidth, 19 | blockHeight, 20 | numberVarHeight, 21 | } = base; 22 | 23 | const innerWidth = ((blockWidth - borderWidth) * blockNum) + borderWidth; 24 | 25 | const listTop = getNumberVarTopPosition(base, 3); 26 | const center = getListItemLeftPosition(base, 3) + (borderWidth / 2); 27 | 28 | const comparison = { 29 | left: center - (numberVarHeight / 2), 30 | top: listTop + blockHeight + padding, 31 | }; 32 | const item = { 33 | left: center - (blockWidth / 2), 34 | top: comparison.top + numberVarHeight + padding, 35 | }; 36 | 37 | const illustrationHeight = item.top + blockHeight + (padding * 2); 38 | 39 | return { 40 | ...base, 41 | color: '#FF8A80', 42 | innerWidth, 43 | listTop, 44 | center, 45 | comparison, 46 | item, 47 | illustrationHeight, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /layout/quicksort.js: -------------------------------------------------------------------------------- 1 | import computeBaseLayout from './base'; 2 | 3 | export default init => { 4 | const base = computeBaseLayout(init); 5 | const { 6 | padding, 7 | borderWidth, 8 | blockNum, 9 | blockWidth, 10 | blockHeight, 11 | labelHeight, 12 | getRelSize, 13 | } = base; 14 | 15 | const labelTopPosition = padding; 16 | const itemGroupTopPosition = labelTopPosition + labelHeight + padding; 17 | const listBottomTopPosition = itemGroupTopPosition + padding + blockHeight; 18 | const illustrationHeight = listBottomTopPosition + blockHeight + padding; 19 | const listCenterTopPosition = (illustrationHeight / 2) - (blockHeight / 2); 20 | 21 | return { 22 | ...base, 23 | color: '#FFD180', 24 | innerWidth: ((blockWidth - borderWidth) * blockNum) + borderWidth, 25 | blockLabelFontSize: getRelSize(8, 2), 26 | labelTopPosition, 27 | itemGroupTopPosition, 28 | listBottomTopPosition, 29 | illustrationHeight, 30 | listCenterTopPosition, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /layout/raw-data.js: -------------------------------------------------------------------------------- 1 | import computeBaseLayout from './base'; 2 | 3 | // Values are relative to a base width of 320px 4 | const FONT_SIZE = 10; 5 | const LINE_HEIGHT = 12; 6 | 7 | export default init => { 8 | const base = computeBaseLayout(init); 9 | const { 10 | getRelSize, 11 | } = base; 12 | 13 | return { 14 | ...base, 15 | 16 | fontSize: getRelSize(FONT_SIZE, 2), 17 | lineHeight: getRelSize(LINE_HEIGHT, 2), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | exportPathMap() { 3 | return { 4 | '/': { page: '/' }, 5 | '/binary-search': { page: '/binary-search' }, 6 | '/quicksort': { page: '/quicksort' }, 7 | '/bfs': { page: '/bfs' } 8 | }; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illustrated-algorithms", 3 | "version": "0.0.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/skidding/illustrated-algorithms.git" 7 | }, 8 | "devDependencies": { 9 | "babel-loader": "^7.1.2", 10 | "babel-plugin-inline-react-svg": "^0.4.0", 11 | "babel-plugin-trace-execution": "^0.2.0", 12 | "eslint-config-xo-react": "^0.13.0", 13 | "eslint-plugin-react": "^7.4.0", 14 | "html-webpack-plugin": "^2.30.1", 15 | "jest": "^21.2.1", 16 | "react-cosmos": "^4.2.0", 17 | "react-cosmos-context-proxy": "^4.2.0", 18 | "react-cosmos-telescope": "^4.2.0", 19 | "webpack": "^3.8.1", 20 | "xo": "^0.18.2" 21 | }, 22 | "scripts": { 23 | "clear-babel-cache": "rm -rf node_modules/.cache/babel-loader/*", 24 | "test:source": "jest __tests__", 25 | "test:cosmos": "jest cosmos/cosmos.test.js", 26 | "test": "xo && npm run test:source && npm run test:cosmos", 27 | "dev": "next", 28 | "build": "next build && next export -o .export", 29 | "upload": "cd .export && now --name algorithms && cd -", 30 | "cosmos": "cosmos --config cosmos/cosmos.config.js", 31 | "cosmos:export": "cosmos-export --config cosmos/cosmos.config.js" 32 | }, 33 | "xo": { 34 | "space": true, 35 | "esnext": true, 36 | "extends": "xo-react", 37 | "plugins": [ 38 | "react" 39 | ], 40 | "rules": { 41 | "comma-dangle": 0, 42 | "object-curly-spacing": 0, 43 | "react/jsx-no-bind": 0 44 | }, 45 | "overrides": [ 46 | { 47 | "files": [ 48 | "**/__tests__/**/*" 49 | ], 50 | "env": [ 51 | "jest", 52 | "browser" 53 | ] 54 | } 55 | ] 56 | }, 57 | "jest": { 58 | "setupFiles": [ 59 | "/jest.setup.js" 60 | ] 61 | }, 62 | "dependencies": { 63 | "classnames": "^2.2.5", 64 | "emojione": "^2.2.7", 65 | "lodash.debounce": "^4.0.8", 66 | "lodash.range": "^3.2.0", 67 | "next": "^4.1.0", 68 | "prop-types": "^15.6.0", 69 | "raf": "^3.4.0", 70 | "react": "^16.0.0", 71 | "react-addons-shallow-compare": "^15.6.2", 72 | "react-dom": "^16.0.0", 73 | "shuffle-array": "^1.0.1" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pages/bfs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from '../components/page'; 3 | import bfs from '../algorithms/bfs'; 4 | import RawData from '../components/illustrations/raw-data'; 5 | import computeBfsLayout from '../layout/bfs'; 6 | import computeRawDataFrame from '../frame/raw-data'; 7 | 8 | const graph = { 9 | you: ['alice', 'bob', 'claire'], 10 | bob: ['anuj', 'peggy'], 11 | alice: ['peggy'], 12 | claire: ['thom', 'jonny'], 13 | anuj: [], 14 | peggy: [], 15 | thom: [], 16 | jonny: [], 17 | }; 18 | const name = 'you'; 19 | 20 | export default class BfsPage extends React.Component { 21 | render() { 22 | const { steps } = bfs(graph, name); 23 | 24 | return ( 25 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pages/binary-search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import offsetSteps from '../utils/offset-steps'; 3 | import Page from '../components/page'; 4 | import binarySearch from '../algorithms/binary-search'; 5 | import BinarySearch from '../components/illustrations/binary-search/binary-search'; 6 | import computeBinarySearchLayout from '../layout/binary-search'; 7 | import computeBinarySearchFrame from '../frame/binary-search'; 8 | 9 | const list = ['bear', 'cat', 'dog', 'lion', 'panda', 'snail']; 10 | 11 | const getIntroStep = () => ({ 12 | intro: true, 13 | bindings: { 14 | list, 15 | }, 16 | }); 17 | 18 | export default class BinarySearchPage extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.handleGenerateSteps = this.handleGenerateSteps.bind(this); 23 | 24 | this.state = { 25 | steps: [getIntroStep()] 26 | }; 27 | } 28 | 29 | render() { 30 | return ( 31 | 42 | ); 43 | } 44 | 45 | handleGenerateSteps(item, cb) { 46 | const { steps } = binarySearch(list, item); 47 | 48 | this.setState({ 49 | steps: [getIntroStep(), ...offsetSteps(steps, 1)], 50 | }, cb); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import binarySearch from './binary-search'; 2 | 3 | export default binarySearch; 4 | -------------------------------------------------------------------------------- /pages/quicksort.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import shuffle from 'shuffle-array'; 3 | import offsetSteps from '../utils/offset-steps'; 4 | import Page from '../components/page'; 5 | import quicksort from '../algorithms/quicksort'; 6 | import Quicksort from '../components/illustrations/quicksort/quicksort'; 7 | import computeQuicksortLayout from '../layout/quicksort'; 8 | import computeQuicksortFrame from '../frame/quicksort'; 9 | 10 | const initialList = ['cherries', 'kiwi', 'grapes', 'avocado', 'pineapple', 'peach']; 11 | 12 | const getIntroStep = list => ({ 13 | intro: true, 14 | bindings: { 15 | list, 16 | }, 17 | }); 18 | 19 | export default class QuicksortPage extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.handleShuffleInput = this.handleShuffleInput.bind(this); 24 | this.handleGenerateSteps = this.handleGenerateSteps.bind(this); 25 | 26 | this.state = { 27 | list: initialList, 28 | steps: [getIntroStep(initialList)] 29 | }; 30 | } 31 | 32 | render() { 33 | return ( 34 | 46 | ); 47 | } 48 | 49 | handleShuffleInput(cb) { 50 | const list = shuffle(initialList, { copy: true }); 51 | 52 | this.setState({ 53 | list, 54 | steps: [getIntroStep(list)] 55 | }, cb); 56 | } 57 | 58 | handleGenerateSteps(cb) { 59 | const { list } = this.state; 60 | const { steps } = quicksort(list); 61 | 62 | this.setState({ 63 | steps: [getIntroStep(list), ...offsetSteps(steps, 1)], 64 | }, cb); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /static/FiraCode-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovidiuch/illustrated-algorithms/4d71a66b36e4b48fb09fa64161e995a10cfc9a42/static/FiraCode-Light.woff -------------------------------------------------------------------------------- /utils/__tests__/stack-recursive.js: -------------------------------------------------------------------------------- 1 | import getStack from '../stack'; 2 | 3 | const introStep = { 4 | intro: true, 5 | bindings: { 6 | list: [ 7 | 'dog', 8 | 'cat', 9 | 'snail', 10 | 'bear', 11 | 'pig', 12 | 'rat' 13 | ] 14 | } 15 | }; 16 | 17 | const firstNestedStep = { 18 | parentStepId: 6, 19 | highlight: { 20 | start: 9, 21 | end: 24 22 | }, 23 | bindings: { 24 | list: [ 25 | 'cat', 26 | 'bear' 27 | ] 28 | } 29 | }; 30 | 31 | const secondNestedStep = { 32 | parentStepId: 12, 33 | highlight: { 34 | start: 9, 35 | end: 24 36 | }, 37 | bindings: { 38 | list: [ 39 | 'bear' 40 | ] 41 | } 42 | }; 43 | 44 | const steps = [ 45 | introStep, 46 | { 47 | highlight: { 48 | start: 9, 49 | end: 24 50 | }, 51 | bindings: { 52 | list: [ 53 | 'dog', 54 | 'cat', 55 | 'snail', 56 | 'bear', 57 | 'pig', 58 | 'rat' 59 | ] 60 | } 61 | }, 62 | { 63 | highlight: { 64 | start: 33, 65 | end: 48 66 | }, 67 | bindings: { 68 | list: [ 69 | 'dog', 70 | 'cat', 71 | 'snail', 72 | 'bear', 73 | 'pig', 74 | 'rat' 75 | ] 76 | }, 77 | compared: [ 78 | 'list.length', 79 | '2' 80 | ] 81 | }, 82 | { 83 | highlight: { 84 | start: 76, 85 | end: 103 86 | }, 87 | bindings: { 88 | list: [ 89 | 'dog', 90 | 'cat', 91 | 'snail', 92 | 'bear', 93 | 'pig', 94 | 'rat' 95 | ], 96 | pivot: 'dog' 97 | } 98 | }, 99 | { 100 | highlight: { 101 | start: 106, 102 | end: 147 103 | }, 104 | bindings: { 105 | list: [ 106 | 'dog', 107 | 'cat', 108 | 'snail', 109 | 'bear', 110 | 'pig', 111 | 'rat' 112 | ], 113 | pivot: 'dog', 114 | less: [ 115 | 'cat', 116 | 'bear' 117 | ] 118 | } 119 | }, 120 | { 121 | highlight: { 122 | start: 150, 123 | end: 194 124 | }, 125 | bindings: { 126 | list: [ 127 | 'dog', 128 | 'cat', 129 | 'snail', 130 | 'bear', 131 | 'pig', 132 | 'rat' 133 | ], 134 | pivot: 'dog', 135 | less: [ 136 | 'cat', 137 | 'bear' 138 | ], 139 | greater: [ 140 | 'snail', 141 | 'pig', 142 | 'rat' 143 | ] 144 | } 145 | }, 146 | { 147 | highlight: { 148 | start: 214, 149 | end: 229 150 | }, 151 | bindings: { 152 | list: [ 153 | 'dog', 154 | 'cat', 155 | 'snail', 156 | 'bear', 157 | 'pig', 158 | 'rat' 159 | ], 160 | pivot: 'dog', 161 | less: [ 162 | 'cat', 163 | 'bear' 164 | ], 165 | greater: [ 166 | 'snail', 167 | 'pig', 168 | 'rat' 169 | ] 170 | } 171 | }, 172 | firstNestedStep, 173 | { 174 | parentStepId: 6, 175 | highlight: { 176 | start: 33, 177 | end: 48 178 | }, 179 | bindings: { 180 | list: [ 181 | 'cat', 182 | 'bear' 183 | ] 184 | }, 185 | compared: [ 186 | 'list.length', 187 | '2' 188 | ] 189 | }, 190 | { 191 | parentStepId: 6, 192 | highlight: { 193 | start: 76, 194 | end: 103 195 | }, 196 | bindings: { 197 | list: [ 198 | 'cat', 199 | 'bear' 200 | ], 201 | pivot: 'cat' 202 | } 203 | }, 204 | { 205 | parentStepId: 6, 206 | highlight: { 207 | start: 106, 208 | end: 147 209 | }, 210 | bindings: { 211 | list: [ 212 | 'cat', 213 | 'bear' 214 | ], 215 | pivot: 'cat', 216 | less: [ 217 | 'bear' 218 | ] 219 | } 220 | }, 221 | { 222 | parentStepId: 6, 223 | highlight: { 224 | start: 150, 225 | end: 194 226 | }, 227 | bindings: { 228 | list: [ 229 | 'cat', 230 | 'bear' 231 | ], 232 | pivot: 'cat', 233 | less: [ 234 | 'bear' 235 | ], 236 | greater: [] 237 | } 238 | }, 239 | { 240 | parentStepId: 6, 241 | highlight: { 242 | start: 214, 243 | end: 229 244 | }, 245 | bindings: { 246 | list: [ 247 | 'cat', 248 | 'bear' 249 | ], 250 | pivot: 'cat', 251 | less: [ 252 | 'bear' 253 | ], 254 | greater: [] 255 | } 256 | }, 257 | secondNestedStep, 258 | { 259 | parentStepId: 12, 260 | highlight: { 261 | start: 33, 262 | end: 48 263 | }, 264 | bindings: { 265 | list: [ 266 | 'bear' 267 | ] 268 | }, 269 | compared: [ 270 | 'list.length', 271 | '2' 272 | ] 273 | }, 274 | { 275 | parentStepId: 12, 276 | highlight: { 277 | start: 56, 278 | end: 68 279 | }, 280 | bindings: { 281 | list: [ 282 | 'bear' 283 | ] 284 | }, 285 | returnValue: [ 286 | 'bear' 287 | ] 288 | }, 289 | { 290 | parentStepId: 6, 291 | highlight: { 292 | start: 249, 293 | end: 267 294 | }, 295 | bindings: { 296 | list: [ 297 | 'cat', 298 | 'bear' 299 | ], 300 | pivot: 'cat', 301 | less: [ 302 | 'bear' 303 | ], 304 | greater: [] 305 | } 306 | }, 307 | { 308 | parentStepId: 16, 309 | highlight: { 310 | start: 9, 311 | end: 24 312 | }, 313 | bindings: { 314 | list: [] 315 | } 316 | }, 317 | { 318 | parentStepId: 16, 319 | highlight: { 320 | start: 33, 321 | end: 48 322 | }, 323 | bindings: { 324 | list: [] 325 | }, 326 | compared: [ 327 | 'list.length', 328 | '2' 329 | ] 330 | }, 331 | { 332 | parentStepId: 16, 333 | highlight: { 334 | start: 56, 335 | end: 68 336 | }, 337 | bindings: { 338 | list: [] 339 | }, 340 | returnValue: [] 341 | }, 342 | { 343 | parentStepId: 6, 344 | highlight: { 345 | start: 198, 346 | end: 272 347 | }, 348 | bindings: { 349 | list: [ 350 | 'cat', 351 | 'bear' 352 | ], 353 | pivot: 'cat', 354 | less: [ 355 | 'bear' 356 | ], 357 | greater: [] 358 | }, 359 | returnValue: [ 360 | 'bear', 361 | 'cat' 362 | ] 363 | }, 364 | { 365 | highlight: { 366 | start: 249, 367 | end: 267 368 | }, 369 | bindings: { 370 | list: [ 371 | 'dog', 372 | 'cat', 373 | 'snail', 374 | 'bear', 375 | 'pig', 376 | 'rat' 377 | ], 378 | pivot: 'dog', 379 | less: [ 380 | 'cat', 381 | 'bear' 382 | ], 383 | greater: [ 384 | 'snail', 385 | 'pig', 386 | 'rat' 387 | ] 388 | } 389 | }, 390 | { 391 | parentStepId: 21, 392 | highlight: { 393 | start: 9, 394 | end: 24 395 | }, 396 | bindings: { 397 | list: [ 398 | 'snail', 399 | 'pig', 400 | 'rat' 401 | ] 402 | } 403 | }, 404 | { 405 | parentStepId: 21, 406 | highlight: { 407 | start: 33, 408 | end: 48 409 | }, 410 | bindings: { 411 | list: [ 412 | 'snail', 413 | 'pig', 414 | 'rat' 415 | ] 416 | }, 417 | compared: [ 418 | 'list.length', 419 | '2' 420 | ] 421 | }, 422 | { 423 | parentStepId: 21, 424 | highlight: { 425 | start: 76, 426 | end: 103 427 | }, 428 | bindings: { 429 | list: [ 430 | 'snail', 431 | 'pig', 432 | 'rat' 433 | ], 434 | pivot: 'pig' 435 | } 436 | }, 437 | { 438 | parentStepId: 21, 439 | highlight: { 440 | start: 106, 441 | end: 147 442 | }, 443 | bindings: { 444 | list: [ 445 | 'snail', 446 | 'pig', 447 | 'rat' 448 | ], 449 | pivot: 'pig', 450 | less: [] 451 | } 452 | }, 453 | { 454 | parentStepId: 21, 455 | highlight: { 456 | start: 150, 457 | end: 194 458 | }, 459 | bindings: { 460 | list: [ 461 | 'snail', 462 | 'pig', 463 | 'rat' 464 | ], 465 | pivot: 'pig', 466 | less: [], 467 | greater: [ 468 | 'snail', 469 | 'rat' 470 | ] 471 | } 472 | }, 473 | { 474 | parentStepId: 21, 475 | highlight: { 476 | start: 214, 477 | end: 229 478 | }, 479 | bindings: { 480 | list: [ 481 | 'snail', 482 | 'pig', 483 | 'rat' 484 | ], 485 | pivot: 'pig', 486 | less: [], 487 | greater: [ 488 | 'snail', 489 | 'rat' 490 | ] 491 | } 492 | }, 493 | { 494 | parentStepId: 27, 495 | highlight: { 496 | start: 9, 497 | end: 24 498 | }, 499 | bindings: { 500 | list: [] 501 | } 502 | }, 503 | { 504 | parentStepId: 27, 505 | highlight: { 506 | start: 33, 507 | end: 48 508 | }, 509 | bindings: { 510 | list: [] 511 | }, 512 | compared: [ 513 | 'list.length', 514 | '2' 515 | ] 516 | }, 517 | { 518 | parentStepId: 27, 519 | highlight: { 520 | start: 56, 521 | end: 68 522 | }, 523 | bindings: { 524 | list: [] 525 | }, 526 | returnValue: [] 527 | }, 528 | { 529 | parentStepId: 21, 530 | highlight: { 531 | start: 249, 532 | end: 267 533 | }, 534 | bindings: { 535 | list: [ 536 | 'snail', 537 | 'pig', 538 | 'rat' 539 | ], 540 | pivot: 'pig', 541 | less: [], 542 | greater: [ 543 | 'snail', 544 | 'rat' 545 | ] 546 | } 547 | }, 548 | { 549 | parentStepId: 31, 550 | highlight: { 551 | start: 9, 552 | end: 24 553 | }, 554 | bindings: { 555 | list: [ 556 | 'snail', 557 | 'rat' 558 | ] 559 | } 560 | }, 561 | { 562 | parentStepId: 31, 563 | highlight: { 564 | start: 33, 565 | end: 48 566 | }, 567 | bindings: { 568 | list: [ 569 | 'snail', 570 | 'rat' 571 | ] 572 | }, 573 | compared: [ 574 | 'list.length', 575 | '2' 576 | ] 577 | }, 578 | { 579 | parentStepId: 31, 580 | highlight: { 581 | start: 76, 582 | end: 103 583 | }, 584 | bindings: { 585 | list: [ 586 | 'snail', 587 | 'rat' 588 | ], 589 | pivot: 'snail' 590 | } 591 | }, 592 | { 593 | parentStepId: 31, 594 | highlight: { 595 | start: 106, 596 | end: 147 597 | }, 598 | bindings: { 599 | list: [ 600 | 'snail', 601 | 'rat' 602 | ], 603 | pivot: 'snail', 604 | less: [ 605 | 'rat' 606 | ] 607 | } 608 | }, 609 | { 610 | parentStepId: 31, 611 | highlight: { 612 | start: 150, 613 | end: 194 614 | }, 615 | bindings: { 616 | list: [ 617 | 'snail', 618 | 'rat' 619 | ], 620 | pivot: 'snail', 621 | less: [ 622 | 'rat' 623 | ], 624 | greater: [] 625 | } 626 | }, 627 | { 628 | parentStepId: 31, 629 | highlight: { 630 | start: 214, 631 | end: 229 632 | }, 633 | bindings: { 634 | list: [ 635 | 'snail', 636 | 'rat' 637 | ], 638 | pivot: 'snail', 639 | less: [ 640 | 'rat' 641 | ], 642 | greater: [] 643 | } 644 | }, 645 | { 646 | parentStepId: 36, 647 | highlight: { 648 | start: 9, 649 | end: 24 650 | }, 651 | bindings: { 652 | list: [ 653 | 'rat' 654 | ] 655 | } 656 | }, 657 | { 658 | parentStepId: 36, 659 | highlight: { 660 | start: 33, 661 | end: 48 662 | }, 663 | bindings: { 664 | list: [ 665 | 'rat' 666 | ] 667 | }, 668 | compared: [ 669 | 'list.length', 670 | '2' 671 | ] 672 | }, 673 | { 674 | parentStepId: 36, 675 | highlight: { 676 | start: 56, 677 | end: 68 678 | }, 679 | bindings: { 680 | list: [ 681 | 'rat' 682 | ] 683 | }, 684 | returnValue: [ 685 | 'rat' 686 | ] 687 | }, 688 | { 689 | parentStepId: 31, 690 | highlight: { 691 | start: 249, 692 | end: 267 693 | }, 694 | bindings: { 695 | list: [ 696 | 'snail', 697 | 'rat' 698 | ], 699 | pivot: 'snail', 700 | less: [ 701 | 'rat' 702 | ], 703 | greater: [] 704 | } 705 | }, 706 | { 707 | parentStepId: 41, 708 | highlight: { 709 | start: 9, 710 | end: 24 711 | }, 712 | bindings: { 713 | list: [] 714 | } 715 | }, 716 | { 717 | parentStepId: 41, 718 | highlight: { 719 | start: 33, 720 | end: 48 721 | }, 722 | bindings: { 723 | list: [] 724 | }, 725 | compared: [ 726 | 'list.length', 727 | '2' 728 | ] 729 | }, 730 | { 731 | parentStepId: 41, 732 | highlight: { 733 | start: 56, 734 | end: 68 735 | }, 736 | bindings: { 737 | list: [] 738 | }, 739 | returnValue: [] 740 | }, 741 | { 742 | parentStepId: 31, 743 | highlight: { 744 | start: 198, 745 | end: 272 746 | }, 747 | bindings: { 748 | list: [ 749 | 'snail', 750 | 'rat' 751 | ], 752 | pivot: 'snail', 753 | less: [ 754 | 'rat' 755 | ], 756 | greater: [] 757 | }, 758 | returnValue: [ 759 | 'rat', 760 | 'snail' 761 | ] 762 | }, 763 | { 764 | parentStepId: 21, 765 | highlight: { 766 | start: 198, 767 | end: 272 768 | }, 769 | bindings: { 770 | list: [ 771 | 'snail', 772 | 'pig', 773 | 'rat' 774 | ], 775 | pivot: 'pig', 776 | less: [], 777 | greater: [ 778 | 'snail', 779 | 'rat' 780 | ] 781 | }, 782 | returnValue: [ 783 | 'pig', 784 | 'rat', 785 | 'snail' 786 | ] 787 | }, 788 | { 789 | highlight: { 790 | start: 198, 791 | end: 272 792 | }, 793 | bindings: { 794 | list: [ 795 | 'dog', 796 | 'cat', 797 | 'snail', 798 | 'bear', 799 | 'pig', 800 | 'rat' 801 | ], 802 | pivot: 'dog', 803 | less: [ 804 | 'cat', 805 | 'bear' 806 | ], 807 | greater: [ 808 | 'snail', 809 | 'pig', 810 | 'rat' 811 | ] 812 | }, 813 | returnValue: [ 814 | 'bear', 815 | 'cat', 816 | 'dog', 817 | 'pig', 818 | 'rat', 819 | 'snail' 820 | ] 821 | } 822 | ]; 823 | 824 | test('returns identical sides for intro step', () => { 825 | expect(getStack([introStep], 0).entries).toEqual([ 826 | { 827 | prevStep: introStep, 828 | nextStep: introStep, 829 | } 830 | ]); 831 | }); 832 | 833 | test('returns first two steps', () => { 834 | expect(getStack(steps, 0).entries).toEqual([ 835 | { 836 | prevStep: steps[0], 837 | nextStep: steps[1], 838 | } 839 | ]); 840 | }); 841 | 842 | test('returns two paused entries for last two returning steps', () => { 843 | const { entries, isRemovingFromStack } = getStack(steps, steps.length - 2); 844 | const nestedReturnStep = steps[steps.length - 2]; 845 | 846 | expect(entries).toEqual([ 847 | { 848 | prevStep: nestedReturnStep, 849 | nextStep: nestedReturnStep, 850 | }, 851 | { 852 | prevStep: steps[nestedReturnStep.parentStepId], 853 | nextStep: steps[steps.length - 1], 854 | } 855 | ]); 856 | expect(isRemovingFromStack).toBe(true); 857 | }); 858 | 859 | test('returns identical sides for last step', () => { 860 | expect(getStack(steps, steps.length - 1).entries).toEqual([ 861 | { 862 | prevStep: steps[steps.length - 1], 863 | nextStep: steps[steps.length - 1], 864 | } 865 | ]); 866 | }); 867 | 868 | test('returns two paused entries when stepping into nested call', () => { 869 | const index = steps.indexOf(firstNestedStep) - 1; 870 | const { entries, isAddingToStack } = getStack(steps, index); 871 | 872 | expect(entries).toEqual([ 873 | { 874 | prevStep: steps[index + 1], 875 | nextStep: steps[index + 1], 876 | }, 877 | { 878 | prevStep: steps[index], 879 | nextStep: steps[index] 880 | }, 881 | ]); 882 | expect(isAddingToStack).toBe(true); 883 | }); 884 | 885 | test('returns three paused entries when stepping into nested call', () => { 886 | const index = steps.indexOf(secondNestedStep) - 1; 887 | const { entries, isAddingToStack } = getStack(steps, index); 888 | 889 | expect(entries).toEqual([ 890 | { 891 | prevStep: steps[index + 1], 892 | nextStep: steps[index + 1], 893 | }, 894 | { 895 | prevStep: steps[index], 896 | nextStep: steps[index] 897 | }, 898 | { 899 | prevStep: steps[steps[index].parentStepId], 900 | nextStep: steps[steps[index].parentStepId] 901 | }, 902 | ]); 903 | expect(isAddingToStack).toBe(true); 904 | }); 905 | 906 | test('returns child transition + paused parent inside 1st nested call', () => { 907 | const index = steps.indexOf(firstNestedStep); 908 | const { entries } = getStack(steps, index); 909 | 910 | expect(entries).toEqual([ 911 | { 912 | prevStep: steps[index], 913 | nextStep: steps[index + 1], 914 | }, 915 | { 916 | prevStep: steps[index - 1], 917 | nextStep: steps[index - 1] 918 | }, 919 | ]); 920 | }); 921 | 922 | test('returns child transition + paused parents inside 2nd nested call', () => { 923 | const index = steps.indexOf(secondNestedStep); 924 | const { entries } = getStack(steps, index); 925 | 926 | expect(entries).toEqual([ 927 | { 928 | prevStep: steps[index], 929 | nextStep: steps[index + 1], 930 | }, 931 | { 932 | prevStep: steps[index - 1], 933 | nextStep: steps[index - 1] 934 | }, 935 | { 936 | prevStep: steps[steps[index - 1].parentStepId], 937 | nextStep: steps[steps[index - 1].parentStepId] 938 | } 939 | ]); 940 | }); 941 | -------------------------------------------------------------------------------- /utils/__tests__/stack-single.js: -------------------------------------------------------------------------------- 1 | import getStack from '../stack'; 2 | 3 | const introStep = { 4 | intro: true, 5 | bindings: { 6 | list: [ 7 | 'bear', 8 | 'cat', 9 | 'dog', 10 | 'lion', 11 | 'panda', 12 | 'snail' 13 | ], 14 | item: 'lion' 15 | } 16 | }; 17 | 18 | const steps = [ 19 | introStep, 20 | { 21 | highlight: { 22 | start: 9, 23 | end: 33 24 | }, 25 | bindings: { 26 | list: [ 27 | 'bear', 28 | 'cat', 29 | 'dog', 30 | 'lion', 31 | 'panda', 32 | 'snail' 33 | ], 34 | item: 'lion' 35 | } 36 | }, 37 | { 38 | highlight: { 39 | start: 38, 40 | end: 50 41 | }, 42 | bindings: { 43 | list: [ 44 | 'bear', 45 | 'cat', 46 | 'dog', 47 | 'lion', 48 | 'panda', 49 | 'snail' 50 | ], 51 | item: 'lion', 52 | low: 0 53 | } 54 | }, 55 | { 56 | highlight: { 57 | start: 53, 58 | end: 80 59 | }, 60 | bindings: { 61 | list: [ 62 | 'bear', 63 | 'cat', 64 | 'dog', 65 | 'lion', 66 | 'panda', 67 | 'snail' 68 | ], 69 | item: 'lion', 70 | low: 0, 71 | high: 5 72 | } 73 | }, 74 | { 75 | highlight: { 76 | start: 91, 77 | end: 102 78 | }, 79 | bindings: { 80 | list: [ 81 | 'bear', 82 | 'cat', 83 | 'dog', 84 | 'lion', 85 | 'panda', 86 | 'snail' 87 | ], 88 | item: 'lion', 89 | low: 0, 90 | high: 5 91 | }, 92 | compared: [ 93 | 'low', 94 | 'high' 95 | ] 96 | }, 97 | { 98 | highlight: { 99 | start: 110, 100 | end: 151 101 | }, 102 | bindings: { 103 | list: [ 104 | 'bear', 105 | 'cat', 106 | 'dog', 107 | 'lion', 108 | 'panda', 109 | 'snail' 110 | ], 111 | item: 'lion', 112 | low: 0, 113 | high: 5, 114 | mid: 3 115 | } 116 | }, 117 | { 118 | highlight: { 119 | start: 156, 120 | end: 180 121 | }, 122 | bindings: { 123 | list: [ 124 | 'bear', 125 | 'cat', 126 | 'dog', 127 | 'lion', 128 | 'panda', 129 | 'snail' 130 | ], 131 | item: 'lion', 132 | low: 0, 133 | high: 5, 134 | mid: 3, 135 | guess: 'lion' 136 | } 137 | }, 138 | { 139 | highlight: { 140 | start: 190, 141 | end: 204 142 | }, 143 | bindings: { 144 | list: [ 145 | 'bear', 146 | 'cat', 147 | 'dog', 148 | 'lion', 149 | 'panda', 150 | 'snail' 151 | ], 152 | item: 'lion', 153 | low: 0, 154 | high: 5, 155 | mid: 3, 156 | guess: 'lion' 157 | }, 158 | compared: [ 159 | 'guess', 160 | 'item' 161 | ] 162 | }, 163 | { 164 | highlight: { 165 | start: 214, 166 | end: 225 167 | }, 168 | bindings: { 169 | list: [ 170 | 'bear', 171 | 'cat', 172 | 'dog', 173 | 'lion', 174 | 'panda', 175 | 'snail' 176 | ], 177 | item: 'lion', 178 | low: 0, 179 | high: 5, 180 | mid: 3, 181 | guess: 'lion' 182 | }, 183 | returnValue: 3 184 | } 185 | ]; 186 | 187 | test('returns identical sides for intro step', () => { 188 | expect(getStack([introStep], 0).entries).toEqual([ 189 | { 190 | prevStep: introStep, 191 | nextStep: introStep, 192 | } 193 | ]); 194 | }); 195 | 196 | test('returns first two steps', () => { 197 | expect(getStack(steps, 0).entries).toEqual([ 198 | { 199 | prevStep: steps[0], 200 | nextStep: steps[1], 201 | } 202 | ]); 203 | }); 204 | 205 | test('returns last two steps', () => { 206 | expect(getStack(steps, steps.length - 2).entries).toEqual([ 207 | { 208 | prevStep: steps[steps.length - 2], 209 | nextStep: steps[steps.length - 1], 210 | } 211 | ]); 212 | }); 213 | 214 | test('returns identical sides for last step', () => { 215 | expect(getStack(steps, steps.length - 1).entries).toEqual([ 216 | { 217 | prevStep: steps[steps.length - 1], 218 | nextStep: steps[steps.length - 1], 219 | } 220 | ]); 221 | }); 222 | -------------------------------------------------------------------------------- /utils/cache.js: -------------------------------------------------------------------------------- 1 | export const retrieveFromCache = (cache, ...fields) => { 2 | let index = 0; 3 | let node = cache; 4 | 5 | while (node && index < fields.length) { 6 | node = node.get(fields[index++]); 7 | } 8 | 9 | return node; 10 | }; 11 | 12 | export const addToCache = (cache, value, ...fields) => { 13 | let index = 0; 14 | let node = cache; 15 | 16 | while (index < fields.length - 1) { 17 | const field = fields[index]; 18 | let childNode = node.get(field); 19 | 20 | if (!childNode) { 21 | childNode = new Map(); 22 | node.set(field, childNode); 23 | } 24 | 25 | node = childNode; 26 | index++; 27 | } 28 | 29 | node.set(fields[index], value); 30 | }; 31 | -------------------------------------------------------------------------------- /utils/names.js: -------------------------------------------------------------------------------- 1 | const names = { 2 | '/binary-search': 'Binary Search', 3 | '/quicksort': 'Quicksort', 4 | '/bfs': 'BFS', 5 | }; 6 | 7 | export default path => names[path]; 8 | -------------------------------------------------------------------------------- /utils/offset-steps.js: -------------------------------------------------------------------------------- 1 | // Used when prepending intro steps to those generated by algorithm 2 | export default (steps, offsetBy) => steps.map(step => ( 3 | step.parentStepId ? { 4 | ...step, 5 | parentStepId: step.parentStepId + offsetBy, 6 | } : step 7 | )); 8 | -------------------------------------------------------------------------------- /utils/pure-layout-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import shallowCompare from 'react-addons-shallow-compare'; 3 | 4 | class PureLayoutComponent extends React.Component { 5 | shouldComponentUpdate(nextProps, nextState, nextContext) { 6 | return shallowCompare(this, nextProps, nextState) || nextContext.layout !== this.context.layout; 7 | } 8 | } 9 | 10 | export default PureLayoutComponent; 11 | -------------------------------------------------------------------------------- /utils/stack.js: -------------------------------------------------------------------------------- 1 | export default (steps, index) => { 2 | const { length } = steps; 3 | 4 | if (length === 0) { 5 | return { 6 | entries: [] 7 | }; 8 | } 9 | 10 | if (length === 1) { 11 | return { 12 | entries: [{ 13 | prevStep: steps[0], 14 | nextStep: steps[0], 15 | }] 16 | }; 17 | } 18 | 19 | const entries = []; 20 | const prevStep = steps[index]; 21 | const nextStep = index < length - 1 ? steps[index + 1] : steps[index]; 22 | let parentStepId = prevStep.parentStepId; 23 | let isAddingToStack = false; 24 | let isRemovingFromStack = false; 25 | 26 | if (parentStepId === nextStep.parentStepId) { 27 | entries.push({ 28 | prevStep, 29 | nextStep, 30 | }); 31 | } else if (nextStep.parentStepId === index) { 32 | entries.push({ 33 | prevStep: nextStep, 34 | nextStep, 35 | }, { 36 | prevStep, 37 | nextStep: prevStep 38 | }); 39 | 40 | parentStepId = prevStep.parentStepId; 41 | isAddingToStack = true; 42 | } else if (nextStep.parentStepId === steps[parentStepId].parentStepId) { 43 | entries.push({ 44 | prevStep, 45 | nextStep: prevStep 46 | }, { 47 | prevStep: steps[parentStepId], 48 | nextStep, 49 | }); 50 | 51 | parentStepId = steps[parentStepId].parentStepId; 52 | isRemovingFromStack = true; 53 | } 54 | 55 | while (parentStepId) { 56 | const lastFromParentCall = steps[parentStepId]; 57 | 58 | entries.push({ 59 | prevStep: lastFromParentCall, 60 | nextStep: lastFromParentCall, 61 | }); 62 | 63 | parentStepId = lastFromParentCall.parentStepId; 64 | } 65 | 66 | return { 67 | entries, 68 | isAddingToStack, 69 | isRemovingFromStack, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /utils/transition.js: -------------------------------------------------------------------------------- 1 | export const transitionValue = (prev, next, progress) => { 2 | const nextHasIt = next !== undefined; 3 | const prevHasIt = prev !== undefined; 4 | if (nextHasIt && prevHasIt) { 5 | return prev + (progress * (next - prev)); 6 | } 7 | 8 | return nextHasIt ? next : prev; 9 | }; 10 | 11 | export const transitionValues = (prev, next, progress) => { 12 | const curr = {}; 13 | const uniqueKeys = new Set(Object.keys(next).concat(Object.keys(prev))); 14 | 15 | uniqueKeys.forEach(key => { 16 | curr[key] = transitionValue(prev[key], next[key], progress); 17 | }); 18 | 19 | return curr; 20 | }; 21 | 22 | export const getBindingValue = (prevStep, nextStep, key) => { 23 | if (nextStep.bindings[key] !== undefined) { 24 | return nextStep.bindings[key]; 25 | } 26 | 27 | if (prevStep !== undefined) { 28 | return prevStep.bindings[key]; 29 | } 30 | 31 | return undefined; 32 | }; 33 | -------------------------------------------------------------------------------- /utils/wobble.js: -------------------------------------------------------------------------------- 1 | const TIMES = 2; 2 | const MAX_ANGLE = 5; 3 | 4 | const getWobbleRotation = stepProgress => { 5 | const rotation = Math.sin(Math.PI * 2 * TIMES * stepProgress) * MAX_ANGLE; 6 | return Math.round(rotation * 100) / 100; 7 | }; 8 | 9 | export default getWobbleRotation; 10 | --------------------------------------------------------------------------------