├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── docs └── index.js ├── package.json ├── rollup.config.js ├── src ├── index.js └── test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "modules": false }], "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # builds 7 | build 8 | dist 9 | site 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 9 4 | - 8 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # resnow ❄️ 2 | 3 | > React component for snow on your webpage ☃️ 4 | 5 | [![npm](https://img.shields.io/npm/v/resnow.svg)](https://www.npmjs.com/package/resnow) [![Build Status](https://travis-ci.org/lachlanjc/resnow.svg?branch=master)](https://travis-ci.org/lachlanjc/resnow) 6 | 7 | - Used on/extracted from [Hack Club Secret Santa](https://hackclub.com/santa) 8 | - Code heavily inspired by [`react-snow-effect`](https://github.com/jungledre/react-snow-effect), but updated for React 16, supports server-rendering, & offers more options 9 | - Bootstrapped with [`create-react-library`](https://www.npmjs.com/package/create-react-library) 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install --save resnow 15 | yarn add resnow 16 | ``` 17 | 18 | ## Usage 19 | 20 | Requires React 16.3 or later. 21 | 22 | ```jsx 23 | import React from 'react' 24 | import Snow from 'resnow' 25 | 26 | export default () => ( 27 |
28 | 29 |
30 | ) 31 | ``` 32 | 33 | | Prop | Effect | 34 | | ---------------- | ------------------------------------------------------------------- | 35 | | `max` | Number of particles. Default: `32` | 36 | | `speed` | Speed of particles, in ms. Default: `32` | 37 | | `color` | Color of particles. Default: `rgba(255, 255, 255, 0.75)` | 38 | | `width`/`height` | Size of ``. Default: `1024`x`768`, but fills browser window | 39 | 40 | ## License 41 | 42 | MIT © [@lachlanjc](https://github.com/lachlanjc) 43 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import Snow from '../dist' 3 | import { createGlobalStyle } from 'styled-components' 4 | 5 | const Styles = createGlobalStyle` 6 | body { 7 | background: #0069ff; 8 | } 9 | 10 | canvas { 11 | position: fixed; 12 | z-index: -1; 13 | top: 0; 14 | left: 0; 15 | } 16 | 17 | article { 18 | position: fixed; 19 | top: 50%; 20 | left: 50%; 21 | transform: translateX(-50%) translateY(-50%); 22 | padding: 1rem; 23 | text-align: center; 24 | color: white; 25 | } 26 | 27 | h1 { 28 | font-size: 4rem; 29 | font-weight: bold; 30 | margin: 0; 31 | } 32 | 33 | p { 34 | font-size: 1.5rem; 35 | margin-top: 0.5rem; 36 | margin-bottom: 2rem; 37 | } 38 | 39 | nav a { 40 | margin: 1rem; 41 | } 42 | 43 | a { 44 | color: inherit; 45 | transition: color 0.125s ease-in-out; 46 | &:hover { 47 | color: #eee; 48 | } 49 | } 50 | ` 51 | 52 | export default () => ( 53 | 54 | 55 | 56 | 66 | 67 | ) 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resnow", 3 | "version": "1.0.0", 4 | "description": "React component for snow", 5 | "author": "lachlanjc", 6 | "license": "MIT", 7 | "homepage": "https://lachlanjc.me/resnow", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/lachlanjc/resnow.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/lachlanjc/resnow/issues" 14 | }, 15 | "main": "dist/index.js", 16 | "module": "dist/index.es.js", 17 | "jsnext:main": "dist/index.es.js", 18 | "engines": { 19 | "node": ">=8", 20 | "npm": ">=5" 21 | }, 22 | "scripts": { 23 | "test": "cross-env CI=1 react-scripts test --env=jsdom", 24 | "test:watch": "react-scripts test --env=jsdom", 25 | "dev": "x0 docs -op 8080", 26 | "build": "rollup -c", 27 | "start": "rollup -c -w", 28 | "prepare": "yarn run build", 29 | "predeploy": "yarn prepare && x0 build docs", 30 | "deploy": "gh-pages -d site" 31 | }, 32 | "peerDependencies": { 33 | "prop-types": "^15.5.4", 34 | "react": "^16.3.0", 35 | "react-dom": "^16.3.0" 36 | }, 37 | "devDependencies": { 38 | "@compositor/x0": "^6.0.7", 39 | "babel-core": "^6.26.3", 40 | "babel-plugin-external-helpers": "^6.22.0", 41 | "babel-preset-env": "^1.7.0", 42 | "babel-preset-react": "^6.24.1", 43 | "babel-preset-stage-0": "^6.24.1", 44 | "cross-env": "^5.1.4", 45 | "gh-pages": "^1.2.0", 46 | "react": "^16.7.0", 47 | "react-dom": "^16.7.0", 48 | "react-scripts": "^1.1.4", 49 | "rollup": "^0.64.1", 50 | "rollup-plugin-babel": "^3.0.7", 51 | "rollup-plugin-commonjs": "^9.1.3", 52 | "rollup-plugin-node-resolve": "^3.3.0", 53 | "rollup-plugin-peer-deps-external": "^2.2.0", 54 | "styled-components": "^4.1.3" 55 | }, 56 | "files": [ 57 | "README.md", 58 | "dist" 59 | ], 60 | "x0": { 61 | "static": true, 62 | "outDir": "site", 63 | "title": "resnow", 64 | "basename": "/resnow" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import external from 'rollup-plugin-peer-deps-external' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | 6 | import pkg from './package.json' 7 | 8 | export default { 9 | input: 'src/index.js', 10 | output: [ 11 | { 12 | file: pkg.main, 13 | format: 'cjs', 14 | sourcemap: true 15 | }, 16 | { 17 | file: pkg.module, 18 | format: 'es', 19 | sourcemap: true 20 | } 21 | ], 22 | plugins: [ 23 | external(), 24 | babel({ 25 | exclude: 'node_modules/**', 26 | plugins: ['external-helpers'] 27 | }), 28 | resolve(), 29 | commonjs() 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class extends Component { 5 | constructor(props) { 6 | super(props) 7 | 8 | const browser = typeof window === 'object' 9 | const width = browser ? window.innerWidth : props.width 10 | const height = browser ? window.innerHeight : props.height 11 | 12 | this.state = { 13 | intervalTracker: null, 14 | canvasCtx: null, 15 | height, 16 | width 17 | } 18 | 19 | this.canvas = React.createRef() 20 | } 21 | 22 | static defaultProps = { 23 | color: 'rgba(255, 255, 255, 0.75)', 24 | speed: 32, 25 | max: 32, 26 | width: 1024, 27 | height: 768 28 | } 29 | 30 | static propTypes = { 31 | color: PropTypes.string, 32 | speed: PropTypes.number, 33 | max: PropTypes.number, 34 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 35 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 36 | style: PropTypes.object 37 | } 38 | 39 | componentDidMount() { 40 | // Canvas init 41 | const canvas = this.canvas.current 42 | const canvasCtx = canvas.getContext('2d') 43 | 44 | const { speed, color } = this.props 45 | const { width: W, height: H } = this.state 46 | this.setState({ canvasCtx }) 47 | 48 | // Particles 49 | const { max } = this.props 50 | const particles = [] 51 | for (let i = 0; i < max; i++) { 52 | particles.push({ 53 | x: Math.random() * W, // x-coordinate 54 | y: Math.random() * H, // y-coordinate 55 | r: Math.random() * 4 + 1, // radius 56 | d: Math.random() * max // density 57 | }) 58 | } 59 | 60 | // Draw the flakes 61 | const draw = () => { 62 | canvasCtx.clearRect(0, 0, W, H) 63 | 64 | canvasCtx.fillStyle = color 65 | canvasCtx.beginPath() 66 | for (let i = 0; i < max; i++) { 67 | const p = particles[i] 68 | canvasCtx.moveTo(p.x, p.y) 69 | canvasCtx.arc(p.x, p.y, p.r, 0, Math.PI * 2, true) 70 | } 71 | canvasCtx.fill() 72 | update() 73 | } 74 | 75 | // Function to move the snowflakes 76 | // angle will be an ongoing incremental flag. Sin and Cos functions will 77 | // be applied to it to create vertical + horizontal movements of the flakes 78 | let angle = 0 79 | 80 | const update = () => { 81 | angle += 0.01 82 | for (let i = 0; i < max; i++) { 83 | const p = particles[i] 84 | // Updating X and Y coordinates 85 | // Adding 1 to cos to prevent negative values (which would move flakes upwards) 86 | // Every particle has its own density to make the downward movement different for each flake 87 | // Make it more random by adding in the radius 88 | p.y += Math.cos(angle + p.d) + 1 + p.r / 2 89 | p.x += Math.sin(angle) * 2 90 | 91 | // Sending flakes back from the top when it exits 92 | // Make it more organic with flakes entering from both sides 93 | if (p.x > W + 5 || p.x < -5 || p.y > H) { 94 | if (i % 3 > 0) { 95 | // 2/3 of the flakes 96 | particles[i] = { x: Math.random() * W, y: -10, r: p.r, d: p.d } 97 | } else { 98 | // If the flake is exitting from the right 99 | if (Math.sin(angle) > 0) { 100 | // Enter from the left 101 | particles[i] = { x: -5, y: Math.random() * H, r: p.r, d: p.d } 102 | } else { 103 | // Enter from the right 104 | particles[i] = { x: W + 5, y: Math.random() * H, r: p.r, d: p.d } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | this.setState({ intervalTracker: setInterval(draw, speed) }) 112 | 113 | // Animation loop 114 | this.state.intervalTracker 115 | } 116 | 117 | componentWillUnmount() { 118 | this.state.canvasCtx.clearRect(0, 0, this.state.width, this.state.height) 119 | clearInterval(this.state.intervalTracker) 120 | } 121 | 122 | render() { 123 | const { style, color, speed, max, ...props } = this.props 124 | const sx = { 125 | margin: 0, 126 | padding: 0, 127 | pointerEvents: 'none', 128 | ...style 129 | } 130 | 131 | return ( 132 | 139 | ) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import Snow from './' 2 | 3 | describe('Snow', () => { 4 | it('is truthy', () => { 5 | expect(Snow).toBeTruthy() 6 | }) 7 | }) 8 | --------------------------------------------------------------------------------