├── .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 | [](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 |
63 |
shuffle
70 |
71 |
72 |
84 |
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 |
83 |
84 |
94 |
95 |
96 |
106 |
107 |
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 |
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 |
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 |
123 |
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 |
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 |
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 |
--------------------------------------------------------------------------------