[
  {
    "path": ".gitignore",
    "content": "bower_components\nbuild/.*\nnode_modules\n# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Swizec Teller\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Animating with React, Redux, and d3\n\n![Gif](https://raw.githubusercontent.com/Swizec/react-particles-experiment/master/particles-step-5.gif)\n\nThat's a particle generator. It makes tiny circles fly out of where you click. Hold down your mouse and move around. The particles keep flying out of your cursor.\n\nOn mobile and only have a finger? That works, too.\n\nI'm a nerd, so this is what I consider fun. Your mileage may vary. Please do click in the embed and look at those circles fly. Ain't it cool?\n\n## Here's how it works\n\nThe whole thing is built with React, Redux, and d3. No tricks for animation; just a bit of cleverness.\n\nHere's the general approach:\n\nWe use **React to render everything**: the page, the SVG element, the particles inside. All of it is built with React components that take some props and return some DOM. This lets us tap into React's algorithms that decide which nodes to update and when to garbage collect old nodes.\n\nThen we use some **d3 calculations and event detection**. D3 has great random generators, so we take advantage of that. D3's mouse and touch event handlers calculate coordinates relative to our SVG. We need those, and React can't do it. React's click handlers are based on DOM nodes, which don't correspond to `(x, y)` coordinates. D3 looks at real cursor position on screen.\n\nAll **particle coordinates are in a Redux store**. Each particle also has a movement vector. The store holds some useful flags and general parameters, too. This lets us treat animation as data transformations. I'll show you what I mean in a bit.\n\nWe use **actions to communicate user events** like creating particles, starting the animation, changing mouse position, and so on. On each requestAnimationFrame, we **dispatch an \"advance animation\" action**.\n\nOn each action, the **reducer calculates a new state** for the whole app. This includes **new particle positions** for each step of the animation.\n\nWhen the store updates, **React flushes changes** via props and because **coordinates are state, the particles move**.\n\nThe result is smooth animation.\n\n[Keep reading to learn the details](http://swizec.com/blog/animating-with-react-redux-and-d3/swizec/6775).\n\nA version of this article will be featured as a chapter in my upcoming [React+d3js ES6 book](http://swizec.com/reactd3js/).\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"react-particles-experiment\",\n    \"version\": \"0.1.0\",\n    \"private\": true,\n    \"dependencies\": {\n        \"d3\": \"^5.7.0\",\n        \"konva\": \"^2.4.2\",\n        \"prop-types\": \"^15.6.2\",\n        \"react\": \"^16.5.2\",\n        \"react-dom\": \"^16.5.2\",\n        \"react-konva\": \"^16.5.2\",\n        \"react-redux\": \"^5.0.7\",\n        \"redux\": \"^4.0.1\"\n    },\n    \"devDependencies\": {\n        \"react-scripts\": \"1.0.17\"\n    },\n    \"scripts\": {\n        \"start\": \"react-scripts start\",\n        \"build\": \"react-scripts build\",\n        \"test\": \"react-scripts test --env=jsdom\",\n        \"eject\": \"react-scripts eject\"\n    }\n}"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"theme-color\" content=\"#000000\">\n    <!--\n      manifest.json provides metadata used when your web app is added to the\n      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\">\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\">\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>\n      You need to enable JavaScript to run this app.\n    </noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \"./index.html\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "src/actions/index.js",
    "content": "export const TIME_TICK = \"TIME_TICK\";\nexport const TICKER_STARTED = \"TICKER_STARTED\";\nexport const CREATE_PARTICLES = \"CREATE_PARTICLES\";\nexport const START_PARTICLES = \"START_PARTICLES\";\nexport const STOP_PARTICLES = \"STOP_PARTICLES\";\nexport const UPDATE_MOUSE_POS = \"UPDATE_MOUSE_POS\";\nexport const RESIZE_SCREEN = \"RESIZE_SCREEN\";\n\nexport function tickTime() {\n    return {\n        type: TIME_TICK\n    };\n}\n\nexport function tickerStarted() {\n    return {\n        type: TICKER_STARTED\n    };\n}\n\nexport function startParticles() {\n    return {\n        type: START_PARTICLES\n    };\n}\n\nexport function stopParticles() {\n    return {\n        type: STOP_PARTICLES\n    };\n}\n\nexport function updateMousePos(x, y) {\n    return {\n        type: UPDATE_MOUSE_POS,\n        x: x,\n        y: y\n    };\n}\n\nexport function resizeScreen(width, height) {\n    return {\n        type: RESIZE_SCREEN,\n        width: width,\n        height: height\n    };\n}"
  },
  {
    "path": "src/components/Footer.jsx",
    "content": "\nimport React from 'react';\n\nconst Footer = ({ N, fps }) => (\n    <div style={{position: 'absolute', bottom: 0}} className=\"container\">\n        <strong>{N} particles</strong>\n    </div>\n);\n\nexport default Footer;\n"
  },
  {
    "path": "src/components/Header.jsx",
    "content": "\nimport React from 'react';\n\nconst Header = ({ N }) => (\n    <div style={{position: 'absolute'}} className=\"container\">\n        <h1>Click or touch anywhere</h1>\n    </div>\n);\n\nexport default Header;\n"
  },
  {
    "path": "src/components/Particles.jsx",
    "content": "import React, { Component } from \"react\";\nimport { FastLayer } from \"react-konva\";\n\nclass Particles extends Component {\n    _particles = {};\n    layerRef = React.createRef();\n\n    componentDidMount() {\n        this.canvas = this.layerRef.current.canvas._canvas;\n        this.canvasContext = this.canvas.getContext(\"2d\");\n\n        this.sprite = new Image();\n        this.sprite.src = \"http://i.imgur.com/m5l6lhr.png\";\n    }\n\n    drawParticle(particle) {\n        let { x, y } = particle;\n\n        this.canvasContext.drawImage(this.sprite, 0, 0, 128, 128, x, y, 35, 35);\n    }\n\n    componentDidUpdate() {\n        let particles = this.props.particles;\n\n        // console.time(\"drawing\");\n        this.canvasContext.clearRect(\n            0,\n            0,\n            this.canvas.width,\n            this.canvas.height\n        );\n\n        this.canvasContext.lineWidth = 1;\n        this.canvasContext.strokeStyle = \"black\";\n\n        for (let i = 0; i < particles.length; i++) {\n            this.drawParticle(particles[i]);\n        }\n        // console.timeEnd(\"drawing\");\n    }\n\n    render() {\n        return <FastLayer ref={this.layerRef} listening=\"false\" />;\n    }\n}\n\nexport default Particles;\n"
  },
  {
    "path": "src/components/index.jsx",
    "content": "import React, { Component } from \"react\";\nimport { select as d3Select, mouse as d3Mouse, touches as d3Touches } from \"d3\";\nimport { Stage } from \"react-konva\";\n\nimport Particles from \"./Particles\";\nimport Footer from \"./Footer\";\nimport Header from \"./Header\";\n\nclass App extends Component {\n    svgWrap = React.createRef();\n\n    componentDidMount() {\n        let svg = d3Select(this.svgWrap.current);\n\n        svg.on(\"mousedown\", () => {\n            this.updateMousePos();\n            this.props.startParticles();\n        });\n        svg.on(\"touchstart\", () => {\n            this.updateTouchPos();\n            this.props.startParticles();\n        });\n        svg.on(\"mousemove\", () => {\n            this.updateMousePos();\n        });\n        svg.on(\"touchmove\", () => {\n            this.updateTouchPos();\n        });\n        svg.on(\"mouseup\", () => {\n            this.props.stopParticles();\n        });\n        svg.on(\"touchend\", () => {\n            this.props.stopParticles();\n        });\n        svg.on(\"mouseleave\", () => {\n            this.props.stopParticles();\n        });\n    }\n\n    updateMousePos() {\n        let [x, y] = d3Mouse(this.svgWrap.current);\n        this.props.updateMousePos(x, y);\n    }\n\n    updateTouchPos() {\n        let [x, y] = d3Touches(this.svgWrap.current)[0];\n        this.props.updateMousePos(x, y);\n    }\n\n    render() {\n        return (\n            <div\n                onMouseDown={e => this.props.startTicker()}\n                style={{ overflow: \"hidden\" }}\n            >\n                <Header />\n                <div\n                    style={{\n                        width: this.props.svgWidth,\n                        height: this.props.svgHeight,\n                        position: \"absolute\",\n                        top: \"0px\",\n                        left: \"0px\",\n                        background: \"rgba(124, 224, 249, .3)\"\n                    }}\n                    ref={this.svgWrap}\n                >\n                    <Stage\n                        width={this.props.svgWidth}\n                        height={this.props.svgHeight}\n                    >\n                        <Particles particles={this.props.particles} />\n                    </Stage>\n                </div>\n                <Footer N={this.props.particles.length} />\n            </div>\n        );\n    }\n}\n\nexport default App;\n"
  },
  {
    "path": "src/containers/AppContainer.jsx",
    "content": "import { connect } from \"react-redux\";\nimport React, { Component } from \"react\";\nimport * as d3 from \"d3\";\n\nimport App from \"../components\";\nimport {\n    tickTime,\n    tickerStarted,\n    startParticles,\n    stopParticles,\n    updateMousePos\n} from \"../actions\";\n\nclass AppContainer extends Component {\n    startTicker = () => {\n        const { isTickerStarted } = this.props;\n\n        if (!isTickerStarted) {\n            console.log(\"Starting ticker\");\n            this.props.tickerStarted();\n            d3.timer(this.props.tickTime);\n        }\n    };\n\n    render() {\n        const { svgWidth, svgHeight, particles } = this.props;\n\n        return (\n            <App\n                svgWidth={svgWidth}\n                svgHeight={svgHeight}\n                particles={particles}\n                startTicker={this.startTicker}\n                startParticles={this.props.startParticles}\n                stopParticles={this.props.stopParticles}\n                updateMousePos={this.props.updateMousePos}\n            />\n        );\n    }\n}\n\nconst mapStateToProps = ({\n    generateParticles,\n    mousePos,\n    particlesPerTick,\n    isTickerStarted,\n    svgWidth,\n    svgHeight,\n    particles\n}) => ({\n    generateParticles,\n    mousePos,\n    particlesPerTick,\n    isTickerStarted,\n    svgWidth,\n    svgHeight,\n    particles\n});\n\nconst mapDispatchToProps = {\n    tickTime,\n    tickerStarted,\n    startParticles,\n    stopParticles,\n    updateMousePos\n};\n\nexport default connect(\n    mapStateToProps,\n    mapDispatchToProps\n)(AppContainer);\n"
  },
  {
    "path": "src/index.js",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { Provider } from \"react-redux\";\nimport { createStore } from \"redux\";\nimport { select as d3Select } from \"d3\";\n\nimport particlesApp from \"./reducers\";\nimport AppContainer from \"./containers/AppContainer\";\nimport { resizeScreen } from \"./actions\";\n\nlet store = createStore(particlesApp);\n\nReactDOM.render(\n    <Provider store={store}>\n        <AppContainer />\n    </Provider>,\n    document.querySelectorAll(\"#root\")[0]\n);\n\nlet onResize = function() {\n    store.dispatch(resizeScreen(window.innerWidth, window.innerHeight));\n};\nonResize();\n\nd3Select(window).on(\"resize\", onResize);\n"
  },
  {
    "path": "src/reducers/index.js",
    "content": "import { randomNormal } from \"d3\";\n\nconst Gravity = 0.5,\n    randNormal = randomNormal(0.3, 2),\n    randNormal2 = randomNormal(0.5, 1.8);\n\nconst initialState = {\n    particles: [],\n    particleIndex: 0,\n    particlesPerTick: 3000,\n    svgWidth: 800,\n    svgHeight: 600,\n    isTickerStarted: false,\n    generateParticles: false,\n    mousePos: [null, null],\n    lastFrameTime: null\n};\n\nfunction appReducer(state, action) {\n    switch (action.type) {\n        case \"TICKER_STARTED\":\n            return Object.assign({}, state, {\n                isTickerStarted: true,\n                lastFrameTime: new Date()\n            });\n        case \"START_PARTICLES\":\n            return Object.assign({}, state, {\n                generateParticles: true\n            });\n        case \"STOP_PARTICLES\":\n            return Object.assign({}, state, {\n                generateParticles: false\n            });\n        case \"UPDATE_MOUSE_POS\":\n            return Object.assign({}, state, {\n                mousePos: [action.x, action.y]\n            });\n        case \"RESIZE_SCREEN\":\n            return Object.assign({}, state, {\n                svgWidth: action.width,\n                svgHeight: action.height\n            });\n        default:\n            return state;\n    }\n}\n\nfunction particlesReducer(state, action) {\n    switch (action.type) {\n        case \"TIME_TICK\":\n            let {\n                    svgWidth,\n                    svgHeight,\n                    lastFrameTime,\n                    generateParticles,\n                    particlesPerTick,\n                    particleIndex,\n                    mousePos\n                } = state,\n                newFrameTime = new Date(),\n                multiplier = (newFrameTime - lastFrameTime) / (1000 / 60),\n                newParticles = state.particles.slice(0);\n\n            if (generateParticles) {\n                for (let i = 0; i < particlesPerTick; i++) {\n                    let particle = {\n                        id: state.particleIndex + i,\n                        x: mousePos[0],\n                        y: mousePos[1]\n                    };\n\n                    particle.vector = [\n                        particle.id % 2 ? -randNormal() : randNormal(),\n                        -randNormal2() * 3.3\n                    ];\n\n                    newParticles.unshift(particle);\n                }\n\n                particleIndex = particleIndex + particlesPerTick + 1;\n            }\n\n            let movedParticles = newParticles\n                .filter(p => {\n                    return !(p.y > svgHeight || p.x < 0 || p.x > svgWidth);\n                })\n                .map(p => {\n                    let [vx, vy] = p.vector;\n                    p.x += vx * multiplier;\n                    p.y += vy * multiplier;\n                    p.vector[1] += Gravity * multiplier;\n                    return p;\n                });\n\n            return {\n                particles: movedParticles,\n                lastFrameTime: new Date(),\n                particleIndex\n            };\n        default:\n            return {\n                particles: state.particles,\n                lastFrameTime: state.lastFrameTime,\n                particleIndex: state.particleIndex\n            };\n    }\n}\n\n// Manually combineReducers\nexport default function(state = initialState, action) {\n    return {\n        ...appReducer(state, action),\n        ...particlesReducer(state, action)\n    };\n}\n"
  }
]