master 37ad5b2e9d8b cached
14 files
16.3 KB
4.1k tokens
27 symbols
1 requests
Download .txt
Repository: Swizec/react-particles-experiment
Branch: master
Commit: 37ad5b2e9d8b
Files: 14
Total size: 16.3 KB

Directory structure:
gitextract_s_pr5jvy/

├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public/
│   ├── index.html
│   └── manifest.json
└── src/
    ├── actions/
    │   └── index.js
    ├── components/
    │   ├── Footer.jsx
    │   ├── Header.jsx
    │   ├── Particles.jsx
    │   └── index.jsx
    ├── containers/
    │   └── AppContainer.jsx
    ├── index.js
    └── reducers/
        └── index.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
bower_components
build/.*
node_modules
# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
/node_modules

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Swizec Teller

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Animating with React, Redux, and d3

![Gif](https://raw.githubusercontent.com/Swizec/react-particles-experiment/master/particles-step-5.gif)

That'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.

On mobile and only have a finger? That works, too.

I'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?

## Here's how it works

The whole thing is built with React, Redux, and d3. No tricks for animation; just a bit of cleverness.

Here's the general approach:

We 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.

Then 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.

All **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.

We 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**.

On each action, the **reducer calculates a new state** for the whole app. This includes **new particle positions** for each step of the animation.

When the store updates, **React flushes changes** via props and because **coordinates are state, the particles move**.

The result is smooth animation.

[Keep reading to learn the details](http://swizec.com/blog/animating-with-react-redux-and-d3/swizec/6775).

A version of this article will be featured as a chapter in my upcoming [React+d3js ES6 book](http://swizec.com/reactd3js/).


================================================
FILE: package.json
================================================
{
    "name": "react-particles-experiment",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
        "d3": "^5.7.0",
        "konva": "^2.4.2",
        "prop-types": "^15.6.2",
        "react": "^16.5.2",
        "react-dom": "^16.5.2",
        "react-konva": "^16.5.2",
        "react-redux": "^5.0.7",
        "redux": "^4.0.1"
    },
    "devDependencies": {
        "react-scripts": "1.0.17"
    },
    "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test --env=jsdom",
        "eject": "react-scripts eject"
    }
}

================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>


================================================
FILE: public/manifest.json
================================================
{
  "short_name": "React App",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "start_url": "./index.html",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}


================================================
FILE: src/actions/index.js
================================================
export const TIME_TICK = "TIME_TICK";
export const TICKER_STARTED = "TICKER_STARTED";
export const CREATE_PARTICLES = "CREATE_PARTICLES";
export const START_PARTICLES = "START_PARTICLES";
export const STOP_PARTICLES = "STOP_PARTICLES";
export const UPDATE_MOUSE_POS = "UPDATE_MOUSE_POS";
export const RESIZE_SCREEN = "RESIZE_SCREEN";

export function tickTime() {
    return {
        type: TIME_TICK
    };
}

export function tickerStarted() {
    return {
        type: TICKER_STARTED
    };
}

export function startParticles() {
    return {
        type: START_PARTICLES
    };
}

export function stopParticles() {
    return {
        type: STOP_PARTICLES
    };
}

export function updateMousePos(x, y) {
    return {
        type: UPDATE_MOUSE_POS,
        x: x,
        y: y
    };
}

export function resizeScreen(width, height) {
    return {
        type: RESIZE_SCREEN,
        width: width,
        height: height
    };
}

================================================
FILE: src/components/Footer.jsx
================================================

import React from 'react';

const Footer = ({ N, fps }) => (
    <div style={{position: 'absolute', bottom: 0}} className="container">
        <strong>{N} particles</strong>
    </div>
);

export default Footer;


================================================
FILE: src/components/Header.jsx
================================================

import React from 'react';

const Header = ({ N }) => (
    <div style={{position: 'absolute'}} className="container">
        <h1>Click or touch anywhere</h1>
    </div>
);

export default Header;


================================================
FILE: src/components/Particles.jsx
================================================
import React, { Component } from "react";
import { FastLayer } from "react-konva";

class Particles extends Component {
    _particles = {};
    layerRef = React.createRef();

    componentDidMount() {
        this.canvas = this.layerRef.current.canvas._canvas;
        this.canvasContext = this.canvas.getContext("2d");

        this.sprite = new Image();
        this.sprite.src = "http://i.imgur.com/m5l6lhr.png";
    }

    drawParticle(particle) {
        let { x, y } = particle;

        this.canvasContext.drawImage(this.sprite, 0, 0, 128, 128, x, y, 35, 35);
    }

    componentDidUpdate() {
        let particles = this.props.particles;

        // console.time("drawing");
        this.canvasContext.clearRect(
            0,
            0,
            this.canvas.width,
            this.canvas.height
        );

        this.canvasContext.lineWidth = 1;
        this.canvasContext.strokeStyle = "black";

        for (let i = 0; i < particles.length; i++) {
            this.drawParticle(particles[i]);
        }
        // console.timeEnd("drawing");
    }

    render() {
        return <FastLayer ref={this.layerRef} listening="false" />;
    }
}

export default Particles;


================================================
FILE: src/components/index.jsx
================================================
import React, { Component } from "react";
import { select as d3Select, mouse as d3Mouse, touches as d3Touches } from "d3";
import { Stage } from "react-konva";

import Particles from "./Particles";
import Footer from "./Footer";
import Header from "./Header";

class App extends Component {
    svgWrap = React.createRef();

    componentDidMount() {
        let svg = d3Select(this.svgWrap.current);

        svg.on("mousedown", () => {
            this.updateMousePos();
            this.props.startParticles();
        });
        svg.on("touchstart", () => {
            this.updateTouchPos();
            this.props.startParticles();
        });
        svg.on("mousemove", () => {
            this.updateMousePos();
        });
        svg.on("touchmove", () => {
            this.updateTouchPos();
        });
        svg.on("mouseup", () => {
            this.props.stopParticles();
        });
        svg.on("touchend", () => {
            this.props.stopParticles();
        });
        svg.on("mouseleave", () => {
            this.props.stopParticles();
        });
    }

    updateMousePos() {
        let [x, y] = d3Mouse(this.svgWrap.current);
        this.props.updateMousePos(x, y);
    }

    updateTouchPos() {
        let [x, y] = d3Touches(this.svgWrap.current)[0];
        this.props.updateMousePos(x, y);
    }

    render() {
        return (
            <div
                onMouseDown={e => this.props.startTicker()}
                style={{ overflow: "hidden" }}
            >
                <Header />
                <div
                    style={{
                        width: this.props.svgWidth,
                        height: this.props.svgHeight,
                        position: "absolute",
                        top: "0px",
                        left: "0px",
                        background: "rgba(124, 224, 249, .3)"
                    }}
                    ref={this.svgWrap}
                >
                    <Stage
                        width={this.props.svgWidth}
                        height={this.props.svgHeight}
                    >
                        <Particles particles={this.props.particles} />
                    </Stage>
                </div>
                <Footer N={this.props.particles.length} />
            </div>
        );
    }
}

export default App;


================================================
FILE: src/containers/AppContainer.jsx
================================================
import { connect } from "react-redux";
import React, { Component } from "react";
import * as d3 from "d3";

import App from "../components";
import {
    tickTime,
    tickerStarted,
    startParticles,
    stopParticles,
    updateMousePos
} from "../actions";

class AppContainer extends Component {
    startTicker = () => {
        const { isTickerStarted } = this.props;

        if (!isTickerStarted) {
            console.log("Starting ticker");
            this.props.tickerStarted();
            d3.timer(this.props.tickTime);
        }
    };

    render() {
        const { svgWidth, svgHeight, particles } = this.props;

        return (
            <App
                svgWidth={svgWidth}
                svgHeight={svgHeight}
                particles={particles}
                startTicker={this.startTicker}
                startParticles={this.props.startParticles}
                stopParticles={this.props.stopParticles}
                updateMousePos={this.props.updateMousePos}
            />
        );
    }
}

const mapStateToProps = ({
    generateParticles,
    mousePos,
    particlesPerTick,
    isTickerStarted,
    svgWidth,
    svgHeight,
    particles
}) => ({
    generateParticles,
    mousePos,
    particlesPerTick,
    isTickerStarted,
    svgWidth,
    svgHeight,
    particles
});

const mapDispatchToProps = {
    tickTime,
    tickerStarted,
    startParticles,
    stopParticles,
    updateMousePos
};

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(AppContainer);


================================================
FILE: src/index.js
================================================
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { select as d3Select } from "d3";

import particlesApp from "./reducers";
import AppContainer from "./containers/AppContainer";
import { resizeScreen } from "./actions";

let store = createStore(particlesApp);

ReactDOM.render(
    <Provider store={store}>
        <AppContainer />
    </Provider>,
    document.querySelectorAll("#root")[0]
);

let onResize = function() {
    store.dispatch(resizeScreen(window.innerWidth, window.innerHeight));
};
onResize();

d3Select(window).on("resize", onResize);


================================================
FILE: src/reducers/index.js
================================================
import { randomNormal } from "d3";

const Gravity = 0.5,
    randNormal = randomNormal(0.3, 2),
    randNormal2 = randomNormal(0.5, 1.8);

const initialState = {
    particles: [],
    particleIndex: 0,
    particlesPerTick: 3000,
    svgWidth: 800,
    svgHeight: 600,
    isTickerStarted: false,
    generateParticles: false,
    mousePos: [null, null],
    lastFrameTime: null
};

function appReducer(state, action) {
    switch (action.type) {
        case "TICKER_STARTED":
            return Object.assign({}, state, {
                isTickerStarted: true,
                lastFrameTime: new Date()
            });
        case "START_PARTICLES":
            return Object.assign({}, state, {
                generateParticles: true
            });
        case "STOP_PARTICLES":
            return Object.assign({}, state, {
                generateParticles: false
            });
        case "UPDATE_MOUSE_POS":
            return Object.assign({}, state, {
                mousePos: [action.x, action.y]
            });
        case "RESIZE_SCREEN":
            return Object.assign({}, state, {
                svgWidth: action.width,
                svgHeight: action.height
            });
        default:
            return state;
    }
}

function particlesReducer(state, action) {
    switch (action.type) {
        case "TIME_TICK":
            let {
                    svgWidth,
                    svgHeight,
                    lastFrameTime,
                    generateParticles,
                    particlesPerTick,
                    particleIndex,
                    mousePos
                } = state,
                newFrameTime = new Date(),
                multiplier = (newFrameTime - lastFrameTime) / (1000 / 60),
                newParticles = state.particles.slice(0);

            if (generateParticles) {
                for (let i = 0; i < particlesPerTick; i++) {
                    let particle = {
                        id: state.particleIndex + i,
                        x: mousePos[0],
                        y: mousePos[1]
                    };

                    particle.vector = [
                        particle.id % 2 ? -randNormal() : randNormal(),
                        -randNormal2() * 3.3
                    ];

                    newParticles.unshift(particle);
                }

                particleIndex = particleIndex + particlesPerTick + 1;
            }

            let movedParticles = newParticles
                .filter(p => {
                    return !(p.y > svgHeight || p.x < 0 || p.x > svgWidth);
                })
                .map(p => {
                    let [vx, vy] = p.vector;
                    p.x += vx * multiplier;
                    p.y += vy * multiplier;
                    p.vector[1] += Gravity * multiplier;
                    return p;
                });

            return {
                particles: movedParticles,
                lastFrameTime: new Date(),
                particleIndex
            };
        default:
            return {
                particles: state.particles,
                lastFrameTime: state.lastFrameTime,
                particleIndex: state.particleIndex
            };
    }
}

// Manually combineReducers
export default function(state = initialState, action) {
    return {
        ...appReducer(state, action),
        ...particlesReducer(state, action)
    };
}
Download .txt
gitextract_s_pr5jvy/

├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public/
│   ├── index.html
│   └── manifest.json
└── src/
    ├── actions/
    │   └── index.js
    ├── components/
    │   ├── Footer.jsx
    │   ├── Header.jsx
    │   ├── Particles.jsx
    │   └── index.jsx
    ├── containers/
    │   └── AppContainer.jsx
    ├── index.js
    └── reducers/
        └── index.js
Download .txt
SYMBOL INDEX (27 symbols across 5 files)

FILE: src/actions/index.js
  constant TIME_TICK (line 1) | const TIME_TICK = "TIME_TICK";
  constant TICKER_STARTED (line 2) | const TICKER_STARTED = "TICKER_STARTED";
  constant CREATE_PARTICLES (line 3) | const CREATE_PARTICLES = "CREATE_PARTICLES";
  constant START_PARTICLES (line 4) | const START_PARTICLES = "START_PARTICLES";
  constant STOP_PARTICLES (line 5) | const STOP_PARTICLES = "STOP_PARTICLES";
  constant UPDATE_MOUSE_POS (line 6) | const UPDATE_MOUSE_POS = "UPDATE_MOUSE_POS";
  constant RESIZE_SCREEN (line 7) | const RESIZE_SCREEN = "RESIZE_SCREEN";
  function tickTime (line 9) | function tickTime() {
  function tickerStarted (line 15) | function tickerStarted() {
  function startParticles (line 21) | function startParticles() {
  function stopParticles (line 27) | function stopParticles() {
  function updateMousePos (line 33) | function updateMousePos(x, y) {
  function resizeScreen (line 41) | function resizeScreen(width, height) {

FILE: src/components/Particles.jsx
  class Particles (line 4) | class Particles extends Component {
    method componentDidMount (line 8) | componentDidMount() {
    method drawParticle (line 16) | drawParticle(particle) {
    method componentDidUpdate (line 22) | componentDidUpdate() {
    method render (line 42) | render() {

FILE: src/components/index.jsx
  class App (line 9) | class App extends Component {
    method componentDidMount (line 12) | componentDidMount() {
    method updateMousePos (line 40) | updateMousePos() {
    method updateTouchPos (line 45) | updateTouchPos() {
    method render (line 50) | render() {

FILE: src/containers/AppContainer.jsx
  class AppContainer (line 14) | class AppContainer extends Component {
    method render (line 25) | render() {

FILE: src/reducers/index.js
  function appReducer (line 19) | function appReducer(state, action) {
  function particlesReducer (line 48) | function particlesReducer(state, action) {
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (18K chars).
[
  {
    "path": ".gitignore",
    "chars": 324,
    "preview": "bower_components\nbuild/.*\nnode_modules\n# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dep"
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Swizec Teller\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.md",
    "chars": 2314,
    "preview": "# Animating with React, Redux, and d3\n\n![Gif](https://raw.githubusercontent.com/Swizec/react-particles-experiment/master"
  },
  {
    "path": "package.json",
    "chars": 614,
    "preview": "{\n    \"name\": \"react-particles-experiment\",\n    \"version\": \"0.1.0\",\n    \"private\": true,\n    \"dependencies\": {\n        \""
  },
  {
    "path": "public/index.html",
    "chars": 1590,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-wid"
  },
  {
    "path": "public/manifest.json",
    "chars": 317,
    "preview": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n     "
  },
  {
    "path": "src/actions/index.js",
    "chars": 933,
    "preview": "export const TIME_TICK = \"TIME_TICK\";\nexport const TICKER_STARTED = \"TICKER_STARTED\";\nexport const CREATE_PARTICLES = \"C"
  },
  {
    "path": "src/components/Footer.jsx",
    "chars": 213,
    "preview": "\nimport React from 'react';\n\nconst Footer = ({ N, fps }) => (\n    <div style={{position: 'absolute', bottom: 0}} classNa"
  },
  {
    "path": "src/components/Header.jsx",
    "chars": 199,
    "preview": "\nimport React from 'react';\n\nconst Header = ({ N }) => (\n    <div style={{position: 'absolute'}} className=\"container\">\n"
  },
  {
    "path": "src/components/Particles.jsx",
    "chars": 1192,
    "preview": "import React, { Component } from \"react\";\nimport { FastLayer } from \"react-konva\";\n\nclass Particles extends Component {\n"
  },
  {
    "path": "src/components/index.jsx",
    "chars": 2347,
    "preview": "import React, { Component } from \"react\";\nimport { select as d3Select, mouse as d3Mouse, touches as d3Touches } from \"d3"
  },
  {
    "path": "src/containers/AppContainer.jsx",
    "chars": 1532,
    "preview": "import { connect } from \"react-redux\";\nimport React, { Component } from \"react\";\nimport * as d3 from \"d3\";\n\nimport App f"
  },
  {
    "path": "src/index.js",
    "chars": 648,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { Provider } from \"react-redux\";\nimport { createStor"
  },
  {
    "path": "src/reducers/index.js",
    "chars": 3428,
    "preview": "import { randomNormal } from \"d3\";\n\nconst Gravity = 0.5,\n    randNormal = randomNormal(0.3, 2),\n    randNormal2 = random"
  }
]

About this extraction

This page contains the full source code of the Swizec/react-particles-experiment GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (16.3 KB), approximately 4.1k tokens, and a symbol index with 27 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!