├── src ├── index.ts ├── global.ts ├── examples │ ├── examples.ts │ ├── umd.html │ ├── index.html │ ├── ghpages.html │ └── ghpages.ts ├── snowflake.ts └── snow-scene.ts ├── .vscode └── settings.json ├── .npmignore ├── .prettierrc ├── .gitignore ├── tsconfig.umd.json ├── tsconfig.json ├── webpack.config.js ├── webpack.config.prod.js ├── docs ├── style.css ├── index.html ├── snowflakes-examples.bundle.js └── snowflakes-examples.bundle.js.map ├── webpack.config.ghpages.js ├── package.json ├── README.zh.md ├── README.md └── .github └── workflows └── codeql-analysis.yml /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './snow-scene'; 2 | export * from './snowflake'; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | src/ 3 | examples/ 4 | .prettierrc 5 | webpack.config.js 6 | webpack.config.prod.js 7 | tsconfig.json 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | import { SnowScene } from './snow-scene'; 2 | 3 | if (typeof window !== 'undefined') { 4 | (window as any).SnowScene = SnowScene; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | 4 | # IDE - VSCode 5 | .vscode/* 6 | !.vscode/settings.json 7 | !.vscode/tasks.json 8 | !.vscode/launch.json 9 | !.vscode/extensions.json 10 | 11 | #System Files 12 | .DS_Store 13 | Thumbs.db -------------------------------------------------------------------------------- /tsconfig.umd.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "target": "es5", 5 | "lib": ["es2017", "dom"], 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node" 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "target": "es5", 5 | "lib": ["es2017", "dom"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "node" 13 | }, 14 | "include": ["src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /src/examples/examples.ts: -------------------------------------------------------------------------------- 1 | import { SnowScene } from '../index'; 2 | 3 | const btnStart = document.querySelector('#start')!; 4 | const btnStop = document.querySelector('#stop')!; 5 | const btnToggle = document.querySelector('#toggle')!; 6 | 7 | const scene = new SnowScene('body', { 8 | volumn: 100, // density of the snow 9 | color: '#ffffff', // color of the snowflakes 10 | }); 11 | 12 | scene.play(); 13 | 14 | btnStart.onclick = () => { 15 | scene.play(); 16 | }; 17 | 18 | btnStop.onclick = () => { 19 | scene.pause(); 20 | }; 21 | 22 | btnToggle.onclick = () => { 23 | scene.toggle(); 24 | }; 25 | 26 | // for debugging purpose 27 | (window as any).sss = scene; 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/examples/examples.ts', 6 | output: { 7 | filename: 'examples.js', 8 | path: path.resolve(__dirname, 'dist', 'examples'), 9 | }, 10 | mode: 'development', 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.ts$/, 15 | use: 'ts-loader', 16 | exclude: /node_modules/, 17 | }, 18 | ], 19 | }, 20 | resolve: { 21 | extensions: ['.ts', '.js'], 22 | }, 23 | plugins: [new HtmlWebpackPlugin({ template: './src/examples/index.html' })], 24 | devtool: 'inline-source-map', 25 | devServer: { 26 | contentBase: './dist/examples', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/global.ts', 5 | output: { 6 | filename: 'snowflakes.bundle.min.js', 7 | path: path.resolve(__dirname, 'dist'), 8 | }, 9 | mode: 'production', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts$/, 14 | use: [ 15 | { 16 | loader: 'ts-loader', 17 | options: { 18 | configFile: 'tsconfig.umd.json', 19 | }, 20 | }, 21 | ], 22 | exclude: /node_modules/, 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: ['.ts', '.js'], 28 | }, 29 | plugins: [], 30 | devtool: 'source-map', 31 | devServer: { 32 | contentBase: './dist/examples', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: white; 4 | text-align: center; 5 | font-family: Lato; 6 | height: 100vh; 7 | padding: 1px; 8 | } 9 | 10 | section { 11 | display: flex; 12 | align-items: center; 13 | /* justify-content: center; */ 14 | flex-direction: column; 15 | } 16 | 17 | .demo-wrap { 18 | width: 260px; 19 | height: 200px; 20 | border: 1px solid white; 21 | /* box-shadow: 0 0 6px 0 rgba(255, 255, 255, .4); */ 22 | } 23 | 24 | aside { 25 | margin-top: 32px; 26 | } 27 | 28 | button { 29 | font-size: 16px; 30 | padding: 0.5em 1em; 31 | background-color: black; 32 | border: 1px solid white; 33 | color: white; 34 | margin: 4px; 35 | cursor: pointer; 36 | outline: none; 37 | } 38 | 39 | a:visited { 40 | color: white; 41 | } 42 | -------------------------------------------------------------------------------- /webpack.config.ghpages.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/examples/ghpages.ts', 6 | output: { 7 | filename: 'snowflakes-examples.bundle.js', 8 | path: path.resolve(__dirname, 'docs'), 9 | }, 10 | mode: 'production', 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.ts$/, 15 | use: [ 16 | { 17 | loader: 'ts-loader', 18 | options: { 19 | configFile: 'tsconfig.umd.json', 20 | }, 21 | }, 22 | ], 23 | exclude: /node_modules/, 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: ['.ts', '.js'], 29 | }, 30 | plugins: [new HtmlWebpackPlugin({ template: './src/examples/ghpages.html' })], 31 | devtool: 'source-map', 32 | }; 33 | -------------------------------------------------------------------------------- /src/examples/umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SnowflakesJS UMD module 8 | 23 | 24 | 25 |

SnowflakesJS UMD module

26 | 27 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SnowflakesJS example 8 | 27 | 28 | 29 |

SnowflakesJS example

30 |
31 | 32 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /src/examples/ghpages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SnowflakesJS 8 | 9 | 10 | 11 |
12 |

SnowflakesJS

13 |

Let it snow ~ Ho! Ho! Ho!

14 |
15 |
16 | 23 |
24 |

View on Github

25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SnowflakesJS 8 | 9 | 10 | 11 |
12 |

SnowflakesJS

13 |

Let it snow ~ Ho! Ho! Ho!

14 |
15 |
16 | 23 |
24 |

View on Github

25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /src/examples/ghpages.ts: -------------------------------------------------------------------------------- 1 | import { SnowScene } from '../snow-scene'; 2 | 3 | // add snowflakes to a container with extra customisations 4 | const scene = new SnowScene('#demo1', { 5 | color: '#8ad3ff', // change snowflake color (default #ffffff) 6 | volumn: 1000, // change snow volumn to make it a storm (default 300) 7 | }); 8 | 9 | // add snowflakes to the whole webpage 10 | const fullScreenScene = new SnowScene(); 11 | 12 | const demo1 = document.querySelector('#demo1')!; 13 | 14 | // register demo actions 15 | const btnStart = document.getElementById('btnStart')!; 16 | const btnPause = document.getElementById('btnPause')!; 17 | const btnResize = document.getElementById('btnResize')!; 18 | 19 | btnStart.onclick = () => scene.play(); 20 | btnPause.onclick = () => scene.pause(); 21 | btnResize.onclick = () => { 22 | demo1.style.width = demo1.style.width === '360px' ? '260px' : '360px'; 23 | demo1.style.height = demo1.style.height === '300px' ? '200px' : '300px'; 24 | }; 25 | 26 | const btnStartFull = document.getElementById('btnStartFull')!; 27 | const btnPauseFull = document.getElementById('btnPauseFull')!; 28 | 29 | btnStartFull.onclick = () => fullScreenScene.play(); 30 | btnPauseFull.onclick = () => fullScreenScene.pause(); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snowflakesjs", 3 | "version": "1.0.2", 4 | "description": "Beautiful snowflakes falling down on your webpages.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "start": "webpack-dev-server", 9 | "clean": "rm -rf dist", 10 | "build:es6": "tsc", 11 | "build:umd": "webpack --config webpack.config.prod.js", 12 | "build:ghpages": "webpack --config webpack.config.ghpages.js", 13 | "build": "yarn clean && yarn build:umd && yarn build:es6" 14 | }, 15 | "keywords": [ 16 | "snowflake", 17 | "snowflakes", 18 | "snow", 19 | "snowfall", 20 | "Christmas", 21 | "winter", 22 | "animation" 23 | ], 24 | "author": "Xinan", 25 | "license": "ISC", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/owen26/snowflakesjs" 29 | }, 30 | "homepage": "https://github.com/owen26/snowflakesjs", 31 | "dependencies": { 32 | "element-resize-detector": "^1.1.14" 33 | }, 34 | "devDependencies": { 35 | "@types/element-resize-detector": "^1.1.0", 36 | "clean-webpack-plugin": "^3.0.0", 37 | "html-webpack-plugin": "^3.2.0", 38 | "ts-loader": "^6.2.1", 39 | "typescript": "^3.7.3", 40 | "webpack": "^4.41.2", 41 | "webpack-cli": "^3.3.10", 42 | "webpack-dev-server": "^3.9.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # SnowflakesJS 落雪效果 2 | 3 | 一个很酷的网页落雪效果,完全支持各种前端框架环境以及纯浏览器加载。 4 | 5 | 完全支持 TypeScript。 6 | 7 | [English](README.md) 8 | 9 | ## 演示 10 | 11 | [GitHub Pages](https://owen26.github.io/snowflakesjs/) 12 | 13 | [Stackblitz](https://stackblitz.com/edit/snowflakesjs-demo) 14 | 15 | ## 安装 16 | 17 | ``` 18 | npm install snowflakesjs 19 | 20 | // 或者使用 yarn 21 | 22 | yarn add snowflakesjs 23 | ``` 24 | 25 | ## 使用 26 | 27 | ### 基础使用 28 | 29 | ```js 30 | // 引入包文件 31 | import { SnowScene } from 'snowflakesjs'; 32 | 33 | // 添加落雪效果到网页body元素(通畅这意味着全屏模式,除非你的css样式不合理导致body没有覆盖全屏) 34 | const scene = new SnowScene(); 35 | 36 | // 开始落雪 37 | scene.play(); 38 | 39 | // 停止 40 | scene.pause(); 41 | 42 | // 切换落雪效果 开始/停止 43 | scene.toggle(); 44 | ``` 45 | 46 | ### 参数解释 47 | 48 | ```js 49 | // 引入包文件 50 | import { SnowScene } from 'snowflakesjs'; 51 | 52 | // 添加落雪效果至特定的DOM容器,并更改落雪行为参数 53 | const scene = new SnowScene('#mycontainerid', { 54 | color: '#bfeaff', // 雪花颜色 (默认 #ffffff) 55 | volumn: 1000, // 雪花数量/强度 (默认 300) 56 | }); 57 | 58 | // 开始落雪 59 | scene.play(); 60 | ``` 61 | 62 | ### 直接引入浏览器 63 | 64 | 常用的 CDN 位置如下,此外你也可以手动下载此 js 文件并包含到你的项目中去。 65 | 66 | - [cdn.jsdelivr.net](https://cdn.jsdelivr.net/npm/snowflakesjs/dist/snowflakes.bundle.min.js)(国内友好) 67 | - [unpkg.com](https://unpkg.com/snowflakesjs/dist/snowflakes.bundle.min.js) 68 | 69 | ```html 70 | 74 | 78 | ``` 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SnowflakesJS 2 | 3 | Beautiful snowflakes falling down on your webpages. 4 | 5 | Full TypeScript support. 6 | 7 | [中文](README.zh.md) 8 | 9 | ## Demo 10 | 11 | [GitHub Pages](https://owen26.github.io/snowflakesjs/) 12 | 13 | [Stackblitz](https://stackblitz.com/edit/snowflakesjs-demo) 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install snowflakesjs 19 | 20 | // or yarn 21 | 22 | yarn add snowflakesjs 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### Basic 28 | 29 | ```js 30 | // import package in your code 31 | import { SnowScene } from 'snowflakesjs'; 32 | 33 | // add snowflakes to the webpage's body element (usually this means fullscreen) 34 | const scene = new SnowScene(); 35 | 36 | // start snow fall 37 | scene.play(); 38 | 39 | // stop snow fall 40 | scene.pause(); 41 | 42 | // toggle snow fall on/off 43 | scene.toggle(); 44 | ``` 45 | 46 | ### Customisation 47 | 48 | ```js 49 | // import package in your code 50 | import { SnowScene } from 'snowflakesjs'; 51 | 52 | // add snowflakes to a container with extra customisations 53 | const scene = new SnowScene('#mycontainerid', { 54 | color: '#bfeaff', // change snowflake color (default #ffffff) 55 | volumn: 1000, // change snow volumn to make it a storm (default 300) 56 | }); 57 | 58 | // start the snow fall 59 | scene.play(); 60 | ``` 61 | 62 | ### Directly reference in browser 63 | 64 | ```html 65 | 69 | 73 | ``` 74 | -------------------------------------------------------------------------------- /src/snowflake.ts: -------------------------------------------------------------------------------- 1 | export enum SnowflakePosition { 2 | TOP, 3 | BOTTOM, 4 | LEFT, 5 | RIGHT, 6 | ONSTAGE, 7 | } 8 | 9 | export class Snowflake { 10 | /** Snowflake colour */ 11 | color = '#ffffff'; 12 | /** snowflake recycle activated */ 13 | active = true; 14 | pos = SnowflakePosition.TOP; 15 | 16 | private x!: number; 17 | private y!: number; 18 | private vx!: number; 19 | private vy!: number; 20 | private radius!: number; 21 | private alpha!: number; 22 | 23 | private canvas: HTMLCanvasElement; 24 | private ctx: CanvasRenderingContext2D; 25 | 26 | constructor(canvas: HTMLCanvasElement) { 27 | this.canvas = canvas; 28 | 29 | const ctx = canvas.getContext('2d'); 30 | if (ctx) { 31 | this.ctx = ctx; 32 | } else { 33 | throw new Error( 34 | 'Canvas 2D context not found, please check it is running in Browser environment.' 35 | ); 36 | } 37 | 38 | this.allocate(); 39 | } 40 | 41 | draw(): void { 42 | this.updatePosition(); 43 | 44 | // freeze all frame render calculation if not active and not on stage 45 | if (!this.active && this.pos !== SnowflakePosition.ONSTAGE) { 46 | return; 47 | } 48 | 49 | if ( 50 | this.pos === SnowflakePosition.LEFT || 51 | this.pos === SnowflakePosition.RIGHT || 52 | this.pos === SnowflakePosition.BOTTOM 53 | ) { 54 | this.allocate(); 55 | return; 56 | } 57 | 58 | this.y += this.vy; 59 | this.x += this.vx; 60 | 61 | this.ctx.globalAlpha = this.alpha; 62 | this.ctx.beginPath(); 63 | this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false); 64 | this.ctx.closePath(); 65 | this.ctx.fillStyle = this.color; 66 | this.ctx.fill(); 67 | } 68 | 69 | private updatePosition(): void { 70 | if (this.y < -this.radius) { 71 | this.pos = SnowflakePosition.TOP; 72 | } else if (this.y > this.canvas.height + this.radius) { 73 | this.pos = SnowflakePosition.BOTTOM; 74 | } else if (this.x < -this.radius) { 75 | this.pos = SnowflakePosition.LEFT; 76 | } else if (this.x > this.canvas.width + this.radius) { 77 | this.pos = SnowflakePosition.RIGHT; 78 | } else { 79 | this.pos = SnowflakePosition.ONSTAGE; 80 | } 81 | } 82 | 83 | private allocate(): void { 84 | this.x = Math.random() * this.canvas.width; 85 | this.y = Math.random() * -this.canvas.height; 86 | 87 | this.vy = 1 + Math.random() * 3; 88 | this.vx = 0.5 - Math.random(); 89 | 90 | this.radius = 1 + Math.random() * 2; 91 | this.alpha = 0.5 + Math.random() * 0.5; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '19 19 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /src/snow-scene.ts: -------------------------------------------------------------------------------- 1 | import { Snowflake, SnowflakePosition } from './snowflake'; 2 | import elementResizeDetector from 'element-resize-detector'; 3 | 4 | export interface SnowSceneConfig { 5 | /** Snowflake colour */ 6 | color: string; 7 | /** Volumn of snowflakes (increase it to make a strom) */ 8 | volumn: number; 9 | } 10 | 11 | const defaultSnowSceneConfig: SnowSceneConfig = { 12 | color: '#ffffff', 13 | volumn: 300, 14 | }; 15 | 16 | export class SnowScene { 17 | config: SnowSceneConfig; 18 | 19 | private container: HTMLElement; 20 | private canvas: HTMLCanvasElement | undefined; 21 | private ctx: CanvasRenderingContext2D | undefined; 22 | 23 | private snowflakes!: Snowflake[]; 24 | private active = false; 25 | private initialised = false; 26 | private animationId = 0; 27 | 28 | private resizeDetector = elementResizeDetector({ 29 | strategy: 'scroll', 30 | }); 31 | 32 | constructor(container: string | HTMLElement = 'body', config?: SnowSceneConfig) { 33 | const containerElement = 34 | typeof container === 'string' ? document.querySelector(container) : container; 35 | 36 | if (containerElement) { 37 | this.container = containerElement; 38 | } else { 39 | throw new Error('can not find container by specified selector'); 40 | } 41 | 42 | this.config = { ...defaultSnowSceneConfig, ...config }; 43 | 44 | this.buildScene(); 45 | } 46 | 47 | play(): void { 48 | if (!this.initialised) { 49 | this.buildScene(); 50 | } 51 | 52 | this.active = true; 53 | this.snowflakes.forEach(s => (s.active = true)); 54 | 55 | // check if there is still animation going on, if there is, do not intialize a new loop 56 | if (!this.animationId) { 57 | this.animationId = requestAnimationFrame(() => this.updateFrame()); 58 | } 59 | } 60 | 61 | pause(): void { 62 | this.active = false; 63 | this.snowflakes.forEach(s => (s.active = false)); 64 | } 65 | 66 | toggle(): void { 67 | if (this.active) { 68 | this.pause(); 69 | } else { 70 | this.play(); 71 | } 72 | } 73 | 74 | private buildScene(): void { 75 | const canvas = document.createElement('canvas'); 76 | 77 | canvas.style.position = 'absolute'; 78 | canvas.style.left = '0'; 79 | canvas.style.top = '0'; 80 | canvas.style.pointerEvents = 'none'; 81 | canvas.width = this.container.clientWidth; 82 | canvas.height = this.container.clientHeight; 83 | 84 | this.container.appendChild(canvas); 85 | 86 | this.canvas = canvas; 87 | 88 | const ctx = canvas.getContext('2d'); 89 | if (ctx) { 90 | this.ctx = ctx; 91 | } else { 92 | throw new Error( 93 | 'Canvas 2D context not found, please check it is running in Browser environment.' 94 | ); 95 | } 96 | 97 | // generate snowflakes 98 | this.snowflakes = []; 99 | for (let i = 0; i < this.config.volumn; i++) { 100 | const flake = new Snowflake(this.canvas); 101 | 102 | flake.color = this.config.color; 103 | 104 | this.snowflakes.push(flake); 105 | } 106 | 107 | this.resizeDetector.listenTo(this.container, () => { 108 | this.onResize(); 109 | }); 110 | 111 | this.initialised = true; 112 | } 113 | 114 | private destroyScene(): void { 115 | this.canvas?.remove(); 116 | this.resizeDetector.uninstall(this.container); 117 | this.initialised = false; 118 | } 119 | 120 | private updateFrame(): void { 121 | if (!this.canvas || !this.ctx) { 122 | return; 123 | } 124 | 125 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 126 | 127 | this.snowflakes.forEach(flake => { 128 | flake.draw(); 129 | }); 130 | 131 | if (!this.active && this.snowflakes.every(flake => flake.pos !== SnowflakePosition.ONSTAGE)) { 132 | this.animationId = 0; 133 | this.destroyScene(); 134 | } else { 135 | this.animationId = requestAnimationFrame(() => this.updateFrame()); 136 | } 137 | } 138 | 139 | private onResize(): void { 140 | if (!this.canvas || !this.ctx) { 141 | return; 142 | } 143 | 144 | this.canvas.width = this.container.clientWidth; 145 | this.canvas.height = this.container.clientHeight; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /docs/snowflakes-examples.bundle.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(i,o,function(e){return t[e]}.bind(null,o));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=13)}([function(t,e,n){"use strict";(t.exports={}).forEach=function(t,e){for(var n=0;n4?t:void 0}());var e},i.isLegacyOpera=function(){return!!window.opera}},function(t,e,n){"use strict";var i=n(0).forEach,o=n(3),r=n(4),a=n(5),s=n(6),l=n(7),c=n(1),d=n(8),u=n(10),h=n(11),f=n(12);function p(t){return Array.isArray(t)||void 0!==t.length}function v(t){if(Array.isArray(t))return t;var e=[];return i(t,(function(t){e.push(t)})),e}function g(t){return t&&1===t.nodeType}function m(t,e,n){var i=t[e];return null==i&&void 0!==n?n:i}t.exports=function(t){var e;if((t=t||{}).idHandler)e={get:function(e){return t.idHandler.get(e,!0)},set:t.idHandler.set};else{var n=a(),y=s({idGenerator:n,stateHandler:u});e=y}var b=t.reporter;b||(b=l(!1===b));var w=m(t,"batchProcessor",d({reporter:b})),x={};x.callOnAdd=!!m(t,"callOnAdd",!0),x.debug=!!m(t,"debug",!1);var E,S=r(e),T=o({stateHandler:u}),k=m(t,"strategy","object"),A={reporter:b,batchProcessor:w,stateHandler:u,idHandler:e};if("scroll"===k&&(c.isLegacyOpera()?(b.warn("Scroll strategy is not supported on legacy Opera. Changing to object strategy."),k="object"):c.isIE(9)&&(b.warn("Scroll strategy is not supported on IE9. Changing to object strategy."),k="object")),"scroll"===k)E=f(A);else{if("object"!==k)throw new Error("Invalid strategy name: "+k);E=h(A)}var O={};return{listenTo:function(t,n,o){function r(t){var e=S.get(t);i(e,(function(e){e(t)}))}function a(t,e,n){S.add(e,n),t&&n(e)}if(o||(o=n,n=t,t={}),!n)throw new Error("At least one element required.");if(!o)throw new Error("Listener required.");if(g(n))n=[n];else{if(!p(n))return b.error("Invalid arguments. Must be a DOM element or a collection of DOM elements.");n=v(n)}var s=0,l=m(t,"callOnAdd",x.callOnAdd),c=m(t,"onReady",(function(){})),d=m(t,"debug",x.debug);i(n,(function(t){u.getState(t)||(u.initState(t),e.set(t));var h=e.get(t);if(d&&b.log("Attaching listener to element",h,t),!T.isDetectable(t))return d&&b.log(h,"Not detectable."),T.isBusy(t)?(d&&b.log(h,"System busy making it detectable"),a(l,t,o),O[h]=O[h]||[],void O[h].push((function(){++s===n.length&&c()}))):(d&&b.log(h,"Making detectable..."),T.markBusy(t,!0),E.makeDetectable({debug:d},t,(function(t){if(d&&b.log(h,"onElementDetectable"),u.getState(t)){T.markAsDetectable(t),T.markBusy(t,!1),E.addListener(t,r),a(l,t,o);var e=u.getState(t);if(e&&e.startSize){var f=t.offsetWidth,p=t.offsetHeight;e.startSize.width===f&&e.startSize.height===p||r(t)}O[h]&&i(O[h],(function(t){t()}))}else d&&b.log(h,"Element uninstalled before being detectable.");delete O[h],++s===n.length&&c()})));d&&b.log(h,"Already detecable, adding listener."),a(l,t,o),s++})),s===n.length&&c()},removeListener:S.removeListener,removeAllListeners:S.removeAllListeners,uninstall:function(t){if(!t)return b.error("At least one element is required.");if(g(t))t=[t];else{if(!p(t))return b.error("Invalid arguments. Must be a DOM element or a collection of DOM elements.");t=v(t)}i(t,(function(t){S.removeAllListeners(t),E.uninstall(t),u.cleanState(t)}))}}}},function(t,e,n){"use strict";t.exports=function(t){var e=t.stateHandler.getState;return{isDetectable:function(t){var n=e(t);return n&&!!n.isDetectable},markAsDetectable:function(t){e(t).isDetectable=!0},isBusy:function(t){return!!e(t).busy},markBusy:function(t,n){e(t).busy=!!n}}}},function(t,e,n){"use strict";t.exports=function(t){var e={};function n(n){var i=t.get(n);return void 0===i?[]:e[i]||[]}return{get:n,add:function(n,i){var o=t.get(n);e[o]||(e[o]=[]),e[o].push(i)},removeListener:function(t,e){for(var i=n(t),o=0,r=i.length;on?n=o:o div::-webkit-scrollbar { display: none; }\n\n",i+="."+(e+"_animation_active")+" { -webkit-animation-duration: 0.1s; animation-duration: 0.1s; -webkit-animation-name: "+n+"; animation-name: "+n+"; }\n",i+="@-webkit-keyframes "+n+" { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } }\n",function(e,n){n=n||function(t){document.head.appendChild(t)};var i=document.createElement("style");i.innerHTML=e,i.id=t,n(i)}(i+="@keyframes "+n+" { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } }")}}("erd_scroll_detection_scrollbar_style",s),{makeDetectable:function(t,c,h){function f(){if(t.debug){var n=Array.prototype.slice.call(arguments);if(n.unshift(r.get(c),"Scroll: "),e.log.apply)e.log.apply(null,n);else for(var i=0;ithis.canvas.height+this.radius?this.pos=i.BOTTOM:this.x<-this.radius?this.pos=i.LEFT:this.x>this.canvas.width+this.radius?this.pos=i.RIGHT:this.pos=i.ONSTAGE},t.prototype.allocate=function(){this.x=Math.random()*this.canvas.width,this.y=Math.random()*-this.canvas.height,this.vy=1+3*Math.random(),this.vx=.5-Math.random(),this.radius=1+2*Math.random(),this.alpha=.5+.5*Math.random()},t}(),r=n(2),a=n.n(r),s=function(){return(s=Object.assign||function(t){for(var e,n=1,i=arguments.length;n