├── .gitignore ├── .netlify ├── .storybook ├── addons.js └── config.js ├── .vscode └── settings.json ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── package-lock.json ├── package.json ├── plan.md ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── assets │ ├── all-the-things-fast.mp4 │ ├── banana-path.png │ ├── basketball.gif │ ├── clock-stock-photos.gif │ ├── converging-square.gif │ ├── curves1.png │ ├── curves2.png │ ├── district-v2.mp4 │ ├── district.mp4 │ ├── explorables.gif │ ├── guppy-demo.mp4 │ ├── hadouken.jpeg │ ├── heaven.jpg │ ├── how-its-made.jpg │ ├── how-sound-works.png │ ├── judge-judy.gif │ ├── khan-academy.jpeg │ ├── kitten own.jpg │ ├── lego.jpeg │ ├── me.jpeg │ ├── modular-synth.jpg │ ├── opioid.gif │ ├── oprah.gif │ ├── phase-2.gif │ ├── phase-3.gif │ ├── phase-4.gif │ ├── phase.gif │ ├── shifting.gif │ ├── slinky.jpg │ ├── spacer.png │ ├── stopwatch.jpg │ ├── storage-wars.jpg │ ├── tello-demo.mp4 │ ├── timekeeper-thanos.jpg │ ├── timekeeper.jpg │ ├── traditional-sound-article.png │ ├── unlikely-animal-friendships-1.gif │ └── unlikely-animal-friendships-2.mp4 ├── code │ ├── path-calculator-broken.example │ ├── path-calculator.example │ ├── playground │ │ ├── react-spring-pre.example │ │ ├── react-spring.example │ │ ├── timekeeper-multiplier.example │ │ └── timekeeper.example │ ├── react-rally-waveform-initial.example │ ├── react-rally-waveform-timekeeper-multiplier.example │ ├── react-rally-waveform-timekeeper.example │ ├── tweening.example │ ├── waveform-axes-alt-universe.example │ ├── waveform-axes.example │ ├── waveform-initial.example │ ├── waveform-with-calculator.example │ ├── waveform-with-internal-tween.example │ ├── waveform-with-path.example │ ├── waveform-with-spring.example │ └── waveform-with-time-elapsed.example ├── components │ ├── AirGrid │ │ ├── AirGrid.helpers.js │ │ ├── AirGrid.js │ │ ├── AirGrid.stories.js │ │ └── index.js │ ├── AmplitudeFrequencyManager │ │ ├── AmplitudeFrequencyManager.js │ │ ├── AmplitudeFrequencyManager.stories.js │ │ └── index.js │ ├── AudioOutput │ │ ├── AudioOutput.js │ │ └── index.js │ ├── Button │ │ ├── Button.js │ │ └── index.js │ ├── Canvas │ │ ├── Canvas.emotion.css │ │ ├── Canvas.js │ │ ├── Canvas.story.js │ │ ├── hidpi-canvas-polyfill.js │ │ └── index.js │ ├── Confetti │ │ ├── Canvas.js │ │ ├── Confetti.helpers.js │ │ ├── Confetti.js │ │ ├── confetti-shapes.js │ │ └── index.js │ ├── FadeTransition │ │ ├── FadeTransition.js │ │ └── index.js │ ├── FullscreenConfetti │ │ ├── FullscreenConfetti.js │ │ └── index.js │ ├── GameOfLife │ │ ├── GameOfLife.js │ │ ├── GameOfLife.stories.js │ │ ├── GameOfLife.utils.js │ │ ├── GameOfLifePropConverter.js │ │ └── index.js │ ├── GridVsWave │ │ ├── GridVsWave.js │ │ └── index.js │ ├── Highlighted │ │ ├── Highlighted.js │ │ └── index.js │ ├── Label │ │ ├── Label.js │ │ └── index.js │ ├── Oscillator │ │ ├── Oscillator.helpers.js │ │ ├── Oscillator.js │ │ └── index.js │ ├── PathCalculator │ │ ├── PathCalculator.broken.js │ │ ├── PathCalculator.js │ │ └── index.js │ ├── Quote │ │ ├── Quote.js │ │ └── index.js │ ├── ReactRallyWaveformTimeSlider │ │ ├── ReactRallyWaveformTimeSlider.js │ │ └── index.js │ ├── ReactRallyWaveformV1 │ │ ├── ReactRallyWaveformV1.js │ │ ├── ReactRallyWaveformV1.stories.js │ │ └── index.js │ ├── ReactRallyWaveformV2 │ │ ├── ReactRallyWaveformV2.js │ │ ├── ReactRallyWaveformV2.old.js │ │ ├── ReactRallyWaveformV2.stories.js │ │ └── index.js │ ├── ReactRallyWaveformV3 │ │ ├── ReactRallyWaveformV3.js │ │ ├── ReactRallyWaveformV3.stories.js │ │ └── index.js │ ├── ReactRallyWaveformV4 │ │ ├── ReactRallyWaveformV4.js │ │ ├── ReactRallyWaveformV4.stories.js │ │ └── index.js │ ├── Slider │ │ ├── Slider.js │ │ └── index.js │ ├── Spacer │ │ ├── Spacer.js │ │ └── index.js │ ├── Timekeeper │ │ ├── Timekeeper.js │ │ └── index.js │ ├── Title │ │ ├── Title.js │ │ └── index.js │ ├── Toggle │ │ ├── Toggle.js │ │ └── index.js │ ├── UnsplashCredit │ │ ├── UnsplashCredit.js │ │ └── index.js │ ├── VennDiagram │ │ ├── VennDiagram.js │ │ └── index.js │ ├── Waveform │ │ ├── Waveform.js │ │ └── index.js │ ├── WaveformAxis │ │ ├── WaveformAxis.js │ │ └── index.js │ ├── WaveformCalculator │ │ ├── WaveformCalculator.js │ │ ├── WaveformCalculator.stories.js │ │ └── index.js │ ├── WaveformCycleIndicator │ │ ├── WaveformCycleIndicator.helpers.js │ │ ├── WaveformCycleIndicator.js │ │ └── index.js │ ├── WaveformIntercept │ │ ├── WaveformIntercept.js │ │ └── index.js │ ├── WaveformOld │ │ ├── Waveform.js │ │ ├── Waveform.stories.js │ │ └── index.js │ ├── WaveformPointManager │ │ ├── WaveformPointManager.js │ │ └── index.js │ ├── WaveformState │ │ ├── WaveformState.js │ │ ├── WaveformState.withContainer.js │ │ └── index.js │ ├── WaveformStopwatch │ │ ├── WaveformStopwatch.js │ │ ├── WaveformStopwatch.stories.js │ │ ├── WaveformStopwatchSimple.js │ │ └── index.js │ └── WindowDimensions │ │ ├── WindowDimensions.js │ │ └── index.js ├── constants │ └── index.js ├── helpers │ └── waveform.helpers.js ├── index.js ├── presentation.js ├── types.js ├── utils │ └── index.js └── vendor │ ├── prism.css │ └── prism.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.netlify: -------------------------------------------------------------------------------- 1 | {"site_id":"85032da0-0bbd-4266-a6a2-124bb9c7dcad","path":"./build"} -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ 2 | 3 | import { configure } from '@storybook/react'; 4 | 5 | const components = require.context('../src/components', true, /.stories.js$/); 6 | 7 | function loadStories() { 8 | components.keys().forEach(filename => components(filename)); 9 | } 10 | 11 | configure(loadStories, module); 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "prettier.printWidth": 80, 4 | "editor.formatOnSave": true 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Explorable Explanations with React 2 | 3 | This repo holds the presentation and associated code for my 2018 React Rally talk, "Explorable Explanations with React". 4 | 5 | ### Slides 6 | 7 | View the slides here: [http://jovial-hypatia-a56434.netlify.com](http://jovial-hypatia-a56434.netlify.com) 8 | 9 | ### Waveforms Project 10 | 11 | View the Waveforms project live on [The Pudding](https://pudding.cool/2018/02/waveforms/). You can also view the original repo [on Github](https://github.com/joshwcomeau/waveforms). 12 | 13 | ### More Info on Explorable Explanations 14 | 15 | - Explorable explanations was coined by Bret Victor, someone who has done a ton of interesting work in the space. In particular, I really like his [talk on "Thinking the Unthinkable"](http://worrydream.com/MediaForThinkingTheUnthinkable/). He has a bunch of great stuff on his website, [worrydream.com](http://worrydream.com/). 16 | - Browse some explorable explanations on [explorabl.es](https://explorabl.es/) 17 | - A really exciting project is [Idyll](https://idyll-lang.org/), a React-based markup language that allows creation of explorable explanations. 18 | - [Nicky Case](https://twitter.com/ncasenmare) is an ultra-talented developer, and his work is how I first discovered this space. Check out [Nicky's website](https://ncase.me/), or some of my favourite pieces: [To Build a Better Ballot](https://ncase.me/ballot/), or [The Evolution of Trust](https://ncase.me/trust/). 19 | 20 | ### Code Used in the Presentation 21 | 22 | Something you should know: The code _used_ this presentation differs here and there from the code _shown_ in the presentation. In some cases, there's a good reason for this (eg. the presentation needs to show the waveform without , which isn't something we want in the real waveforms project). In other cases, I just didn't have time to backtrack and refactor. 23 | 24 | The code used/shown in this project is also quite different from the code in the [actual Waveforms project](https://github.com/joshwcomeau/waveforms). The reason for this is that I refactored/rethought a bunch of stuff when preparing this presentation, and didn't have the chance to retrofit the original project. Many of the underlying concepts are the same, but the component architecture is different. 25 | 26 | Remember, real-world projects are messy! 27 | 28 | With these caveats in mind, here is some of the interesting "real-world" code from this presentation: 29 | 30 | - [ReactRallyWaveformV4](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/components/ReactRallyWaveformV4/ReactRallyWaveformV4.js) - This is the final, all-bells-and-whistles parent component 31 | - [Waveform](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/components/Waveform/Waveform.js) 32 | - [waveform.helpers.js](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/helpers/waveform.helpers.js) - This holds the methods for calculating the waveform points, AKA the fun trigonometry-involving stuff I briefly mentioned! 33 | - [Timekeeper](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/components/Timekeeper/Timekeeper.js) - Learn how our time-travelling timekeeper keeps (and manipulates!) the time. 34 | - [AirGrid](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/components/AirGrid/AirGrid.js) - The grid of particles shown in the beginning 35 | - [GridVsWave](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/components/GridVsWave/GridVsWave.js) - This component combines an AirGrid and a Waveform, in the side-by-side from early in the presentation 36 | - [AudioOutput](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/components/AudioOutput/AudioOutput.js) and [Oscillator](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/components/Oscillator/Oscillator.js) - Not explicitly called out in the presentation, but these neat components are used to render to audio, for the tone heard near the beginning. 37 | - [presentation](https://github.com/joshwcomeau/explorable-explanations-with-react/blob/master/src/presentation.js) - Want to see all the slides themselves? Fair warning, it's one big hideous file. 38 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce((env, key) => { 82 | env[key] = JSON.stringify(raw[key]); 83 | return env; 84 | }, {}), 85 | }; 86 | 87 | return { raw, stringified }; 88 | } 89 | 90 | module.exports = getClientEnvironment; 91 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right